Verrouillage
La capacité Locking est la manière dont un plugin indique « un seul appelant à la fois, s'il vous plaît ». Un LockingPlugin implémente trois opérations — try_lock, renew, release — sur un backend partagé, et tout autre plugin ou le runtime bext lui-même peut verrouiller une clé de chaîne et savoir que personne d'autre sur le cluster n'exécute le même travail.
C'est la forme E2 qui fait passer la capacité Scheduled de l'E1 de « presque correcte » à « réellement sûre pour les déploiements multi-instances ». Le câblage prévu est que si vous exécutez deux nœuds bext derrière un équilibreur de charge et que votre tâche planifiée déclare LockingHint::RequireGlobal, le planificateur acquiert un verrou sur schedule_id avant d'appeler run() - le nœud qui ne parvient pas à acquérir ignore simplement la coche. Sans Locking, les deux nœuds lanceraient la même tâche en même temps.
Le trait#
pub trait LockingPlugin: Send + Sync {
fn name(&self) -> &str;
fn try_lock(&self, key: &str, ttl_ms: u64) -> Result<Option<LockHandle>, String>;
fn renew(&self, handle: &LockHandle) -> Result<(), String>;
fn release(&self, handle: LockHandle) -> Result<(), String>;
fn cleanup(&self) -> Result<(), String> { Ok(()) }
}
Trois choses méritent d’être soulignées concernant la forme.
Une contestation n’est pas un échec. try_lock renvoie Ok(Some(handle)) lorsque vous avez acquis le verrou, Ok(None) lorsque quelqu'un d'autre le détient et Err(..) uniquement lorsque le backend lui-même est cassé (Redis inaccessible, connexion Postgres interrompue). Les appelants se basent presque toujours sur cette différence - réessayez dans un instant en cas de conflit, appel de page en cas d'échec du backend - donc l'encoder dans le Result externe est plus propre qu'une variante typée LockError::Held que chaque site d'appel doit correspondre. Il s'agit du choix de la forme porteuse et de la raison pour laquelle le trait n'utilise pas d'énumération d'erreur de frappe.
Tout le reste est Result<_, String>. renew et release ne peuvent pas distinguer de manière significative « votre verrou a expiré sous vous » de « le backend est cassé ». Dans les deux cas, la bonne réponse est « vous ne possédez plus la serrure ; arrêtez de la détenir ». En les pliant dans Err(String), le trait reste léger et convivial WASM-ABI.
Opaque LockHandle. Le handle porte un jeton aléatoire émis par le plugin au moment de l'acquisition. Les backends l'utilisent pour implémenter une version de style CAS afin qu'un appelant obsolète ne puisse pas écraser un détenteur de verrou plus récent - le SET NX PX de Redlock avec une valeur aléatoire et un script Lua le vérifiant avant DEL est l'exemple canonique. Les appelants doivent traiter le handle comme opaque et le renvoyer tel quel à renew et release.
Les trois backends de référence#
Trois plugins de référence sont livrés avec le trait. Choisissez celui qui correspond à votre forme de déploiement.
@bext/locking-memory#
Le backend de mémoire est non distribué — il est invisible pour tout autre processus ou nœud. Ne l'utilisez pas sur des déploiements multi-instances pour des planifications qui ne doivent se déclencher qu'une seule fois dans la flotte. Utilisez @bext/locking-redis ou @bext/locking-pg pour ceux-là.
Un Mutex<HashMap<String, (lock_id, expires_at)>> dans un seul processus. Non distribué — deux processus sur la même machine, sans parler de deux nœuds, ne verront pas les verrous de chacun. Il existe pour le développement local, les déploiements à instance unique et comme solution de secours toujours correcte lorsque LockingHint::PreferGlobal est déclaré mais qu'aucun backend distribué n'est configuré.
Les entrées expirées sont balayées paresseusement à chaque accès afin qu'un verrou mort de longue durée ne coince jamais une clé.
@bext/locking-redis#
Serrures Redis de style Redlock. L'algorithme est simple :
- Acquérir —
SET key value NX PX ttl_ms.NXgarantit que la clé est définie uniquement si elle n'existe pas déjà ;PXapplique un TTL de précision à la milliseconde afin que le verrou expire automatiquement si le support tombe en panne.valueest un UUIDv4 aléatoire créé par appel. - Renew — un script Lua qui vérifie que la valeur stockée correspond au jeton de l'appelant, puis
PEXPIREest la clé. Côté serveur atomique. - Release — un script Lua qui
DELest la clé uniquement si la valeur stockée correspond au jeton de l'appelant. Aussi atomique.
Il s'agit de la variante Redlock à nœud unique. Le Redlock complet contre une majorité de N instances Redis indépendantes est hors de portée du plugin de référence - les projets qui ont besoin de l'histoire multi-primaire peuvent piloter une caisse de redlock appropriée à partir de leur propre wrapper tout en satisfaisant le trait.
@bext/locking-pg#
Verrous consultatifs au niveau de la session Postgres via pg_try_advisory_lock(hashtext(key)::bigint). La distribution hashtext mappe les clés de chaîne arbitraires dans l'espace consultatif d'espace entier de 64 bits requis par les verrous consultatifs ; les collisions sont astronomiquement improbables pour des jeux de clés raisonnables.
Contrairement aux verrous Redis, les verrous consultatifs Postgres n'ont pas de TTL — ils restent en vigueur jusqu'à ce qu'ils soient explicitement libérés ou jusqu'à la fermeture de la connexion. Cela impose une contrainte inhabituelle au plugin : chaque verrou en direct doit maintenir sa propre connexion Postgres ouverte, saisie en interne par le lock_id créé. release recherche la connexion, appelle pg_advisory_unlock et l'abandonne ; l'abandon de la connexion est une solution de secours, car Postgres libère chaque verrou consultatif à la fin de la session.
renew sur le backend Postgres est un ping d'activité sur la connexion maintenue - il n'y a rien à réellement étendre, mais vérifier que la connexion fonctionne toujours avant de dire à l'appelant "vous possédez toujours ceci" est utile.
Redlock contre pg-advisory#
Les deux backends distribués ont des formes différentes et échouent différemment sous la partition. Connaître les compromis vous aide à choisir.
Redlock est le bon choix lorsque vous avez déjà Redis sur le chemin (stockage de session, cache, limiteur de débit) et que vous souhaitez que les verrous soient aussi libres que peuvent l'être les verrous distribués. Les opérations sont en temps constant, s'exécutent en un seul aller-retour et gèrent des milliers d'acquisitions par seconde sans transpirer. Le TTL est un véritable minuteur : si un support tombe en panne, le verrou se libère automatiquement après ttl_ms, ce qui signifie que le décalage d'horloge entre le support et le Redis est important : une horloge qui tourne rapidement sur le support libérera le verrou plus tôt de son propre point de vue tandis que Redis le considère toujours comme maintenu. Gardez les TTL généreux et les horloges synchronisées.
Les verrous consultatifs Postgres sont le bon choix lorsque vous disposez déjà de Postgres et que vous ne voulez pas d'autre pièce mobile. Ils sont fortement cohérents par rapport à tout le reste de la base de données : si votre travail lit les lignes protégées par le verrou, vous obtenez gratuitement une seule limite de sérialisabilité. L'inconvénient est le modèle de connexion par verrou : mille verrous simultanés signifient mille connexions Postgres ouvertes, ce que la plupart des déploiements ne peuvent pas se permettre. Utilisez des verrous consultatifs lorsque le nombre de verrous détenus simultanément est faible (des dizaines, pas des milliers) et que vous privilégiez la cohérence plutôt que le débit.
Le backend memory n’est ni l’un ni l’autre des éléments ci-dessus : il s’agit d’une solution de secours à nœud unique. N’y expédiez pas de trafic de production multi-nœuds.
Comment Scheduled l’utilise#
La fonctionnalité E1 Scheduled comportait déjà un champ LockingHint sur chaque planification avec trois valeurs : NodeLocal, RequireGlobal, PreferGlobal. Le trait E2 Locking et ses backends fournissent la moitié manquante ; une fois que le planificateur est configuré pour les consulter, l'indice se résout comme ceci :
NodeLocal— le planificateur ignore complètement le verrouillage. Chaque nœud peut déclencher sa propre copie. À utiliser pour la maintenance par nœud comme le cache local GC.RequireGlobal— à chaque tick, le planificateur appelleLockingPlugin::try_locksaisi surschedule_idavant d'invoquerrun(). SurOk(None), un autre nœud l'exécute ; ignorez cette coche. SurErr(..), connectez-vous et ignorez.PreferGlobal— essayez le verrou comme ci-dessus, mais si aucunLockingPluginn'est configuré, revenez au comportement deNodeLocalplutôt que d'échouer l'installation. Cela permet à un plugin de fonctionner correctement dans les déploiements à nœud unique et multi-nœuds sans échec brutal pour les opérateurs qui n'ont pas configuré de backend de verrouillage.
La durée de vie utilisée par le planificateur correspond à la durée d'exécution prévue du travail plus une marge de sécurité ; run() prenant plus de temps que le TTL est un bug dans la déclaration de planification, pas dans le plugin de verrouillage. Le plugin déclare ce qu'il attend via le jitter_ms du planning et la configuration de l'opérateur.
Configuration#
# bext.config.toml
[locking]
backend = "redis" # one of: "memory", "redis", "pg"
[locking.redis]
url = "redis://127.0.0.1:6379"
key_prefix = "bext:lock:"
[locking.pg]
url = "postgres://postgres@localhost/bext"
# scheduled + locking together
[[scheduled]]
plugin = "@bext/cron"
hint = "require_global" # or "prefer_global" / "node_local"
L'échange entre redis et pg est un changement de configuration sur une seule ligne ; votre code qui appelle lock.try(key, ttl) via l'API de fonction hôte ne change pas.
Fonctions d'accueil (futur)#
Le trait est disponible pour les plugins à implémenter. Un futur changement de noyau bext le câblera à la table des fonctions hôtes afin que d'autres plugins puissent prendre des verrous via une API lock.try(key, ttl) / lock.release(handle) sans dépendre d'un backend spécifique. En attendant, les plugins qui nécessitent un verrouillage peuvent intégrer directement un objet de trait LockingPlugin, comme le fait le planificateur.
Où aller ensuite#
- Le Capacité planifiée est le consommateur canonique – son champ
LockingHintindique la façon dont vous optez pour le chemin de verrouillage à partir d’un plugin cron. - Le aperçu des capacités couvre l'échelle de promotion, les règles anti-fragmentation et la manière dont les nouvelles fonctionnalités passent à la version stable de première partie.