mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
chore: Review expiring locks.
This commit is contained in:
parent
9fd8440525
commit
6f4c4a15b4
@ -2,12 +2,13 @@ import type { EventEmitter } from 'events';
|
|||||||
import type { Lock } from './Lock';
|
import type { Lock } from './Lock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExpiringLock used by a {@link ExpiringResourceLocker} for non-atomic operations.
|
* Interface for a lock that expires after a certain period of inactivity.
|
||||||
* Emits an "expired" event when internal timer runs out and should call release function when this happen.
|
* Activity can be signaled by calling `renew`, which resets the expiration timeout.
|
||||||
|
* When the lock has expired, an `expired` event is emitted and the lock is released.
|
||||||
*/
|
*/
|
||||||
export interface ExpiringLock extends Lock, EventEmitter {
|
export interface ExpiringLock extends Lock, EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Reset the unlock timer.
|
* Reset the lock expiration timeout.
|
||||||
*/
|
*/
|
||||||
renew: () => void;
|
renew: () => void;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import type { ExpiringLock } from './ExpiringLock';
|
|||||||
import type { ResourceLocker } from './ResourceLocker';
|
import type { ResourceLocker } from './ResourceLocker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the locking of resources which is needed for non-atomic {@link ResourceStore}s.
|
* Interface for a factory of expiring locks.
|
||||||
* Specific {@link ResourceLocker} to work with {@link ExpiringLock}s.
|
|
||||||
*/
|
*/
|
||||||
export interface ExpiringResourceLocker<T extends ExpiringLock = ExpiringLock> extends ResourceLocker<T> {}
|
export interface ExpiringResourceLocker<T extends ExpiringLock = ExpiringLock> extends ResourceLocker<T> {}
|
||||||
|
@ -120,12 +120,9 @@ export class LockingResourceStore implements AtomicResourceStore {
|
|||||||
*/
|
*/
|
||||||
protected createExpiringReadable(source: Readable, lock: ExpiringLock): Readable {
|
protected createExpiringReadable(source: Readable, lock: ExpiringLock): Readable {
|
||||||
// Destroy the source when a timeout occurs.
|
// Destroy the source when a timeout occurs.
|
||||||
const destroySource = (): void => {
|
lock.on('expired', (): void => {
|
||||||
source.destroy(new Error(`Stream reading timout exceeded`));
|
source.destroy(new Error(`Stream reading timout exceeded`));
|
||||||
};
|
});
|
||||||
|
|
||||||
// Handle the destruction of the source when the lock expires.
|
|
||||||
lock.on('expired', destroySource);
|
|
||||||
|
|
||||||
// Spy on the source to renew the lock upon reading.
|
// Spy on the source to renew the lock upon reading.
|
||||||
return Object.create(source, {
|
return Object.create(source, {
|
||||||
|
@ -14,21 +14,21 @@ export class WrappedExpiringLock extends EventEmitter implements ExpiringLock {
|
|||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
protected readonly innerLock: Lock;
|
protected readonly innerLock: Lock;
|
||||||
protected readonly readTimeout: number;
|
protected readonly expiration: number;
|
||||||
protected readonly identifier: ResourceIdentifier;
|
protected readonly identifier: ResourceIdentifier;
|
||||||
protected timeout: NodeJS.Timeout;
|
protected timeoutHandle?: NodeJS.Timeout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param innerLock - Instance of ResourceLocker to use for acquiring a lock.
|
* @param innerLock - Instance of ResourceLocker to use for acquiring a lock.
|
||||||
* @param readTimeout - Time in ms after which reading a representation times out, causing the lock to be released.
|
* @param expiration - Time in ms after which the lock expires.
|
||||||
* @param identifier - Identifier of the resource that needs to be locked.
|
* @param identifier - Identifier of the resource that needs to be locked.
|
||||||
*/
|
*/
|
||||||
public constructor(innerLock: Lock, readTimeout: number, identifier: ResourceIdentifier) {
|
public constructor(innerLock: Lock, expiration: number, identifier: ResourceIdentifier) {
|
||||||
super();
|
super();
|
||||||
this.innerLock = innerLock;
|
this.innerLock = innerLock;
|
||||||
this.readTimeout = readTimeout;
|
this.expiration = expiration;
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
this.timeout = setTimeout((): any => this.emitExpired(), readTimeout);
|
this.scheduleTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +36,7 @@ export class WrappedExpiringLock extends EventEmitter implements ExpiringLock {
|
|||||||
* @returns A promise resolving when the release is finished.
|
* @returns A promise resolving when the release is finished.
|
||||||
*/
|
*/
|
||||||
public async release(): Promise<void> {
|
public async release(): Promise<void> {
|
||||||
clearTimeout(this.timeout);
|
this.clearTimeout();
|
||||||
return this.innerLock.release();
|
return this.innerLock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,17 +44,26 @@ export class WrappedExpiringLock extends EventEmitter implements ExpiringLock {
|
|||||||
* Reset the unlock timer.
|
* Reset the unlock timer.
|
||||||
*/
|
*/
|
||||||
public renew(): void {
|
public renew(): void {
|
||||||
this.logger.verbose(`Renewed expiring timer of the lock for ${this.identifier.path}`);
|
this.clearTimeout();
|
||||||
clearTimeout(this.timeout);
|
this.scheduleTimeout();
|
||||||
this.timeout = setTimeout((): any => this.emitExpired(), this.readTimeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async expire(): Promise<void> {
|
||||||
* This function will be called when the timer expires.
|
this.logger.verbose(`Lock for ${this.identifier.path} expired after ${this.expiration}ms`);
|
||||||
*/
|
|
||||||
protected async emitExpired(): Promise<void> {
|
|
||||||
this.logger.verbose(`Lock expired after exceeding timeout of ${this.readTimeout}ms for ${this.identifier.path}`);
|
|
||||||
this.emit('expired');
|
this.emit('expired');
|
||||||
return this.innerLock.release();
|
try {
|
||||||
|
await this.innerLock.release();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.emit('error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTimeout(): void {
|
||||||
|
clearTimeout(this.timeoutHandle!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleTimeout(): void {
|
||||||
|
this.logger.verbose(`Renewed expiring lock for ${this.identifier.path}`);
|
||||||
|
this.timeoutHandle = setTimeout((): any => this.expire(), this.expiration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,15 @@ export class WrappedExpiringResourceLocker implements ExpiringResourceLocker {
|
|||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
protected readonly locker: ResourceLocker;
|
protected readonly locker: ResourceLocker;
|
||||||
protected readonly readTimeout: number;
|
protected readonly expiration: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param locker - Instance of ResourceLocker to use for acquiring a lock.
|
* @param locker - Instance of ResourceLocker to use for acquiring a lock.
|
||||||
* @param readTimeout - Time in ms after which reading a representation times out, causing the lock to be released.
|
* @param expiration - Time in ms after which the lock expires.
|
||||||
*/
|
*/
|
||||||
public constructor(locker: ResourceLocker, readTimeout: number) {
|
public constructor(locker: ResourceLocker, expiration: number) {
|
||||||
this.locker = locker;
|
this.locker = locker;
|
||||||
this.readTimeout = readTimeout;
|
this.expiration = expiration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +32,6 @@ export class WrappedExpiringResourceLocker implements ExpiringResourceLocker {
|
|||||||
*/
|
*/
|
||||||
public async acquire(identifier: ResourceIdentifier): Promise<ExpiringLock> {
|
public async acquire(identifier: ResourceIdentifier): Promise<ExpiringLock> {
|
||||||
const innerLock = await this.locker.acquire(identifier);
|
const innerLock = await this.locker.acquire(identifier);
|
||||||
return new WrappedExpiringLock(innerLock, this.readTimeout, identifier);
|
return new WrappedExpiringLock(innerLock, this.expiration, identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,9 +194,9 @@ describe('A LockingResourceStore', (): void => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
// Spy on a real ResourceLocker instance
|
// Spy on a real ResourceLocker instance
|
||||||
const strLocker = new WrappedExpiringResourceLocker(locker, 1000);
|
const expiringLocker = new WrappedExpiringResourceLocker(locker, 1000);
|
||||||
store = new LockingResourceStore(source, strLocker);
|
store = new LockingResourceStore(source, expiringLocker);
|
||||||
const acquireSpy = jest.spyOn(strLocker, 'acquire');
|
const acquireSpy = jest.spyOn(expiringLocker, 'acquire');
|
||||||
|
|
||||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||||
const errorCallback = jest.fn();
|
const errorCallback = jest.fn();
|
||||||
@ -221,9 +221,9 @@ describe('A LockingResourceStore', (): void => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
// Spy on a real ResourceLocker instance
|
// Spy on a real ResourceLocker instance
|
||||||
const strLocker = new WrappedExpiringResourceLocker(locker, 1000);
|
const expiringLocker = new WrappedExpiringResourceLocker(locker, 1000);
|
||||||
store = new LockingResourceStore(source, strLocker);
|
store = new LockingResourceStore(source, expiringLocker);
|
||||||
const acquireSpy = jest.spyOn(strLocker, 'acquire');
|
const acquireSpy = jest.spyOn(expiringLocker, 'acquire');
|
||||||
|
|
||||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||||
const errorCallback = jest.fn();
|
const errorCallback = jest.fn();
|
||||||
|
30
test/unit/storage/WrappedExpiringResourceLocker.test.ts
Normal file
30
test/unit/storage/WrappedExpiringResourceLocker.test.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { WrappedExpiringResourceLocker } from '../../../src/storage/WrappedExpiringResourceLocker';
|
||||||
|
|
||||||
|
describe('A WrappedExpiringResourceLocker', (): void => {
|
||||||
|
it('emits an error event when releasing the lock errors.', async(): Promise<void> => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
// Create a locker that fails upon release
|
||||||
|
const faultyLocker = {
|
||||||
|
acquire(): any {
|
||||||
|
return {
|
||||||
|
async release(): Promise<never> {
|
||||||
|
throw new Error('Release error');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expiringLocker = new WrappedExpiringResourceLocker(faultyLocker, 1000);
|
||||||
|
const expiringLock = await expiringLocker.acquire({} as any);
|
||||||
|
const errorCallback = jest.fn();
|
||||||
|
expiringLock.on('error', errorCallback);
|
||||||
|
|
||||||
|
// Let the lock expire
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
// Verify the error has been emitted
|
||||||
|
expect(errorCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorCallback).toHaveBeenLastCalledWith(new Error('Release error'));
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user