@pikku/mongodb 0.12.21 → 0.12.23

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.
@@ -1,238 +0,0 @@
1
- import { describe, test, before, after, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { MongoClient, type Db } from 'mongodb'
4
- import { MongoMemoryServer } from 'mongodb-memory-server'
5
-
6
- import type { StepState } from '@pikku/core/workflow'
7
- import { MongoDBWorkflowService } from './mongodb-workflow-service.js'
8
-
9
- let mongod: MongoMemoryServer
10
- let client: MongoClient
11
- let db: Db
12
- let ws: MongoDBWorkflowService
13
- let dbCount = 0
14
-
15
- before(async () => {
16
- mongod = await MongoMemoryServer.create()
17
- client = new MongoClient(mongod.getUri())
18
- await client.connect()
19
- })
20
-
21
- after(async () => {
22
- await client?.close()
23
- await mongod?.stop()
24
- })
25
-
26
- beforeEach(async () => {
27
- db = client.db(`wf-${++dbCount}`)
28
- ws = new MongoDBWorkflowService(db)
29
- await ws.init()
30
- })
31
-
32
- const seedStep = async () => {
33
- const runId = await ws.createRun('flow', {}, false, 'hash', {
34
- type: 'test',
35
- } as any)
36
- const step = await ws.insertStepState(runId, 'step-1', 'rpc.fn', { x: 1 })
37
- return { runId, step }
38
- }
39
-
40
- /** The history rows of a run, oldest first. */
41
- const historyOf = async (runId: string) => ws.getRunHistory(runId)
42
-
43
- describe('a step transition reaches the history', () => {
44
- test('running stamps the attempt it is running', async () => {
45
- const { runId, step } = await seedStep()
46
-
47
- await ws.setStepRunning(step.stepId)
48
-
49
- const [attempt] = await historyOf(runId)
50
- assert.equal(attempt!.status, 'running')
51
- assert.ok(attempt!.runningAt, 'no runningAt, so the step has no duration')
52
- })
53
-
54
- test('scheduled stamps the attempt it is queueing', async () => {
55
- const { runId, step } = await seedStep()
56
-
57
- await ws.setStepScheduled(step.stepId)
58
-
59
- const [attempt] = await historyOf(runId)
60
- assert.equal(
61
- attempt!.status,
62
- 'scheduled',
63
- 'the history still says pending, so a queued step reads as never dispatched'
64
- )
65
- assert.ok(attempt!.scheduledAt, 'no scheduledAt on the queued attempt')
66
- })
67
-
68
- test('succeeded carries the result', async () => {
69
- const { runId, step } = await seedStep()
70
-
71
- await ws.setStepRunning(step.stepId)
72
- await ws.setStepResult(step.stepId, { ok: true })
73
-
74
- const [attempt] = await historyOf(runId)
75
- assert.equal(attempt!.status, 'succeeded')
76
- assert.deepEqual(attempt!.result, { ok: true })
77
- assert.ok(attempt!.succeededAt)
78
- })
79
-
80
- test('failed carries the error', async () => {
81
- const { runId, step } = await seedStep()
82
-
83
- await ws.setStepRunning(step.stepId)
84
- await ws.setStepError(step.stepId, new Error('exploded'))
85
-
86
- const [attempt] = await historyOf(runId)
87
- assert.equal(attempt!.status, 'failed')
88
- assert.equal(attempt!.error?.message, 'exploded')
89
- assert.ok(attempt!.failedAt)
90
- })
91
-
92
- test('a retry writes to the new attempt, leaving the failed one intact', async () => {
93
- const { runId, step } = await seedStep()
94
-
95
- await ws.setStepRunning(step.stepId)
96
- await ws.setStepError(step.stepId, new Error('first go'))
97
- await ws.createRetryAttempt(step.stepId, 'pending')
98
- await ws.setStepRunning(step.stepId)
99
- await ws.setStepResult(step.stepId, 'second go')
100
-
101
- const history = await historyOf(runId)
102
- assert.equal(history.length, 2)
103
- assert.equal(history[0]!.status, 'failed')
104
- assert.equal(history[0]!.error?.message, 'first go')
105
- assert.equal(history[1]!.status, 'succeeded')
106
- assert.equal(history[1]!.result, 'second go')
107
- })
108
-
109
- test('the step row keeps only its latest outcome', async () => {
110
- const { runId, step } = await seedStep()
111
-
112
- await ws.setStepError(step.stepId, new Error('first go'))
113
- await ws.createRetryAttempt(step.stepId, 'pending')
114
- await ws.setStepResult(step.stepId, 'second go')
115
-
116
- const state = await ws.getStepState(runId, 'step-1')
117
- assert.equal(state.status, 'succeeded')
118
- assert.equal(state.result, 'second go')
119
- assert.equal(state.error, undefined)
120
- assert.equal(state.attemptCount, 2)
121
- })
122
- })
123
-
124
- /**
125
- * Dispatch is at-least-once — the relay re-dispatches a step it believes was
126
- * dropped, and a queue can redeliver a job it already handed out — so the claim
127
- * is the only thing standing between a duplicate dispatch and a second
128
- * execution of a side-effecting step.
129
- */
130
- describe('claiming a step for execution', () => {
131
- const claim = (runId: string, stepName: string, rpcName = 'rpc.fn') =>
132
- (ws as any).claimStepForExecution(
133
- runId,
134
- stepName,
135
- rpcName
136
- ) as Promise<StepState | null>
137
-
138
- const seedRun = () =>
139
- ws.createRun('flow', {}, false, 'hash', { type: 'test' } as any)
140
-
141
- test('two dispatches racing for the same pending step: exactly one wins', async () => {
142
- const runId = await seedRun()
143
- await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
144
-
145
- const claims = await Promise.all([claim(runId, 's1'), claim(runId, 's1')])
146
- const winners = claims.filter((c) => c !== null)
147
-
148
- assert.equal(
149
- winners.length,
150
- 1,
151
- `both dispatches claimed the step, so a side-effecting step would run twice: ${JSON.stringify(claims)}`
152
- )
153
- assert.equal((await ws.getStepState(runId, 's1')).status, 'running')
154
- })
155
-
156
- test('two dispatches racing to retry the same failed step: exactly one wins', async () => {
157
- const runId = await seedRun()
158
- const step = await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
159
- await ws.setStepRunning(step.stepId)
160
- await ws.setStepError(step.stepId, new Error('boom'))
161
-
162
- const claims = await Promise.all([claim(runId, 's1'), claim(runId, 's1')])
163
- const winners = claims.filter((c) => c !== null)
164
-
165
- assert.equal(
166
- winners.length,
167
- 1,
168
- `both dispatches started a retry, so the failed step would be retried twice concurrently: ${JSON.stringify(claims)}`
169
- )
170
- assert.equal(
171
- (await ws.getStepState(runId, 's1')).attemptCount,
172
- 2,
173
- 'the retry raced itself and burned more than one attempt'
174
- )
175
- })
176
-
177
- test('two dispatches racing for the same scheduled step: exactly one wins', async () => {
178
- const runId = await seedRun()
179
- const step = await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
180
- await ws.setStepScheduled(step.stepId)
181
-
182
- const claims = await Promise.all([claim(runId, 's1'), claim(runId, 's1')])
183
- const winners = claims.filter((c) => c !== null)
184
-
185
- assert.equal(
186
- winners.length,
187
- 1,
188
- `both dispatches claimed the queued step, so a redelivered job would run it a second time: ${JSON.stringify(claims)}`
189
- )
190
- assert.equal((await ws.getStepState(runId, 's1')).status, 'running')
191
- })
192
-
193
- test('a step already claimed is not claimable again', async () => {
194
- const runId = await seedRun()
195
- await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
196
-
197
- assert.notEqual(await claim(runId, 's1'), null)
198
- assert.equal(
199
- await claim(runId, 's1'),
200
- null,
201
- 'a redelivered job re-claimed a step that is already running'
202
- )
203
- })
204
-
205
- test('a succeeded step is not claimable', async () => {
206
- const runId = await seedRun()
207
- const step = await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
208
- await ws.setStepRunning(step.stepId)
209
- await ws.setStepResult(step.stepId, { ok: true })
210
-
211
- assert.equal(await claim(runId, 's1'), null)
212
- })
213
-
214
- test('a claim that fails releases the step, so it is retryable', async () => {
215
- const runId = await seedRun()
216
- const step = await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
217
-
218
- assert.notEqual(await claim(runId, 's1'), null)
219
- await ws.setStepError(step.stepId, new Error('boom'))
220
-
221
- const retry = await claim(runId, 's1')
222
- assert.notEqual(
223
- retry,
224
- null,
225
- 'the failed step stayed claimed, so it can never be retried'
226
- )
227
- assert.equal(retry?.status, 'running')
228
- assert.equal(retry?.attemptCount, 2)
229
- })
230
-
231
- test('a claim for a different function than the step was dispatched with is refused', async () => {
232
- const runId = await seedRun()
233
- await ws.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
234
-
235
- await assert.rejects(() => claim(runId, 's1', 'rpc.other'))
236
- assert.equal((await ws.getStepState(runId, 's1')).status, 'pending')
237
- })
238
- })