mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Make sure locker allows reentrant lock acquisition
This commit is contained in:
@@ -501,11 +501,13 @@ export * from './util/identifiers/SingleRootIdentifierStrategy';
|
||||
export * from './util/identifiers/SubdomainIdentifierStrategy';
|
||||
|
||||
// Util/Locking
|
||||
export * from './util/locking/BaseReadWriteLocker';
|
||||
export * from './util/locking/ExpiringReadWriteLocker';
|
||||
export * from './util/locking/EqualReadWriteLocker';
|
||||
export * from './util/locking/FileSystemResourceLocker';
|
||||
export * from './util/locking/GreedyReadWriteLocker';
|
||||
export * from './util/locking/MemoryResourceLocker';
|
||||
export * from './util/locking/PartialReadWriteLocker';
|
||||
export * from './util/locking/ReadWriteLocker';
|
||||
export * from './util/locking/RedisLocker';
|
||||
export * from './util/locking/ResourceLocker';
|
||||
|
||||
@@ -64,10 +64,14 @@ export class AuthorizingHttpHandler extends OperationHttpHandler {
|
||||
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
|
||||
|
||||
const requestedModes = await this.modesExtractor.handleSafe(operation);
|
||||
this.logger.verbose(`Retrieved required modes: ${[ ...requestedModes.entrySets() ]}`);
|
||||
this.logger.verbose(`Retrieved required modes: ${
|
||||
[ ...requestedModes.entrySets() ].map(([ id, set ]): string => `{ ${id.path}: ${[ ...set ]} }`)
|
||||
}`);
|
||||
|
||||
const availablePermissions = await this.permissionReader.handleSafe({ credentials, requestedModes });
|
||||
this.logger.verbose(`Available permissions are ${[ ...availablePermissions.entries() ]}`);
|
||||
this.logger.verbose(`Available permissions are ${
|
||||
[ ...availablePermissions.entries() ].map(([ id, map ]): string => `{ ${id.path}: ${JSON.stringify(map)} }`)
|
||||
}`);
|
||||
|
||||
try {
|
||||
await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions });
|
||||
|
||||
@@ -71,6 +71,8 @@ export class WacAllowHttpHandler extends OperationHttpHandler {
|
||||
} else {
|
||||
// Need to determine public permissions
|
||||
this.logger.debug('Determining public permissions');
|
||||
// Note that this call can potentially create a new lock on a resource that is already locked,
|
||||
// so a locker that allows multiple read locks on the same resource is required.
|
||||
const permissionMap = await this.permissionReader.handleSafe({ credentials: {}, requestedModes });
|
||||
everyone = permissionMap.get(operation.target) ?? {};
|
||||
}
|
||||
|
||||
95
src/util/locking/BaseReadWriteLocker.ts
Normal file
95
src/util/locking/BaseReadWriteLocker.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import type { PromiseOrValue } from '../PromiseUtil';
|
||||
import { EqualReadWriteLocker } from './EqualReadWriteLocker';
|
||||
import type { ResourceLocker } from './ResourceLocker';
|
||||
|
||||
/**
|
||||
* A {@link ReadWriteLocker} that allows for multiple simultaneous read operations.
|
||||
* Write operations will be blocked as long as read operations are not finished.
|
||||
* New read operations are allowed while this is going on, which will cause write operations to wait longer.
|
||||
*
|
||||
* Based on https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes .
|
||||
* As soon as 1 read lock request is made, the main lock is locked.
|
||||
* Internally a counter keeps track of the amount of active read locks.
|
||||
* Only when this number reaches 0 will the main lock be released again.
|
||||
* The internal count lock is only locked to increase/decrease this counter and is released afterwards.
|
||||
* This allows for multiple read operations, although only 1 at the time can update the counter,
|
||||
* which means there can still be a small waiting period if there are multiple simultaneous read operations.
|
||||
*
|
||||
* Classes extending this need to implement `getCountLockIdentifier` and `modifyCount`.
|
||||
*/
|
||||
export abstract class BaseReadWriteLocker extends EqualReadWriteLocker {
|
||||
protected readonly countLocker: ResourceLocker;
|
||||
|
||||
/**
|
||||
* @param resourceLocker - Used for creating read and write locks.
|
||||
* @param countLocker - Used for creating locks when updating the counter.
|
||||
*/
|
||||
protected constructor(resourceLocker: ResourceLocker, countLocker: ResourceLocker) {
|
||||
super(resourceLocker);
|
||||
this.countLocker = countLocker;
|
||||
}
|
||||
|
||||
public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
|
||||
await this.acquireReadLock(identifier);
|
||||
try {
|
||||
return await whileLocked();
|
||||
} finally {
|
||||
await this.releaseReadLock(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely updates the count before starting a read operation.
|
||||
*/
|
||||
private async acquireReadLock(identifier: ResourceIdentifier): Promise<void> {
|
||||
await this.withInternalCountLock(identifier, async(): Promise<void> => {
|
||||
const count = await this.modifyCount(identifier, +1);
|
||||
if (count === 1) {
|
||||
await this.locker.acquire(identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely decreases the count after the read operation is finished.
|
||||
*/
|
||||
private async releaseReadLock(identifier: ResourceIdentifier): Promise<void> {
|
||||
await this.withInternalCountLock(identifier, async(): Promise<void> => {
|
||||
const count = await this.modifyCount(identifier, -1);
|
||||
if (count === 0) {
|
||||
await this.locker.release(identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely runs an action on the count.
|
||||
*/
|
||||
private async withInternalCountLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>):
|
||||
Promise<T> {
|
||||
const read = this.getCountLockIdentifier(identifier);
|
||||
await this.countLocker.acquire(read);
|
||||
try {
|
||||
return await whileLocked();
|
||||
} finally {
|
||||
await this.countLocker.release(read);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the identifier that will be used to acquire the count lock for the given identifier.
|
||||
* There will be cases where this lock needs to be acquired
|
||||
* while an active lock on the main resource is still being maintained.
|
||||
* This means that if the input `resourceLocker` and `countLocker` use the same locking system
|
||||
* this generated identifier needs to be different.
|
||||
*/
|
||||
protected abstract getCountLockIdentifier(identifier: ResourceIdentifier): ResourceIdentifier;
|
||||
|
||||
/**
|
||||
* Update the counter that keeps track of having open read locks there currently are.
|
||||
* @param identifier - Identifier on which to update the number of read locks.
|
||||
* @param mod - `+1` or `-1`.
|
||||
*/
|
||||
protected abstract modifyCount(identifier: ResourceIdentifier, mod: number): PromiseOrValue<number>;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import type { ResourceLocker } from './ResourceLocker';
|
||||
* A {@link ReadWriteLocker} that gives no priority to read or write operations: both use the same lock.
|
||||
*/
|
||||
export class EqualReadWriteLocker implements ReadWriteLocker {
|
||||
private readonly locker: ResourceLocker;
|
||||
protected readonly locker: ResourceLocker;
|
||||
|
||||
public constructor(locker: ResourceLocker) {
|
||||
this.locker = locker;
|
||||
@@ -27,7 +27,7 @@ export class EqualReadWriteLocker implements ReadWriteLocker {
|
||||
* @param identifier - Identifier of resource that needs to be locked.
|
||||
* @param whileLocked - Function to resolve while the resource is locked.
|
||||
*/
|
||||
private async withLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
|
||||
protected async withLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
|
||||
await this.locker.acquire(identifier);
|
||||
try {
|
||||
return await whileLocked();
|
||||
|
||||
@@ -1,132 +1,44 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||
import { ForbiddenHttpError } from '../errors/ForbiddenHttpError';
|
||||
import { InternalServerError } from '../errors/InternalServerError';
|
||||
import type { PromiseOrValue } from '../PromiseUtil';
|
||||
import type { ReadWriteLocker } from './ReadWriteLocker';
|
||||
import { BaseReadWriteLocker } from './BaseReadWriteLocker';
|
||||
import type { ResourceLocker } from './ResourceLocker';
|
||||
|
||||
export interface GreedyReadWriteSuffixes {
|
||||
count: string;
|
||||
read: string;
|
||||
write: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ReadWriteLocker} that allows for multiple simultaneous read operations.
|
||||
* Write operations will be blocked as long as read operations are not finished.
|
||||
* New read operations are allowed while this is going on, which will cause write operations to wait longer.
|
||||
* A {@link BaseReadWriteLocker} that uses the same locker for the main lock and the count lock,
|
||||
* and uses a {@link KeyValueStorage} for keeping track of the counter.
|
||||
*
|
||||
* Based on https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes .
|
||||
* As soon as 1 read lock request is made, the write lock is locked.
|
||||
* Internally a counter keeps track of the amount of active read locks.
|
||||
* Only when this number reaches 0 will the write lock be released again.
|
||||
* The internal read lock is only locked to increase/decrease this counter and is released afterwards.
|
||||
* This allows for multiple read operations, although only 1 at the time can update the counter,
|
||||
* which means there can still be a small waiting period if there are multiple simultaneous read operations.
|
||||
* Since it is completely dependent on other implementations,
|
||||
* this locker is threadsafe if its inputs are as well.
|
||||
*/
|
||||
export class GreedyReadWriteLocker implements ReadWriteLocker {
|
||||
private readonly locker: ResourceLocker;
|
||||
private readonly storage: KeyValueStorage<string, number>;
|
||||
private readonly suffixes: GreedyReadWriteSuffixes;
|
||||
export class GreedyReadWriteLocker extends BaseReadWriteLocker {
|
||||
protected readonly storage: KeyValueStorage<string, number>;
|
||||
protected readonly readSuffix: string;
|
||||
protected readonly countSuffix: string;
|
||||
|
||||
/**
|
||||
* @param locker - Used for creating read and write locks.
|
||||
* @param storage - Used for storing the amount of active read operations on a resource.
|
||||
* @param suffixes - Used to generate identifiers with the given suffixes.
|
||||
* `count` is used for the identifier used to store the counter.
|
||||
* `read` and `write` are used for the 2 types of locks that are needed.
|
||||
* @param readSuffix - Used to generate the identifier for the lock that is applied when updating the counter.
|
||||
* @param countSuffix - Used to generate the identifier that will be used in the storage for storing the counter.
|
||||
*/
|
||||
public constructor(locker: ResourceLocker, storage: KeyValueStorage<string, number>,
|
||||
suffixes: GreedyReadWriteSuffixes = { count: 'count', read: 'read', write: 'write' }) {
|
||||
this.locker = locker;
|
||||
readSuffix = 'read', countSuffix = 'count') {
|
||||
super(locker, locker);
|
||||
this.storage = storage;
|
||||
this.suffixes = suffixes;
|
||||
this.readSuffix = readSuffix;
|
||||
this.countSuffix = countSuffix;
|
||||
}
|
||||
|
||||
public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
|
||||
await this.preReadSetup(identifier);
|
||||
try {
|
||||
return await whileLocked();
|
||||
} finally {
|
||||
await this.postReadCleanup(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
|
||||
if (identifier.path.endsWith(`.${this.suffixes.count}`)) {
|
||||
throw new ForbiddenHttpError('This resource is used for internal purposes.');
|
||||
}
|
||||
const write = this.getWriteLockKey(identifier);
|
||||
await this.locker.acquire(write);
|
||||
try {
|
||||
return await whileLocked();
|
||||
} finally {
|
||||
await this.locker.release(write);
|
||||
}
|
||||
protected getCountLockIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return { path: `${identifier.path}.${this.readSuffix}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* This key is used for storing the count of active read operations.
|
||||
*/
|
||||
private getCountKey(identifier: ResourceIdentifier): string {
|
||||
return `${identifier.path}.${this.suffixes.count}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the identifier for the read lock: the lock that is used to safely update and read the count.
|
||||
*/
|
||||
private getReadLockKey(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return { path: `${identifier.path}.${this.suffixes.read}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the identifier for the write lock, making sure there is at most 1 write operation active.
|
||||
*/
|
||||
private getWriteLockKey(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return { path: `${identifier.path}.${this.suffixes.write}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely updates the count before starting a read operation.
|
||||
*/
|
||||
private async preReadSetup(identifier: ResourceIdentifier): Promise<void> {
|
||||
await this.withInternalReadLock(identifier, async(): Promise<void> => {
|
||||
const count = await this.incrementCount(identifier, +1);
|
||||
if (count === 1) {
|
||||
// There is at least 1 read operation so write operations are blocked
|
||||
const write = this.getWriteLockKey(identifier);
|
||||
await this.locker.acquire(write);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely decreases the count after the read operation is finished.
|
||||
*/
|
||||
private async postReadCleanup(identifier: ResourceIdentifier): Promise<void> {
|
||||
await this.withInternalReadLock(identifier, async(): Promise<void> => {
|
||||
const count = await this.incrementCount(identifier, -1);
|
||||
if (count === 0) {
|
||||
// All read locks have been released so a write operation is possible again
|
||||
const write = this.getWriteLockKey(identifier);
|
||||
await this.locker.release(write);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely runs an action on the count.
|
||||
*/
|
||||
private async withInternalReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>):
|
||||
Promise<T> {
|
||||
const read = this.getReadLockKey(identifier);
|
||||
await this.locker.acquire(read);
|
||||
try {
|
||||
return await whileLocked();
|
||||
} finally {
|
||||
await this.locker.release(read);
|
||||
}
|
||||
protected getCountKey(identifier: ResourceIdentifier): string {
|
||||
return `${identifier.path}.${this.countSuffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +46,7 @@ export class GreedyReadWriteLocker implements ReadWriteLocker {
|
||||
* Creates the data if it didn't exist yet.
|
||||
* Deletes the data when the count reaches zero.
|
||||
*/
|
||||
private async incrementCount(identifier: ResourceIdentifier, mod: number): Promise<number> {
|
||||
protected async modifyCount(identifier: ResourceIdentifier, mod: number): Promise<number> {
|
||||
const countKey = this.getCountKey(identifier);
|
||||
let number = await this.storage.get(countKey) ?? 0;
|
||||
number += mod;
|
||||
|
||||
43
src/util/locking/PartialReadWriteLocker.ts
Normal file
43
src/util/locking/PartialReadWriteLocker.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { BaseReadWriteLocker } from './BaseReadWriteLocker';
|
||||
import { MemoryResourceLocker } from './MemoryResourceLocker';
|
||||
import type { ResourceLocker } from './ResourceLocker';
|
||||
|
||||
/**
|
||||
* A {@link BaseReadWriteLocker} that stores the counter and its associated locks in memory.
|
||||
* The consequence of this is that multiple read requests are possible as long as they occur on the same worker thread.
|
||||
* A read request from a different worker thread will have to wait
|
||||
* until those from the current worker thread are finished.
|
||||
*
|
||||
* The main reason for this class is due to the file locker that we use only allowing locks to be released
|
||||
* by the same worker thread that acquired them.
|
||||
*/
|
||||
export class PartialReadWriteLocker extends BaseReadWriteLocker {
|
||||
private readonly readCount: Map<string, number>;
|
||||
|
||||
public constructor(locker: ResourceLocker) {
|
||||
// This goes against how we generally link classes together using Components.js.
|
||||
// The reason for doing this is that `MemoryResourceLocker` implements `SingleThreaded`,
|
||||
// meaning that when the server is started with worker threads an error will be thrown by Components.js.
|
||||
// Instantiating it here "hides" it from Components.js.
|
||||
// If at some point in the future this causes issues because we want to split up the code,
|
||||
// this should not be blocking and an alternative solution should be used,
|
||||
// such as removing the SingleThreaded interface from the locker.
|
||||
super(locker, new MemoryResourceLocker());
|
||||
this.readCount = new Map();
|
||||
}
|
||||
|
||||
protected getCountLockIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
protected modifyCount(identifier: ResourceIdentifier, mod: number): number {
|
||||
const modified = (this.readCount.get(identifier.path) ?? 0) + mod;
|
||||
if (modified === 0) {
|
||||
this.readCount.delete(identifier.path);
|
||||
} else {
|
||||
this.readCount.set(identifier.path, modified);
|
||||
}
|
||||
return modified;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user