Skip to content

Commit 950bfad

Browse files
committed
feat: replaced custom transaction with Monitor
1 parent 7717f9f commit 950bfad

File tree

5 files changed

+68
-110
lines changed

5 files changed

+68
-110
lines changed

Diff for: src/DB.ts

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class DB {
3737
public static async createDB({
3838
dbPath,
3939
crypto,
40+
deadlock = false,
4041
fs = require('fs'),
4142
logger = new Logger(this.name),
4243
fresh = false,
@@ -47,13 +48,15 @@ class DB {
4748
key: Buffer;
4849
ops: Crypto;
4950
};
51+
deadlock?: boolean;
5052
fs?: FileSystem;
5153
logger?: Logger;
5254
fresh?: boolean;
5355
} & DBOptions): Promise<DB> {
5456
logger.info(`Creating ${this.name}`);
5557
const db = new this({
5658
dbPath,
59+
deadlock,
5760
fs,
5861
logger,
5962
});
@@ -75,6 +78,7 @@ class DB {
7578
protected logger: Logger;
7679
protected workerManager?: DBWorkerManagerInterface;
7780
protected _lockBox: LockBox<RWLockWriter> = new LockBox();
81+
protected _locksPending?: Map<string, { count: number }>;
7882
protected _db: RocksDBDatabase;
7983
/**
8084
* References to iterators
@@ -109,15 +113,20 @@ class DB {
109113

110114
constructor({
111115
dbPath,
116+
deadlock,
112117
fs,
113118
logger,
114119
}: {
115120
dbPath: string;
121+
deadlock: boolean;
116122
fs: FileSystem;
117123
logger: Logger;
118124
}) {
119125
this.logger = logger;
120126
this.dbPath = dbPath;
127+
if (deadlock) {
128+
this._locksPending = new Map();
129+
}
121130
this.fs = fs;
122131
}
123132

@@ -213,6 +222,7 @@ class DB {
213222
const tran = new DBTransaction({
214223
db: this,
215224
lockBox: this._lockBox,
225+
locksPending: this._locksPending,
216226
logger: this.logger,
217227
});
218228
return [

Diff for: src/DBTransaction.ts

+27-95
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import type { ResourceRelease } from '@matrixai/resources';
2-
import type {
3-
LockBox,
4-
MultiLockRequest as AsyncLocksMultiLockRequest,
5-
} from '@matrixai/async-locks';
1+
import type { LockBox } from '@matrixai/async-locks';
62
import type DB from './DB';
73
import type {
8-
ToString,
94
KeyPath,
105
LevelPath,
116
DBIteratorOptions,
127
DBClearOptions,
138
DBCountOptions,
14-
MultiLockRequest,
159
} from './types';
1610
import type {
1711
RocksDBTransaction,
@@ -20,7 +14,12 @@ import type {
2014
} from './native/types';
2115
import Logger from '@matrixai/logger';
2216
import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy';
23-
import { Lock, RWLockWriter } from '@matrixai/async-locks';
17+
import {
18+
Monitor,
19+
Lock,
20+
RWLockWriter,
21+
errors as asyncLocksErrors,
22+
} from '@matrixai/async-locks';
2423
import DBIterator from './DBIterator';
2524
import { rocksdbP } from './native';
2625
import * as utils from './utils';
@@ -33,15 +32,7 @@ class DBTransaction {
3332

3433
protected _db: DB;
3534
protected logger: Logger;
36-
protected lockBox: LockBox<RWLockWriter>;
37-
protected _locks: Map<
38-
string,
39-
{
40-
lock: RWLockWriter;
41-
type: 'read' | 'write';
42-
release: ResourceRelease;
43-
}
44-
> = new Map();
35+
protected monitor: Monitor<RWLockWriter>;
4536
protected _options: RocksDBTransactionOptions;
4637
protected _transaction: RocksDBTransaction;
4738
protected _snapshot: RocksDBTransactionSnapshot;
@@ -58,18 +49,20 @@ class DBTransaction {
5849
public constructor({
5950
db,
6051
lockBox,
52+
locksPending,
6153
logger,
6254
...options
6355
}: {
6456
db: DB;
6557
lockBox: LockBox<RWLockWriter>;
58+
locksPending?: Map<string, { count: number }>;
6659
logger?: Logger;
6760
} & RocksDBTransactionOptions) {
6861
logger = logger ?? new Logger(this.constructor.name);
6962
logger.debug(`Constructing ${this.constructor.name}`);
7063
this.logger = logger;
7164
this._db = db;
72-
this.lockBox = lockBox;
65+
this.monitor = new Monitor(lockBox, RWLockWriter, locksPending);
7366
const options_ = {
7467
...options,
7568
// Transactions should be synchronous
@@ -96,9 +89,7 @@ class DBTransaction {
9689
// this then allows the destruction to proceed
9790
await this.commitOrRollbackLock.waitForUnlock();
9891
this._db.transactionRefs.delete(this);
99-
// Unlock all locked keys in reverse
100-
const lockedKeys = [...this._locks.keys()].reverse();
101-
await this.unlock(...lockedKeys);
92+
await this.monitor.unlockAll();
10293
this.logger.debug(`Destroyed ${this.constructor.name} ${this.id}`);
10394
}
10495

@@ -150,15 +141,8 @@ class DBTransaction {
150141
return this._rollbacked;
151142
}
152143

153-
get locks(): ReadonlyMap<
154-
string,
155-
{
156-
lock: RWLockWriter;
157-
type: 'read' | 'write';
158-
release: ResourceRelease;
159-
}
160-
> {
161-
return this._locks;
144+
get locks(): Monitor<RWLockWriter>['locks'] {
145+
return this.monitor.locks;
162146
}
163147

164148
/**
@@ -168,78 +152,26 @@ class DBTransaction {
168152
return this._iteratorRefs;
169153
}
170154

171-
/**
172-
* Lock a sequence of lock requests
173-
* If the lock request doesn't specify, it
174-
* defaults to using `RWLockWriter` with `write` type
175-
* Keys are locked in string sorted order
176-
* Even though keys can be arbitrary strings, by convention, you should use
177-
* keys that correspond to keys in the database
178-
* Locking with the same key is idempotent therefore lock re-entrancy is enabled
179-
* Keys are automatically unlocked in reverse sorted order
180-
* when the transaction is destroyed
181-
* There is no support for lock upgrading or downgrading
182-
* There is no deadlock detection
183-
*/
184155
public async lock(
185-
...requests: Array<MultiLockRequest | ToString>
156+
...params: Parameters<Monitor<RWLockWriter>['lock']>
186157
): Promise<void> {
187-
const requests_: Array<AsyncLocksMultiLockRequest<RWLockWriter>> = [];
188-
for (const request of requests) {
189-
if (Array.isArray(request)) {
190-
const [key, ...lockingParams] = request;
191-
const key_ = key.toString();
192-
const lock = this._locks.get(key_);
193-
// Default the lock type to `write`
194-
const lockType = (lockingParams[0] = lockingParams[0] ?? 'write');
195-
if (lock == null) {
196-
requests_.push([key_, RWLockWriter, ...lockingParams]);
197-
} else if (lock.type !== lockType) {
198-
throw new errors.ErrorDBTransactionLockType();
199-
}
200-
} else {
201-
const key_ = request.toString();
202-
const lock = this._locks.get(key_);
203-
if (lock == null) {
204-
// Default to using `RWLockWriter` write lock for just string keys
205-
requests_.push([key_, RWLockWriter, 'write']);
206-
} else if (lock.type !== 'write') {
207-
throw new errors.ErrorDBTransactionLockType();
208-
}
158+
try {
159+
await this.monitor.lock(...params)();
160+
} catch (e) {
161+
if (e instanceof asyncLocksErrors.ErrorAsyncLocksMonitorLockType) {
162+
throw new errors.ErrorDBTransactionLockType(undefined, { cause: e });
209163
}
210-
}
211-
if (requests_.length > 0) {
212-
// Duplicates are eliminated, and the returned acquisitions are sorted
213-
const lockAcquires = this.lockBox.lockMulti(...requests_);
214-
for (const [key, lockAcquire, ...lockingParams] of lockAcquires) {
215-
const [lockRelease, lock] = await lockAcquire();
216-
// The `Map` will maintain insertion order
217-
// these must be unlocked in reverse order
218-
// when the transaction is destroyed
219-
this._locks.set(key as string, {
220-
lock: lock!,
221-
type: lockingParams[0]!, // The `type` is defaulted to `write`
222-
release: lockRelease,
223-
});
164+
if (e instanceof asyncLocksErrors.ErrorAsyncLocksMonitorDeadlock) {
165+
throw new errors.ErrorDBTransactionDeadlock(undefined, { cause: e });
224166
}
167+
throw e;
225168
}
226169
}
227170

228-
/**
229-
* Unlock a sequence of lock keys
230-
* Unlocking will be done in the order of the keys
231-
* A transaction instance is only allowed to unlock keys that it previously
232-
* locked, all keys that are not part of the `this._locks` is ignored
233-
* Unlocking the same keys is idempotent
234-
*/
235-
public async unlock(...keys: Array<ToString>): Promise<void> {
236-
for (const key of keys) {
237-
const key_ = key.toString();
238-
const lock = this._locks.get(key_);
239-
if (lock == null) continue;
240-
this._locks.delete(key_);
241-
await lock.release();
242-
}
171+
public async unlock(
172+
...params: Parameters<Monitor<RWLockWriter>['unlock']>
173+
): Promise<void> {
174+
await this.monitor.unlock(...params);
243175
}
244176

245177
public async get<T>(

Diff for: src/errors.ts

+5
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class ErrorDBTransactionLockType<T> extends ErrorDBTransaction<T> {
8787
'DBTransaction does not support upgrading or downgrading the lock type';
8888
}
8989

90+
class ErrorDBTransactionDeadlock<T> extends ErrorDBTransaction<T> {
91+
static description = 'DBTransaction encountered a pessimistic deadlock';
92+
}
93+
9094
export {
9195
ErrorDB,
9296
ErrorDBRunning,
@@ -109,4 +113,5 @@ export {
109113
ErrorDBTransactionNotCommittedNorRollbacked,
110114
ErrorDBTransactionConflict,
111115
ErrorDBTransactionLockType,
116+
ErrorDBTransactionDeadlock,
112117
};

Diff for: src/types.ts

-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type fs from 'fs';
2-
import type { RWLockWriter } from '@matrixai/async-locks';
32
import type { WorkerManagerInterface } from '@matrixai/workers';
43
import type {
54
RocksDBDatabaseOptions,
@@ -17,13 +16,6 @@ import type {
1716
*/
1817
type POJO = { [key: string]: any };
1918

20-
/**
21-
* Any type that can be turned into a string
22-
*/
23-
interface ToString {
24-
toString(): string;
25-
}
26-
2719
/**
2820
* Opaque types are wrappers of existing types
2921
* that require smart constructors
@@ -159,14 +151,8 @@ type DBOp =
159151

160152
type DBOps = Array<DBOp>;
161153

162-
type MultiLockRequest = [
163-
key: ToString,
164-
...lockingParams: Parameters<RWLockWriter['lock']>,
165-
];
166-
167154
export type {
168155
POJO,
169-
ToString,
170156
Opaque,
171157
Callback,
172158
Merge,
@@ -182,5 +168,4 @@ export type {
182168
DBBatch,
183169
DBOp,
184170
DBOps,
185-
MultiLockRequest,
186171
};

Diff for: tests/DBTransaction.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1139,4 +1139,30 @@ describe(DBTransaction.name, () => {
11391139
});
11401140
});
11411141
});
1142+
test('deadlock detection', async () => {
1143+
const dbPath = `${dataDir}/db2`;
1144+
const db = await DB.createDB({ dbPath, crypto, deadlock: true, logger });
1145+
const barrier = await Barrier.createBarrier(2);
1146+
const results = await Promise.allSettled([
1147+
db.withTransactionF(async (tran1) => {
1148+
await tran1.lock('foo');
1149+
await barrier.wait();
1150+
await tran1.lock('bar');
1151+
}),
1152+
db.withTransactionF(async (tran2) => {
1153+
await tran2.lock('bar');
1154+
await barrier.wait();
1155+
await tran2.lock('foo');
1156+
}),
1157+
]);
1158+
expect(
1159+
results.some(
1160+
(r) =>
1161+
r.status === 'rejected' &&
1162+
r.reason instanceof errors.ErrorDBTransactionDeadlock,
1163+
),
1164+
).toBe(true);
1165+
expect(results.some((r) => r.status === 'fulfilled')).toBe(true);
1166+
await db.stop();
1167+
});
11421168
});

0 commit comments

Comments
 (0)