1
+ import type { ResourceRelease } from '@matrixai/resources' ;
2
+ import type {
3
+ LockBox ,
4
+ MultiLockRequest as AsyncLocksMultiLockRequest ,
5
+ } from '@matrixai/async-locks' ;
1
6
import type DB from './DB' ;
2
7
import type {
8
+ ToString ,
3
9
KeyPath ,
4
10
LevelPath ,
5
11
DBIteratorOptions ,
6
12
DBClearOptions ,
7
13
DBCountOptions ,
14
+ MultiLockRequest ,
8
15
} from './types' ;
9
16
import type {
10
17
RocksDBTransaction ,
@@ -13,6 +20,7 @@ import type {
13
20
} from './rocksdb/types' ;
14
21
import Logger from '@matrixai/logger' ;
15
22
import { CreateDestroy , ready } from '@matrixai/async-init/dist/CreateDestroy' ;
23
+ import { RWLockWriter } from '@matrixai/async-locks' ;
16
24
import DBIterator from './DBIterator' ;
17
25
import { rocksdbP } from './rocksdb' ;
18
26
import * as utils from './utils' ;
@@ -21,37 +29,44 @@ import * as errors from './errors';
21
29
interface DBTransaction extends CreateDestroy { }
22
30
@CreateDestroy ( )
23
31
class DBTransaction {
32
+ public readonly id : number ;
33
+
24
34
protected _db : DB ;
25
35
protected logger : Logger ;
26
-
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 ( ) ;
27
45
protected _options : RocksDBTransactionOptions ;
28
46
protected _transaction : RocksDBTransaction ;
29
- protected _id : number ;
30
47
protected _snapshot : RocksDBTransactionSnapshot ;
31
-
48
+ protected _iteratorRefs : Set < DBIterator < any , any > > = new Set ( ) ;
32
49
protected _callbacksSuccess : Array < ( ) => any > = [ ] ;
33
50
protected _callbacksFailure : Array < ( e ?: Error ) => any > = [ ] ;
34
51
protected _callbacksFinally : Array < ( e ?: Error ) => any > = [ ] ;
35
52
protected _committed : boolean = false ;
36
53
protected _rollbacked : boolean = false ;
37
54
38
- /**
39
- * References to iterators
40
- */
41
- protected _iteratorRefs : Set < DBIterator < any , any > > = new Set ( ) ;
42
-
43
55
public constructor ( {
44
56
db,
57
+ lockBox,
45
58
logger,
46
59
...options
47
60
} : {
48
61
db : DB ;
62
+ lockBox : LockBox < RWLockWriter > ;
49
63
logger ?: Logger ;
50
64
} & RocksDBTransactionOptions ) {
51
65
logger = logger ?? new Logger ( this . constructor . name ) ;
52
66
logger . debug ( `Constructing ${ this . constructor . name } ` ) ;
53
67
this . logger = logger ;
54
68
this . _db = db ;
69
+ this . lockBox = lockBox ;
55
70
const options_ = {
56
71
...options ,
57
72
// Transactions should be synchronous
@@ -61,21 +76,24 @@ class DBTransaction {
61
76
this . _options = options_ ;
62
77
this . _transaction = rocksdbP . transactionInit ( db . db , options_ ) ;
63
78
db . transactionRefs . add ( this ) ;
64
- this . _id = rocksdbP . transactionId ( this . _transaction ) ;
65
- logger . debug ( `Constructed ${ this . constructor . name } ${ this . _id } ` ) ;
79
+ this . id = rocksdbP . transactionId ( this . _transaction ) ;
80
+ logger . debug ( `Constructed ${ this . constructor . name } ${ this . id } ` ) ;
66
81
}
67
82
68
83
/**
69
84
* Destroy the transaction
70
85
* This cannot be called until the transaction is committed or rollbacked
71
86
*/
72
87
public async destroy ( ) {
73
- this . logger . debug ( `Destroying ${ this . constructor . name } ${ this . _id } ` ) ;
74
- this . _db . transactionRefs . delete ( this ) ;
88
+ this . logger . debug ( `Destroying ${ this . constructor . name } ${ this . id } ` ) ;
75
89
if ( ! this . _committed && ! this . _rollbacked ) {
76
90
throw new errors . ErrorDBTransactionNotCommittedNorRollbacked ( ) ;
77
91
}
78
- this . logger . debug ( `Destroyed ${ this . constructor . name } ${ this . _id } ` ) ;
92
+ this . _db . transactionRefs . delete ( this ) ;
93
+ // Unlock all locked keys in reverse
94
+ const lockedKeys = [ ...this . _locks . keys ( ) ] . reverse ( ) ;
95
+ await this . unlock ( ...lockedKeys ) ;
96
+ this . logger . debug ( `Destroyed ${ this . constructor . name } ${ this . id } ` ) ;
79
97
}
80
98
81
99
get db ( ) : Readonly < DB > {
@@ -86,17 +104,6 @@ class DBTransaction {
86
104
return this . _transaction ;
87
105
}
88
106
89
- get id ( ) : number {
90
- return this . _id ;
91
- }
92
-
93
- /**
94
- * @internal
95
- */
96
- get iteratorRefs ( ) : Readonly < Set < DBIterator < any , any > > > {
97
- return this . _iteratorRefs ;
98
- }
99
-
100
107
get callbacksSuccess ( ) : Readonly < Array < ( ) => any > > {
101
108
return this . _callbacksSuccess ;
102
109
}
@@ -117,6 +124,98 @@ class DBTransaction {
117
124
return this . _rollbacked ;
118
125
}
119
126
127
+ get locks ( ) : ReadonlyMap <
128
+ string ,
129
+ {
130
+ lock : RWLockWriter ;
131
+ type : 'read' | 'write' ;
132
+ release : ResourceRelease ;
133
+ }
134
+ > {
135
+ return this . _locks ;
136
+ }
137
+
138
+ /**
139
+ * @internal
140
+ */
141
+ get iteratorRefs ( ) : Readonly < Set < DBIterator < any , any > > > {
142
+ return this . _iteratorRefs ;
143
+ }
144
+
145
+ /**
146
+ * Lock a sequence of lock requests
147
+ * If the lock request doesn't specify, it
148
+ * defaults to using `RWLockWriter` with `write` type
149
+ * Keys are locked in string sorted order
150
+ * Even though keys can be arbitrary strings, by convention, you should use
151
+ * keys that correspond to keys in the database
152
+ * Locking with the same key is idempotent therefore lock re-entrancy is enabled
153
+ * Keys are automatically unlocked in reverse sorted order
154
+ * when the transaction is destroyed
155
+ * There is no support for lock upgrading or downgrading
156
+ * There is no deadlock detection
157
+ */
158
+ public async lock (
159
+ ...requests : Array < MultiLockRequest | string >
160
+ ) : Promise < void > {
161
+ const requests_ : Array < AsyncLocksMultiLockRequest < RWLockWriter > > = [ ] ;
162
+ for ( const request of requests ) {
163
+ if ( Array . isArray ( request ) ) {
164
+ const [ key , ...lockingParams ] = request ;
165
+ const key_ = key . toString ( ) ;
166
+ const lock = this . _locks . get ( key_ ) ;
167
+ // Default the lock type to `write`
168
+ const lockType = ( lockingParams [ 0 ] = lockingParams [ 0 ] ?? 'write' ) ;
169
+ if ( lock == null ) {
170
+ requests_ . push ( [ key_ , RWLockWriter , ...lockingParams ] ) ;
171
+ } else if ( lock . type !== lockType ) {
172
+ throw new errors . ErrorDBTransactionLockType ( ) ;
173
+ }
174
+ } else {
175
+ const key_ = request . toString ( ) ;
176
+ const lock = this . _locks . get ( key_ ) ;
177
+ if ( lock == null ) {
178
+ // Default to using `RWLockWriter` write lock for just string keys
179
+ requests_ . push ( [ key_ , RWLockWriter , 'write' ] ) ;
180
+ } else if ( lock . type !== 'write' ) {
181
+ throw new errors . ErrorDBTransactionLockType ( ) ;
182
+ }
183
+ }
184
+ }
185
+ if ( requests_ . length > 0 ) {
186
+ // Duplicates are eliminated, and the returned acquisitions are sorted
187
+ const lockAcquires = this . lockBox . lockMulti ( ...requests_ ) ;
188
+ for ( const [ key , lockAcquire , ...lockingParams ] of lockAcquires ) {
189
+ const [ lockRelease , lock ] = await lockAcquire ( ) ;
190
+ // The `Map` will maintain insertion order
191
+ // these must be unlocked in reverse order
192
+ // when the transaction is destroyed
193
+ this . _locks . set ( key as string , {
194
+ lock : lock ! ,
195
+ type : lockingParams [ 0 ] ! , // The `type` is defaulted to `write`
196
+ release : lockRelease ,
197
+ } ) ;
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Unlock a sequence of lock keys
204
+ * Unlocking will be done in the order of the keys
205
+ * A transaction instance is only allowed to unlock keys that it previously
206
+ * locked, all keys that are not part of the `this._locks` is ignored
207
+ * Unlocking the same keys is idempotent
208
+ */
209
+ public async unlock ( ...keys : Array < ToString > ) : Promise < void > {
210
+ for ( const key of keys ) {
211
+ const key_ = key . toString ( ) ;
212
+ const lock = this . _locks . get ( key_ ) ;
213
+ if ( lock == null ) continue ;
214
+ this . _locks . delete ( key_ ) ;
215
+ await lock . release ( ) ;
216
+ }
217
+ }
218
+
120
219
public async get < T > (
121
220
keyPath : KeyPath | string | Buffer ,
122
221
raw ?: false ,
@@ -344,7 +443,7 @@ class DBTransaction {
344
443
if ( this . _committed ) {
345
444
return ;
346
445
}
347
- this . logger . debug ( `Committing ${ this . constructor . name } ${ this . _id } ` ) ;
446
+ this . logger . debug ( `Committing ${ this . constructor . name } ${ this . id } ` ) ;
348
447
for ( const iterator of this . _iteratorRefs ) {
349
448
await iterator . destroy ( ) ;
350
449
}
@@ -357,12 +456,14 @@ class DBTransaction {
357
456
} catch ( e ) {
358
457
if ( e . code === 'TRANSACTION_CONFLICT' ) {
359
458
this . logger . debug (
360
- `Failed Committing ${ this . constructor . name } ${ this . _id } due to ${ errors . ErrorDBTransactionConflict . name } ` ,
459
+ `Failed Committing ${ this . constructor . name } ${ this . id } due to ${ errors . ErrorDBTransactionConflict . name } ` ,
361
460
) ;
362
- throw new errors . ErrorDBTransactionConflict ( undefined , { cause : e } ) ;
461
+ throw new errors . ErrorDBTransactionConflict ( undefined , {
462
+ cause : e ,
463
+ } ) ;
363
464
} else {
364
465
this . logger . debug (
365
- `Failed Committing ${ this . constructor . name } ${ this . _id } due to ${ e . message } ` ,
466
+ `Failed Committing ${ this . constructor . name } ${ this . id } due to ${ e . message } ` ,
366
467
) ;
367
468
throw e ;
368
469
}
@@ -376,7 +477,7 @@ class DBTransaction {
376
477
}
377
478
}
378
479
await this . destroy ( ) ;
379
- this . logger . debug ( `Committed ${ this . constructor . name } ${ this . _id } ` ) ;
480
+ this . logger . debug ( `Committed ${ this . constructor . name } ${ this . id } ` ) ;
380
481
}
381
482
382
483
@ready ( new errors . ErrorDBTransactionDestroyed ( ) )
@@ -387,7 +488,7 @@ class DBTransaction {
387
488
if ( this . _rollbacked ) {
388
489
return ;
389
490
}
390
- this . logger . debug ( `Rollbacking ${ this . constructor . name } ${ this . _id } ` ) ;
491
+ this . logger . debug ( `Rollbacking ${ this . constructor . name } ${ this . id } ` ) ;
391
492
for ( const iterator of this . _iteratorRefs ) {
392
493
await iterator . destroy ( ) ;
393
494
}
@@ -405,7 +506,20 @@ class DBTransaction {
405
506
}
406
507
}
407
508
await this . destroy ( ) ;
408
- this . logger . debug ( `Rollbacked ${ this . constructor . name } ${ this . _id } ` ) ;
509
+ this . logger . debug ( `Rollbacked ${ this . constructor . name } ${ this . id } ` ) ;
510
+ }
511
+
512
+ /**
513
+ * Set the snapshot manually
514
+ * This ensures that consistent reads and writes start
515
+ * after this method is executed
516
+ * This is idempotent
517
+ * Note that normally snapshots are set lazily upon the first
518
+ * transaction db operation
519
+ */
520
+ @ready ( new errors . ErrorDBTransactionDestroyed ( ) )
521
+ public setSnapshot ( ) : void {
522
+ this . setupSnapshot ( ) ;
409
523
}
410
524
411
525
/**
0 commit comments