@optimystic/db-core 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/btree/btree.d.ts +2 -0
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +72 -52
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +13 -0
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/collection/collection.d.ts +10 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +34 -0
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.js +1 -1
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/logger.d.ts +4 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +8 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/transaction/coordinator.d.ts +31 -8
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +206 -53
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/index.d.ts +2 -2
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/session.d.ts +11 -7
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +27 -14
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +9 -3
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +14 -7
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts +9 -2
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +26 -6
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +84 -9
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +4 -0
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -9
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic-proxy.d.ts +26 -0
- package/dist/src/transform/atomic-proxy.d.ts.map +1 -0
- package/dist/src/transform/atomic-proxy.js +47 -0
- package/dist/src/transform/atomic-proxy.js.map +1 -0
- package/dist/src/transform/cache-source.d.ts +3 -2
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +15 -3
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/index.d.ts +1 -0
- package/dist/src/transform/index.d.ts.map +1 -1
- package/dist/src/transform/index.js +1 -0
- package/dist/src/transform/index.js.map +1 -1
- package/dist/src/utility/batch-coordinator.d.ts +2 -0
- package/dist/src/utility/batch-coordinator.d.ts.map +1 -1
- package/dist/src/utility/batch-coordinator.js +6 -1
- package/dist/src/utility/batch-coordinator.js.map +1 -1
- package/dist/src/utility/hash-string.d.ts +3 -6
- package/dist/src/utility/hash-string.d.ts.map +1 -1
- package/dist/src/utility/hash-string.js +8 -11
- package/dist/src/utility/hash-string.js.map +1 -1
- package/dist/src/utility/lru-map.d.ts +18 -0
- package/dist/src/utility/lru-map.d.ts.map +1 -0
- package/dist/src/utility/lru-map.js +52 -0
- package/dist/src/utility/lru-map.js.map +1 -0
- package/package.json +15 -8
- package/src/btree/btree.ts +71 -50
- package/src/cluster/structs.ts +11 -0
- package/src/collection/collection.ts +44 -0
- package/src/index.ts +1 -0
- package/src/log/log.ts +1 -1
- package/src/logger.ts +10 -0
- package/src/transaction/coordinator.ts +244 -57
- package/src/transaction/index.ts +4 -2
- package/src/transaction/session.ts +38 -14
- package/src/transaction/transaction.ts +23 -10
- package/src/transaction/validator.ts +34 -7
- package/src/transactor/network-transactor.ts +94 -13
- package/src/transactor/transactor-source.ts +28 -9
- package/src/transform/atomic-proxy.ts +49 -0
- package/src/transform/cache-source.ts +18 -4
- package/src/transform/index.ts +1 -0
- package/src/utility/batch-coordinator.ts +9 -1
- package/src/utility/hash-string.ts +14 -17
- package/src/utility/lru-map.ts +55 -0
- package/dist/index.min.js +0 -9
- package/dist/index.min.js.map +0 -7
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ITransactor, BlockId, CollectionId, Transforms, PendRequest, CommitRequest, ActionId, IBlock, BlockOperations } from "../index.js";
|
|
2
|
-
import type { Transaction, ExecutionResult, ITransactionEngine, CollectionActions } from "./transaction.js";
|
|
2
|
+
import type { Transaction, ExecutionResult, ITransactionEngine, CollectionActions, ReadDependency } from "./transaction.js";
|
|
3
3
|
import type { PeerId } from "../network/types.js";
|
|
4
4
|
import type { Collection } from "../collection/collection.js";
|
|
5
5
|
import { TransactionContext } from "./context.js";
|
|
6
6
|
import { ActionsEngine } from "./actions-engine.js";
|
|
7
|
-
import { createActionsStatements, createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
7
|
+
import { createActionsStatements, createTransactionStamp, createTransactionId, isTransactionExpired } from "./transaction.js";
|
|
8
8
|
import { Log, blockIdsForTransforms, transformsFromTransform, hashString } from "../index.js";
|
|
9
|
+
import { createLogger } from "../logger.js";
|
|
10
|
+
|
|
11
|
+
const log = createLogger('trx:coordinator');
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Represents an operation on a block within a collection.
|
|
@@ -26,6 +29,14 @@ type Operation =
|
|
|
26
29
|
* - Commit transactions by running consensus phases (GATHER, PEND, COMMIT)
|
|
27
30
|
*/
|
|
28
31
|
export class TransactionCoordinator {
|
|
32
|
+
/** Per-stampId tracking: snapshot before first apply + accumulated actions for replay */
|
|
33
|
+
private stampData = new Map<string, {
|
|
34
|
+
order: number;
|
|
35
|
+
preSnapshot: Map<CollectionId, Transforms>;
|
|
36
|
+
actionBatches: CollectionActions[][];
|
|
37
|
+
}>();
|
|
38
|
+
private nextStampOrder = 0;
|
|
39
|
+
|
|
29
40
|
constructor(
|
|
30
41
|
private readonly transactor: ITransactor,
|
|
31
42
|
private readonly collections: Map<CollectionId, Collection<any>>
|
|
@@ -44,15 +55,37 @@ export class TransactionCoordinator {
|
|
|
44
55
|
async applyActions(
|
|
45
56
|
actions: CollectionActions[],
|
|
46
57
|
stampId: string
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
// On first call for this stampId, snapshot all collections for potential rollback
|
|
60
|
+
if (!this.stampData.has(stampId)) {
|
|
61
|
+
const snapshot = new Map<CollectionId, Transforms>();
|
|
62
|
+
for (const [id, col] of this.collections) {
|
|
63
|
+
snapshot.set(id, structuredClone(col.tracker.transforms));
|
|
64
|
+
}
|
|
65
|
+
this.stampData.set(stampId, {
|
|
66
|
+
order: this.nextStampOrder++,
|
|
67
|
+
preSnapshot: snapshot,
|
|
68
|
+
actionBatches: []
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
this.stampData.get(stampId)!.actionBatches.push(actions);
|
|
72
|
+
|
|
73
|
+
await this.applyActionsRaw(actions, stampId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Apply actions without tracking (used internally and for replay during rollback).
|
|
78
|
+
*/
|
|
79
|
+
private async applyActionsRaw(
|
|
80
|
+
actions: CollectionActions[],
|
|
81
|
+
stampId: string
|
|
47
82
|
): Promise<void> {
|
|
48
83
|
for (const { collectionId, actions: collectionActions } of actions) {
|
|
49
|
-
// Get collection
|
|
50
84
|
const collection = this.collections.get(collectionId);
|
|
51
85
|
if (!collection) {
|
|
52
86
|
throw new Error(`Collection not found: ${collectionId}`);
|
|
53
87
|
}
|
|
54
88
|
|
|
55
|
-
// Apply each action (tagged with stampId)
|
|
56
89
|
for (const action of collectionActions) {
|
|
57
90
|
const taggedAction = { ...(action as any), transaction: stampId };
|
|
58
91
|
await collection.act(taggedAction);
|
|
@@ -70,6 +103,10 @@ export class TransactionCoordinator {
|
|
|
70
103
|
* @param transaction - The transaction to commit
|
|
71
104
|
*/
|
|
72
105
|
async commit(transaction: Transaction): Promise<void> {
|
|
106
|
+
if (isTransactionExpired(transaction.stamp)) {
|
|
107
|
+
throw new Error(`Transaction expired at ${transaction.stamp.expiration}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
// Collect transforms and determine critical blocks for each affected collection
|
|
74
111
|
const collectionData = Array.from(this.collections.entries())
|
|
75
112
|
.map(([collectionId, collection]) => ({
|
|
@@ -122,7 +159,7 @@ export class TransactionCoordinator {
|
|
|
122
159
|
)
|
|
123
160
|
]);
|
|
124
161
|
|
|
125
|
-
const operationsHash = this.hashOperations(allOperations);
|
|
162
|
+
const operationsHash = await this.hashOperations(allOperations);
|
|
126
163
|
|
|
127
164
|
// Execute consensus phases (GATHER, PEND, COMMIT)
|
|
128
165
|
const coordResult = await this.coordinateTransaction(
|
|
@@ -135,24 +172,75 @@ export class TransactionCoordinator {
|
|
|
135
172
|
if (!coordResult.success) {
|
|
136
173
|
throw new Error(`Transaction commit failed: ${coordResult.error}`);
|
|
137
174
|
}
|
|
175
|
+
|
|
176
|
+
// Reset trackers and update actionContext after successful commit
|
|
177
|
+
for (const { collection } of collectionData) {
|
|
178
|
+
const newRev = (collection['source'].actionContext?.rev ?? 0) + 1;
|
|
179
|
+
collection['source'].actionContext = {
|
|
180
|
+
committed: [
|
|
181
|
+
...(collection['source'].actionContext?.committed ?? []),
|
|
182
|
+
{ actionId: transaction.id, rev: newRev }
|
|
183
|
+
],
|
|
184
|
+
rev: newRev,
|
|
185
|
+
};
|
|
186
|
+
collection.tracker.reset();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Clean up stamp tracking data
|
|
190
|
+
this.stampData.delete(transaction.stamp.id);
|
|
138
191
|
}
|
|
139
192
|
|
|
140
193
|
/**
|
|
141
|
-
* Rollback a transaction (undo applied actions).
|
|
194
|
+
* Rollback a transaction (undo only the given stampId's applied actions).
|
|
142
195
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
196
|
+
* Restores tracker state to the snapshot taken before the stampId's first
|
|
197
|
+
* applyActions call, then replays any later stamps' actions to preserve
|
|
198
|
+
* other sessions' transforms.
|
|
145
199
|
*
|
|
146
|
-
* @param
|
|
200
|
+
* @param stampId - The transaction stamp ID to rollback
|
|
147
201
|
*/
|
|
148
|
-
async rollback(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
202
|
+
async rollback(stampId: string): Promise<void> {
|
|
203
|
+
const data = this.stampData.get(stampId);
|
|
204
|
+
if (!data) return;
|
|
205
|
+
|
|
206
|
+
this.stampData.delete(stampId);
|
|
207
|
+
|
|
208
|
+
// Collect all remaining stamps to replay
|
|
209
|
+
const toReplay = [...this.stampData.entries()]
|
|
210
|
+
.sort(([, a], [, b]) => a.order - b.order);
|
|
211
|
+
|
|
212
|
+
// Find the earliest snapshot among the rolled-back stamp and all remaining stamps.
|
|
213
|
+
// This is necessary because interleaved execution means a lower-order stamp
|
|
214
|
+
// may have batches applied after a higher-order stamp's snapshot was taken.
|
|
215
|
+
let earliestSnapshot = data.preSnapshot;
|
|
216
|
+
let earliestOrder = data.order;
|
|
217
|
+
for (const [, d] of toReplay) {
|
|
218
|
+
if (d.order < earliestOrder) {
|
|
219
|
+
earliestSnapshot = d.preSnapshot;
|
|
220
|
+
earliestOrder = d.order;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Restore to the earliest snapshot
|
|
225
|
+
for (const [collectionId, transforms] of earliestSnapshot) {
|
|
226
|
+
const collection = this.collections.get(collectionId);
|
|
227
|
+
if (collection) {
|
|
228
|
+
collection.tracker.reset(structuredClone(transforms));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Replay all remaining stamps' batches in order
|
|
233
|
+
for (const [replayStampId, replayData] of toReplay) {
|
|
234
|
+
// Update the snapshot to reflect current (post-replay) state
|
|
235
|
+
const newSnapshot = new Map<CollectionId, Transforms>();
|
|
236
|
+
for (const [id, col] of this.collections) {
|
|
237
|
+
newSnapshot.set(id, structuredClone(col.tracker.transforms));
|
|
238
|
+
}
|
|
239
|
+
replayData.preSnapshot = newSnapshot;
|
|
240
|
+
|
|
241
|
+
for (const actionBatch of replayData.actionBatches) {
|
|
242
|
+
await this.applyActionsRaw(actionBatch, replayStampId);
|
|
243
|
+
}
|
|
156
244
|
}
|
|
157
245
|
}
|
|
158
246
|
|
|
@@ -190,14 +278,34 @@ export class TransactionCoordinator {
|
|
|
190
278
|
}
|
|
191
279
|
}
|
|
192
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Collect read dependencies from all participating collections.
|
|
283
|
+
*/
|
|
284
|
+
getReadDependencies(): ReadDependency[] {
|
|
285
|
+
const reads: ReadDependency[] = [];
|
|
286
|
+
for (const collection of this.collections.values()) {
|
|
287
|
+
reads.push(...collection.getReadDependencies());
|
|
288
|
+
}
|
|
289
|
+
return reads;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Clear read dependencies from all collections.
|
|
294
|
+
*/
|
|
295
|
+
clearReadDependencies(): void {
|
|
296
|
+
for (const collection of this.collections.values()) {
|
|
297
|
+
collection.clearReadDependencies();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
193
301
|
/**
|
|
194
302
|
* Compute hash of all operations in a transaction.
|
|
195
303
|
* This hash is used for validation - validators re-execute the transaction
|
|
196
304
|
* and compare their computed operations hash with this one.
|
|
197
305
|
*/
|
|
198
|
-
private hashOperations(operations: readonly Operation[]): string {
|
|
306
|
+
private async hashOperations(operations: readonly Operation[]): Promise<string> {
|
|
199
307
|
const operationsData = JSON.stringify(operations);
|
|
200
|
-
return `ops:${hashString(operationsData)}`;
|
|
308
|
+
return `ops:${await hashString(operationsData)}`;
|
|
201
309
|
}
|
|
202
310
|
|
|
203
311
|
/**
|
|
@@ -223,7 +331,7 @@ export class TransactionCoordinator {
|
|
|
223
331
|
const reads = context.getReads();
|
|
224
332
|
|
|
225
333
|
// Create stamp from context
|
|
226
|
-
const stamp = createTransactionStamp(
|
|
334
|
+
const stamp = await createTransactionStamp(
|
|
227
335
|
'local', // TODO: Get from context or coordinator
|
|
228
336
|
Date.now(),
|
|
229
337
|
'', // TODO: Get from engine
|
|
@@ -234,7 +342,7 @@ export class TransactionCoordinator {
|
|
|
234
342
|
stamp,
|
|
235
343
|
statements,
|
|
236
344
|
reads,
|
|
237
|
-
id: createTransactionId(stamp.id, statements, reads)
|
|
345
|
+
id: await createTransactionId(stamp.id, statements, reads)
|
|
238
346
|
};
|
|
239
347
|
|
|
240
348
|
const engine = new ActionsEngine(this);
|
|
@@ -254,12 +362,22 @@ export class TransactionCoordinator {
|
|
|
254
362
|
* @returns Execution result with actions and results
|
|
255
363
|
*/
|
|
256
364
|
async execute(transaction: Transaction, engine: ITransactionEngine): Promise<ExecutionResult> {
|
|
365
|
+
const trxId = transaction.id;
|
|
366
|
+
const t0 = Date.now();
|
|
367
|
+
|
|
368
|
+
if (isTransactionExpired(transaction.stamp)) {
|
|
369
|
+
return { success: false, error: `Transaction expired at ${transaction.stamp.expiration}` };
|
|
370
|
+
}
|
|
371
|
+
|
|
257
372
|
// 1. Validate engine matches transaction
|
|
258
373
|
// Note: We don't enforce this strictly since the engine is passed in explicitly
|
|
259
374
|
// The caller is responsible for ensuring the correct engine is used
|
|
260
375
|
|
|
376
|
+
const tEngine = Date.now();
|
|
261
377
|
const result = await engine.execute(transaction);
|
|
378
|
+
const engineMs = Date.now() - tEngine;
|
|
262
379
|
if (!result.success) {
|
|
380
|
+
log('execute:done trxId=%s engine=%dms success=false total=%dms', trxId, engineMs, Date.now() - t0);
|
|
263
381
|
return result;
|
|
264
382
|
}
|
|
265
383
|
|
|
@@ -268,6 +386,7 @@ export class TransactionCoordinator {
|
|
|
268
386
|
}
|
|
269
387
|
|
|
270
388
|
// 2. Apply actions to collections and collect transforms
|
|
389
|
+
const tApply = Date.now();
|
|
271
390
|
const collectionTransforms = new Map<CollectionId, Transforms>();
|
|
272
391
|
const criticalBlocks = new Map<CollectionId, BlockId>();
|
|
273
392
|
const actionResults = new Map<CollectionId, any[]>();
|
|
@@ -301,9 +420,12 @@ export class TransactionCoordinator {
|
|
|
301
420
|
({ type: 'delete' as const, collectionId, blockId })
|
|
302
421
|
)
|
|
303
422
|
]);
|
|
304
|
-
const operationsHash = this.hashOperations(allOperations);
|
|
423
|
+
const operationsHash = await this.hashOperations(allOperations);
|
|
424
|
+
|
|
425
|
+
const applyMs = Date.now() - tApply;
|
|
305
426
|
|
|
306
427
|
// 4. Coordinate (GATHER if multi-collection)
|
|
428
|
+
const tCoord = Date.now();
|
|
307
429
|
const coordResult = await this.coordinateTransaction(
|
|
308
430
|
transaction,
|
|
309
431
|
operationsHash,
|
|
@@ -311,11 +433,34 @@ export class TransactionCoordinator {
|
|
|
311
433
|
criticalBlocks
|
|
312
434
|
);
|
|
313
435
|
|
|
436
|
+
const coordMs = Date.now() - tCoord;
|
|
314
437
|
if (!coordResult.success) {
|
|
438
|
+
log('execute:done trxId=%s engine=%dms apply=%dms coordinate=%dms success=false total=%dms', trxId, engineMs, applyMs, coordMs, Date.now() - t0);
|
|
315
439
|
return coordResult;
|
|
316
440
|
}
|
|
317
441
|
|
|
318
|
-
//
|
|
442
|
+
// 5. Update actionContext and reset trackers after successful commit
|
|
443
|
+
for (const collectionActions of result.actions) {
|
|
444
|
+
const collection = this.collections.get(collectionActions.collectionId);
|
|
445
|
+
if (collection) {
|
|
446
|
+
const newRev = (collection['source'].actionContext?.rev ?? 0) + 1;
|
|
447
|
+
const actionId = transaction.id;
|
|
448
|
+
collection['source'].actionContext = {
|
|
449
|
+
committed: [
|
|
450
|
+
...(collection['source'].actionContext?.committed ?? []),
|
|
451
|
+
{ actionId, rev: newRev }
|
|
452
|
+
],
|
|
453
|
+
rev: newRev,
|
|
454
|
+
};
|
|
455
|
+
collection.tracker.reset();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Clean up stamp tracking data
|
|
460
|
+
this.stampData.delete(transaction.stamp.id);
|
|
461
|
+
|
|
462
|
+
// 6. Return results from actions
|
|
463
|
+
log('execute:done trxId=%s engine=%dms apply=%dms coordinate=%dms total=%dms', trxId, engineMs, applyMs, coordMs, Date.now() - t0);
|
|
319
464
|
return {
|
|
320
465
|
success: true,
|
|
321
466
|
actions: result.actions,
|
|
@@ -399,36 +544,52 @@ export class TransactionCoordinator {
|
|
|
399
544
|
collectionTransforms: Map<CollectionId, Transforms>,
|
|
400
545
|
criticalBlocks: Map<CollectionId, BlockId>
|
|
401
546
|
): Promise<{ success: boolean; error?: string }> {
|
|
547
|
+
const trxId = transaction.id;
|
|
548
|
+
const t0 = Date.now();
|
|
549
|
+
|
|
402
550
|
// 1. GATHER phase: collect critical cluster nominees (skip if single collection)
|
|
403
551
|
const criticalBlockIds = Array.from(criticalBlocks.values());
|
|
552
|
+
const tGather = Date.now();
|
|
404
553
|
const superclusterNominees = await this.gatherPhase(criticalBlockIds);
|
|
554
|
+
const gatherMs = Date.now() - tGather;
|
|
405
555
|
|
|
406
556
|
// 2. PEND phase: distribute to all block clusters
|
|
557
|
+
const tPend = Date.now();
|
|
407
558
|
const pendResult = await this.pendPhase(
|
|
408
559
|
transaction,
|
|
409
560
|
operationsHash,
|
|
410
561
|
collectionTransforms,
|
|
411
562
|
superclusterNominees
|
|
412
563
|
);
|
|
564
|
+
const pendMs = Date.now() - tPend;
|
|
413
565
|
if (!pendResult.success) {
|
|
566
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms (failed) total=%dms', trxId, gatherMs, pendMs, Date.now() - t0);
|
|
414
567
|
return pendResult;
|
|
415
568
|
}
|
|
416
569
|
|
|
417
|
-
// 3. COMMIT phase: commit to all critical blocks
|
|
570
|
+
// 3. COMMIT phase: commit to all critical blocks (with retry for forward recovery)
|
|
571
|
+
const tCommit = Date.now();
|
|
418
572
|
const commitResult = await this.commitPhase(
|
|
419
573
|
transaction.id as ActionId,
|
|
420
574
|
criticalBlockIds,
|
|
421
575
|
pendResult.pendedBlockIds!
|
|
422
576
|
);
|
|
577
|
+
const commitMs = Date.now() - tCommit;
|
|
423
578
|
if (!commitResult.success) {
|
|
424
|
-
//
|
|
425
|
-
await this.cancelPhase(
|
|
426
|
-
|
|
579
|
+
// Targeted cancel: only cancel collections that are still pending (not already committed)
|
|
580
|
+
await this.cancelPhase(
|
|
581
|
+
transaction.id as ActionId,
|
|
582
|
+
pendResult.pendedBlockIds!,
|
|
583
|
+
commitResult.committedCollections
|
|
584
|
+
);
|
|
585
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms commit=%dms (failed) total=%dms', trxId, gatherMs, pendMs, commitMs, Date.now() - t0);
|
|
586
|
+
return { success: false, error: commitResult.error };
|
|
427
587
|
}
|
|
428
588
|
|
|
429
589
|
// 4. PROPAGATE and CHECKPOINT phases are handled by clusters automatically
|
|
430
590
|
// (as per user's note: "managed by each cluster, the client doesn't have to worry about them")
|
|
431
591
|
|
|
592
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms commit=%dms total=%dms', trxId, gatherMs, pendMs, commitMs, Date.now() - t0);
|
|
432
593
|
return { success: true };
|
|
433
594
|
}
|
|
434
595
|
|
|
@@ -518,6 +679,10 @@ export class TransactionCoordinator {
|
|
|
518
679
|
// Pend the transaction
|
|
519
680
|
const pendResult = await this.transactor.pend(pendRequest);
|
|
520
681
|
if (!pendResult.success) {
|
|
682
|
+
// Cancel any already-pended collections before returning
|
|
683
|
+
for (const [pendedCollectionId, pendedBlockIdList] of pendedBlockIds.entries()) {
|
|
684
|
+
await this.transactor.cancel({ actionId, blockIds: pendedBlockIdList });
|
|
685
|
+
}
|
|
521
686
|
return {
|
|
522
687
|
success: false,
|
|
523
688
|
error: `Pend failed for collection ${collectionId}: ${pendResult.reason}`
|
|
@@ -532,18 +697,36 @@ export class TransactionCoordinator {
|
|
|
532
697
|
}
|
|
533
698
|
|
|
534
699
|
/**
|
|
535
|
-
* COMMIT phase: Commit to all critical blocks.
|
|
700
|
+
* COMMIT phase: Commit to all critical blocks with retry for transient failures.
|
|
701
|
+
*
|
|
702
|
+
* Once all collections are pended (Phase 1 passes), the coordinator has decided
|
|
703
|
+
* to commit. Failed commits are retried (forward recovery) before giving up.
|
|
704
|
+
* Returns which collections committed vs failed so the caller can do targeted cancel.
|
|
536
705
|
*/
|
|
537
706
|
private async commitPhase(
|
|
538
707
|
actionId: ActionId,
|
|
539
708
|
criticalBlockIds: BlockId[],
|
|
540
709
|
pendedBlockIds: Map<CollectionId, BlockId[]>
|
|
541
|
-
): Promise<{
|
|
542
|
-
|
|
710
|
+
): Promise<{
|
|
711
|
+
success: boolean;
|
|
712
|
+
error?: string;
|
|
713
|
+
committedCollections: Set<CollectionId>;
|
|
714
|
+
failedCollections: Set<CollectionId>;
|
|
715
|
+
}> {
|
|
716
|
+
const committedCollections = new Set<CollectionId>();
|
|
717
|
+
const failedCollections = new Set<CollectionId>();
|
|
718
|
+
|
|
719
|
+
// Commit each collection's transaction with retry
|
|
543
720
|
for (const [collectionId, blockIds] of pendedBlockIds.entries()) {
|
|
544
721
|
const collection = this.collections.get(collectionId);
|
|
545
722
|
if (!collection) {
|
|
546
|
-
|
|
723
|
+
failedCollections.add(collectionId);
|
|
724
|
+
return {
|
|
725
|
+
success: false,
|
|
726
|
+
error: `Collection not found: ${collectionId}`,
|
|
727
|
+
committedCollections,
|
|
728
|
+
failedCollections
|
|
729
|
+
};
|
|
547
730
|
}
|
|
548
731
|
|
|
549
732
|
// Get revision
|
|
@@ -555,9 +738,12 @@ export class TransactionCoordinator {
|
|
|
555
738
|
);
|
|
556
739
|
|
|
557
740
|
if (!logTailBlockId) {
|
|
741
|
+
failedCollections.add(collectionId);
|
|
558
742
|
return {
|
|
559
743
|
success: false,
|
|
560
|
-
error: `Log tail block not found for collection ${collectionId}
|
|
744
|
+
error: `Log tail block not found for collection ${collectionId}`,
|
|
745
|
+
committedCollections,
|
|
746
|
+
failedCollections
|
|
561
747
|
};
|
|
562
748
|
}
|
|
563
749
|
|
|
@@ -569,41 +755,42 @@ export class TransactionCoordinator {
|
|
|
569
755
|
rev
|
|
570
756
|
};
|
|
571
757
|
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
758
|
+
// Retry up to 3 attempts for transient failures
|
|
759
|
+
let committed = false;
|
|
760
|
+
for (let attempt = 0; attempt < 3 && !committed; attempt++) {
|
|
761
|
+
const commitResult = await this.transactor.commit(commitRequest);
|
|
762
|
+
if (commitResult.success) {
|
|
763
|
+
committed = true;
|
|
764
|
+
committedCollections.add(collectionId);
|
|
765
|
+
} else if (attempt === 2) {
|
|
766
|
+
failedCollections.add(collectionId);
|
|
767
|
+
return {
|
|
768
|
+
success: false,
|
|
769
|
+
error: `Commit failed for collection ${collectionId} after 3 attempts`,
|
|
770
|
+
committedCollections,
|
|
771
|
+
failedCollections
|
|
772
|
+
};
|
|
773
|
+
}
|
|
579
774
|
}
|
|
580
775
|
}
|
|
581
776
|
|
|
582
|
-
return { success: true };
|
|
777
|
+
return { success: true, committedCollections, failedCollections };
|
|
583
778
|
}
|
|
584
779
|
|
|
585
780
|
/**
|
|
586
|
-
* CANCEL phase: Cancel pending actions on
|
|
781
|
+
* CANCEL phase: Cancel pending actions on affected blocks.
|
|
782
|
+
*
|
|
783
|
+
* Uses the authoritative pended block IDs from pendPhase rather than
|
|
784
|
+
* recomputing from transforms. Optionally skips already-committed collections.
|
|
587
785
|
*/
|
|
588
786
|
private async cancelPhase(
|
|
589
787
|
actionId: ActionId,
|
|
590
|
-
|
|
788
|
+
pendedBlockIds: Map<CollectionId, BlockId[]>,
|
|
789
|
+
excludeCollections?: Set<CollectionId>
|
|
591
790
|
): Promise<void> {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (!collection) {
|
|
596
|
-
continue; // Skip if collection not found
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Get the block IDs from transforms
|
|
600
|
-
const blockIds = blockIdsForTransforms(transforms);
|
|
601
|
-
|
|
602
|
-
// Cancel the transaction
|
|
603
|
-
await this.transactor.cancel({
|
|
604
|
-
actionId,
|
|
605
|
-
blockIds
|
|
606
|
-
});
|
|
791
|
+
for (const [collectionId, blockIds] of pendedBlockIds.entries()) {
|
|
792
|
+
if (excludeCollections?.has(collectionId)) continue;
|
|
793
|
+
await this.transactor.cancel({ actionId, blockIds });
|
|
607
794
|
}
|
|
608
795
|
}
|
|
609
796
|
|
package/src/transaction/index.ts
CHANGED
|
@@ -14,7 +14,9 @@ export type {
|
|
|
14
14
|
export {
|
|
15
15
|
createTransactionStamp,
|
|
16
16
|
createTransactionId,
|
|
17
|
-
createActionsStatements
|
|
17
|
+
createActionsStatements,
|
|
18
|
+
DEFAULT_TRANSACTION_TTL_MS,
|
|
19
|
+
isTransactionExpired
|
|
18
20
|
} from './transaction.js';
|
|
19
21
|
|
|
20
22
|
export {
|
|
@@ -26,4 +28,4 @@ export { TransactionCoordinator } from './coordinator.js';
|
|
|
26
28
|
export { TransactionContext } from './context.js';
|
|
27
29
|
export { TransactionSession } from './session.js';
|
|
28
30
|
export { TransactionValidator } from './validator.js';
|
|
29
|
-
export type { EngineRegistration, ValidationCoordinatorFactory } from './validator.js';
|
|
31
|
+
export type { EngineRegistration, ValidationCoordinatorFactory, BlockStateProvider } from './validator.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TransactionCoordinator } from "./coordinator.js";
|
|
2
2
|
import type { Transaction, ExecutionResult, ITransactionEngine, TransactionStamp, CollectionActions } from "./transaction.js";
|
|
3
|
-
import { createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
3
|
+
import { createTransactionStamp, createTransactionId, isTransactionExpired } from "./transaction.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* TransactionSession manages incremental transaction building.
|
|
@@ -14,7 +14,7 @@ import { createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
|
14
14
|
* - The Transaction is then committed through coordinator.commit() for PEND/COMMIT orchestration
|
|
15
15
|
*
|
|
16
16
|
* Usage:
|
|
17
|
-
* const session =
|
|
17
|
+
* const session = await TransactionSession.create(coordinator, engine);
|
|
18
18
|
* await session.execute('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Alice']);
|
|
19
19
|
* await session.execute('SELECT * FROM orders WHERE user_id = ?', [1]);
|
|
20
20
|
* const result = await session.commit();
|
|
@@ -27,19 +27,33 @@ export class TransactionSession {
|
|
|
27
27
|
private committed = false;
|
|
28
28
|
private rolledBack = false;
|
|
29
29
|
|
|
30
|
-
constructor(
|
|
30
|
+
private constructor(
|
|
31
31
|
private readonly coordinator: TransactionCoordinator,
|
|
32
32
|
private readonly engine: ITransactionEngine,
|
|
33
|
-
|
|
34
|
-
schemaHash: string = '' // TODO: Get from engine
|
|
33
|
+
stamp: TransactionStamp
|
|
35
34
|
) {
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
this.stamp = stamp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new TransactionSession.
|
|
40
|
+
* Uses async factory because stamp creation requires SHA-256 hashing.
|
|
41
|
+
*/
|
|
42
|
+
static async create(
|
|
43
|
+
coordinator: TransactionCoordinator,
|
|
44
|
+
engine: ITransactionEngine,
|
|
45
|
+
peerId: string = 'local',
|
|
46
|
+
schemaHash: string = '',
|
|
47
|
+
ttlMs?: number
|
|
48
|
+
): Promise<TransactionSession> {
|
|
49
|
+
const stamp = await createTransactionStamp(
|
|
38
50
|
peerId,
|
|
39
51
|
Date.now(),
|
|
40
52
|
schemaHash,
|
|
41
|
-
'unknown' // TODO: Get engine ID from engine
|
|
53
|
+
'unknown', // TODO: Get engine ID from engine
|
|
54
|
+
ttlMs
|
|
42
55
|
);
|
|
56
|
+
return new TransactionSession(coordinator, engine, stamp);
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
@@ -107,28 +121,37 @@ export class TransactionSession {
|
|
|
107
121
|
if (this.rolledBack) {
|
|
108
122
|
return { success: false, error: 'Transaction already rolled back' };
|
|
109
123
|
}
|
|
124
|
+
if (isTransactionExpired(this.stamp)) {
|
|
125
|
+
return { success: false, error: `Transaction expired at ${this.stamp.expiration}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Collect read dependencies from all participating collections
|
|
129
|
+
const reads = this.coordinator.getReadDependencies();
|
|
110
130
|
|
|
111
131
|
// Create the complete transaction
|
|
112
132
|
const transaction: Transaction = {
|
|
113
133
|
stamp: this.stamp,
|
|
114
134
|
statements: this.statements,
|
|
115
|
-
reads
|
|
116
|
-
id: createTransactionId(this.stamp.id, this.statements,
|
|
135
|
+
reads,
|
|
136
|
+
id: await createTransactionId(this.stamp.id, this.statements, reads)
|
|
117
137
|
};
|
|
118
138
|
|
|
119
139
|
// Commit through coordinator (which will orchestrate PEND/COMMIT)
|
|
120
140
|
await this.coordinator.commit(transaction);
|
|
121
141
|
|
|
142
|
+
// Clear read dependencies after successful commit
|
|
143
|
+
this.coordinator.clearReadDependencies();
|
|
144
|
+
|
|
122
145
|
this.committed = true;
|
|
123
146
|
return { success: true };
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
/**
|
|
127
|
-
* Rollback the transaction (
|
|
150
|
+
* Rollback the transaction (undo this session's applied actions).
|
|
128
151
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
152
|
+
* Delegates to coordinator.rollback(stampId) which restores collection
|
|
153
|
+
* trackers to the pre-session snapshot and replays any later sessions'
|
|
154
|
+
* actions to preserve their transforms.
|
|
132
155
|
*/
|
|
133
156
|
async rollback(): Promise<void> {
|
|
134
157
|
if (this.committed) {
|
|
@@ -140,6 +163,7 @@ export class TransactionSession {
|
|
|
140
163
|
|
|
141
164
|
// Rollback through coordinator
|
|
142
165
|
await this.coordinator.rollback(this.stamp.id);
|
|
166
|
+
this.coordinator.clearReadDependencies();
|
|
143
167
|
this.rolledBack = true;
|
|
144
168
|
this.statements.length = 0;
|
|
145
169
|
}
|