mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Release lock only when stream ends or is abandoned (#60)
* fix: Release lock only when stream has ended reading or an error occurs * refactor: Refactor code and tests * refactor: Move function mock to onBefore and remove unnecessary data drain * fix: Make functions protected, add extra listener and add extra tests * fix: Add extra TSDoc comment * fix: Adjust tests to expect both end and close event * refactor: Move test to other file * refactor: make lockedRun method-independent * fix: ensure lock release happens only once * fix: make locked resources time out * fix: destroy readable on error Co-authored-by: Ruben Verborgh <ruben@verborgh.org>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Lock } from '../../../src/storage/Lock';
|
||||
import { LockingResourceStore } from '../../../src/storage/LockingResourceStore';
|
||||
import { Patch } from '../../../src/ldp/http/Patch';
|
||||
import { Representation } from '../../../src/ldp/representation/Representation';
|
||||
import { ResourceLocker } from '../../../src/storage/ResourceLocker';
|
||||
import { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||
import streamifyArray from 'streamify-array';
|
||||
|
||||
describe('A LockingResourceStore', (): void => {
|
||||
let store: LockingResourceStore;
|
||||
@@ -15,17 +17,19 @@ describe('A LockingResourceStore', (): void => {
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
order = [];
|
||||
const delayedResolve = (resolve: () => void, name: string): void => {
|
||||
const delayedResolve = (resolve: (resolveParams: any) => void, name: string, resolveParams?: any): void => {
|
||||
// `setImmediate` is introduced to make sure the promise doesn't execute immediately
|
||||
setImmediate((): void => {
|
||||
order.push(name);
|
||||
resolve();
|
||||
resolve(resolveParams);
|
||||
});
|
||||
};
|
||||
|
||||
const readable = streamifyArray([ 1, 2, 3 ]);
|
||||
source = {
|
||||
getRepresentation: jest.fn(async(): Promise<any> =>
|
||||
new Promise((resolve): any => delayedResolve(resolve, 'getRepresentation'))),
|
||||
new Promise((resolve): any => delayedResolve(resolve, 'getRepresentation', { data: readable } as
|
||||
Representation))),
|
||||
addResource: jest.fn(async(): Promise<any> =>
|
||||
new Promise((resolve): any => delayedResolve(resolve, 'addResource'))),
|
||||
setRepresentation: jest.fn(async(): Promise<any> =>
|
||||
@@ -46,14 +50,14 @@ describe('A LockingResourceStore', (): void => {
|
||||
store = new LockingResourceStore(source, locker);
|
||||
});
|
||||
|
||||
it('acquires a lock on the resource when getting it.', async(): Promise<void> => {
|
||||
await store.getRepresentation({ path: 'path' }, {});
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'release' ]);
|
||||
});
|
||||
const registerEventOrder = async(eventSource: EventEmitter, event: string): Promise<void> => {
|
||||
await new Promise((resolve): any => {
|
||||
eventSource.prependListener(event, (): any => {
|
||||
order.push(event);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
it('acquires a lock on the container when adding a representation.', async(): Promise<void> => {
|
||||
await store.addResource({ path: 'path' }, {} as Representation);
|
||||
@@ -101,4 +105,120 @@ describe('A LockingResourceStore', (): void => {
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'release' ]);
|
||||
});
|
||||
|
||||
it('releases the lock on the resource when data has been read.', async(): Promise<void> => {
|
||||
// Read all data from the representation
|
||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||
representation.data.on('data', (): any => true);
|
||||
await registerEventOrder(representation.data, 'end');
|
||||
|
||||
// Verify the lock was acquired and released at the right time
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'end', 'release' ]);
|
||||
});
|
||||
|
||||
it('destroys the resource and releases the lock when the readable errors.', async(): Promise<void> => {
|
||||
// Make the representation error
|
||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||
Promise.resolve().then((): any =>
|
||||
representation.data.emit('error', new Error('Error on the readable')), null);
|
||||
await registerEventOrder(representation.data, 'error');
|
||||
await registerEventOrder(representation.data, 'close');
|
||||
|
||||
// Verify the lock was acquired and released at the right time
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'error', 'close', 'release' ]);
|
||||
});
|
||||
|
||||
it('releases the lock on the resource when readable is destroyed.', async(): Promise<void> => {
|
||||
// Make the representation close
|
||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||
representation.data.destroy();
|
||||
await registerEventOrder(representation.data, 'close');
|
||||
|
||||
// Verify the lock was acquired and released at the right time
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'close', 'release' ]);
|
||||
});
|
||||
|
||||
it('releases the lock only once when multiple events are triggered.', async(): Promise<void> => {
|
||||
// Read all data from the representation and trigger an additional close event
|
||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||
representation.data.on('data', (): any => true);
|
||||
representation.data.prependListener('end', (): any => {
|
||||
order.push('end');
|
||||
representation.data.destroy();
|
||||
});
|
||||
await registerEventOrder(representation.data, 'close');
|
||||
|
||||
// Verify the lock was acquired and released at the right time
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'end', 'close', 'release' ]);
|
||||
});
|
||||
|
||||
it('destroys the stream when nothing is read after 1000ms.', async(): Promise<void> => {
|
||||
jest.useFakeTimers();
|
||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||
const errorCallback = jest.fn();
|
||||
representation.data.on('error', errorCallback);
|
||||
|
||||
// Wait 1000ms and read
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(representation.data.read()).toBe(null);
|
||||
await registerEventOrder(representation.data, 'close');
|
||||
|
||||
// Verify a timeout error was thrown
|
||||
expect(errorCallback).toHaveBeenCalledTimes(1);
|
||||
expect(errorCallback).toHaveBeenLastCalledWith(new Error('Stream reading timout of 1000ms exceeded'));
|
||||
|
||||
// Verify the lock was acquired and released at the right time
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'close', 'release' ]);
|
||||
});
|
||||
|
||||
it('destroys the stream when pauses between reads exceed 1000ms.', async(): Promise<void> => {
|
||||
jest.useFakeTimers();
|
||||
const representation = await store.getRepresentation({ path: 'path' }, {});
|
||||
const errorCallback = jest.fn();
|
||||
representation.data.on('error', errorCallback);
|
||||
|
||||
// Wait 750ms and read
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(representation.data.read()).toBe(1);
|
||||
|
||||
// Wait 750ms and read
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(representation.data.read()).toBe(2);
|
||||
|
||||
// Wait 1000ms and watch the stream be destroyed
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(representation.data.read()).toBe(null);
|
||||
await registerEventOrder(representation.data, 'close');
|
||||
|
||||
// Verify a timeout error was thrown
|
||||
expect(errorCallback).toHaveBeenCalledTimes(1);
|
||||
expect(errorCallback).toHaveBeenLastCalledWith(new Error('Stream reading timout of 1000ms exceeded'));
|
||||
|
||||
// Verify the lock was acquired and released at the right time
|
||||
expect(locker.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(lock.release).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'close', 'release' ]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user