Skip to content

Commit

Permalink
Merge pull request #14 from MatrixAI/feature-default
Browse files Browse the repository at this point in the history
Integrating `LockBox` to `DBTransaction` - introducing `lockMulti`
  • Loading branch information
CMCDragonkai authored Jun 30, 2022
2 parents cdbe3fe + e3d4d97 commit 6504452
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 48 deletions.
2 changes: 1 addition & 1 deletion docs/assets/search.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/classes/Lock.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/classes/LockBox.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/classes/RWLockReader.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/classes/RWLockWriter.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ <h3>Publishing</h3>
</a>
<pre><code class="language-sh"><span class="hl-1"># npm login</span><br/><span class="hl-0">npm version patch </span><span class="hl-1"># major/minor/patch</span><br/><span class="hl-0">npm run build</span><br/><span class="hl-0">npm publish --access public</span><br/><span class="hl-0">git push</span><br/><span class="hl-0">git push --tags</span>
</code></pre>
</div></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class="current"><a href="modules.html">Exports</a></li><li class=" tsd-kind-namespace"><a href="modules/errors.html">errors</a></li><li class=" tsd-kind-namespace"><a href="modules/utils.html">utils</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-class"><a href="classes/Lock.html" class="tsd-kind-icon">Lock</a></li><li class="tsd-kind-class tsd-has-type-parameter"><a href="classes/LockBox.html" class="tsd-kind-icon">Lock<wbr/>Box</a></li><li class="tsd-kind-class"><a href="classes/RWLockReader.html" class="tsd-kind-icon">RWLock<wbr/>Reader</a></li><li class="tsd-kind-class"><a href="classes/RWLockWriter.html" class="tsd-kind-icon">RWLock<wbr/>Writer</a></li><li class="tsd-kind-interface"><a href="interfaces/Lockable.html" class="tsd-kind-icon">Lockable</a></li><li class="tsd-kind-interface"><a href="interfaces/ToString.html" class="tsd-kind-icon">To<wbr/>String</a></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#LockRequest" class="tsd-kind-icon">Lock<wbr/>Request</a></li><li class="tsd-kind-type-alias"><a href="modules.html#POJO" class="tsd-kind-icon">POJO</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-property tsd-parent-kind-interface"><span class="tsd-kind-icon">Property</span></li><li class="tsd-kind-method tsd-parent-kind-interface"><span class="tsd-kind-icon">Method</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-constructor tsd-parent-kind-class"><span class="tsd-kind-icon">Constructor</span></li><li class="tsd-kind-method tsd-parent-kind-class"><span class="tsd-kind-icon">Method</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-property tsd-parent-kind-class tsd-is-protected"><span class="tsd-kind-icon">Protected property</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="assets/main.js"></script></body></html>
</div></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class="current"><a href="modules.html">Exports</a></li><li class=" tsd-kind-namespace"><a href="modules/errors.html">errors</a></li><li class=" tsd-kind-namespace"><a href="modules/utils.html">utils</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-class"><a href="classes/Lock.html" class="tsd-kind-icon">Lock</a></li><li class="tsd-kind-class tsd-has-type-parameter"><a href="classes/LockBox.html" class="tsd-kind-icon">Lock<wbr/>Box</a></li><li class="tsd-kind-class"><a href="classes/RWLockReader.html" class="tsd-kind-icon">RWLock<wbr/>Reader</a></li><li class="tsd-kind-class"><a href="classes/RWLockWriter.html" class="tsd-kind-icon">RWLock<wbr/>Writer</a></li><li class="tsd-kind-interface"><a href="interfaces/Lockable.html" class="tsd-kind-icon">Lockable</a></li><li class="tsd-kind-interface"><a href="interfaces/ToString.html" class="tsd-kind-icon">To<wbr/>String</a></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#MultiLockAcquire" class="tsd-kind-icon">Multi<wbr/>Lock<wbr/>Acquire</a></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#MultiLockAcquired" class="tsd-kind-icon">Multi<wbr/>Lock<wbr/>Acquired</a></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#MultiLockRequest" class="tsd-kind-icon">Multi<wbr/>Lock<wbr/>Request</a></li><li class="tsd-kind-type-alias"><a href="modules.html#POJO" class="tsd-kind-icon">POJO</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-property tsd-parent-kind-interface"><span class="tsd-kind-icon">Property</span></li><li class="tsd-kind-method tsd-parent-kind-interface"><span class="tsd-kind-icon">Method</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-constructor tsd-parent-kind-class"><span class="tsd-kind-icon">Constructor</span></li><li class="tsd-kind-method tsd-parent-kind-class"><span class="tsd-kind-icon">Method</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-property tsd-parent-kind-class tsd-is-protected"><span class="tsd-kind-icon">Protected property</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="assets/main.js"></script></body></html>
4 changes: 2 additions & 2 deletions docs/modules.html

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/Lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ class Lock implements Lockable {
--this._count;
throw e;
}
let released = false;
return [
async () => {
if (released) return;
released = true;
--this._count;
release();
// Allow semaphore to settle https://github.com/DirtyHairy/async-mutex/issues/54
Expand Down
205 changes: 173 additions & 32 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> implements Lockable {
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,42 +34,48 @@ class LockBox<L extends 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 [
async () => {
if (released) return;
released = true;
// Release all 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);
}
Expand All @@ -72,6 +86,76 @@ class LockBox<L extends 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 @@ -116,31 +200,88 @@ class LockBox<L extends 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;
8 changes: 7 additions & 1 deletion 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,8 +74,11 @@ class RWLockReader implements Lockable {
// Yield for the first reader to finish locking
await yieldMicro();
}
let released = false;
return [
async () => {
if (released) return;
released = true;
readersRelease = await this.readersLock.acquire();
const readerCount = --this._readerCount;
// The last reader unlocks
Expand Down Expand Up @@ -109,8 +112,11 @@ class RWLockReader implements Lockable {
--this._writerCount;
throw e;
}
let released = false;
return [
async () => {
if (released) return;
released = true;
release();
--this._writerCount;
// Allow semaphore to settle https://github.com/DirtyHairy/async-mutex/issues/54
Expand Down
8 changes: 7 additions & 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 Expand Up @@ -74,8 +74,11 @@ class RWLockWriter implements Lockable {
// Yield for the first reader to finish locking
await yieldMicro();
}
let released = false;
return [
async () => {
if (released) return;
released = true;
const readerCount = --this._readerCount;
// The last reader unlocks
if (readerCount === 0) {
Expand Down Expand Up @@ -126,8 +129,11 @@ class RWLockWriter implements Lockable {
await yieldMicro();
throw e;
}
let released = false;
return [
async () => {
if (released) return;
released = true;
this.readersRelease();
writersRelease();
--this._writerCount;
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,
};
Loading

0 comments on commit 6504452

Please sign in to comment.