@pikku/redis 0.12.20 → 0.12.21
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/CHANGELOG.md +28 -0
- package/dist/src/redis-agent-run-service.js.map +1 -1
- package/dist/src/redis-deployment-service.js.map +1 -1
- package/dist/src/redis-workflow-run-service.js.map +1 -1
- package/dist/src/redis-workflow-service.js.map +1 -1
- package/package.json +10 -6
- package/run-tests.sh +59 -24
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/index.ts +0 -10
- package/src/redis-agent-run-service.ts +0 -207
- package/src/redis-channel-store.ts +0 -158
- package/src/redis-deployment-service.ts +0 -171
- package/src/redis-eventhub-store.ts +0 -104
- package/src/redis-secret-service.test.ts +0 -122
- package/src/redis-secret-service.ts +0 -210
- package/src/redis-session-store.ts +0 -71
- package/src/redis-workflow-run-service.ts +0 -231
- package/src/redis-workflow-service.test.ts +0 -115
- package/src/redis-workflow-service.ts +0 -1025
- package/tsconfig.json +0 -17
|
@@ -1,1025 +0,0 @@
|
|
|
1
|
-
import type { SerializedError } from '@pikku/core/errors'
|
|
2
|
-
import { PikkuWorkflowService } from '@pikku/core/workflow'
|
|
3
|
-
import type {
|
|
4
|
-
WorkflowPlannedStep,
|
|
5
|
-
WorkflowQueueOptions,
|
|
6
|
-
WorkflowRun,
|
|
7
|
-
WorkflowRunWire,
|
|
8
|
-
StepState,
|
|
9
|
-
StepStatus,
|
|
10
|
-
WorkflowStatus,
|
|
11
|
-
WorkflowVersionStatus,
|
|
12
|
-
} from '@pikku/core/workflow'
|
|
13
|
-
import { Redis, type RedisOptions } from 'ioredis'
|
|
14
|
-
import { randomUUID } from 'crypto'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Redis-based implementation of WorkflowStateService
|
|
18
|
-
*
|
|
19
|
-
* Stores workflow run state and step state in Redis with row-level locking.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```typescript
|
|
23
|
-
* const redis = new Redis('redis://localhost:6379')
|
|
24
|
-
* const workflowService = new RedisWorkflowService(redis, 'workflows')
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export class RedisWorkflowService extends PikkuWorkflowService {
|
|
28
|
-
private redis: Redis
|
|
29
|
-
private keyPrefix: string
|
|
30
|
-
private ownsConnection: boolean
|
|
31
|
-
private lockTTL = 30000 // Lock TTL in milliseconds (30 seconds)
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param connectionOrConfig - ioredis Redis instance, RedisOptions config, or connection string
|
|
35
|
-
* @param keyPrefix - Redis key prefix (default: 'workflows')
|
|
36
|
-
*/
|
|
37
|
-
constructor(
|
|
38
|
-
connectionOrConfig: Redis | RedisOptions | string | undefined,
|
|
39
|
-
keyPrefix = 'workflows',
|
|
40
|
-
options: WorkflowQueueOptions = {}
|
|
41
|
-
) {
|
|
42
|
-
super(options)
|
|
43
|
-
this.keyPrefix = keyPrefix
|
|
44
|
-
|
|
45
|
-
// An already-connected client is recognised by the commands it exposes
|
|
46
|
-
// rather than by `instanceof Redis`, so a compatible client that is not an
|
|
47
|
-
// ioredis subclass — a test double, a wrapper — is used as given instead of
|
|
48
|
-
// being silently ignored in favour of a fresh connection to localhost.
|
|
49
|
-
if (
|
|
50
|
-
typeof connectionOrConfig === 'object' &&
|
|
51
|
-
connectionOrConfig !== null &&
|
|
52
|
-
'hgetall' in connectionOrConfig &&
|
|
53
|
-
'hset' in connectionOrConfig
|
|
54
|
-
) {
|
|
55
|
-
this.redis = connectionOrConfig as Redis
|
|
56
|
-
this.ownsConnection = false
|
|
57
|
-
} else {
|
|
58
|
-
this.redis = new Redis(connectionOrConfig as any)
|
|
59
|
-
this.ownsConnection = true
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Initialize the service (no-op for Redis, always ready)
|
|
65
|
-
*/
|
|
66
|
-
public async init(): Promise<void> {
|
|
67
|
-
// Redis doesn't require schema initialization
|
|
68
|
-
await this.redis.ping()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private runKey(runId: string): string {
|
|
72
|
-
return `${this.keyPrefix}:run:${runId}`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private stepKey(runId: string, stepName: string): string {
|
|
76
|
-
return `${this.keyPrefix}:step:${runId}:${stepName}`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private stepHistoryKey(stepId: string): string {
|
|
80
|
-
return `${this.keyPrefix}:step-history:${stepId}`
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private lockKey(runId: string): string {
|
|
84
|
-
return `${this.keyPrefix}:lock:${runId}`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private stepLockKey(runId: string, stepName: string): string {
|
|
88
|
-
return `${this.keyPrefix}:step-lock:${runId}:${stepName}`
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Save a step history entry (creates new entry)
|
|
93
|
-
*/
|
|
94
|
-
private async saveStepHistory(
|
|
95
|
-
stepId: string,
|
|
96
|
-
stepName: string,
|
|
97
|
-
attemptCount: number,
|
|
98
|
-
status: 'pending' | 'running' | 'scheduled' | 'succeeded' | 'failed',
|
|
99
|
-
result?: any,
|
|
100
|
-
error?: SerializedError,
|
|
101
|
-
retries?: number,
|
|
102
|
-
retryDelay?: string
|
|
103
|
-
): Promise<void> {
|
|
104
|
-
const historyKey = this.stepHistoryKey(stepId)
|
|
105
|
-
const now = Date.now()
|
|
106
|
-
|
|
107
|
-
const entry: any = {
|
|
108
|
-
stepId,
|
|
109
|
-
stepName,
|
|
110
|
-
attemptCount,
|
|
111
|
-
status,
|
|
112
|
-
result: result !== undefined ? JSON.stringify(result) : undefined,
|
|
113
|
-
error: error !== undefined ? JSON.stringify(error) : undefined,
|
|
114
|
-
retries,
|
|
115
|
-
retryDelay,
|
|
116
|
-
createdAt: now,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Add status-specific timestamp
|
|
120
|
-
switch (status) {
|
|
121
|
-
case 'running':
|
|
122
|
-
entry.runningAt = now
|
|
123
|
-
break
|
|
124
|
-
case 'scheduled':
|
|
125
|
-
entry.scheduledAt = now
|
|
126
|
-
break
|
|
127
|
-
case 'succeeded':
|
|
128
|
-
entry.succeededAt = now
|
|
129
|
-
break
|
|
130
|
-
case 'failed':
|
|
131
|
-
entry.failedAt = now
|
|
132
|
-
break
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Remove undefined fields
|
|
136
|
-
Object.keys(entry).forEach(
|
|
137
|
-
(key) => entry[key] === undefined && delete entry[key]
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
// Store in sorted set with attemptCount as score for ordering
|
|
141
|
-
await this.redis.zadd(historyKey, attemptCount, JSON.stringify(entry))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Update the current history entry in-place (for state transitions within same attempt)
|
|
146
|
-
*/
|
|
147
|
-
private async updateCurrentHistoryRecord(
|
|
148
|
-
stepId: string,
|
|
149
|
-
stepName: string,
|
|
150
|
-
attemptCount: number,
|
|
151
|
-
status: 'running' | 'scheduled' | 'succeeded' | 'failed',
|
|
152
|
-
result?: any,
|
|
153
|
-
error?: SerializedError,
|
|
154
|
-
retries?: number,
|
|
155
|
-
retryDelay?: string
|
|
156
|
-
): Promise<void> {
|
|
157
|
-
const historyKey = this.stepHistoryKey(stepId)
|
|
158
|
-
|
|
159
|
-
// Get current history entry for this attempt
|
|
160
|
-
const historyEntries = await this.redis.zrangebyscore(
|
|
161
|
-
historyKey,
|
|
162
|
-
attemptCount,
|
|
163
|
-
attemptCount
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
if (historyEntries.length === 0) {
|
|
167
|
-
// No existing entry - this shouldn't happen, but create one if missing
|
|
168
|
-
await this.saveStepHistory(
|
|
169
|
-
stepId,
|
|
170
|
-
stepName,
|
|
171
|
-
attemptCount,
|
|
172
|
-
status,
|
|
173
|
-
result,
|
|
174
|
-
error,
|
|
175
|
-
retries,
|
|
176
|
-
retryDelay
|
|
177
|
-
)
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Parse existing entry
|
|
182
|
-
const existingEntry = JSON.parse(historyEntries[0]!)
|
|
183
|
-
const now = Date.now()
|
|
184
|
-
|
|
185
|
-
// Update the entry with new status and keep original createdAt
|
|
186
|
-
const updatedEntry: any = {
|
|
187
|
-
...existingEntry,
|
|
188
|
-
status,
|
|
189
|
-
result: result !== undefined ? JSON.stringify(result) : undefined,
|
|
190
|
-
error: error !== undefined ? JSON.stringify(error) : undefined,
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Add status-specific timestamp
|
|
194
|
-
switch (status) {
|
|
195
|
-
case 'running':
|
|
196
|
-
updatedEntry.runningAt = now
|
|
197
|
-
break
|
|
198
|
-
case 'scheduled':
|
|
199
|
-
updatedEntry.scheduledAt = now
|
|
200
|
-
break
|
|
201
|
-
case 'succeeded':
|
|
202
|
-
updatedEntry.succeededAt = now
|
|
203
|
-
break
|
|
204
|
-
case 'failed':
|
|
205
|
-
updatedEntry.failedAt = now
|
|
206
|
-
break
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Remove old entry and add updated one (Redis sorted set will replace if score is same)
|
|
210
|
-
await this.redis.zremrangebyscore(historyKey, attemptCount, attemptCount)
|
|
211
|
-
await this.redis.zadd(
|
|
212
|
-
historyKey,
|
|
213
|
-
attemptCount,
|
|
214
|
-
JSON.stringify(updatedEntry)
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
protected async createRunImpl(
|
|
219
|
-
workflowName: string,
|
|
220
|
-
input: any,
|
|
221
|
-
inline: boolean,
|
|
222
|
-
graphHash: string,
|
|
223
|
-
wire: WorkflowRunWire,
|
|
224
|
-
options?: {
|
|
225
|
-
deterministic?: boolean
|
|
226
|
-
plannedSteps?: WorkflowPlannedStep[]
|
|
227
|
-
}
|
|
228
|
-
): Promise<string> {
|
|
229
|
-
const id = randomUUID()
|
|
230
|
-
const now = Date.now()
|
|
231
|
-
|
|
232
|
-
const key = this.runKey(id)
|
|
233
|
-
|
|
234
|
-
await this.redis.hmset(
|
|
235
|
-
key,
|
|
236
|
-
'id',
|
|
237
|
-
id,
|
|
238
|
-
'workflow',
|
|
239
|
-
workflowName,
|
|
240
|
-
'status',
|
|
241
|
-
'running',
|
|
242
|
-
'input',
|
|
243
|
-
JSON.stringify(input),
|
|
244
|
-
'inline',
|
|
245
|
-
inline ? 'true' : 'false',
|
|
246
|
-
'graphHash',
|
|
247
|
-
graphHash,
|
|
248
|
-
'deterministic',
|
|
249
|
-
options?.deterministic ? 'true' : 'false',
|
|
250
|
-
'plannedSteps',
|
|
251
|
-
options?.plannedSteps ? JSON.stringify(options.plannedSteps) : '',
|
|
252
|
-
'wire',
|
|
253
|
-
JSON.stringify(wire),
|
|
254
|
-
'createdAt',
|
|
255
|
-
now.toString(),
|
|
256
|
-
'updatedAt',
|
|
257
|
-
now.toString()
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
return id
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async getRun(id: string): Promise<WorkflowRun | null> {
|
|
264
|
-
const key = this.runKey(id)
|
|
265
|
-
const data = await this.redis.hgetall(key)
|
|
266
|
-
|
|
267
|
-
if (!data.id) {
|
|
268
|
-
return null
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
id: data.id!,
|
|
273
|
-
workflow: data.workflow!,
|
|
274
|
-
status: data.status! as WorkflowStatus,
|
|
275
|
-
input: JSON.parse(data.input!),
|
|
276
|
-
output: data.output ? JSON.parse(data.output) : undefined,
|
|
277
|
-
error: data.error ? JSON.parse(data.error) : undefined,
|
|
278
|
-
inline: data.inline === 'true' ? true : undefined,
|
|
279
|
-
graphHash: data.graphHash || undefined,
|
|
280
|
-
deterministic: data.deterministic === 'true' ? true : undefined,
|
|
281
|
-
plannedSteps: data.plannedSteps
|
|
282
|
-
? JSON.parse(data.plannedSteps)
|
|
283
|
-
: undefined,
|
|
284
|
-
wire: data.wire ? JSON.parse(data.wire) : { type: 'unknown' },
|
|
285
|
-
createdAt: new Date(Number(data.createdAt!)),
|
|
286
|
-
updatedAt: new Date(Number(data.updatedAt!)),
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
protected async updateRunStatusImpl(
|
|
291
|
-
id: string,
|
|
292
|
-
status: WorkflowStatus,
|
|
293
|
-
output?: any,
|
|
294
|
-
error?: SerializedError
|
|
295
|
-
): Promise<void> {
|
|
296
|
-
const now = Date.now()
|
|
297
|
-
const key = this.runKey(id)
|
|
298
|
-
|
|
299
|
-
const fields: Record<string, string> = {
|
|
300
|
-
status,
|
|
301
|
-
updatedAt: now.toString(),
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (output !== undefined) {
|
|
305
|
-
fields.output = JSON.stringify(output)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (error !== undefined) {
|
|
309
|
-
fields.error = JSON.stringify(error)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
await this.redis.hmset(key, fields)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
protected async insertStepStateImpl(
|
|
316
|
-
runId: string,
|
|
317
|
-
stepName: string,
|
|
318
|
-
rpcName: string | null,
|
|
319
|
-
data: any,
|
|
320
|
-
stepOptions?: { retries?: number; retryDelay?: string | number },
|
|
321
|
-
fromStepName?: string
|
|
322
|
-
): Promise<StepState> {
|
|
323
|
-
const now = Date.now()
|
|
324
|
-
const stepId = `${runId}:${stepName}:${now}`
|
|
325
|
-
const key = this.stepKey(runId, stepName)
|
|
326
|
-
|
|
327
|
-
const fields: Record<string, string> = {
|
|
328
|
-
stepId,
|
|
329
|
-
data: JSON.stringify(data),
|
|
330
|
-
status: 'pending',
|
|
331
|
-
attemptCount: '1',
|
|
332
|
-
createdAt: now.toString(),
|
|
333
|
-
updatedAt: now.toString(),
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (rpcName !== null) {
|
|
337
|
-
fields.rpcName = rpcName
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (stepOptions?.retries !== undefined) {
|
|
341
|
-
fields.retries = stepOptions.retries.toString()
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (stepOptions?.retryDelay !== undefined) {
|
|
345
|
-
fields.retryDelay = stepOptions.retryDelay.toString()
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (fromStepName !== undefined) {
|
|
349
|
-
fields.fromStepName = fromStepName
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
await this.redis.hmset(key, fields)
|
|
353
|
-
|
|
354
|
-
// Save initial history entry
|
|
355
|
-
await this.saveStepHistory(
|
|
356
|
-
stepId,
|
|
357
|
-
stepName,
|
|
358
|
-
1,
|
|
359
|
-
'pending',
|
|
360
|
-
undefined,
|
|
361
|
-
undefined,
|
|
362
|
-
stepOptions?.retries,
|
|
363
|
-
stepOptions?.retryDelay?.toString()
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
stepId,
|
|
368
|
-
status: 'pending',
|
|
369
|
-
rpcName,
|
|
370
|
-
attemptCount: 1,
|
|
371
|
-
retries: stepOptions?.retries,
|
|
372
|
-
retryDelay:
|
|
373
|
-
stepOptions?.retryDelay !== undefined
|
|
374
|
-
? stepOptions.retryDelay.toString()
|
|
375
|
-
: undefined,
|
|
376
|
-
fromStepName,
|
|
377
|
-
createdAt: new Date(now),
|
|
378
|
-
updatedAt: new Date(now),
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async getStepState(runId: string, stepName: string): Promise<StepState> {
|
|
383
|
-
const key = this.stepKey(runId, stepName)
|
|
384
|
-
const data = await this.redis.hgetall(key)
|
|
385
|
-
|
|
386
|
-
if (!data.stepId) {
|
|
387
|
-
throw new Error(
|
|
388
|
-
`Step not found: runId=${runId}, stepName=${stepName}. Use insertStepState to create it.`
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
stepId: data.stepId,
|
|
394
|
-
status: data.status as any,
|
|
395
|
-
rpcName: data.rpcName ?? null,
|
|
396
|
-
result: data.result ? JSON.parse(data.result) : undefined,
|
|
397
|
-
error: data.error ? JSON.parse(data.error) : undefined,
|
|
398
|
-
attemptCount: Number(data.attemptCount || 1),
|
|
399
|
-
retries: data.retries ? Number(data.retries) : undefined,
|
|
400
|
-
retryDelay: data.retryDelay,
|
|
401
|
-
fromStepName: data.fromStepName || undefined,
|
|
402
|
-
createdAt: new Date(Number(data.createdAt!)),
|
|
403
|
-
updatedAt: new Date(Number(data.updatedAt!)),
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async getRunHistory(
|
|
408
|
-
runId: string
|
|
409
|
-
): Promise<Array<StepState & { stepName: string }>> {
|
|
410
|
-
// Find all step keys for this run to get their stepIds
|
|
411
|
-
const pattern = `${this.keyPrefix}:step:${runId}:*`
|
|
412
|
-
const stepKeys: string[] = []
|
|
413
|
-
|
|
414
|
-
// Use SCAN to find all step keys for this run
|
|
415
|
-
let cursor = '0'
|
|
416
|
-
do {
|
|
417
|
-
const [newCursor, foundKeys] = await this.redis.scan(
|
|
418
|
-
cursor,
|
|
419
|
-
'MATCH',
|
|
420
|
-
pattern,
|
|
421
|
-
'COUNT',
|
|
422
|
-
100
|
|
423
|
-
)
|
|
424
|
-
cursor = newCursor
|
|
425
|
-
stepKeys.push(...foundKeys)
|
|
426
|
-
} while (cursor !== '0')
|
|
427
|
-
|
|
428
|
-
// Fetch all history entries for all steps
|
|
429
|
-
const allHistoryEntries: Array<StepState & { stepName: string }> = []
|
|
430
|
-
|
|
431
|
-
for (const stepKey of stepKeys) {
|
|
432
|
-
const stepData = await this.redis.hgetall(stepKey)
|
|
433
|
-
if (!stepData.stepId) continue
|
|
434
|
-
|
|
435
|
-
const stepId = stepData.stepId
|
|
436
|
-
const historyKey = this.stepHistoryKey(stepId)
|
|
437
|
-
|
|
438
|
-
// Get all history entries for this step (sorted by attemptCount)
|
|
439
|
-
const historyEntries = await this.redis.zrange(historyKey, 0, '-1')
|
|
440
|
-
|
|
441
|
-
for (const entryStr of historyEntries) {
|
|
442
|
-
const entry = JSON.parse(entryStr)
|
|
443
|
-
|
|
444
|
-
allHistoryEntries.push({
|
|
445
|
-
stepId: entry.stepId,
|
|
446
|
-
stepName: entry.stepName,
|
|
447
|
-
// Provenance lives on the step row, not the per-attempt history entry.
|
|
448
|
-
fromStepName: stepData.fromStepName || undefined,
|
|
449
|
-
status: entry.status,
|
|
450
|
-
result: entry.result ? JSON.parse(entry.result) : undefined,
|
|
451
|
-
error: entry.error ? JSON.parse(entry.error) : undefined,
|
|
452
|
-
attemptCount: entry.attemptCount,
|
|
453
|
-
retries: entry.retries,
|
|
454
|
-
retryDelay: entry.retryDelay,
|
|
455
|
-
createdAt: new Date(entry.createdAt),
|
|
456
|
-
updatedAt: new Date(entry.createdAt), // Use createdAt for both
|
|
457
|
-
runningAt: entry.runningAt ? new Date(entry.runningAt) : undefined,
|
|
458
|
-
scheduledAt: entry.scheduledAt
|
|
459
|
-
? new Date(entry.scheduledAt)
|
|
460
|
-
: undefined,
|
|
461
|
-
succeededAt: entry.succeededAt
|
|
462
|
-
? new Date(entry.succeededAt)
|
|
463
|
-
: undefined,
|
|
464
|
-
failedAt: entry.failedAt ? new Date(entry.failedAt) : undefined,
|
|
465
|
-
})
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Sort all entries by creation time (oldest first)
|
|
470
|
-
return allHistoryEntries.sort(
|
|
471
|
-
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
472
|
-
)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
protected async setStepRunningImpl(stepId: string): Promise<void> {
|
|
476
|
-
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
477
|
-
const parts = stepId.split(':')
|
|
478
|
-
const runId = parts[0]!
|
|
479
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
480
|
-
|
|
481
|
-
const now = Date.now()
|
|
482
|
-
const key = this.stepKey(runId, stepName)
|
|
483
|
-
|
|
484
|
-
// Get current attempt count and retries config
|
|
485
|
-
const data = await this.redis.hgetall(key)
|
|
486
|
-
const attemptCount = Number(data.attemptCount || 1)
|
|
487
|
-
const retries = data.retries ? Number(data.retries) : undefined
|
|
488
|
-
const retryDelay = data.retryDelay
|
|
489
|
-
|
|
490
|
-
await this.redis.hmset(
|
|
491
|
-
key,
|
|
492
|
-
'status',
|
|
493
|
-
'running',
|
|
494
|
-
'updatedAt',
|
|
495
|
-
now.toString()
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
// Update current history record to running (update in-place)
|
|
499
|
-
await this.updateCurrentHistoryRecord(
|
|
500
|
-
stepId,
|
|
501
|
-
stepName,
|
|
502
|
-
attemptCount,
|
|
503
|
-
'running',
|
|
504
|
-
undefined,
|
|
505
|
-
undefined,
|
|
506
|
-
retries,
|
|
507
|
-
retryDelay
|
|
508
|
-
)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
protected async setStepScheduledImpl(stepId: string): Promise<void> {
|
|
512
|
-
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
513
|
-
const parts = stepId.split(':')
|
|
514
|
-
const runId = parts[0]!
|
|
515
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
516
|
-
|
|
517
|
-
const now = Date.now()
|
|
518
|
-
const key = this.stepKey(runId, stepName)
|
|
519
|
-
|
|
520
|
-
// Get current attempt count and retries config
|
|
521
|
-
const data = await this.redis.hgetall(key)
|
|
522
|
-
const attemptCount = Number(data.attemptCount || 1)
|
|
523
|
-
const retries = data.retries ? Number(data.retries) : undefined
|
|
524
|
-
const retryDelay = data.retryDelay
|
|
525
|
-
|
|
526
|
-
await this.redis.hmset(
|
|
527
|
-
key,
|
|
528
|
-
'status',
|
|
529
|
-
'scheduled',
|
|
530
|
-
'updatedAt',
|
|
531
|
-
now.toString()
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
// Update current history record to scheduled (update in-place)
|
|
535
|
-
await this.updateCurrentHistoryRecord(
|
|
536
|
-
stepId,
|
|
537
|
-
stepName,
|
|
538
|
-
attemptCount,
|
|
539
|
-
'scheduled',
|
|
540
|
-
undefined,
|
|
541
|
-
undefined,
|
|
542
|
-
retries,
|
|
543
|
-
retryDelay
|
|
544
|
-
)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
protected async setStepChildRunIdImpl(
|
|
548
|
-
stepId: string,
|
|
549
|
-
childRunId: string
|
|
550
|
-
): Promise<void> {
|
|
551
|
-
const parts = stepId.split(':')
|
|
552
|
-
const runId = parts[0]!
|
|
553
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
554
|
-
const key = this.stepKey(runId, stepName)
|
|
555
|
-
await this.redis.hmset(
|
|
556
|
-
key,
|
|
557
|
-
'childRunId',
|
|
558
|
-
childRunId,
|
|
559
|
-
'updatedAt',
|
|
560
|
-
Date.now().toString()
|
|
561
|
-
)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
protected async setStepResultImpl(
|
|
565
|
-
stepId: string,
|
|
566
|
-
result: any
|
|
567
|
-
): Promise<void> {
|
|
568
|
-
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
569
|
-
const parts = stepId.split(':')
|
|
570
|
-
const runId = parts[0]!
|
|
571
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
572
|
-
|
|
573
|
-
const now = Date.now()
|
|
574
|
-
const key = this.stepKey(runId, stepName)
|
|
575
|
-
|
|
576
|
-
// Get current attempt count and retries config
|
|
577
|
-
const data = await this.redis.hgetall(key)
|
|
578
|
-
const attemptCount = Number(data.attemptCount || 1)
|
|
579
|
-
const retries = data.retries ? Number(data.retries) : undefined
|
|
580
|
-
const retryDelay = data.retryDelay
|
|
581
|
-
|
|
582
|
-
await this.redis.hmset(
|
|
583
|
-
key,
|
|
584
|
-
'status',
|
|
585
|
-
'succeeded',
|
|
586
|
-
'result',
|
|
587
|
-
JSON.stringify(result),
|
|
588
|
-
'updatedAt',
|
|
589
|
-
now.toString()
|
|
590
|
-
)
|
|
591
|
-
|
|
592
|
-
// Remove error field if it exists
|
|
593
|
-
await this.redis.hdel(key, 'error')
|
|
594
|
-
|
|
595
|
-
// Update current history record to succeeded (update in-place)
|
|
596
|
-
await this.updateCurrentHistoryRecord(
|
|
597
|
-
stepId,
|
|
598
|
-
stepName,
|
|
599
|
-
attemptCount,
|
|
600
|
-
'succeeded',
|
|
601
|
-
result,
|
|
602
|
-
undefined,
|
|
603
|
-
retries,
|
|
604
|
-
retryDelay
|
|
605
|
-
)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
protected async setStepErrorImpl(
|
|
609
|
-
stepId: string,
|
|
610
|
-
error: Error
|
|
611
|
-
): Promise<void> {
|
|
612
|
-
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
613
|
-
const parts = stepId.split(':')
|
|
614
|
-
const runId = parts[0]!
|
|
615
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
616
|
-
|
|
617
|
-
const now = Date.now()
|
|
618
|
-
const key = this.stepKey(runId, stepName)
|
|
619
|
-
|
|
620
|
-
// Get current attempt count and retries config
|
|
621
|
-
const data = await this.redis.hgetall(key)
|
|
622
|
-
const attemptCount = Number(data.attemptCount || 1)
|
|
623
|
-
const retries = data.retries ? Number(data.retries) : undefined
|
|
624
|
-
const retryDelay = data.retryDelay
|
|
625
|
-
|
|
626
|
-
const serializedError: SerializedError = {
|
|
627
|
-
message: error.message,
|
|
628
|
-
stack: error.stack,
|
|
629
|
-
code: (error as any).code,
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
await this.redis.hmset(
|
|
633
|
-
key,
|
|
634
|
-
'status',
|
|
635
|
-
'failed',
|
|
636
|
-
'error',
|
|
637
|
-
JSON.stringify(serializedError),
|
|
638
|
-
'updatedAt',
|
|
639
|
-
now.toString()
|
|
640
|
-
)
|
|
641
|
-
|
|
642
|
-
// Remove result field if it exists
|
|
643
|
-
await this.redis.hdel(key, 'result')
|
|
644
|
-
|
|
645
|
-
// Update current history record to failed (update in-place)
|
|
646
|
-
await this.updateCurrentHistoryRecord(
|
|
647
|
-
stepId,
|
|
648
|
-
stepName,
|
|
649
|
-
attemptCount,
|
|
650
|
-
'failed',
|
|
651
|
-
undefined,
|
|
652
|
-
serializedError,
|
|
653
|
-
retries,
|
|
654
|
-
retryDelay
|
|
655
|
-
)
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private async withLock<T>(
|
|
659
|
-
lockKey: string,
|
|
660
|
-
errorMessage: string,
|
|
661
|
-
fn: () => Promise<T>
|
|
662
|
-
): Promise<T> {
|
|
663
|
-
const lockValue = randomUUID()
|
|
664
|
-
const maxRetries = 10
|
|
665
|
-
const retryDelay = 100 // ms
|
|
666
|
-
|
|
667
|
-
// Try to acquire lock with retries
|
|
668
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
669
|
-
const acquired = await this.redis.set(
|
|
670
|
-
lockKey,
|
|
671
|
-
lockValue,
|
|
672
|
-
'PX',
|
|
673
|
-
this.lockTTL,
|
|
674
|
-
'NX'
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
if (acquired === 'OK') {
|
|
678
|
-
try {
|
|
679
|
-
// Lock acquired, execute function
|
|
680
|
-
return await fn()
|
|
681
|
-
} finally {
|
|
682
|
-
// Release lock using Lua script to ensure we only delete our own lock
|
|
683
|
-
await this.redis.eval(
|
|
684
|
-
`if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`,
|
|
685
|
-
1,
|
|
686
|
-
lockKey,
|
|
687
|
-
lockValue
|
|
688
|
-
)
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Lock not acquired, wait and retry
|
|
693
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay))
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
throw new Error(`${errorMessage} after ${maxRetries} retries`)
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
async withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
|
|
700
|
-
return this.withLock(
|
|
701
|
-
this.lockKey(id),
|
|
702
|
-
`Failed to acquire lock for run ${id}`,
|
|
703
|
-
fn
|
|
704
|
-
)
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
async withStepLock<T>(
|
|
708
|
-
runId: string,
|
|
709
|
-
stepName: string,
|
|
710
|
-
fn: () => Promise<T>
|
|
711
|
-
): Promise<T> {
|
|
712
|
-
return this.withLock(
|
|
713
|
-
this.stepLockKey(runId, stepName),
|
|
714
|
-
`Failed to acquire step lock for run ${runId}, step ${stepName}`,
|
|
715
|
-
fn
|
|
716
|
-
)
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
protected async createRetryAttemptImpl(
|
|
720
|
-
stepId: string,
|
|
721
|
-
status: 'pending' | 'running'
|
|
722
|
-
): Promise<StepState> {
|
|
723
|
-
// TODO: If status is 'running', we need to set the running_at timestamp in history
|
|
724
|
-
|
|
725
|
-
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
726
|
-
const parts = stepId.split(':')
|
|
727
|
-
const runId = parts[0]!
|
|
728
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
729
|
-
|
|
730
|
-
const now = Date.now()
|
|
731
|
-
const key = this.stepKey(runId, stepName)
|
|
732
|
-
|
|
733
|
-
// Get current attempt count and retries config
|
|
734
|
-
const data = await this.redis.hgetall(key)
|
|
735
|
-
const currentAttempt = Number(data.attemptCount || 1)
|
|
736
|
-
const newAttemptCount = currentAttempt + 1
|
|
737
|
-
const retries = data.retries ? Number(data.retries) : undefined
|
|
738
|
-
const retryDelay = data.retryDelay
|
|
739
|
-
|
|
740
|
-
// Reset step to pending for retry (keeps result/error for visibility)
|
|
741
|
-
await this.redis.hmset(
|
|
742
|
-
key,
|
|
743
|
-
'status',
|
|
744
|
-
status,
|
|
745
|
-
'attemptCount',
|
|
746
|
-
newAttemptCount.toString(),
|
|
747
|
-
'updatedAt',
|
|
748
|
-
now.toString()
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
// Insert NEW history record for retry attempt
|
|
752
|
-
await this.saveStepHistory(
|
|
753
|
-
stepId,
|
|
754
|
-
stepName,
|
|
755
|
-
newAttemptCount,
|
|
756
|
-
'pending',
|
|
757
|
-
undefined,
|
|
758
|
-
undefined,
|
|
759
|
-
retries,
|
|
760
|
-
retryDelay
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
return {
|
|
764
|
-
stepId: data.stepId!,
|
|
765
|
-
status: 'pending',
|
|
766
|
-
rpcName: data.rpcName ?? null,
|
|
767
|
-
result: data.result ? JSON.parse(data.result) : undefined,
|
|
768
|
-
error: data.error ? JSON.parse(data.error) : undefined,
|
|
769
|
-
attemptCount: newAttemptCount,
|
|
770
|
-
retries: retries,
|
|
771
|
-
retryDelay: retryDelay,
|
|
772
|
-
fromStepName: data.fromStepName || undefined,
|
|
773
|
-
createdAt: new Date(Number(data.createdAt!)),
|
|
774
|
-
updatedAt: new Date(now),
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// ============================================================================
|
|
779
|
-
// Workflow Graph Methods
|
|
780
|
-
// ============================================================================
|
|
781
|
-
|
|
782
|
-
async getCompletedGraphState(runId: string): Promise<{
|
|
783
|
-
completedNodeIds: string[]
|
|
784
|
-
failedNodeIds: string[]
|
|
785
|
-
branchKeys: Record<string, string>
|
|
786
|
-
}> {
|
|
787
|
-
const completedNodeIds: string[] = []
|
|
788
|
-
const failedNodeIds: string[] = []
|
|
789
|
-
const branchKeys: Record<string, string> = {}
|
|
790
|
-
|
|
791
|
-
const pattern = `${this.keyPrefix}:step:${runId}:*`
|
|
792
|
-
let cursor = '0'
|
|
793
|
-
|
|
794
|
-
do {
|
|
795
|
-
const [newCursor, foundKeys] = await this.redis.scan(
|
|
796
|
-
cursor,
|
|
797
|
-
'MATCH',
|
|
798
|
-
pattern,
|
|
799
|
-
'COUNT',
|
|
800
|
-
100
|
|
801
|
-
)
|
|
802
|
-
cursor = newCursor
|
|
803
|
-
|
|
804
|
-
for (const key of foundKeys) {
|
|
805
|
-
const data = await this.redis.hmget(
|
|
806
|
-
key,
|
|
807
|
-
'status',
|
|
808
|
-
'branchTaken',
|
|
809
|
-
'attemptCount',
|
|
810
|
-
'retries'
|
|
811
|
-
)
|
|
812
|
-
const [status, branchTaken, attemptCount, retries] = data
|
|
813
|
-
|
|
814
|
-
const parts = key.split(':')
|
|
815
|
-
const stepIndex = parts.lastIndexOf('step')
|
|
816
|
-
if (stepIndex === -1 || stepIndex + 1 >= parts.length) continue
|
|
817
|
-
if (parts[stepIndex + 1] !== runId) continue
|
|
818
|
-
const nodeId = parts.slice(stepIndex + 2).join(':')
|
|
819
|
-
|
|
820
|
-
if (status === 'succeeded') {
|
|
821
|
-
completedNodeIds.push(nodeId)
|
|
822
|
-
if (branchTaken) {
|
|
823
|
-
branchKeys[nodeId] = branchTaken
|
|
824
|
-
}
|
|
825
|
-
} else if (status === 'failed') {
|
|
826
|
-
const maxAttempts = (parseInt(retries || '0', 10) || 0) + 1
|
|
827
|
-
const attempts = parseInt(attemptCount || '0', 10) || 0
|
|
828
|
-
if (attempts >= maxAttempts) {
|
|
829
|
-
failedNodeIds.push(nodeId)
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
} while (cursor !== '0')
|
|
834
|
-
|
|
835
|
-
return { completedNodeIds, failedNodeIds, branchKeys }
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
async getStepInstances(
|
|
839
|
-
runId: string
|
|
840
|
-
): Promise<
|
|
841
|
-
Array<{ stepName: string; status: StepStatus; fromStepName?: string }>
|
|
842
|
-
> {
|
|
843
|
-
const instances: Array<{
|
|
844
|
-
stepName: string
|
|
845
|
-
status: StepStatus
|
|
846
|
-
fromStepName?: string
|
|
847
|
-
}> = []
|
|
848
|
-
const pattern = `${this.keyPrefix}:step:${runId}:*`
|
|
849
|
-
let cursor = '0'
|
|
850
|
-
do {
|
|
851
|
-
const [newCursor, foundKeys] = await this.redis.scan(
|
|
852
|
-
cursor,
|
|
853
|
-
'MATCH',
|
|
854
|
-
pattern,
|
|
855
|
-
'COUNT',
|
|
856
|
-
100
|
|
857
|
-
)
|
|
858
|
-
cursor = newCursor
|
|
859
|
-
for (const key of foundKeys) {
|
|
860
|
-
const [status, fromStepName] = await this.redis.hmget(
|
|
861
|
-
key,
|
|
862
|
-
'status',
|
|
863
|
-
'fromStepName'
|
|
864
|
-
)
|
|
865
|
-
const parts = key.split(':')
|
|
866
|
-
const stepIndex = parts.lastIndexOf('step')
|
|
867
|
-
if (stepIndex === -1 || parts[stepIndex + 1] !== runId) continue
|
|
868
|
-
instances.push({
|
|
869
|
-
stepName: parts.slice(stepIndex + 2).join(':'),
|
|
870
|
-
status: status as StepStatus,
|
|
871
|
-
fromStepName: fromStepName || undefined,
|
|
872
|
-
})
|
|
873
|
-
}
|
|
874
|
-
} while (cursor !== '0')
|
|
875
|
-
return instances
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
async getNodeResults(
|
|
879
|
-
runId: string,
|
|
880
|
-
nodeIds: string[]
|
|
881
|
-
): Promise<Record<string, any>> {
|
|
882
|
-
if (nodeIds.length === 0) return {}
|
|
883
|
-
|
|
884
|
-
const results: Record<string, any> = {}
|
|
885
|
-
|
|
886
|
-
for (const nodeId of nodeIds) {
|
|
887
|
-
const key = this.stepKey(runId, nodeId)
|
|
888
|
-
const data = await this.redis.hmget(key, 'status', 'result')
|
|
889
|
-
const [status, result] = data
|
|
890
|
-
|
|
891
|
-
if (status === 'succeeded' && result) {
|
|
892
|
-
results[nodeId] = JSON.parse(result)
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
return results
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
protected async setBranchTakenImpl(
|
|
900
|
-
stepId: string,
|
|
901
|
-
branchKey: string
|
|
902
|
-
): Promise<void> {
|
|
903
|
-
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
904
|
-
const parts = stepId.split(':')
|
|
905
|
-
const runId = parts[0]!
|
|
906
|
-
const stepName = parts.slice(1, -1).join(':')
|
|
907
|
-
|
|
908
|
-
const now = Date.now()
|
|
909
|
-
const key = this.stepKey(runId, stepName)
|
|
910
|
-
|
|
911
|
-
await this.redis.hmset(
|
|
912
|
-
key,
|
|
913
|
-
'branchTaken',
|
|
914
|
-
branchKey,
|
|
915
|
-
'updatedAt',
|
|
916
|
-
now.toString()
|
|
917
|
-
)
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* Hash holding one field per state variable of a run.
|
|
922
|
-
*
|
|
923
|
-
* The whole state used to be a single JSON blob on the run hash, which made
|
|
924
|
-
* every write a read-modify-write: two parallel branches each setting their
|
|
925
|
-
* own variable would both read the blob, add their key, and write it back,
|
|
926
|
-
* so whichever wrote second silently dropped the other's variable.
|
|
927
|
-
*/
|
|
928
|
-
private runStateKey(runId: string): string {
|
|
929
|
-
return `${this.keyPrefix}:run-state:${runId}`
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
protected async updateRunStateImpl(
|
|
933
|
-
runId: string,
|
|
934
|
-
name: string,
|
|
935
|
-
value: unknown
|
|
936
|
-
): Promise<void> {
|
|
937
|
-
// HSET touches only the field it is named, so concurrent writers to
|
|
938
|
-
// different variables no longer race, and nothing has to parse and
|
|
939
|
-
// re-encode the values it is not writing.
|
|
940
|
-
await this.redis.hset(
|
|
941
|
-
this.runStateKey(runId),
|
|
942
|
-
name,
|
|
943
|
-
JSON.stringify(value) ?? 'null'
|
|
944
|
-
)
|
|
945
|
-
await this.redis.hset(
|
|
946
|
-
this.runKey(runId),
|
|
947
|
-
'updatedAt',
|
|
948
|
-
Date.now().toString()
|
|
949
|
-
)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
async getRunState(runId: string): Promise<Record<string, unknown>> {
|
|
953
|
-
// The blob is what a run started before the per-key layout still holds; a
|
|
954
|
-
// field written since wins, so a run in flight across the deploy keeps
|
|
955
|
-
// everything it had.
|
|
956
|
-
const blob = await this.redis.hget(this.runKey(runId), 'state')
|
|
957
|
-
const state: Record<string, unknown> = blob ? JSON.parse(blob) : {}
|
|
958
|
-
|
|
959
|
-
const fields = await this.redis.hgetall(this.runStateKey(runId))
|
|
960
|
-
for (const [name, value] of Object.entries(fields)) {
|
|
961
|
-
state[name] = JSON.parse(value)
|
|
962
|
-
}
|
|
963
|
-
return state
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
private versionKey(name: string, graphHash: string): string {
|
|
967
|
-
return `${this.keyPrefix}:version:${name}:${graphHash}`
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
protected async upsertWorkflowVersionImpl(
|
|
971
|
-
name: string,
|
|
972
|
-
graphHash: string,
|
|
973
|
-
graph: any,
|
|
974
|
-
source: string,
|
|
975
|
-
status?: WorkflowVersionStatus
|
|
976
|
-
): Promise<void> {
|
|
977
|
-
const key = this.versionKey(name, graphHash)
|
|
978
|
-
const exists = await this.redis.exists(key)
|
|
979
|
-
if (!exists) {
|
|
980
|
-
await this.redis.hmset(
|
|
981
|
-
key,
|
|
982
|
-
'workflowName',
|
|
983
|
-
name,
|
|
984
|
-
'graphHash',
|
|
985
|
-
graphHash,
|
|
986
|
-
'graph',
|
|
987
|
-
JSON.stringify(graph),
|
|
988
|
-
'source',
|
|
989
|
-
source,
|
|
990
|
-
'status',
|
|
991
|
-
status ?? 'active',
|
|
992
|
-
'createdAt',
|
|
993
|
-
Date.now().toString()
|
|
994
|
-
)
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
protected async updateWorkflowVersionStatusImpl(
|
|
999
|
-
name: string,
|
|
1000
|
-
graphHash: string,
|
|
1001
|
-
status: WorkflowVersionStatus
|
|
1002
|
-
): Promise<void> {
|
|
1003
|
-
const key = this.versionKey(name, graphHash)
|
|
1004
|
-
await this.redis.hset(key, 'status', status)
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
async getWorkflowVersion(
|
|
1008
|
-
name: string,
|
|
1009
|
-
graphHash: string
|
|
1010
|
-
): Promise<{ graph: any; source: string } | null> {
|
|
1011
|
-
const key = this.versionKey(name, graphHash)
|
|
1012
|
-
const data = await this.redis.hgetall(key)
|
|
1013
|
-
if (!data.graph) return null
|
|
1014
|
-
return {
|
|
1015
|
-
graph: JSON.parse(data.graph),
|
|
1016
|
-
source: data.source!,
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
async close(): Promise<void> {
|
|
1021
|
-
if (this.ownsConnection) {
|
|
1022
|
-
await this.redis.quit()
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|