Skip to content
← Tous les articles
· Delta1 Labs LicensingKeyright

Sièges flottants avec Keyright : licencier à l'usage simultané, pas à la machine

L'activation par nœud lie une licence à une machine. La licence flottante la lie à l'usage simultané : vous achetez 25 sièges, et n'importe lesquelles de vos 400 installations peuvent s'exécuter à hauteur de 25 en même temps. Le mécanisme est un bail (lease) : un client réserve un siège, envoie des battements pour le conserver, puis le rend en quittant. Un client qui plante ne le rend jamais, donc son bail expire simplement et le siège revient au pool. Voici comment bâtir la licence à sièges concurrents sur une instance Keyright auto-hébergée, avec réservation par bail, comptage côté serveur, récupération par TTL et une gestion élégante du cas « tous les sièges occupés ».

La plupart des modèles de licence répondent à la question « cette copie a-t-elle le droit de s”exécuter sur cette machine ? ». La licence flottante en pose une autre : « trop de copies s”exécutent-elles en ce moment ? ». Ce basculement — du comptage des installations au comptage de l”usage simultané — est ce qui permet à un studio de 400 postes d”acheter 25 sièges, car à aucun instant plus de 25 artistes ne sont réellement dans l”application. Les sièges flottent : celui qui lance ensuite prend un siège libre, et celui qui quitte en rend un.

Le mécanisme sous-jacent est un bail (lease). Un client réserve un siège avant d”activer les fonctions sous licence, maintient le bail en vie par un battement pendant qu”il s”exécute, puis rend le siège en quittant. Le service d”émission compte les baux actifs et refuse d”en distribuer plus que ce que vous avez acheté. La partie élégante est ce qui se passe quand un client ne rend pas son siège : un plantage, un kill -9, un câble réseau arraché. Il cesse simplement d”envoyer des battements, son bail expire, et le siège revient au pool sans intervention. Cet article montre comment bâtir cela sur une instance Keyright auto-hébergée.

Par nœud ou flottant

Il vaut la peine d”être précis sur ce qui change. L”activation par défaut de Keyright est par nœud : ActivateAsync lie une clé de licence à l”empreinte d”une machine et consomme un siège pour toute la durée de cette activation. Installez sur une seconde machine et vous dépensez un second siège, que la première soit même allumée ou non. C”est exactement ce qu”il faut pour des postes dédiés, toujours à la même personne.

La flottante inverse le lien. La licence n”est pas liée à une machine du tout ; elle est liée à un décompte de baux concurrents. Vous pouvez déployer l”application sur toutes les machines du bâtiment. Ce que vous ne pouvez pas faire, c”est en avoir plus exécutant la fonctionnalité sous licence en même temps que vous n”avez de sièges.

Activation par nœudSiège flottant
Lié àL”empreinte d”une machineUn bail vivant, pour la durée de l”usage
Consommé lors deL”activationLa réservation d”un siège (lancement de l”app / entrée dans une fonction)
Libéré lors deLa désactivationLe rendu en quittant, ou l”expiration du TTL du bail
Sur-installationBloquée — chaque installation est un siègeAutorisée — installez largement, exécutez jusqu”à N à la fois
Besoin du serviceÀ l”activation / revalidationEn continu, pour conserver et compter les baux
Idéal pourPostes dédiés, tolérants au hors-ligneLaboratoires partagés, utilisateurs itinérants, pools CI

Le cycle de vie du bail

Un siège flottant parcourt une petite machine à états : réservé → renouvelé (de façon répétée) → rendu, avec une voie d”expiration qui rejoint « rendu » chaque fois qu”un renouvellement n”arrive pas à temps.

Réserver siège réservé Actif + battement renouveler avant le TTL Rendu (propre) siège rendu Expiré (récupéré) TTL écoulé, sans renouvellement tous les ~TTL/3 sortie plantage / perte réseau La sortie propre et la mort silencieuse rendent toutes deux le siège au pool.

Les deux états terminaux sont l”essentiel : une sortie propre et une mort silencieuse rendent toutes deux le siège. L”une est immédiate, l”autre attend la fin du TTL, mais aucune ne le perd.

Réserver un siège

Initialisez le client exactement comme pour la licence par nœud — la même clé publique de vérification seule et l”URL de votre instance auto-hébergée — puis réservez un siège au lieu (ou en plus) d”activer. AcquireSeatAsync renvoie un SeatLease : un descripteur portant l”identifiant du bail, son expiration et l”intervalle de renouvellement recommandé que le serveur a calculé à partir du TTL.

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);

En coulisses, c”est un unique POST. Le client envoie un identifiant d”instance client stable (un par installation, persisté localement) pour que le service reconnaisse une nouvelle réservation du même client et pour que vous puissiez afficher « qui détient un siège » dans une vue d”administration. Une Idempotency-Key rend la réservation sûre à réessayer en cas de réponse perdue : une réservation expirée qui a en fait réussi rejoue le même bail au lieu de brûler un second siège :

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
}

Quand le pool est plein, le service n”invente pas de siège ; il répond 409 Conflict avec un corps problem+json (type: .../seats-exhausted, seatsInUse, maxSeats), que le SDK expose sous forme de SeatsExhaustedException. Le décompte fait autorité sur le serveur : un client modifié ne peut pas se convaincre lui-même d”un 26e siège.

Battre pour conserver le siège

Un bail est délibérément de courte durée. Le conserver signifie le renouveler avant expiresAt, ce qui à la fois prouve que le client est vivant et repousse l”expiration. Renouvelez sur un minuteur à environ renewAfterSeconds — confortablement à l”intérieur du TTL — pour qu”un seul battement manqué ne lâche pas le siège.

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
    }
}

Deux choix de conception gagnent leur place ici. Premièrement, un intervalle de renouvellement nettement inférieur au TTL (un tiers est une valeur par défaut raisonnable) vous offre des réessais gratuits : un renouvellement manqué est inoffensif car le bail reste valide jusqu”au tic suivant. Deuxièmement, un échec transitoire ne panique pas et ne désactive pas les fonctions : le client détient déjà le siège jusqu”à expiresAt, il traverse donc une brève coupure et n”abandonne que si le bail expire vraiment.

Quand un client ne rend jamais son siège

C”est le cas que la licence par nœud gère mal et que la flottante gère gratuitement. Si le processus plante, que la machine s”éteint ou que le réseau disparaît, le battement s”arrête. Aucun rendu n”est jamais envoyé. Sur le serveur, le bail atteint simplement expiresAt sans renouvellement, et le décompte suivant le considère comme disparu : le siège revient au pool. Pas de ticket de support pour siège coincé ni d”étape de nettoyage côté administration.

Le seul paramètre réglable est le délai de récupération : l”écart entre la mort d”un client et la disponibilité de son siège est borné par le TTL. Un TTL court (disons 120 secondes) récupère vite mais coûte plus de trafic de battements ; un TTL plus long est plus discret mais laisse le siège d”un client mort immobilisé plus longtemps. Choisissez le TTL selon la vitesse à laquelle vous devez recycler les sièges, pas selon le bavardage que vous tolérez : les battements sont minuscules. Pour la plupart des applications de bureau, un TTL d”une à trois minutes est le point idéal.

Notez l”asymétrie avec un rendu propre : ReleaseSeatAsync rend le siège immédiatement, c”est pourquoi vous devriez toujours le câbler à l”arrêt, même si l”expiration finirait par faire la même chose. Le rendu immédiat garde le pool serré pour tous les autres ; l”expiration est le filet de sécurité, pas la voie principale.

Lire le pool côté serveur

Comme le service est propriétaire du décompte, vous pouvez l”exposer. Le GetSeatPoolAsync du SDK (ou un simple GET) rapporte l”usage en direct, utile à la fois pour une indication dans l”app « 23 sièges sur 25 occupés » et pour un tableau de bord d”administration listant les détenteurs actuels.

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

Faire apparaître le décompte en direct dans l”application est la plus grande amélioration possible de l”expérience utilisateur, car elle transforme « l”app ne démarre pas et je ne sais pas pourquoi » en « tous les sièges sont occupés en ce moment, et je peux voir quand l”un se libère ».

Gérer avec élégance « tous les sièges occupés »

Échouez de façon fermée, mais échouez avec bienveillance. Une réservation refusée est un état normal et attendu, pas une erreur : ne jetez donc pas une trace de pile à l”utilisateur. Attrapez SeatsExhaustedException, dites-le clairement et proposez de continuer à essayer. Comme les sièges se libèrent dès que quelqu”un quitte (ou que son bail expire), une boucle de réessai courte et bornée décroche très souvent un siège en quelques secondes.

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
        }
    }
}

La différence produit entre une licence flottante que les utilisateurs tolèrent et une qui les agace tient presque entièrement à ce gestionnaire. Un sec « erreur de licence » fait passer la concurrence pour un défaut ; un clair « les 25 sièges sont occupés, nouvel essai… » la fait passer pour exactement ce que le client a acheté.

Pour conclure

La licence flottante, c”est du comptage, rendu durable face à des clients qui disparaissent sans prévenir. Le bail est l”unité de comptage ; le battement le maintient honnête ; le TTL est ce qui transforme un plantage en un événement qui se répare tout seul plutôt qu”en un siège perdu.

  • Initialisez le client Keyright comme d”habitude, puis AcquireSeatAsync au lieu de lier à une machine.
  • Traitez une réservation refusée (SeatsExhaustedException / 409) comme un état normal de pool plein et gérez-le avec bienveillance, avec un réessai borné.
  • Maintenez le bail en vie avec RenewSeatAsync sur un minuteur réglé à environ un tiers du TTL ; tolérez un seul battement manqué.
  • Faites toujours ReleaseSeatAsync à l”arrêt pour un rendu immédiat ; comptez sur l”expiration par TTL comme filet de sécurité en cas de plantage.
  • Laissez le serveur être propriétaire du décompte : l”application, le maximum et l”expiration font tous autorité sur le service d”émission.
  • Choisissez le TTL selon la vitesse à laquelle vous devez récupérer les sièges morts ; plus court signifie un recyclage plus rapide au prix d”un peu plus de trafic de battements.
  • Utilisez l”activation par nœud pour les machines dédiées ou tolérantes au hors-ligne ; utilisez la flottante pour l”usage partagé, itinérant et intermittent.

Achetez 25 sièges, déployez sur 400 machines, et laissez le pool déterminer qui travaille réellement en ce moment. C”est toute la promesse de la licence flottante — et avec la réservation par bail, le comptage côté serveur et la récupération par TTL, elle tient même quand un client s”en va en pleine session sans jamais dire au revoir.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.