UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

339 lines 14.8 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import { MissingArgumentError, SoloError } from '../errors.js'; import { LeaseHolder } from './lease_holder.js'; import { LeaseAcquisitionError, LeaseRelinquishmentError } from './lease_errors.js'; import { sleep } from '../helpers.js'; import { Duration } from '../time/duration.js'; import { StatusCodes } from 'http-status-codes'; /** * Concrete implementation of a Kubernetes based time-based mutually exclusive lock via the Coordination API. * Applies a namespace/deployment wide lock to ensure that only one process, machine, and user can hold the lease at a time. * The lease is automatically renewed in the background to prevent expiration and ensure the holder maintains the lease. * If the process die, the lease is automatically released after the lease duration. * * @public */ export class IntervalLease { k8Factory; renewalService; /** The default duration in seconds for which the lease is to be held before being considered expired. */ static DEFAULT_LEASE_DURATION = 20; /** The holder of the lease. */ _leaseHolder; /** The namespace which contains the lease. */ _namespace; /** The name of the lease. */ _leaseName; /** The duration in seconds for which the lease is to be held. */ _durationSeconds; /** The identifier of the scheduled lease renewal. */ _scheduleId = null; /** * @param k8Factory - Injected kubernetes K8Factory need by the methods to create, renew, and delete leases. * @param renewalService - Injected lease renewal service need to support automatic (background) lease renewals. * @param leaseHolder - The holder of the lease. * @param namespace - The namespace in which the lease is to be acquired. * @param leaseName - The name of the lease to be acquired; if not provided, the namespace is used. * @param durationSeconds - The duration in seconds for which the lease is to be held; if not provided, the default value is used. */ constructor(k8Factory, renewalService, leaseHolder, namespace, leaseName = null, durationSeconds = null) { this.k8Factory = k8Factory; this.renewalService = renewalService; if (!k8Factory) throw new MissingArgumentError('k8Factory is required'); if (!renewalService) throw new MissingArgumentError('renewalService is required'); if (!leaseHolder) throw new MissingArgumentError('_leaseHolder is required'); if (!namespace) throw new MissingArgumentError('_namespace is required'); this._leaseHolder = leaseHolder; this._namespace = namespace; if (!leaseName) { this._leaseName = this._namespace.name; } // In most production cases, the environment variable should be preferred over the constructor argument. if (!durationSeconds) { this._durationSeconds = +process.env.SOLO_LEASE_DURATION || IntervalLease.DEFAULT_LEASE_DURATION; } else { this._durationSeconds = durationSeconds; } } /** * The name of the lease. */ get leaseName() { return this._leaseName; } /** * The holder of the lease. */ get leaseHolder() { return this._leaseHolder; } /** * The namespace in which the lease is to be acquired. By default, the namespace is used as the lease name. * The defaults assume there is only a single deployment in a given namespace. */ get namespace() { return this._namespace; } /** * The duration in seconds for which the lease is held before being considered expired. By default, the duration * is set to 20 seconds. It is recommended to renew the lease at 50% of the duration to prevent unexpected expiration. */ get durationSeconds() { return this._durationSeconds; } /** * The identifier of the scheduled lease renewal task. */ get scheduleId() { return this._scheduleId; } /** * Internal setter for the scheduleId property. External callers should not use this method. * * @param scheduleId - The identifier of the scheduled lease renewal task. */ set scheduleId(scheduleId) { this._scheduleId = scheduleId; } /** * Acquires the lease. If the lease is already acquired, it checks if the lease is expired or held by the same process. * If the lease is expired, it creates a new lease. If the lease is held by the same process, it renews the lease. * If the lease is held by another process, then an exception is thrown. * * @throws LeaseAcquisitionError - If the lease is already acquired by another process or an error occurs during acquisition. */ async acquire() { const lease = await this.retrieveLease(); if (!lease || this.heldBySameProcess(lease)) { return this.createOrRenewLease(lease); } else if (IntervalLease.checkExpiration(lease)) { return this.transferLease(lease); } const otherHolder = LeaseHolder.fromJson(lease.spec.holderIdentity); if (this.heldBySameMachineIdentity(lease) && !otherHolder.isProcessAlive()) { return this.transferLease(lease); } throw new LeaseAcquisitionError(`acquire: lease already acquired by '${otherHolder.username}' on the ` + `'${otherHolder.hostname}' machine (PID: '${otherHolder.processId}')`, null, { self: this.leaseHolder.toObject(), other: otherHolder.toObject() }); } /** * Attempts to acquire the lease, by calling the acquire method. If an exception is thrown, it is caught and false is returned. * If the lease is successfully acquired, true is returned; otherwise, false is returned. * * @returns true if the lease is successfully acquired; otherwise, false. */ async tryAcquire() { try { await this.acquire(); return true; } catch (e) { return false; } } /** * Renews the lease. If the lease is expired or held by the same process, it creates or renews the lease. * If the lease is held by another process, then an exception is thrown. * * @throws LeaseAcquisitionError - If the lease is already acquired by another process or an error occurs during renewal. */ async renew() { const lease = await this.retrieveLease(); if (!lease || this.heldBySameProcess(lease)) { return await this.createOrRenewLease(lease); } throw new LeaseAcquisitionError(`renew: lease already acquired by '${this._leaseHolder.username}' on the ` + `'${this._leaseHolder.hostname}' machine (PID: '${this._leaseHolder.processId}')`, null, { self: this._leaseHolder.toObject(), other: this._leaseHolder.toObject() }); } /** * Attempts to renew the lease, by calling the renew method. If an exception is thrown, it is caught and false is returned. * If the lease is successfully renewed, true is returned; otherwise, false is returned. * * @returns true if the lease is successfully renewed; otherwise, false. */ async tryRenew() { try { await this.renew(); return true; } catch (e) { return false; } } /** * Releases the lease. If the lease is expired or held by the same process, it deletes the lease. * If the lease is held by another process, then an exception is thrown. * * @throws LeaseRelinquishmentError - If the lease is already acquired by another process or an error occurs during relinquishment. */ async release() { const lease = await this.retrieveLease(); if (this.scheduleId) { await this.renewalService.cancel(this.scheduleId); // Needed to ensure any pending renewals are truly cancelled before proceeding to delete the Lease. // This is required because clearInterval() is not guaranteed to abort any pending interval. await sleep(this.renewalService.calculateRenewalDelay(this)); } this.scheduleId = null; if (!lease) { return; } const otherHolder = LeaseHolder.fromJson(lease.spec.holderIdentity); if (this.heldBySameProcess(lease) || IntervalLease.checkExpiration(lease)) { return await this.deleteLease(); } throw new LeaseRelinquishmentError(`release: lease already acquired by '${otherHolder.username}' on the ` + `'${otherHolder.hostname}' machine (PID: '${otherHolder.processId}')`, null, { self: this._leaseHolder.toObject(), other: otherHolder.toObject() }); } /** * Attempts to release the lease, by calling the release method. If an exception is thrown, it is caught and false is returned. * If the lease is successfully released, true is returned; otherwise, false is returned. * * @returns true if the lease is successfully released; otherwise, false. */ async tryRelease() { try { await this.release(); return true; } catch (e) { return false; } } /** * Checks if the lease is acquired. If the lease is acquired and not expired, it returns true; otherwise, false. * * @returns true if the lease is acquired and not expired; otherwise, false. */ async isAcquired() { const lease = await this.retrieveLease(); return !!lease && !IntervalLease.checkExpiration(lease) && this.heldBySameProcess(lease); } /** * Checks if the lease is expired. If the lease is expired, it returns true; otherwise, false. * This method does not verify if the lease is acquired by the current process. * * @returns true if the lease is expired; otherwise, false. */ async isExpired() { const lease = await this.retrieveLease(); return !!lease && IntervalLease.checkExpiration(lease); } /** * Retrieves the lease from the Kubernetes API server. * * @returns the Kubernetes lease object if it exists; otherwise, null. * @throws LeaseAcquisitionError - If an error occurs during retrieval. */ async retrieveLease() { try { return await this.k8Factory.default().leases().read(this.namespace, this.leaseName); } catch (e) { if (!(e instanceof SoloError)) { throw new LeaseAcquisitionError(`failed to read the lease named '${this.leaseName}' in the ` + `'${this.namespace}' namespace, caused by: ${e.message}`, e); } if (e.meta.statusCode !== StatusCodes.NOT_FOUND) { throw new LeaseAcquisitionError('failed to read existing leases, unexpected server response of ' + `'${e.meta.statusCode}' received`, e); } } return null; } /** * Creates or renews the lease. If the lease does not exist, it creates a new lease. If the lease exists, it renews the lease. * * @param lease - The lease to be created or renewed. */ async createOrRenewLease(lease) { try { if (!lease) { await this.k8Factory .default() .leases() .create(this.namespace, this.leaseName, this.leaseHolder.toJson(), this.durationSeconds); } else { await this.k8Factory.default().leases().renew(this.namespace, this.leaseName, lease); } if (!this.scheduleId) { this.scheduleId = await this.renewalService.schedule(this); } } catch (e) { throw new LeaseAcquisitionError(`failed to create or renew the lease named '${this.leaseName}' in the ` + `'${this.namespace}' namespace`, e); } } /** * Transfers an existing (expired) lease to the current process. * * @param lease - The lease to be transferred. */ async transferLease(lease) { try { await this.k8Factory.default().leases().transfer(lease, this.leaseHolder.toJson()); if (!this.scheduleId) { this.scheduleId = await this.renewalService.schedule(this); } } catch (e) { throw new LeaseAcquisitionError(`failed to transfer the lease named '${this.leaseName}' in the ` + `'${this.namespace}' namespace`, e); } } /** * Deletes the lease from the Kubernetes API server. */ async deleteLease() { try { await this.k8Factory.default().leases().delete(this.namespace, this.leaseName); } catch (e) { throw new LeaseRelinquishmentError(`failed to delete the lease named '${this.leaseName}' in the ` + `'${this.namespace}' namespace`, e); } } /** * Determines if the lease has expired by comparing the delta in seconds between the current time and the last renewal time. * * @param lease - The lease to be checked for expiration. * @returns true if the lease has expired; otherwise, false. */ static checkExpiration(lease) { const now = Duration.ofMillis(Date.now()); const durationSec = lease.spec.leaseDurationSeconds || IntervalLease.DEFAULT_LEASE_DURATION; const lastRenewalTime = lease.spec?.renewTime || lease.spec?.acquireTime; const lastRenewal = Duration.ofMillis(new Date(lastRenewalTime).valueOf()); const deltaSec = now.minus(lastRenewal).seconds; return deltaSec > durationSec; } /** * Determines if the lease is held by the same process. This comparison is based on the user, machine, and * process identifier of the leaseholder. * * @param lease - The lease to be checked for ownership. * @returns true if the lease is held by the same process; otherwise, false. */ heldBySameProcess(lease) { const holder = LeaseHolder.fromJson(lease.spec.holderIdentity); return this.leaseHolder.equals(holder); } /** * Determines if the lease is held by the same machine identity. This comparison is based on the user and machine only. * The process identifier is not considered in this comparison. * * @param lease - The lease to be checked for ownership. * @returns true if the lease is held by the same user and machine; otherwise, false. */ heldBySameMachineIdentity(lease) { const holder = LeaseHolder.fromJson(lease.spec.holderIdentity); return this.leaseHolder.isSameMachineIdentity(holder); } } //# sourceMappingURL=interval_lease.js.map