@dbos-inc/node-pg-datasource 4.26.6-preview → 4.26.8-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 +36 -31
- package/dist/index.js.map +1 -1
- package/index.ts +46 -35
- package/package.json +1 -1
- package/tests/datasource.test.ts +95 -0
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAUL,mBAAmB,EACnB,cAAc,EAIf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAU,KAAK,UAAU,EAAE,KAAK,YAAY,EAAQ,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AASvF,UAAU,8BAA+B,SAAQ,mBAAmB;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,OAAO,EAAE,8BAA8B,EAAE,CAAC;AAoO1C,qBAAa,sBAAuB,YAAW,cAAc,CAAC,8BAA8B,CAAC;;IAqCzF,QAAQ,CAAC,IAAI,EAAE,MAAM;IAtBvB,MAAM,KAAK,MAAM,eAEhB;IAED,IAAI,MAAM,eAET;WAEY,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,GAAE,MAAe,GAAG,OAAO,CAAC,IAAI,CAAC;gBAcxF,IAAI,EAAE,MAAM,EACrB,MAAM,EAAE,UAAU,EAClB,UAAU,GAAE,MAAe;IAMvB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,8BAA8B;IAIvF,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,8BAA8B,GAAG,YAAY,GACrD,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAIjD,WAAW,CAAC,MAAM,CAAC,EAAE,8BAA8B,kDAIvC,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
|
@@ -72,36 +72,34 @@ class NodePostgresTransactionHandler {
|
|
|
72
72
|
return error !== null ? { error } : { output };
|
|
73
73
|
}
|
|
74
74
|
static async #recordOutput(client, workflowID, stepID, output, schemaName) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
75
|
+
const { rows } = await client.query(
|
|
76
|
+
/*sql*/
|
|
77
|
+
`INSERT INTO "${schemaName}".transaction_completion (workflow_id, function_num, output)
|
|
78
|
+
VALUES ($1, $2, $3)
|
|
79
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
80
|
+
RETURNING workflow_id`, [workflowID, stepID, output]);
|
|
81
|
+
if (rows.length === 0) {
|
|
82
|
+
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
88
83
|
}
|
|
89
84
|
}
|
|
90
85
|
async #recordError(workflowID, stepID, error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
const { rows } = await this.#pool.query(
|
|
87
|
+
/*sql*/
|
|
88
|
+
`INSERT INTO "${this.schemaName}".transaction_completion (workflow_id, function_num, error)
|
|
89
|
+
VALUES ($1, $2, $3)
|
|
90
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
91
|
+
RETURNING workflow_id`, [workflowID, stepID, error]);
|
|
92
|
+
if (rows.length === 0) {
|
|
93
|
+
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
96
94
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
95
|
+
}
|
|
96
|
+
// A duplicate execution won the race, so its recorded outcome is the durable one.
|
|
97
|
+
async #replayConflictingStep(workflowID, stepID) {
|
|
98
|
+
const recorded = await this.#checkExecution(workflowID, stepID);
|
|
99
|
+
if (recorded === undefined) {
|
|
100
|
+
throw new datasource_1.DBOSError(`Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`);
|
|
104
101
|
}
|
|
102
|
+
return (0, datasource_1.replayRecordedStep)(recorded);
|
|
105
103
|
}
|
|
106
104
|
async #transaction(func, config = {}) {
|
|
107
105
|
const isolationLevel = config?.isolationLevel ? `ISOLATION LEVEL ${config.isolationLevel}` : '';
|
|
@@ -138,11 +136,7 @@ class NodePostgresTransactionHandler {
|
|
|
138
136
|
// Check to see if this tx has already been executed
|
|
139
137
|
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID) : undefined;
|
|
140
138
|
if (previousResult) {
|
|
141
|
-
|
|
142
|
-
if ('error' in previousResult) {
|
|
143
|
-
throw superjson_1.SuperJSON.parse(previousResult.error);
|
|
144
|
-
}
|
|
145
|
-
return (previousResult.output ? superjson_1.SuperJSON.parse(previousResult.output) : null);
|
|
139
|
+
return (0, datasource_1.replayRecordedStep)(previousResult);
|
|
146
140
|
}
|
|
147
141
|
try {
|
|
148
142
|
const result = await this.#transaction(async (client) => {
|
|
@@ -159,6 +153,9 @@ class NodePostgresTransactionHandler {
|
|
|
159
153
|
return result;
|
|
160
154
|
}
|
|
161
155
|
catch (error) {
|
|
156
|
+
if (saveResults && error instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
157
|
+
return await this.#replayConflictingStep(workflowID, stepID);
|
|
158
|
+
}
|
|
162
159
|
if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
|
|
163
160
|
dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
164
161
|
await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
|
|
@@ -168,7 +165,15 @@ class NodePostgresTransactionHandler {
|
|
|
168
165
|
else {
|
|
169
166
|
if (saveResults) {
|
|
170
167
|
const message = superjson_1.SuperJSON.stringify(error);
|
|
171
|
-
|
|
168
|
+
try {
|
|
169
|
+
await this.#recordError(workflowID, stepID, message);
|
|
170
|
+
}
|
|
171
|
+
catch (recordError) {
|
|
172
|
+
if (recordError instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
173
|
+
return await this.#replayConflictingStep(workflowID, stepID);
|
|
174
|
+
}
|
|
175
|
+
throw recordError;
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
throw error;
|
|
174
179
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";AAAA,gDAAgD;;;AAEhD,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// using https://github.com/brianc/node-postgres
|
|
2
2
|
|
|
3
|
-
import { DBOS,
|
|
3
|
+
import { DBOS, FunctionName } from '@dbos-inc/dbos-sdk';
|
|
4
4
|
import {
|
|
5
5
|
type DataSourceTransactionHandler,
|
|
6
6
|
createTransactionCompletionSchemaPG,
|
|
7
7
|
createTransactionCompletionTablePG,
|
|
8
8
|
isPGRetriableTransactionError,
|
|
9
|
-
|
|
9
|
+
DBOSError,
|
|
10
|
+
DBOSStepAlreadyRecordedError,
|
|
11
|
+
replayRecordedStep,
|
|
10
12
|
registerTransaction,
|
|
11
13
|
runTransaction,
|
|
12
14
|
PGTransactionConfig,
|
|
@@ -118,37 +120,42 @@ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
118
120
|
output: string | null,
|
|
119
121
|
schemaName: string,
|
|
120
122
|
): Promise<void> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
123
|
+
const { rows } = await client.query(
|
|
124
|
+
/*sql*/
|
|
125
|
+
`INSERT INTO "${schemaName}".transaction_completion (workflow_id, function_num, output)
|
|
126
|
+
VALUES ($1, $2, $3)
|
|
127
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
128
|
+
RETURNING workflow_id`,
|
|
129
|
+
[workflowID, stepID, output],
|
|
130
|
+
);
|
|
131
|
+
if (rows.length === 0) {
|
|
132
|
+
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
const { rows } = await this.#pool.query(
|
|
138
|
+
/*sql*/
|
|
139
|
+
`INSERT INTO "${this.schemaName}".transaction_completion (workflow_id, function_num, error)
|
|
140
|
+
VALUES ($1, $2, $3)
|
|
141
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
142
|
+
RETURNING workflow_id`,
|
|
143
|
+
[workflowID, stepID, error],
|
|
144
|
+
);
|
|
145
|
+
if (rows.length === 0) {
|
|
146
|
+
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A duplicate execution won the race, so its recorded outcome is the durable one.
|
|
151
|
+
async #replayConflictingStep<Return>(workflowID: string, stepID: number): Promise<Return> {
|
|
152
|
+
const recorded = await this.#checkExecution(workflowID, stepID);
|
|
153
|
+
if (recorded === undefined) {
|
|
154
|
+
throw new DBOSError(
|
|
155
|
+
`Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`,
|
|
144
156
|
);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (isPGKeyConflictError(error)) {
|
|
147
|
-
throw new DBOSWorkflowConflictError(workflowID);
|
|
148
|
-
} else {
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
157
|
}
|
|
158
|
+
return replayRecordedStep<Return>(recorded);
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
async #transaction<Return>(
|
|
@@ -197,13 +204,7 @@ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
197
204
|
// Check to see if this tx has already been executed
|
|
198
205
|
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
|
|
199
206
|
if (previousResult) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if ('error' in previousResult) {
|
|
203
|
-
throw SuperJSON.parse(previousResult.error);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
|
|
207
|
+
return replayRecordedStep<Return>(previousResult);
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
try {
|
|
@@ -229,6 +230,9 @@ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
229
230
|
|
|
230
231
|
return result;
|
|
231
232
|
} catch (error) {
|
|
233
|
+
if (saveResults && error instanceof DBOSStepAlreadyRecordedError) {
|
|
234
|
+
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
235
|
+
}
|
|
232
236
|
if (isPGRetriableTransactionError(error)) {
|
|
233
237
|
DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
234
238
|
await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
|
|
@@ -237,7 +241,14 @@ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
237
241
|
} else {
|
|
238
242
|
if (saveResults) {
|
|
239
243
|
const message = SuperJSON.stringify(error);
|
|
240
|
-
|
|
244
|
+
try {
|
|
245
|
+
await this.#recordError(workflowID, stepID!, message);
|
|
246
|
+
} catch (recordError) {
|
|
247
|
+
if (recordError instanceof DBOSStepAlreadyRecordedError) {
|
|
248
|
+
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
249
|
+
}
|
|
250
|
+
throw recordError;
|
|
251
|
+
}
|
|
241
252
|
}
|
|
242
253
|
|
|
243
254
|
throw error;
|
package/package.json
CHANGED
package/tests/datasource.test.ts
CHANGED
|
@@ -297,6 +297,57 @@ describe('NodePostgresDataSource', () => {
|
|
|
297
297
|
]);
|
|
298
298
|
expect(txResults.rows.length).toBe(0);
|
|
299
299
|
});
|
|
300
|
+
|
|
301
|
+
test('duplicate execution replays the winner result', async () => {
|
|
302
|
+
await userDB.query('DELETE FROM race_side_effects');
|
|
303
|
+
raceState.callCount = 0;
|
|
304
|
+
|
|
305
|
+
// A loser whose body succeeds: the collision happens on the result-recording insert.
|
|
306
|
+
raceState.plantWinner = true;
|
|
307
|
+
raceState.plantWinnerError = false;
|
|
308
|
+
raceState.fail = false;
|
|
309
|
+
const wfid1 = randomUUID();
|
|
310
|
+
await expect(DBOS.withNextWorkflowID(wfid1, () => regRaceWorkflow())).resolves.toBe('winner-result');
|
|
311
|
+
expect(raceState.callCount).toBe(1); // the loser did run its body
|
|
312
|
+
|
|
313
|
+
// A loser whose body fails: the collision moves to the error-recording insert,
|
|
314
|
+
// and the winner's result still wins over the loser's own error.
|
|
315
|
+
raceState.plantWinner = true;
|
|
316
|
+
raceState.plantWinnerError = false;
|
|
317
|
+
raceState.fail = true;
|
|
318
|
+
const wfid2 = randomUUID();
|
|
319
|
+
await expect(DBOS.withNextWorkflowID(wfid2, () => regRaceWorkflow())).resolves.toBe('winner-result');
|
|
320
|
+
expect(raceState.callCount).toBe(2);
|
|
321
|
+
|
|
322
|
+
// A winner that recorded an error: the loser replays that throw instead of its own result.
|
|
323
|
+
raceState.plantWinner = true;
|
|
324
|
+
raceState.plantWinnerError = true;
|
|
325
|
+
raceState.fail = false;
|
|
326
|
+
const wfid3 = randomUUID();
|
|
327
|
+
const winnerError = await throws(() => DBOS.withNextWorkflowID(wfid3, () => regRaceWorkflow()));
|
|
328
|
+
expect((winnerError as Error).message).toBe('winner-error');
|
|
329
|
+
expect(raceState.callCount).toBe(3);
|
|
330
|
+
|
|
331
|
+
// Every loser's writes were discarded and the winners' records still stand.
|
|
332
|
+
const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
|
|
333
|
+
expect(tags).toHaveLength(0);
|
|
334
|
+
const { rows: txOutput } = await userDB.query<transaction_completion>(
|
|
335
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = ANY($1)',
|
|
336
|
+
[[wfid1, wfid2]],
|
|
337
|
+
);
|
|
338
|
+
expect(txOutput).toHaveLength(2);
|
|
339
|
+
for (const row of txOutput) {
|
|
340
|
+
expect(row.error).toBeNull(); // no loser error was ever recorded
|
|
341
|
+
expect(SuperJSON.parse(row.output!)).toBe('winner-result');
|
|
342
|
+
}
|
|
343
|
+
const { rows: txError } = await userDB.query<transaction_completion>(
|
|
344
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
345
|
+
[wfid3],
|
|
346
|
+
);
|
|
347
|
+
expect(txError).toHaveLength(1);
|
|
348
|
+
expect(txError[0].output).toBeNull(); // the loser never overwrote the winner's error with its own result
|
|
349
|
+
expect(SuperJSON.parse<Error>(txError[0].error!).message).toBe('winner-error');
|
|
350
|
+
});
|
|
300
351
|
});
|
|
301
352
|
|
|
302
353
|
export interface greetings {
|
|
@@ -324,6 +375,7 @@ async function createDatabases(userDB: Pool, createTxCompletion: boolean) {
|
|
|
324
375
|
await client.query(
|
|
325
376
|
'CREATE TABLE greetings(name text NOT NULL, greet_count integer DEFAULT 0, PRIMARY KEY(name))',
|
|
326
377
|
);
|
|
378
|
+
await client.query('CREATE TABLE race_side_effects(tag text NOT NULL)');
|
|
327
379
|
} finally {
|
|
328
380
|
client.release();
|
|
329
381
|
}
|
|
@@ -352,6 +404,41 @@ async function errorFunction(user: string) {
|
|
|
352
404
|
throw new Error(`test error ${Date.now()}`);
|
|
353
405
|
}
|
|
354
406
|
|
|
407
|
+
const raceState = { callCount: 0, plantWinner: false, plantWinnerError: false, fail: false };
|
|
408
|
+
|
|
409
|
+
async function raceFunction() {
|
|
410
|
+
raceState.callCount += 1;
|
|
411
|
+
await NodePostgresDataSource.client.query('INSERT INTO race_side_effects(tag) VALUES ($1)', [
|
|
412
|
+
`run-${raceState.callCount}`,
|
|
413
|
+
]);
|
|
414
|
+
if (raceState.plantWinner) {
|
|
415
|
+
raceState.plantWinner = false;
|
|
416
|
+
await plantWinnerCompletion();
|
|
417
|
+
}
|
|
418
|
+
if (raceState.fail) {
|
|
419
|
+
throw new Error("loser's own failure");
|
|
420
|
+
}
|
|
421
|
+
return `result-${raceState.callCount}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// A duplicate execution commits its result while this transaction is still open, so this
|
|
425
|
+
// execution's pre-check missed it but its completion insert will collide with it.
|
|
426
|
+
async function plantWinnerCompletion() {
|
|
427
|
+
const winner = new Client(config);
|
|
428
|
+
try {
|
|
429
|
+
await winner.connect();
|
|
430
|
+
const [column, value] = raceState.plantWinnerError
|
|
431
|
+
? ['error', SuperJSON.stringify(new Error('winner-error'))]
|
|
432
|
+
: ['output', SuperJSON.stringify('winner-result')];
|
|
433
|
+
await winner.query(
|
|
434
|
+
`INSERT INTO dbos.transaction_completion (workflow_id, function_num, ${column}) VALUES ($1, $2, $3)`,
|
|
435
|
+
[DBOS.workflowID, DBOS.stepID, value],
|
|
436
|
+
);
|
|
437
|
+
} finally {
|
|
438
|
+
await winner.end();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
355
442
|
async function readFunction(user: string) {
|
|
356
443
|
const { rows } = await dataSource.client.query<Pick<greetings, 'greet_count'>>(
|
|
357
444
|
`SELECT greet_count
|
|
@@ -363,6 +450,14 @@ async function readFunction(user: string) {
|
|
|
363
450
|
return { user, greet_count: row?.greet_count, now: Date.now() };
|
|
364
451
|
}
|
|
365
452
|
|
|
453
|
+
const regRaceFunction = dataSource.registerTransaction(raceFunction);
|
|
454
|
+
|
|
455
|
+
async function raceWorkflow() {
|
|
456
|
+
return await regRaceFunction();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const regRaceWorkflow = DBOS.registerWorkflow(raceWorkflow);
|
|
460
|
+
|
|
366
461
|
const regInsertFunction = dataSource.registerTransaction(insertFunction);
|
|
367
462
|
const regErrorFunction = dataSource.registerTransaction(errorFunction);
|
|
368
463
|
const regReadFunction = dataSource.registerTransaction(readFunction, { readOnly: true });
|