@nullplatform/k8s-lease-lock
Version:
The **K8SLock** module is a Node.js library designed to provide distributed locking functionality using Kubernetes leases. It allows you to manage locks in a Kubernetes cluster, ensuring that only one client or process can hold a lock at any given time. T
122 lines (113 loc) • 4.47 kB
JavaScript
const {KubeConfig, V1MicroTime} = require("@kubernetes/client-node");
const k8s = require("@kubernetes/client-node");
class K8SLock {
constructor({kubeConfig, leaseName, namespace, createLeaseIfNotExist = true, labels = {}, lockLeaserId, leaseDurationInSeconds=30, refreshLockInterval, lockTryInterval}={}) {
this.kubeConfig = kubeConfig;
if(!this.kubeConfig) {
this.kubeConfig = new KubeConfig();
this.kubeConfig.loadFromDefault();
}
this.leaseName = leaseName;
this.namespace = namespace;
this.createLeaseIfNotExist = createLeaseIfNotExist;
this.labels = labels;
this.lockLeaserId = lockLeaserId;
this.leaseDurationInSeconds = leaseDurationInSeconds;
this.refreshLock = refreshLockInterval || this.leaseDurationInSeconds*1000/2; //half time lease guarantee lease will be keep
this.lockTryInterval = lockTryInterval ||this.leaseDurationInSeconds*1000;
this.isLocking = false;
}
async _lock() {
const k8sApi = this.kubeConfig.makeApiClient(k8s.CoordinationV1Api);
let lease;
try {
lease = await k8sApi.readNamespacedLease(this.leaseName, this.namespace);
}catch (e) {
if(e?.statusCode === 404 && this.createLeaseIfNotExist) {
lease = await k8sApi.createNamespacedLease(this.namespace,{
metadata: {
name: this.leaseName,
labels: this.labels
},
spec: {
}
});
} else {
throw e;
}
}
if(this.isLocking && lease.body.spec.holderIdentity === this.lockLeaserId) {
this.isLocking = false;
}
if(new Date(lease.body.spec.renewTime || 0 ) < new Date() || lease.body.spec.holderIdentity === this.lockLeaserId) {
const currentDate = new V1MicroTime();
try {
const body = {
metadata: {
labels: this.labels,
resourceVersion: lease.body.metadata.resourceVersion
},
spec: {
leaseDurationSeconds: this.leaseDurationInSeconds,
holderIdentity: this.lockLeaserId,
renewTime: new V1MicroTime(currentDate.getTime() + this.leaseDurationInSeconds * 1000)
}
};
if(lease.body.spec.holderIdentity !== this.lockLeaserId) {
body.spec.leaseTransitions = (lease.body.spec.leaseTransitions || 0) + 1;
body.spec.acquireTime= currentDate;
}
await k8sApi.patchNamespacedLease(this.leaseName, this.namespace, body,undefined,undefined,undefined,undefined,undefined,{
headers: {
"Content-Type": "application/strategic-merge-patch+json"
}
});
}catch (e) {
if(e?.statusCode === 409) {
this.isLocking = false;
return false;
}
throw e;
}
this.isLocking = true;
return true;
} else {
this.isLocking = false;
return false;
}
}
async _keepLocking() {
while(this.keepLocking) {
const resp = await this._lock();
if (!resp) {
this.keepLocking = false;
}
await new Promise((accept) => setTimeout(accept, this.refreshLock));
}
}
async startLocking() {
let self = this;
const lock = await this.getLock(true);
if(this.isLocking) {
this.keepLocking = true;
setTimeout(() => {
self._keepLocking()
}, this.refreshLock); //launch async
return {isLocking: this.isLocking};
}
}
async stopLocking() {
this.keepLocking = false;
}
async getLock(waitUntilLock) {
let locked = await this._lock();
if(waitUntilLock) {
while(!locked) {
await new Promise((accept) => setTimeout(accept, this.lockTryInterval));
locked = await this._lock();
}
}
return locked;
}
}
module.exports = {K8SLock};