Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrating LockBox to DBTransaction - introducing lockMulti #14

Merged
merged 4 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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