Skip to content

Commit

Permalink
feat!: introduce lockMulti for LockBox in order to lock a collect…
Browse files Browse the repository at this point in the history
…ion of keys as separate resources

* Defaulted lock type to `write` for `RWLockReader` and `RWLockWriter`

BREAKING CHANGE: `LockRequest` is now `MultiLockRequest`
  • Loading branch information
CMCDragonkai committed Jun 30, 2022
1 parent 0582b22 commit afe9b5e
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 38 deletions.
200 changes: 169 additions & 31 deletions src/LockBox.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type { ResourceAcquire, ResourceRelease } from '@matrixai/resources';
import type { Lockable, ToString, LockRequest } from './types';
import type {
ToString,
Lockable,
MultiLockRequest,
MultiLockAcquire,
MultiLockAcquired,
} from './types';
import { withF, withG } from '@matrixai/resources';
import { ErrorAsyncLocksLockBoxConflict } from './errors';

class LockBox<L extends Lockable = Lockable> implements Lockable {
protected _locks: Map<string, L> = new Map();

public lock(...requests: Array<LockRequest<L>>): ResourceAcquire<LockBox<L>> {
public lock(
...requests: Array<MultiLockRequest<L>>
): ResourceAcquire<LockBox<L>> {
return async () => {
// Convert to strings
// This creates a copy of the requests
Expand All @@ -26,35 +34,36 @@ class LockBox<L extends Lockable = Lockable> implements Lockable {
([key], i, arr) => i === 0 || key !== arr[i - 1][0],
);
const locks: Array<[string, ResourceRelease, L]> = [];
for (const [key, LockConstructor, ...lockingParams] of requests_) {
let lock = this._locks.get(key);
if (lock == null) {
lock = new LockConstructor();
this._locks.set(key, lock);
} else {
// It is possible to swap the lock class, but only after the lock key is released
if (!(lock instanceof LockConstructor)) {
throw new ErrorAsyncLocksLockBoxConflict(
`Lock ${key} is already locked with class ${lock.constructor.name}, which conflicts with class ${LockConstructor.name}`,
);
try {
for (const [key, LockConstructor, ...lockingParams] of requests_) {
let lock = this._locks.get(key);
if (lock == null) {
lock = new LockConstructor();
this._locks.set(key, lock);
} else {
// It is possible to swap the lock class, but only after the lock key is released
if (!(lock instanceof LockConstructor)) {
throw new ErrorAsyncLocksLockBoxConflict(
`Lock ${key} is already locked with class ${lock.constructor.name}, which conflicts with class ${LockConstructor.name}`,
);
}
}
const lockAcquire = lock.lock(...lockingParams);
const [lockRelease] = await lockAcquire();
locks.push([key, lockRelease, lock]);
}
const lockAcquire = lock.lock(...lockingParams);
let lockRelease: ResourceRelease;
try {
[lockRelease] = await lockAcquire();
} catch (e) {
// Release all intermediate locks in reverse order
locks.reverse();
for (const [key, lockRelease, lock] of locks) {
await lockRelease();
if (!lock.isLocked()) {
this._locks.delete(key);
}
} catch (e) {
// Release all intermediate locks in reverse order
locks.reverse();
for (const [key, lockRelease, lock] of locks) {
await lockRelease();
// If it is still locked, then it is held by a different context
// only delete if no contexts are locking the lock
if (!lock.isLocked()) {
this._locks.delete(key);
}
throw e;
}
locks.push([key, lockRelease, lock]);
throw e;
}
let released = false;
return [
Expand All @@ -65,6 +74,8 @@ class LockBox<L extends Lockable = Lockable> implements Lockable {
locks.reverse();
for (const [key, lockRelease, lock] of locks) {
await lockRelease();
// If it is still locked, then it is held by a different context
// only delete if no contexts are locking the lock
if (!lock.isLocked()) {
this._locks.delete(key);
}
Expand All @@ -75,6 +86,76 @@ class LockBox<L extends Lockable = Lockable> implements Lockable {
};
}

public lockMulti(
...requests: Array<MultiLockRequest<L>>
): Array<MultiLockAcquire<L>> {
// Convert to strings
// This creates a copy of the requests
let requests_: Array<
[string, ToString, new () => L, ...Parameters<L['lock']>]
> = requests.map(([key, ...rest]) =>
typeof key === 'string'
? [key, key, ...rest]
: [key.toString(), key, ...rest],
);
// Sort to ensure lock hierarchy
requests_.sort(([key1], [key2]) => {
// Deterministic string comparison according to 16-bit code units
if (key1 < key2) return -1;
if (key1 > key2) return 1;
return 0;
});
// Avoid duplicate locking
requests_ = requests_.filter(
([key], i, arr) => i === 0 || key !== arr[i - 1][0],
);
const lockAcquires: Array<MultiLockAcquire<L>> = [];
for (const [key, keyOrig, LockConstructor, ...lockingParams] of requests_) {
const lockAcquire: ResourceAcquire<L> = async () => {
let lock = this._locks.get(key);
let lockRelease: ResourceRelease;
try {
if (lock == null) {
lock = new LockConstructor();
this._locks.set(key, lock);
} else {
// It is possible to swap the lock class, but only after the lock key is released
if (!(lock instanceof LockConstructor)) {
throw new ErrorAsyncLocksLockBoxConflict(
`Lock ${key} is already locked with class ${lock.constructor.name}, which conflicts with class ${LockConstructor.name}`,
);
}
}
const lockAcquire = lock.lock(...lockingParams);
[lockRelease] = await lockAcquire();
} catch (e) {
// If it is still locked, then it is held by a different context
// only delete if no contexts are locking the lock
if (!lock!.isLocked()) {
this._locks.delete(key);
}
throw e;
}
let released = false;
return [
async () => {
if (released) return;
released = true;
await lockRelease();
// If it is still locked, then it is held by a different context
// only delete if no contexts are locking the lock
if (!lock!.isLocked()) {
this._locks.delete(key);
}
},
lock,
];
};
lockAcquires.push([keyOrig, lockAcquire, ...lockingParams]);
}
return lockAcquires;
}

get locks(): ReadonlyMap<string, L> {
return this._locks;
}
Expand Down Expand Up @@ -119,31 +200,88 @@ class LockBox<L extends Lockable = Lockable> implements Lockable {

public async withF<T>(
...params: [
...requests: Array<LockRequest<L>>,
...requests: Array<MultiLockRequest<L>>,
f: (lockBox: LockBox<L>) => Promise<T>,
]
): Promise<T> {
const f = params.pop() as (lockBox: LockBox<L>) => Promise<T>;
return withF(
[this.lock(...(params as Array<LockRequest<L>>))],
[this.lock(...(params as Array<MultiLockRequest<L>>))],
([lockBox]) => f(lockBox),
);
}

public async withMultiF<T>(
...params: [
...requests: Array<MultiLockRequest<L>>,
f: (multiLocks: Array<MultiLockAcquired<L>>) => Promise<T>,
]
): Promise<T> {
const f = params.pop() as (
multiLocks: Array<MultiLockAcquired<L>>,
) => Promise<T>;
const lockAcquires = this.lockMulti(
...(params as Array<MultiLockRequest<L>>),
);

const lockAcquires_: Array<ResourceAcquire<MultiLockAcquired<L>>> =
lockAcquires.map(
([key, lockAcquire, ...lockingParams]) =>
(...r) =>
lockAcquire(...r).then(
([lockRelease, lock]) =>
[lockRelease, [key, lock, ...lockingParams]] as [
ResourceRelease,
MultiLockAcquired<L>,
],
),
);
return withF(lockAcquires_, f);
}

public withG<T, TReturn, TNext>(
...params: [
...requests: Array<LockRequest<L>>,
...requests: Array<MultiLockRequest<L>>,
g: (lockBox: LockBox<L>) => AsyncGenerator<T, TReturn, TNext>,
]
): AsyncGenerator<T, TReturn, TNext> {
const g = params.pop() as (
lockBox: LockBox<L>,
) => AsyncGenerator<T, TReturn, TNext>;
return withG(
[this.lock(...(params as Array<LockRequest<L>>))],
[this.lock(...(params as Array<MultiLockRequest<L>>))],
([lockBox]) => g(lockBox),
);
}

public withMultiG<T, TReturn, TNext>(
...params: [
...requests: Array<MultiLockRequest<L>>,
g: (
multiLocks: Array<MultiLockAcquired<L>>,
) => AsyncGenerator<T, TReturn, TNext>,
]
) {
const g = params.pop() as (
multiLocks: Array<MultiLockAcquired<L>>,
) => AsyncGenerator<T, TReturn, TNext>;
const lockAcquires = this.lockMulti(
...(params as Array<MultiLockRequest<L>>),
);
const lockAcquires_: Array<ResourceAcquire<MultiLockAcquired<L>>> =
lockAcquires.map(
([key, lockAcquire, ...lockingParams]) =>
(...r) =>
lockAcquire(...r).then(
([lockRelease, lock]) =>
[lockRelease, [key, lock, ...lockingParams]] as [
ResourceRelease,
MultiLockAcquired<L>,
],
),
);
return withG(lockAcquires_, g);
}
}

export default LockBox;
4 changes: 2 additions & 2 deletions src/RWLockReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class RWLockReader implements Lockable {
protected _writerCount: number = 0;

public lock(
type: 'read' | 'write',
type: 'read' | 'write' = 'write',
timeout?: number,
): ResourceAcquire<RWLockReader> {
switch (type) {
Expand Down Expand Up @@ -74,7 +74,7 @@ class RWLockReader implements Lockable {
// Yield for the first reader to finish locking
await yieldMicro();
}
let released= false;
let released = false;
return [
async () => {
if (released) return;
Expand Down
2 changes: 1 addition & 1 deletion src/RWLockWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class RWLockWriter implements Lockable {
protected _writerCount: number = 0;

public lock(
type: 'read' | 'write',
type: 'read' | 'write' = 'write',
timeout?: number,
): ResourceAcquire<RWLockWriter> {
switch (type) {
Expand Down
23 changes: 21 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,29 @@ interface Lockable {
): AsyncGenerator<T, TReturn, TNext>;
}

type LockRequest<L extends Lockable = Lockable> = [
type MultiLockRequest<L extends Lockable = Lockable> = [
key: ToString,
lockConstructor: new () => L,
...lockingParams: Parameters<L['lock']>,
];

export type { POJO, ToString, Lockable, LockRequest };
type MultiLockAcquire<L extends Lockable = Lockable> = [
key: ToString,
lockAcquire: ResourceAcquire<L>,
...lockingParams: Parameters<L['lock']>,
];

type MultiLockAcquired<L extends Lockable = Lockable> = [
key: ToString,
lock: L,
...lockingParams: Parameters<L['lock']>,
];

export type {
POJO,
ToString,
Lockable,
MultiLockRequest,
MultiLockAcquire,
MultiLockAcquired,
};
41 changes: 39 additions & 2 deletions tests/LockBox.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { LockRequest } from '@/types';
import type { ResourceRelease } from '@matrixai/resources';
import type { MultiLockRequest } from '@/types';
import { withF, withG } from '@matrixai/resources';
import LockBox from '@/LockBox';
import Lock from '@/Lock';
Expand Down Expand Up @@ -338,7 +339,7 @@ describe(LockBox.name, () => {
test('can map keys to LockBox locks', async () => {
const lockBox = new LockBox();
const keys = ['1', '2', '3', '4'];
const locks: Array<LockRequest<RWLockWriter>> = keys.map((key) => [
const locks: Array<MultiLockRequest<RWLockWriter>> = keys.map((key) => [
key,
RWLockWriter,
'write',
Expand All @@ -360,4 +361,40 @@ describe(LockBox.name, () => {
await lockRelease();
expect(lockBox.count).toBe(0);
});
test('lockMulti provides fine grained lock acquisitions', async () => {
const lockBox = new LockBox();
const lockAcquires = lockBox.lockMulti(['1', Lock], ['2', Lock]);
// Returned multi lock acquires should be sorted
expect(lockAcquires.map(([key]) => key)).toStrictEqual(['1', '2'].sort());
const lockReleasers: Map<string, ResourceRelease> = new Map();
for (const [key, lockAcquire] of lockAcquires) {
const [lockRelease] = await lockAcquire();
lockReleasers.set(key as string, lockRelease);
}
// Unlock '1'
await lockReleasers.get('1')!();
// Lock releasing is idempotent
await lockReleasers.get('1')!();
expect(lockBox.isLocked('1')).toBe(false);
expect(lockBox.isLocked('2')).toBe(true);
await lockBox.withF(['1', Lock], async () => {
expect(lockBox.isLocked('1')).toBe(true);
expect(lockBox.isLocked('2')).toBe(true);
});
expect(lockBox.isLocked('1')).toBe(false);
expect(lockBox.isLocked('2')).toBe(true);
// Unlock '2'
await lockReleasers.get('2')!();
await lockBox.withMultiF(['1', Lock], ['2', Lock], async (multiLocks) => {
expect(lockBox.isLocked('1')).toBe(true);
expect(lockBox.isLocked('2')).toBe(true);
const [[k1, l1], [k2, l2]] = multiLocks;
expect(k1).toBe('1');
expect(k2).toBe('2');
expect(l1.isLocked()).toBe(true);
expect(l2.isLocked()).toBe(true);
});
expect(lockBox.isLocked('1')).toBe(false);
expect(lockBox.isLocked('2')).toBe(false);
});
});

0 comments on commit afe9b5e

Please sign in to comment.