Skip to content
← Alle Beiträge
· Delta1 Labs LicensingKeyright

Floating-Seats mit Keyright: nach gleichzeitiger Nutzung lizenzieren statt pro Maschine

Die knotengebundene Aktivierung bindet eine Lizenz an eine Maschine. Floating-Lizenzierung bindet sie an die gleichzeitige Nutzung: Sie kaufen 25 Seats, und beliebige 25 Ihrer 400 Installationen dürfen gleichzeitig laufen. Der Mechanismus ist ein Lease: Ein Client checkt einen Seat aus, hält ihn mit Heartbeats und gibt ihn beim Beenden zurück. Ein abgestürzter Client gibt nie zurück, also läuft sein Lease einfach ab und der Seat kehrt in den Pool zurück. So bauen Sie Concurrent-Seat-Lizenzierung auf einer selbstgehosteten Keyright-Instanz mit Lease-Checkout, serverseitiger Zählung, TTL-basierter Rückgewinnung und einer eleganten Behandlung des Falls „alle Seats belegt".

Die meisten Lizenzmodelle beantworten die Frage „Darf diese Kopie auf dieser Maschine laufen?”. Floating-Lizenzierung beantwortet eine andere: „Laufen gerade jetzt zu viele Kopien?”. Dieser Wechsel — vom Zählen der Installationen zum Zählen der gleichzeitigen Nutzung — erlaubt einem Studio mit 400 Arbeitsplätzen, 25 Seats zu kaufen, denn zu keinem Zeitpunkt sind mehr als 25 Künstler wirklich in der Anwendung. Die Seats sind flottierend: Wer als Nächstes startet, nimmt einen freien, und wer beendet, gibt einen zurück.

Der zugrunde liegende Mechanismus ist ein Lease. Ein Client checkt einen Seat aus, bevor er die lizenzierten Funktionen freischaltet, hält das Lease mit einem Heartbeat am Leben, während er läuft, und checkt den Seat beim Beenden wieder ein. Der Ausstellungsdienst zählt die aktiven Leases und weigert sich, mehr auszugeben, als Sie gekauft haben. Der elegante Teil ist, was passiert, wenn ein Client nicht zurückgibt — ein Absturz, ein kill -9, ein herausgerissenes Netzwerkkabel. Er hört einfach auf zu senden, sein Lease läuft ab, und der Seat kehrt ohne Eingriff in den Pool zurück. Dieser Artikel zeigt, wie Sie das auf einer selbstgehosteten Keyright-Instanz bauen.

Knotengebunden versus flottierend

Es lohnt sich, genau zu sein, was sich ändert. Keyrights Standardaktivierung ist knotengebunden: ActivateAsync bindet einen Lizenzschlüssel an den Fingerabdruck einer Maschine und verbraucht für die Lebensdauer dieser Aktivierung einen Seat. Installieren Sie auf einer zweiten Maschine, geben Sie einen zweiten Seat aus — unabhängig davon, ob die erste überhaupt eingeschaltet ist. Das ist genau richtig für dedizierte Arbeitsplätze, stets derselben Person.

Floating kehrt die Bindung um. Die Lizenz ist überhaupt nicht an eine Maschine gebunden; sie ist an eine Zählung gleichzeitiger Leases gebunden. Sie können die Anwendung auf jede Maschine im Gebäude ausrollen. Was Sie nicht können, ist, mehr davon gleichzeitig die lizenzierte Funktionalität ausführen zu lassen, als Sie Seats haben.

Knotengebundene AktivierungFloating-Seat
Gebunden anDen Fingerabdruck einer MaschineEin lebendes Lease, für die Dauer der Nutzung
Verbraucht beiDer AktivierungDem Holen eines Seats (App-Start / Eintritt in eine Funktion)
Freigegeben beiDer DeaktivierungDer Rückgabe beim Beenden oder dem Ablauf des Lease-TTL
ÜberinstallationBlockiert — jede Installation ist ein SeatErlaubt — breit installieren, bis zu N gleichzeitig ausführen
Benötigt den DienstBei Aktivierung / RevalidierungFortlaufend, um Leases zu halten und zu zählen
Ideal fürDedizierte Arbeitsplätze, offline-tolerantGeteilte Labore, mobile Nutzer, CI-Pools

Der Lebenszyklus des Lease

Ein Floating-Seat durchläuft eine kleine Zustandsmaschine: geholt → erneuert (wiederholt) → zurückgegeben, mit einem Ablauf-Pfad, der sich wieder mit „zurückgegeben” vereint, sooft eine Erneuerung nicht rechtzeitig eintrifft.

Holen Seat ausgecheckt Aktiv + Heartbeat vor dem TTL erneuern Zurückgegeben (sauber) Seat zurückgegeben Abgelaufen (zurückgewonnen) TTL verstrichen, keine Erneuerung alle ~TTL/3 Beenden Absturz / Netzverlust Sauberes Beenden und stiller Tod enden beide mit dem Seat zurück im Pool.

Die beiden Endzustände sind der Punkt: Ein sauberes Beenden und ein stiller Tod geben beide den Seat zurück. Das eine ist sofort, das andere wartet das TTL ab, aber keines verliert ihn.

Einen Seat holen

Initialisieren Sie den Client genau wie bei der knotengebundenen Lizenzierung — denselben reinen Verifikations-Public-Key und die URL Ihrer selbstgehosteten Instanz — und holen Sie dann einen Seat, statt (oder zusätzlich zum) Aktivieren. AcquireSeatAsync liefert ein SeatLease: ein Handle mit der Lease-ID, seinem Ablauf und dem empfohlenen Erneuerungsintervall, das der Server aus dem TTL berechnet hat.

var client = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "acme-studio",
    PublicKeyBase64 = config["Keyright:PublicKey"],   // verify-only, not a secret
    ServiceUrl      = config["Keyright:ServiceUrl"],   // your self-hosted instance
});

SeatLease lease;
try
{
    lease = await client.AcquireSeatAsync(userKey);
}
catch (SeatsExhaustedException ex)
{
    // Not an error condition in the app's sense — the pool is simply full.
    return Gate.Deny($"All {ex.MaxSeats} seats are in use. Try again shortly.");
}

// Fail closed: licensed features stay off until we actually hold a seat.
LicenseFeatures.Enable(lease);

Darunter ist das ein einziges POST. Der Client sendet eine stabile Client-Instanz-ID (eine pro Installation, lokal persistiert), damit der Dienst ein erneutes Holen desselben Clients erkennt und Sie in einer Admin-Ansicht anzeigen können, „wer einen Seat hält”. Ein Idempotency-Key macht das Holen sicher gegen eine verlorene Antwort wiederholbar: Ein abgelaufenes Holen, das tatsächlich erfolgreich war, spielt dasselbe Lease erneut ab, statt einen zweiten Seat zu verbrennen:

POST /v1/seats/acquire HTTP/1.1
Host: licensing.example.com
Idempotency-Key: 5f2c-acme-studio-inst-91a7
Content-Type: application/json

{
  "product": "acme-studio",
  "licenseKey": "ACME-STU-7F3K-...",
  "clientInstanceId": "inst-91a7",
  "metadata": { "user": "r.okafor", "host": "lab-ws-14" }
}
{
  "leaseId": "lease_3b9a0c",
  "expiresAt": "2026-11-13T14:32:05Z",
  "ttlSeconds": 120,
  "renewAfterSeconds": 40,
  "seatsInUse": 24,
  "maxSeats": 25
}

Ist der Pool voll, erfindet der Dienst keinen Seat; er antwortet mit 409 Conflict und einem problem+json-Body (type: .../seats-exhausted, seatsInUse, maxSeats), den das SDK als SeatsExhaustedException zugänglich macht. Die Zählung ist auf dem Server maßgeblich: Ein gepatchter Client kann sich keinen 26. Seat einreden.

Heartbeaten, um den Seat zu halten

Ein Lease ist bewusst kurzlebig. Es zu halten bedeutet, es vor expiresAt zu erneuern, was zugleich beweist, dass der Client lebt, und den Ablauf nach vorn schiebt. Erneuern Sie per Timer bei etwa renewAfterSeconds — bequem innerhalb des TTL —, damit ein einziger verpasster Schlag den Seat nicht fallen lässt.

public sealed class SeatKeepAlive : IAsyncDisposable
{
    private readonly KeyrightClient _client;
    private readonly PeriodicTimer _timer;
    private readonly CancellationTokenSource _cts = new();
    private SeatLease _lease;

    public SeatKeepAlive(KeyrightClient client, SeatLease lease)
    {
        _client = client;
        _lease  = lease;
        _timer  = new PeriodicTimer(TimeSpan.FromSeconds(lease.RenewAfterSeconds));
        _ = RunAsync();
    }

    private async Task RunAsync()
    {
        while (await _timer.WaitForNextTickAsync(_cts.Token))
        {
            try
            {
                _lease = await _client.RenewSeatAsync(_lease);   // POST /v1/seats/{id}/renew
            }
            catch (LeaseExpiredException)
            {
                // The server already reclaimed our seat (we were out too long).
                // Stop pretending to be licensed and try to re-acquire from scratch.
                LicenseFeatures.Disable();
                await _client.RecoverSeatAsync(_lease.Product, _cts.Token);
                return;
            }
            catch (TransientException)
            {
                // Network blip: keep the existing lease, let the next tick retry.
                // We still hold the seat until expiresAt regardless.
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        _cts.Cancel();
        _timer.Dispose();
        await _client.ReleaseSeatAsync(_lease);   // clean check-in on shutdown
    }
}

Zwei Designentscheidungen verdienen sich hier ihren Platz. Erstens gibt Ihnen ein Erneuerungsintervall deutlich unter dem TTL (ein Drittel ist ein sinnvoller Standard) kostenlose Wiederholungen: Eine verpasste Erneuerung ist harmlos, weil das Lease bis zum nächsten Tick gültig bleibt. Zweitens gerät ein vorübergehender Fehler nicht in Panik und deaktiviert keine Funktionen: Der Client hält den Seat bereits bis expiresAt, übersteht also einen kurzen Ausfall und gibt nur auf, wenn das Lease wirklich abläuft.

Wenn ein Client nie zurückgibt

Dies ist der Fall, den die knotengebundene Lizenzierung schlecht handhabt und den Floating kostenlos handhabt. Stürzt der Prozess ab, wird die Maschine ausgeschaltet oder verschwindet das Netz, stoppt der Heartbeat. Es wird nie eine Rückgabe gesendet. Auf dem Server erreicht das Lease einfach expiresAt ohne Erneuerung, und die nächste Zählung behandelt es als verschwunden: Der Seat kehrt in den Pool zurück. Es gibt kein Support-Ticket für einen festsitzenden Seat und keinen Admin-Bereinigungsschritt.

Der einzige einstellbare Parameter ist die Rückgewinnungsverzögerung: Der Abstand zwischen dem Tod eines Clients und der Verfügbarkeit seines Seats ist durch das TTL begrenzt. Ein kurzes TTL (sagen wir 120 Sekunden) gewinnt schnell zurück, kostet aber mehr Heartbeat-Verkehr; ein längeres TTL ist leiser, lässt aber den Seat eines toten Clients länger geparkt. Wählen Sie das TTL danach, wie schnell Sie Seats recyceln müssen, nicht danach, wie viel Geschwätz Sie ertragen — Heartbeats sind winzig. Für die meisten Desktop-Anwendungen ist ein TTL von einer bis drei Minuten der optimale Wert.

Beachten Sie die Asymmetrie zur sauberen Rückgabe: ReleaseSeatAsync gibt den Seat sofort zurück, weshalb Sie es immer ins Herunterfahren einbinden sollten, auch wenn der Ablauf letztlich dasselbe täte. Die sofortige Rückgabe hält den Pool für alle anderen eng; der Ablauf ist das Sicherheitsnetz, nicht der Hauptweg.

Den Pool serverseitig lesen

Da der Dienst die Zählung besitzt, können Sie sie offenlegen. Das GetSeatPoolAsync des SDK (oder ein einfaches GET) meldet die Live-Nutzung, nützlich sowohl für einen In-App-Hinweis „23 von 25 Seats belegt” als auch für ein Admin-Dashboard, das die aktuellen Inhaber auflistet.

SeatPool pool = await client.GetSeatPoolAsync("acme-studio");
logger.LogInformation("Floating seats: {InUse}/{Max}", pool.InUse, pool.Max);

foreach (SeatHolder h in pool.Holders)
    logger.LogInformation("  {User}@{Host} since {Since:t}, expires {Exp:t}",
        h.User, h.Host, h.AcquiredAt, h.ExpiresAt);
GET /v1/seats/pool?product=acme-studio HTTP/1.1
Host: licensing.example.com

Die Live-Zählung in der Anwendung sichtbar zu machen, ist die mit Abstand größte Verbesserung der Benutzererfahrung, denn sie verwandelt „die App startet nicht und ich weiß nicht, warum” in „gerade jetzt sind alle Seats belegt, und ich kann sehen, wann einer frei wird”.

„Alle Seats belegt” elegant behandeln

Scheitern Sie geschlossen, aber scheitern Sie freundlich. Eine abgelehnte Anforderung ist ein normaler, erwarteter Zustand, kein Fehler — werfen Sie dem Nutzer also keinen Stacktrace entgegen. Fangen Sie SeatsExhaustedException, sagen Sie es klar und bieten Sie an, es weiter zu versuchen. Da Seats frei werden, sobald jemand beendet (oder sein Lease abläuft), erwischt eine kurze, begrenzte Wiederholungsschleife sehr oft innerhalb von Sekunden einen Seat.

async Task<SeatLease?> AcquireWithWaitAsync(KeyrightClient client, string key,
                                            TimeSpan budget, CancellationToken ct)
{
    var deadline = DateTimeOffset.UtcNow + budget;
    while (true)
    {
        try
        {
            return await client.AcquireSeatAsync(key, ct);
        }
        catch (SeatsExhaustedException) when (DateTimeOffset.UtcNow < deadline)
        {
            Ui.Status("All seats are in use — waiting for one to free up…");
            await Task.Delay(TimeSpan.FromSeconds(5), ct);   // bounded, with jitter in real code
        }
        catch (SeatsExhaustedException)
        {
            return null;   // gave up within budget; caller shows the final message
        }
    }
}

Der Produktunterschied zwischen einer Floating-Lizenz, die Nutzer tolerieren, und einer, die sie verärgert, liegt fast vollständig in diesem Handler. Ein schroffes „Lizenzfehler” lässt Nebenläufigkeit wie einen Defekt wirken; ein klares „alle 25 Seats sind belegt, erneuter Versuch…” lässt sie wie genau das wirken, was der Kunde gekauft hat.

Alles zusammengeführt

Floating-Lizenzierung ist Zählung, haltbar gemacht gegen Clients, die ohne Vorwarnung verschwinden. Das Lease ist die Zähleinheit; der Heartbeat hält sie ehrlich; das TTL ist das, was einen Absturz in ein selbstheilendes Ereignis statt in einen verlorenen Seat verwandelt.

  • Initialisieren Sie den Keyright-Client wie gewohnt, dann AcquireSeatAsync statt der Bindung an eine Maschine.
  • Behandeln Sie eine abgelehnte Anforderung (SeatsExhaustedException / 409) als normalen Zustand eines vollen Pools und gehen Sie freundlich damit um, mit einer begrenzten Wiederholung.
  • Halten Sie das Lease mit RenewSeatAsync auf einem Timer am Leben, der auf etwa ein Drittel des TTL eingestellt ist; tolerieren Sie einen einzelnen verpassten Schlag.
  • Rufen Sie beim Herunterfahren immer ReleaseSeatAsync für eine sofortige Rückgabe auf; verlassen Sie sich auf den TTL-Ablauf als Sicherheitsnetz bei Abstürzen.
  • Lassen Sie den Server die Zählung besitzen: Durchsetzung, Maximum und Ablauf sind alle auf dem Ausstellungsdienst maßgeblich.
  • Wählen Sie das TTL danach, wie schnell Sie tote Seats zurückgewinnen müssen; kürzer bedeutet schnelleres Recycling auf Kosten von etwas mehr Heartbeat-Verkehr.
  • Verwenden Sie die knotengebundene Aktivierung für dedizierte oder offline-tolerante Maschinen; verwenden Sie Floating für geteilte, mobile und sporadische Nutzung.

Kaufen Sie 25 Seats, rollen Sie auf 400 Maschinen aus, und lassen Sie den Pool regeln, wer gerade jetzt tatsächlich arbeitet. Das ist das ganze Versprechen der Floating-Lizenzierung — und mit Lease-basiertem Checkout, serverseitiger Zählung und TTL-Rückgewinnung hält es sogar, wenn ein Client mitten in der Sitzung geht und sich nie verabschiedet.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.