@dbos-inc/drizzle-datasource 5.2.4-preview → 5.2.6-preview
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/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/index.ts +11 -2
- package/package.json +1 -1
- package/tests/custom-pool.test.ts +2 -4
- package/tests/datasource.test.ts +156 -45
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAQ,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAQ,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAYL,cAAc,EAIf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAW,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,mBAAmB,EAAE,gBAAgB,GAAG,YAAY,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI/G,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAoOD,qBAAa,iBAAiB,CAAC,EAAE,GAAG,cAAc,CAAC;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,CAC3E,YAAW,cAAc,CAAC,iBAAiB,CAAC;;IA+C1C,QAAQ,CAAC,IAAI,EAAE,MAAM;IA9BvB,MAAM,KAAK,MAAM;;OAEhB;IAED,IAAI,MAAM,OAET;WAEY,oBAAoB,CAC/B,cAAc,EAAE,YAAY,GAAG,MAAM,GAAG,UAAU,EAClD,UAAU,GAAE,MAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;gBAmBL,IAAI,EAAE,MAAM,EACrB,YAAY,EAAE,UAAU,GAAG,IAAI,EAC/B,QAAQ,GAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO,EACxC,UAAU,GAAE,MAAe;IAMvB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,iBAAiB;IAI1E,mBAAmB,CAAC,IAAI,EAAE,IAAI,SAAS,OAAO,EAAE,EAAE,MAAM,EACtD,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,EACpD,MAAM,CAAC,EAAE,iBAAiB,GAAG,YAAY,GACxC,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAKjD,WAAW,CAAC,MAAM,CAAC,EAAE,iBAAiB,kDAI1B,MAAM,eACD,WAAW,cACZ,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,QAAQ,MAAM,CAAC,CAAC,oCAAxC,IAAI,WAAW,IAAI,KAAK,QAAQ,MAAM,CAAC;CAevF"}
|
package/dist/index.js
CHANGED
|
@@ -63,10 +63,13 @@ class DrizzleTransactionHandler {
|
|
|
63
63
|
}
|
|
64
64
|
return this.#connection.db;
|
|
65
65
|
}
|
|
66
|
-
async deleteCheckpoints(workflowID, startStep) {
|
|
67
|
-
await this.#drizzle.
|
|
66
|
+
async deleteCheckpoints(workflowID, startStep, beforeCommit) {
|
|
67
|
+
await this.#drizzle.transaction(async (client) => {
|
|
68
|
+
await client.execute((0, drizzle_orm_1.sql) `
|
|
68
69
|
DELETE FROM ${drizzle_orm_1.sql.identifier(this.schemaName)}.transaction_completion
|
|
69
70
|
WHERE workflow_id = ${workflowID} AND function_num >= ${startStep}`);
|
|
71
|
+
await beforeCommit?.();
|
|
72
|
+
});
|
|
70
73
|
}
|
|
71
74
|
async #checkExecution(workflowID, stepID) {
|
|
72
75
|
const statement = (0, drizzle_orm_1.sql) `
|
|
@@ -89,6 +92,8 @@ class DrizzleTransactionHandler {
|
|
|
89
92
|
if (rows.length === 0) {
|
|
90
93
|
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
91
94
|
}
|
|
95
|
+
// Holding this step's row, so a later owner's insert waits on our commit.
|
|
96
|
+
await (0, datasource_1.assertStillOwnsWorkflow)(workflowID);
|
|
92
97
|
}
|
|
93
98
|
async #recordError(workflowID, stepID, error) {
|
|
94
99
|
const statement = (0, drizzle_orm_1.sql) `
|
|
@@ -146,6 +151,9 @@ class DrizzleTransactionHandler {
|
|
|
146
151
|
if (saveResults && error instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
147
152
|
return await this.#replayConflictingStep(workflowID, stepID);
|
|
148
153
|
}
|
|
154
|
+
// The new owner wins; recording an error here would replay it as this step's outcome.
|
|
155
|
+
if (error instanceof datasource_1.DBOSWorkflowConflictError)
|
|
156
|
+
throw error;
|
|
149
157
|
if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
|
|
150
158
|
dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
151
159
|
// Retry serialization failures.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AAAA,2BAAwE;AACxE,iDAAwD;AACxD,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
isPGRetriableTransactionError,
|
|
8
8
|
DBOSError,
|
|
9
9
|
DBOSStepAlreadyRecordedError,
|
|
10
|
+
DBOSWorkflowConflictError,
|
|
11
|
+
assertStillOwnsWorkflow,
|
|
10
12
|
replayRecordedStep,
|
|
11
13
|
registerTransaction,
|
|
12
14
|
runTransaction,
|
|
@@ -107,10 +109,13 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
107
109
|
return this.#connection.db;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
async deleteCheckpoints(workflowID: string, startStep: number): Promise<void> {
|
|
111
|
-
await this.#drizzle.
|
|
112
|
+
async deleteCheckpoints(workflowID: string, startStep: number, beforeCommit?: () => Promise<void>): Promise<void> {
|
|
113
|
+
await this.#drizzle.transaction(async (client) => {
|
|
114
|
+
await client.execute(sql`
|
|
112
115
|
DELETE FROM ${sql.identifier(this.schemaName)}.transaction_completion
|
|
113
116
|
WHERE workflow_id = ${workflowID} AND function_num >= ${startStep}`);
|
|
117
|
+
await beforeCommit?.();
|
|
118
|
+
});
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
async #checkExecution(
|
|
@@ -148,6 +153,8 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
148
153
|
if (rows.length === 0) {
|
|
149
154
|
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
150
155
|
}
|
|
156
|
+
// Holding this step's row, so a later owner's insert waits on our commit.
|
|
157
|
+
await assertStillOwnsWorkflow(workflowID);
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
|
|
@@ -230,6 +237,8 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
230
237
|
if (saveResults && error instanceof DBOSStepAlreadyRecordedError) {
|
|
231
238
|
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
232
239
|
}
|
|
240
|
+
// The new owner wins; recording an error here would replay it as this step's outcome.
|
|
241
|
+
if (error instanceof DBOSWorkflowConflictError) throw error;
|
|
233
242
|
if (isPGRetriableTransactionError(error)) {
|
|
234
243
|
DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
235
244
|
// Retry serialization failures.
|
package/package.json
CHANGED
|
@@ -3,7 +3,6 @@ import { Client, Pool } from 'pg';
|
|
|
3
3
|
import { DrizzleDataSource } from '..';
|
|
4
4
|
import { dropDB, ensureDB } from './test-helpers';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
|
-
import SuperJSON from 'superjson';
|
|
7
6
|
import { pgTable, text, integer } from 'drizzle-orm/pg-core';
|
|
8
7
|
import { sql } from 'drizzle-orm';
|
|
9
8
|
|
|
@@ -106,9 +105,8 @@ describe('DrizzleDataSource with custom Pool', () => {
|
|
|
106
105
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
107
106
|
[workflowID],
|
|
108
107
|
);
|
|
109
|
-
|
|
110
|
-
expect(rows
|
|
111
|
-
expect(SuperJSON.parse(rows[0].output!)).toMatchObject({ user, greet_count: 1 });
|
|
108
|
+
// Completion cleared the checkpoint the transaction wrote.
|
|
109
|
+
expect(rows).toHaveLength(0);
|
|
112
110
|
});
|
|
113
111
|
|
|
114
112
|
test('transaction runs against the custom pool database', async () => {
|
package/tests/datasource.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
1
|
+
import { DBOS, DBOSWorkflowConflictError } from '@dbos-inc/dbos-sdk';
|
|
2
2
|
import { Client, Pool, PoolConfig } from 'pg';
|
|
3
3
|
import { DrizzleDataSource } from '..';
|
|
4
4
|
import { dropDB, ensureDB } from './test-helpers';
|
|
@@ -64,11 +64,8 @@ describe('DrizzleDataSource', () => {
|
|
|
64
64
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
65
65
|
[workflowID],
|
|
66
66
|
);
|
|
67
|
-
|
|
68
|
-
expect(rows
|
|
69
|
-
expect(rows[0].function_num).toBe(0);
|
|
70
|
-
expect(rows[0].output).not.toBeNull();
|
|
71
|
-
expect(SuperJSON.parse(rows[0].output!)).toMatchObject({ user, greet_count: 1 });
|
|
67
|
+
// Completion cleared the checkpoint the transaction wrote.
|
|
68
|
+
expect(rows).toHaveLength(0);
|
|
72
69
|
});
|
|
73
70
|
|
|
74
71
|
test('rerun insert dataSource.register function', async () => {
|
|
@@ -98,11 +95,8 @@ describe('DrizzleDataSource', () => {
|
|
|
98
95
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
99
96
|
[workflowID],
|
|
100
97
|
);
|
|
101
|
-
|
|
102
|
-
expect(rows
|
|
103
|
-
expect(rows[0].function_num).toBe(0);
|
|
104
|
-
expect(rows[0].output).not.toBeNull();
|
|
105
|
-
expect(SuperJSON.parse(rows[0].output!)).toMatchObject({ user, greet_count: 1 });
|
|
98
|
+
// Completion cleared the checkpoint the transaction wrote.
|
|
99
|
+
expect(rows).toHaveLength(0);
|
|
106
100
|
});
|
|
107
101
|
|
|
108
102
|
test('rerun insert dataSource.runAsTx function', async () => {
|
|
@@ -151,14 +145,8 @@ describe('DrizzleDataSource', () => {
|
|
|
151
145
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
152
146
|
[workflowID],
|
|
153
147
|
);
|
|
154
|
-
|
|
155
|
-
expect(txOutput
|
|
156
|
-
expect(txOutput[0].function_num).toBe(0);
|
|
157
|
-
expect(txOutput[0].output).toBeNull();
|
|
158
|
-
expect(txOutput[0].error).not.toBeNull();
|
|
159
|
-
const $error = SuperJSON.parse(txOutput[0].error!);
|
|
160
|
-
expect($error).toBeInstanceOf(Error);
|
|
161
|
-
expect(($error as Error).message).toMatch(/^test error \d+$/);
|
|
148
|
+
// Completion cleared the checkpoint the transaction wrote.
|
|
149
|
+
expect(txOutput).toHaveLength(0);
|
|
162
150
|
});
|
|
163
151
|
|
|
164
152
|
test('rerun error dataSource.register function', async () => {
|
|
@@ -200,14 +188,8 @@ describe('DrizzleDataSource', () => {
|
|
|
200
188
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
201
189
|
[workflowID],
|
|
202
190
|
);
|
|
203
|
-
|
|
204
|
-
expect(txOutput
|
|
205
|
-
expect(txOutput[0].function_num).toBe(0);
|
|
206
|
-
expect(txOutput[0].output).toBeNull();
|
|
207
|
-
expect(txOutput[0].error).not.toBeNull();
|
|
208
|
-
const $error = SuperJSON.parse(txOutput[0].error!);
|
|
209
|
-
expect($error).toBeInstanceOf(Error);
|
|
210
|
-
expect(($error as Error).message).toMatch(/^test error \d+$/);
|
|
191
|
+
// Completion cleared the checkpoint the transaction wrote.
|
|
192
|
+
expect(txOutput).toHaveLength(0);
|
|
211
193
|
});
|
|
212
194
|
|
|
213
195
|
test('rerun error dataSource.runAsTx function', async () => {
|
|
@@ -350,25 +332,68 @@ describe('DrizzleDataSource', () => {
|
|
|
350
332
|
expect((winnerError as Error).message).toBe('winner-error');
|
|
351
333
|
expect(raceState.callCount).toBe(3);
|
|
352
334
|
|
|
353
|
-
// Every loser's writes were discarded and the winners'
|
|
335
|
+
// Every loser's writes were discarded, and completion cleared the winners' checkpoints.
|
|
354
336
|
const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
|
|
355
337
|
expect(tags).toHaveLength(0);
|
|
356
338
|
const { rows: txOutput } = await userDB.query<transaction_completion>(
|
|
357
339
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = ANY($1)',
|
|
358
|
-
[[wfid1, wfid2]],
|
|
340
|
+
[[wfid1, wfid2, wfid3]],
|
|
359
341
|
);
|
|
360
|
-
expect(txOutput).toHaveLength(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
expect(txOutput).toHaveLength(0);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
/** Completion clears the checkpoints, so the workflow reads them back while it still runs. */
|
|
346
|
+
test('transactions record their outcomes as checkpoints', async () => {
|
|
347
|
+
const user = 'checkpointProbe';
|
|
348
|
+
await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
|
|
349
|
+
const workflowID = randomUUID();
|
|
350
|
+
const message = await DBOS.withNextWorkflowID(workflowID, () => regCheckpointProbeWorkflow(user));
|
|
351
|
+
expect(message).toMatch(/^test error \d+$/);
|
|
352
|
+
|
|
353
|
+
expect(probeState.rows).toHaveLength(2);
|
|
354
|
+
const [output, error] = probeState.rows;
|
|
355
|
+
expect(output).toMatchObject({ workflow_id: workflowID, function_num: 0, error: null });
|
|
356
|
+
expect(SuperJSON.parse(output.output!)).toMatchObject({ user, greet_count: 1 });
|
|
357
|
+
expect(error).toMatchObject({ workflow_id: workflowID, function_num: 1, output: null });
|
|
358
|
+
expect(SuperJSON.parse<Error>(error.error!).message).toBe(message);
|
|
359
|
+
expect(await readCompletions(workflowID)).toHaveLength(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/** The checkpoint may be the new owner's only record of the transaction, so a stale execution's cleanup rolls back. */
|
|
363
|
+
test('a stale execution keeps the checkpoints at completion', async () => {
|
|
364
|
+
const user = 'handOffTest';
|
|
365
|
+
await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
|
|
366
|
+
const handedOff = new Promise<void>((resolve) => (handOffState.signal = resolve));
|
|
367
|
+
const workflowID = randomUUID();
|
|
368
|
+
const execution = DBOS.withNextWorkflowID(workflowID, () => regHandOffWorkflow(user));
|
|
369
|
+
// Surfaces the workflow's own error if it fails before handing off.
|
|
370
|
+
await Promise.race([handedOff, execution]);
|
|
371
|
+
|
|
372
|
+
// Its outcome is refused too, so the execution parks until the workflow is cancelled.
|
|
373
|
+
await DBOS.cancelWorkflow(workflowID);
|
|
374
|
+
await expect(execution).rejects.toThrow(/has been cancelled/);
|
|
375
|
+
expect(await readCompletions(workflowID)).toHaveLength(1);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
/** A stale execution cannot apply a step the new owner also runs, nor leave an outcome for it. */
|
|
379
|
+
test('an execution that loses the workflow mid-transaction rolls back', async () => {
|
|
380
|
+
await userDB.query('DELETE FROM race_side_effects');
|
|
381
|
+
const workflowID = randomUUID();
|
|
382
|
+
await expect(DBOS.withNextWorkflowID(workflowID, () => regStaleWorkflow())).resolves.toBe('conflicted');
|
|
383
|
+
|
|
384
|
+
const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
|
|
385
|
+
expect(tags).toHaveLength(0);
|
|
386
|
+
expect(staleState.completionsAtConflict).toBe(0);
|
|
387
|
+
const sysDB = new Client(sysConfig);
|
|
388
|
+
try {
|
|
389
|
+
await sysDB.connect();
|
|
390
|
+
const { rows: steps } = await sysDB.query('SELECT * FROM dbos.operation_outputs WHERE workflow_uuid = $1', [
|
|
391
|
+
workflowID,
|
|
392
|
+
]);
|
|
393
|
+
expect(steps).toHaveLength(0);
|
|
394
|
+
} finally {
|
|
395
|
+
await sysDB.end();
|
|
364
396
|
}
|
|
365
|
-
const { rows: txError } = await userDB.query<transaction_completion>(
|
|
366
|
-
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
367
|
-
[wfid3],
|
|
368
|
-
);
|
|
369
|
-
expect(txError).toHaveLength(1);
|
|
370
|
-
expect(txError[0].output).toBeNull(); // the loser never overwrote the winner's error with its own result
|
|
371
|
-
expect(SuperJSON.parse<Error>(txError[0].error!).message).toBe('winner-error');
|
|
372
397
|
});
|
|
373
398
|
});
|
|
374
399
|
|
|
@@ -470,6 +495,95 @@ async function readFunction(user: string) {
|
|
|
470
495
|
|
|
471
496
|
const regInsertFunction = dataSource.registerTransaction(insertFunction);
|
|
472
497
|
const regErrorFunction = dataSource.registerTransaction(errorFunction);
|
|
498
|
+
|
|
499
|
+
const sysConfig = { ...config, database: 'drizzle_ds_test_dbos_sys' };
|
|
500
|
+
|
|
501
|
+
/** Set the workflow's ownership token, returning the one it replaced. */
|
|
502
|
+
async function setOwner(workflowID: string, ownerXid: string | null): Promise<string | null> {
|
|
503
|
+
const sysDB = new Client(sysConfig);
|
|
504
|
+
try {
|
|
505
|
+
await sysDB.connect();
|
|
506
|
+
const { rows } = await sysDB.query<{ owner_xid: string | null }>(
|
|
507
|
+
'SELECT owner_xid FROM dbos.workflow_status WHERE workflow_uuid = $1',
|
|
508
|
+
[workflowID],
|
|
509
|
+
);
|
|
510
|
+
await sysDB.query('UPDATE dbos.workflow_status SET owner_xid = $2 WHERE workflow_uuid = $1', [
|
|
511
|
+
workflowID,
|
|
512
|
+
ownerXid,
|
|
513
|
+
]);
|
|
514
|
+
return rows[0].owner_xid;
|
|
515
|
+
} finally {
|
|
516
|
+
await sysDB.end();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** The checkpoints this data source holds for the workflow, read on a connection of their own. */
|
|
521
|
+
async function readCompletions(workflowID: string): Promise<transaction_completion[]> {
|
|
522
|
+
const client = new Client(config);
|
|
523
|
+
try {
|
|
524
|
+
await client.connect();
|
|
525
|
+
const { rows } = await client.query<transaction_completion>(
|
|
526
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1 ORDER BY function_num',
|
|
527
|
+
[workflowID],
|
|
528
|
+
);
|
|
529
|
+
return rows;
|
|
530
|
+
} finally {
|
|
531
|
+
await client.end();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const probeState = { rows: [] as transaction_completion[] };
|
|
536
|
+
|
|
537
|
+
// Reads back the checkpoints its transactions wrote, before completion clears them.
|
|
538
|
+
async function checkpointProbeWorkflow(user: string) {
|
|
539
|
+
await regInsertFunction(user);
|
|
540
|
+
const error = await regErrorFunction(user).then(
|
|
541
|
+
() => undefined,
|
|
542
|
+
(e: Error) => e,
|
|
543
|
+
);
|
|
544
|
+
probeState.rows = await readCompletions(DBOS.workflowID!);
|
|
545
|
+
return error?.message;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const regCheckpointProbeWorkflow = DBOS.registerWorkflow(checkpointProbeWorkflow);
|
|
549
|
+
|
|
550
|
+
const staleState = { originalOwner: null as string | null, completionsAtConflict: -1 };
|
|
551
|
+
|
|
552
|
+
// Another execution claims the workflow while this transaction is still open.
|
|
553
|
+
async function staleFunction() {
|
|
554
|
+
await dataSource.client.execute(sql`INSERT INTO race_side_effects(tag) VALUES ('stale')`);
|
|
555
|
+
staleState.originalOwner = await setOwner(DBOS.workflowID!, 'another-execution');
|
|
556
|
+
return 'stale';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const regStaleFunction = dataSource.registerTransaction(staleFunction);
|
|
560
|
+
|
|
561
|
+
async function staleWorkflow() {
|
|
562
|
+
try {
|
|
563
|
+
return await regStaleFunction();
|
|
564
|
+
} catch (e) {
|
|
565
|
+
if (!(e instanceof DBOSWorkflowConflictError)) throw e;
|
|
566
|
+
// Read before completion clears it: the conflict must not have been recorded as the step's outcome.
|
|
567
|
+
staleState.completionsAtConflict = (await readCompletions(DBOS.workflowID!)).length;
|
|
568
|
+
// A real duplicate parks here instead; reclaimed so this execution's outcome lands.
|
|
569
|
+
await setOwner(DBOS.workflowID!, staleState.originalOwner);
|
|
570
|
+
return 'conflicted';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const regStaleWorkflow = DBOS.registerWorkflow(staleWorkflow);
|
|
575
|
+
|
|
576
|
+
const handOffState = { signal: () => {} };
|
|
577
|
+
|
|
578
|
+
// Loses the workflow after its transaction, so its completion is a stale execution's.
|
|
579
|
+
async function handOffWorkflow(user: string) {
|
|
580
|
+
const result = await regInsertFunction(user);
|
|
581
|
+
await setOwner(DBOS.workflowID!, 'another-execution');
|
|
582
|
+
handOffState.signal();
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const regHandOffWorkflow = DBOS.registerWorkflow(handOffWorkflow);
|
|
473
587
|
const regReadFunction = dataSource.registerTransaction(readFunction, { accessMode: 'read only' });
|
|
474
588
|
|
|
475
589
|
class StaticClass {
|
|
@@ -588,10 +702,7 @@ describe('DrizzleDataSourceCreateTxC', () => {
|
|
|
588
702
|
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
589
703
|
[workflowID],
|
|
590
704
|
);
|
|
591
|
-
|
|
592
|
-
expect(rows
|
|
593
|
-
expect(rows[0].function_num).toBe(0);
|
|
594
|
-
expect(rows[0].output).not.toBeNull();
|
|
595
|
-
expect(SuperJSON.parse(rows[0].output!)).toMatchObject({ user, greet_count: 1 });
|
|
705
|
+
// Completion cleared the checkpoint the transaction wrote.
|
|
706
|
+
expect(rows).toHaveLength(0);
|
|
596
707
|
});
|
|
597
708
|
});
|