@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
202 lines • 7.84 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { IntervalLock } from '../../../src/core/lock/interval-lock.js';
import { LockHolder } from '../../../src/core/lock/lock-holder.js';
import { NamespaceName } from '../../../src/types/namespace/namespace-name.js';
import { LockAcquisitionError } from '../../../src/core/lock/lock-acquisition-error.js';
import { StatusCodes } from 'http-status-codes';
import { Duration } from '../../../src/core/time/duration.js';
describe('IntervalLock', () => {
it('should ignore a renew conflict when latest lease is still held by the same lock holder', async () => {
const namespace = NamespaceName.of('lock-conflict-test');
const lockHolder = LockHolder.of('lock-user');
const leaseName = 'lock-conflict-test';
let readCallCounter = 0;
let renewCallCounter = 0;
let scheduleCallCounter = 0;
const initialLease = createLease(namespace, leaseName, lockHolder.toJson(), '1');
const latestLease = createLease(namespace, leaseName, lockHolder.toJson(), '2');
const renewalService = {
isScheduled: async () => false,
schedule: async () => {
scheduleCallCounter++;
return 99;
},
cancel: async () => true,
cancelAll: async () => new Map(),
calculateRenewalDelay: () => Duration.ofSeconds(1),
};
const leases = {
create: async () => {
throw new Error('not used');
},
delete: async () => {
throw new Error('not used');
},
read: async () => {
readCallCounter++;
return readCallCounter === 1 ? initialLease : latestLease;
},
renew: async () => {
renewCallCounter++;
throw createConflictError();
},
transfer: async () => {
throw new Error('not used');
},
};
const namespaces = {
create: async () => true,
delete: async () => true,
list: async () => [],
has: async () => true,
};
const k8 = {
namespaces: () => namespaces,
leases: () => leases,
};
const k8Factory = {
getK8: () => k8,
default: () => k8,
};
const lock = new IntervalLock(k8Factory, renewalService, lockHolder, namespace, leaseName, 20);
await lock.renew();
expect(readCallCounter).to.equal(2);
expect(renewCallCounter).to.equal(1);
expect(scheduleCallCounter).to.equal(1);
});
it('should fail renew when conflict resolution reads a lease owned by another holder', async () => {
const namespace = NamespaceName.of('lock-conflict-fail-test');
const lockHolder = LockHolder.of('lock-user');
const otherLockHolder = LockHolder.of('other-user');
const leaseName = 'lock-conflict-fail-test';
let readCallCounter = 0;
const initialLease = createLease(namespace, leaseName, lockHolder.toJson(), '1');
const conflictingLease = createLease(namespace, leaseName, otherLockHolder.toJson(), '2');
const renewalService = {
isScheduled: async () => false,
schedule: async () => 99,
cancel: async () => true,
cancelAll: async () => new Map(),
calculateRenewalDelay: () => Duration.ofSeconds(1),
};
const leases = {
create: async () => {
throw new Error('not used');
},
delete: async () => {
throw new Error('not used');
},
read: async () => {
readCallCounter++;
return readCallCounter === 1 ? initialLease : conflictingLease;
},
renew: async () => {
throw createConflictError();
},
transfer: async () => {
throw new Error('not used');
},
};
const namespaces = {
create: async () => true,
delete: async () => true,
list: async () => [],
has: async () => true,
};
const k8 = {
namespaces: () => namespaces,
leases: () => leases,
};
const k8Factory = {
getK8: () => k8,
default: () => k8,
};
const lock = new IntervalLock(k8Factory, renewalService, lockHolder, namespace, leaseName, 20);
await expect(lock.renew()).to.be.rejectedWith(LockAcquisitionError);
});
it('should stop renewal gracefully when namespace is being terminated (403 Forbidden)', async () => {
const namespace = NamespaceName.of('lock-namespace-terminating-test');
const lockHolder = LockHolder.of('lock-user');
const leaseName = 'lock-namespace-terminating-test';
let createCallCounter = 0;
let cancelCallCounter = 0;
let cancelledScheduleId;
const renewalService = {
isScheduled: async () => false,
schedule: async () => 99,
cancel: async (scheduleId) => {
cancelCallCounter++;
cancelledScheduleId = scheduleId;
return true;
},
cancelAll: async () => new Map(),
calculateRenewalDelay: () => Duration.ofSeconds(1),
};
const leases = {
create: async () => {
createCallCounter++;
if (createCallCounter === 1) {
return createLease(namespace, leaseName, lockHolder.toJson(), '1');
}
throw createForbiddenError();
},
delete: async () => {
throw new Error('not used');
},
read: async () => undefined,
renew: async () => {
throw new Error('not used');
},
transfer: async () => {
throw new Error('not used');
},
};
const namespaces = {
create: async () => true,
delete: async () => true,
list: async () => [],
has: async () => true,
};
const k8 = {
namespaces: () => namespaces,
leases: () => leases,
};
const k8Factory = {
getK8: () => k8,
default: () => k8,
};
const lock = new IntervalLock(k8Factory, renewalService, lockHolder, namespace, leaseName, 20);
// First renew: creates lease successfully and sets scheduleId = 99
await lock.renew();
expect(createCallCounter).to.equal(1);
// Second renew: namespace is being terminated → 403 → should cancel schedule and return without error
await lock.renew();
expect(createCallCounter).to.equal(2);
expect(cancelCallCounter).to.equal(1);
expect(cancelledScheduleId).to.equal(99);
});
});
function createLease(namespace, leaseName, holderIdentity, resourceVersion) {
return {
namespace,
leaseName,
holderIdentity,
durationSeconds: 20,
acquireTime: new Date(),
renewTime: new Date(),
resourceVersion,
};
}
function createConflictError() {
const error = new Error('Conflict while replacing lease');
error.meta = { statusCode: StatusCodes.CONFLICT };
return error;
}
function createForbiddenError() {
const error = new Error('Forbidden: namespace is being terminated');
error.meta = { statusCode: StatusCodes.FORBIDDEN };
return error;
}
//# sourceMappingURL=interval-lock.test.js.map