@pikku/redis 0.12.20 → 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.
- package/CHANGELOG.md +64 -0
- package/README.md +13 -10
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- 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-lease-service.d.ts +18 -0
- package/dist/src/redis-lease-service.js +94 -0
- package/dist/src/redis-lease-service.js.map +1 -0
- package/dist/src/redis-workflow-run-service.js.map +1 -1
- package/dist/src/redis-workflow-service.d.ts +10 -10
- package/dist/src/redis-workflow-service.js +61 -58
- 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,231 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
WorkflowRun,
|
|
3
|
-
StepState,
|
|
4
|
-
WorkflowStatus,
|
|
5
|
-
WorkflowRunService,
|
|
6
|
-
} from '@pikku/core/workflow'
|
|
7
|
-
import type { Redis } from 'ioredis'
|
|
8
|
-
|
|
9
|
-
export class RedisWorkflowRunService implements WorkflowRunService {
|
|
10
|
-
constructor(
|
|
11
|
-
private redis: Redis,
|
|
12
|
-
private keyPrefix = 'workflows'
|
|
13
|
-
) {}
|
|
14
|
-
|
|
15
|
-
private runKey(runId: string): string {
|
|
16
|
-
return `${this.keyPrefix}:run:${runId}`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private stepKeyPattern(runId: string): string {
|
|
20
|
-
return `${this.keyPrefix}:step:${runId}:*`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private stepHistoryKey(stepId: string): string {
|
|
24
|
-
return `${this.keyPrefix}:step-history:${stepId}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
private versionKey(name: string, graphHash: string): string {
|
|
28
|
-
return `${this.keyPrefix}:version:${name}:${graphHash}`
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async listRuns(options?: {
|
|
32
|
-
workflowName?: string
|
|
33
|
-
status?: string
|
|
34
|
-
limit?: number
|
|
35
|
-
offset?: number
|
|
36
|
-
}): Promise<WorkflowRun[]> {
|
|
37
|
-
const { workflowName, status, limit = 50, offset = 0 } = options ?? {}
|
|
38
|
-
|
|
39
|
-
const runKeys = await this.scanKeys(`${this.keyPrefix}:run:*`)
|
|
40
|
-
|
|
41
|
-
const runs: WorkflowRun[] = []
|
|
42
|
-
for (const key of runKeys) {
|
|
43
|
-
const data = await this.redis.hgetall(key)
|
|
44
|
-
if (!data.id) continue
|
|
45
|
-
|
|
46
|
-
if (workflowName && data.workflow !== workflowName) continue
|
|
47
|
-
if (status && data.status !== status) continue
|
|
48
|
-
|
|
49
|
-
runs.push(this.mapRunData(data))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
runs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
53
|
-
|
|
54
|
-
return runs.slice(offset, offset + limit)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async getRun(id: string): Promise<WorkflowRun | null> {
|
|
58
|
-
const data = await this.redis.hgetall(this.runKey(id))
|
|
59
|
-
if (!data.id) return null
|
|
60
|
-
return this.mapRunData(data)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async getRunSteps(
|
|
64
|
-
runId: string
|
|
65
|
-
): Promise<
|
|
66
|
-
Array<StepState & { stepName: string; rpcName?: string; data?: any }>
|
|
67
|
-
> {
|
|
68
|
-
const stepKeys = await this.scanKeys(this.stepKeyPattern(runId))
|
|
69
|
-
|
|
70
|
-
const steps: Array<
|
|
71
|
-
StepState & { stepName: string; rpcName?: string; data?: any }
|
|
72
|
-
> = []
|
|
73
|
-
|
|
74
|
-
for (const key of stepKeys) {
|
|
75
|
-
const data = await this.redis.hgetall(key)
|
|
76
|
-
if (!data.stepId) continue
|
|
77
|
-
|
|
78
|
-
steps.push({
|
|
79
|
-
stepId: data.stepId,
|
|
80
|
-
stepName: this.extractStepName(key, runId),
|
|
81
|
-
rpcName: data.rpcName || undefined,
|
|
82
|
-
data: data.data ? JSON.parse(data.data) : undefined,
|
|
83
|
-
status: data.status as any,
|
|
84
|
-
result: data.result ? JSON.parse(data.result) : undefined,
|
|
85
|
-
error: data.error ? JSON.parse(data.error) : undefined,
|
|
86
|
-
attemptCount: Number(data.attemptCount || 1),
|
|
87
|
-
retries: data.retries ? Number(data.retries) : undefined,
|
|
88
|
-
retryDelay: data.retryDelay || undefined,
|
|
89
|
-
createdAt: new Date(Number(data.createdAt)),
|
|
90
|
-
updatedAt: new Date(Number(data.updatedAt)),
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
steps.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
|
95
|
-
|
|
96
|
-
return steps
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async getRunHistory(
|
|
100
|
-
runId: string
|
|
101
|
-
): Promise<Array<StepState & { stepName: string }>> {
|
|
102
|
-
const stepKeys = await this.scanKeys(this.stepKeyPattern(runId))
|
|
103
|
-
|
|
104
|
-
const allHistoryEntries: Array<StepState & { stepName: string }> = []
|
|
105
|
-
|
|
106
|
-
for (const key of stepKeys) {
|
|
107
|
-
const data = await this.redis.hgetall(key)
|
|
108
|
-
if (!data.stepId) continue
|
|
109
|
-
|
|
110
|
-
const historyEntries = await this.redis.zrange(
|
|
111
|
-
this.stepHistoryKey(data.stepId),
|
|
112
|
-
0,
|
|
113
|
-
'-1'
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
for (const entryStr of historyEntries) {
|
|
117
|
-
const entry = JSON.parse(entryStr)
|
|
118
|
-
allHistoryEntries.push({
|
|
119
|
-
stepId: entry.stepId,
|
|
120
|
-
stepName: entry.stepName,
|
|
121
|
-
status: entry.status,
|
|
122
|
-
result: entry.result ? JSON.parse(entry.result) : undefined,
|
|
123
|
-
error: entry.error ? JSON.parse(entry.error) : undefined,
|
|
124
|
-
attemptCount: entry.attemptCount,
|
|
125
|
-
retries: entry.retries,
|
|
126
|
-
retryDelay: entry.retryDelay,
|
|
127
|
-
createdAt: new Date(entry.createdAt),
|
|
128
|
-
updatedAt: new Date(entry.createdAt),
|
|
129
|
-
runningAt: entry.runningAt ? new Date(entry.runningAt) : undefined,
|
|
130
|
-
scheduledAt: entry.scheduledAt
|
|
131
|
-
? new Date(entry.scheduledAt)
|
|
132
|
-
: undefined,
|
|
133
|
-
succeededAt: entry.succeededAt
|
|
134
|
-
? new Date(entry.succeededAt)
|
|
135
|
-
: undefined,
|
|
136
|
-
failedAt: entry.failedAt ? new Date(entry.failedAt) : undefined,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return allHistoryEntries.sort(
|
|
142
|
-
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async getDistinctWorkflowNames(): Promise<string[]> {
|
|
147
|
-
const runKeys = await this.scanKeys(`${this.keyPrefix}:run:*`)
|
|
148
|
-
|
|
149
|
-
const names = new Set<string>()
|
|
150
|
-
for (const key of runKeys) {
|
|
151
|
-
const workflow = await this.redis.hget(key, 'workflow')
|
|
152
|
-
if (workflow) names.add(workflow)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return [...names].sort()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async getWorkflowVersion(
|
|
159
|
-
name: string,
|
|
160
|
-
graphHash: string
|
|
161
|
-
): Promise<{ graph: any; source: string } | null> {
|
|
162
|
-
const data = await this.redis.hgetall(this.versionKey(name, graphHash))
|
|
163
|
-
if (!data.graph) return null
|
|
164
|
-
return {
|
|
165
|
-
graph: JSON.parse(data.graph),
|
|
166
|
-
source: data.source!,
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async deleteRun(id: string): Promise<boolean> {
|
|
171
|
-
const exists = await this.redis.exists(this.runKey(id))
|
|
172
|
-
if (!exists) return false
|
|
173
|
-
|
|
174
|
-
const stepKeys = await this.scanKeys(this.stepKeyPattern(id))
|
|
175
|
-
|
|
176
|
-
const keysToDelete = [this.runKey(id)]
|
|
177
|
-
|
|
178
|
-
for (const stepKey of stepKeys) {
|
|
179
|
-
const stepId = await this.redis.hget(stepKey, 'stepId')
|
|
180
|
-
keysToDelete.push(stepKey)
|
|
181
|
-
if (stepId) {
|
|
182
|
-
keysToDelete.push(this.stepHistoryKey(stepId))
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
await this.redis.del(...keysToDelete)
|
|
187
|
-
return true
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private mapRunData(data: Record<string, string>): WorkflowRun {
|
|
191
|
-
return {
|
|
192
|
-
id: data.id!,
|
|
193
|
-
workflow: data.workflow!,
|
|
194
|
-
status: data.status! as WorkflowStatus,
|
|
195
|
-
input: JSON.parse(data.input!),
|
|
196
|
-
output: data.output ? JSON.parse(data.output) : undefined,
|
|
197
|
-
error: data.error ? JSON.parse(data.error) : undefined,
|
|
198
|
-
inline: data.inline === 'true' ? true : undefined,
|
|
199
|
-
graphHash: data.graphHash || undefined,
|
|
200
|
-
deterministic: data.deterministic === 'true' ? true : undefined,
|
|
201
|
-
plannedSteps: data.plannedSteps
|
|
202
|
-
? JSON.parse(data.plannedSteps)
|
|
203
|
-
: undefined,
|
|
204
|
-
wire: data.wire ? JSON.parse(data.wire) : { type: 'unknown' },
|
|
205
|
-
createdAt: new Date(Number(data.createdAt!)),
|
|
206
|
-
updatedAt: new Date(Number(data.updatedAt!)),
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
private extractStepName(key: string, runId: string): string {
|
|
211
|
-
const prefix = `${this.keyPrefix}:step:${runId}:`
|
|
212
|
-
return key.slice(prefix.length)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private async scanKeys(pattern: string): Promise<string[]> {
|
|
216
|
-
const keys: string[] = []
|
|
217
|
-
let cursor = '0'
|
|
218
|
-
do {
|
|
219
|
-
const [newCursor, foundKeys] = await this.redis.scan(
|
|
220
|
-
cursor,
|
|
221
|
-
'MATCH',
|
|
222
|
-
pattern,
|
|
223
|
-
'COUNT',
|
|
224
|
-
100
|
|
225
|
-
)
|
|
226
|
-
cursor = newCursor
|
|
227
|
-
keys.push(...foundKeys)
|
|
228
|
-
} while (cursor !== '0')
|
|
229
|
-
return keys
|
|
230
|
-
}
|
|
231
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import Redis from 'ioredis-mock'
|
|
4
|
-
|
|
5
|
-
import { RedisWorkflowService } from './redis-workflow-service.js'
|
|
6
|
-
|
|
7
|
-
const KEY_PREFIX = 'workflows'
|
|
8
|
-
|
|
9
|
-
let ws: RedisWorkflowService
|
|
10
|
-
let redis: any
|
|
11
|
-
|
|
12
|
-
const newRun = () =>
|
|
13
|
-
ws.createRun('flow', {}, false, 'hash', { type: 'test' } as any)
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
redis = new Redis()
|
|
17
|
-
ws = new RedisWorkflowService(redis, KEY_PREFIX)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
redis.disconnect()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('workflow run state in redis', () => {
|
|
25
|
-
test('two branches setting different keys both survive', async () => {
|
|
26
|
-
const runId = await newRun()
|
|
27
|
-
|
|
28
|
-
await Promise.all([
|
|
29
|
-
ws.updateRunState(runId, 'left', 'a'),
|
|
30
|
-
ws.updateRunState(runId, 'right', 'b'),
|
|
31
|
-
])
|
|
32
|
-
|
|
33
|
-
assert.deepEqual(
|
|
34
|
-
await ws.getRunState(runId),
|
|
35
|
-
{ left: 'a', right: 'b' },
|
|
36
|
-
'a parallel branch overwrote the other branch’s state'
|
|
37
|
-
)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('many concurrent writers all land', async () => {
|
|
41
|
-
const runId = await newRun()
|
|
42
|
-
const keys = Array.from({ length: 20 }, (_, i) => `k${i}`)
|
|
43
|
-
|
|
44
|
-
await Promise.all(keys.map((k, i) => ws.updateRunState(runId, k, i)))
|
|
45
|
-
|
|
46
|
-
const state = await ws.getRunState(runId)
|
|
47
|
-
assert.equal(Object.keys(state).length, keys.length)
|
|
48
|
-
keys.forEach((k, i) => assert.equal(state[k], i))
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('an empty array stays an array when another key is written', async () => {
|
|
52
|
-
const runId = await newRun()
|
|
53
|
-
|
|
54
|
-
await ws.updateRunState(runId, 'items', [])
|
|
55
|
-
await ws.updateRunState(runId, 'count', 0)
|
|
56
|
-
|
|
57
|
-
const state = await ws.getRunState(runId)
|
|
58
|
-
assert.ok(
|
|
59
|
-
Array.isArray(state.items),
|
|
60
|
-
`an empty array came back as ${JSON.stringify(state.items)}`
|
|
61
|
-
)
|
|
62
|
-
assert.equal(state.count, 0)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test('values keep their types, including null and nested objects', async () => {
|
|
66
|
-
const runId = await newRun()
|
|
67
|
-
|
|
68
|
-
await ws.updateRunState(runId, 'nothing', null)
|
|
69
|
-
await ws.updateRunState(runId, 'nested', { a: [1, { b: true }] })
|
|
70
|
-
await ws.updateRunState(runId, 'flag', false)
|
|
71
|
-
|
|
72
|
-
assert.deepEqual(await ws.getRunState(runId), {
|
|
73
|
-
nothing: null,
|
|
74
|
-
nested: { a: [1, { b: true }] },
|
|
75
|
-
flag: false,
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('a later write to the same key replaces it', async () => {
|
|
80
|
-
const runId = await newRun()
|
|
81
|
-
|
|
82
|
-
await ws.updateRunState(runId, 'attempt', 1)
|
|
83
|
-
await ws.updateRunState(runId, 'attempt', 2)
|
|
84
|
-
|
|
85
|
-
assert.deepEqual(await ws.getRunState(runId), { attempt: 2 })
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('the state of a run that has none is empty', async () => {
|
|
89
|
-
assert.deepEqual(await ws.getRunState(await newRun()), {})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('state written before the per-key layout is still readable', async () => {
|
|
93
|
-
const runId = await newRun()
|
|
94
|
-
const redis = (ws as any).redis
|
|
95
|
-
await redis.hset(
|
|
96
|
-
(ws as any).runKey(runId),
|
|
97
|
-
'state',
|
|
98
|
-
JSON.stringify({ legacy: 'kept', shared: 'old' })
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
assert.deepEqual(await ws.getRunState(runId), {
|
|
102
|
-
legacy: 'kept',
|
|
103
|
-
shared: 'old',
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
await ws.updateRunState(runId, 'shared', 'new')
|
|
107
|
-
await ws.updateRunState(runId, 'fresh', 1)
|
|
108
|
-
|
|
109
|
-
assert.deepEqual(
|
|
110
|
-
await ws.getRunState(runId),
|
|
111
|
-
{ legacy: 'kept', shared: 'new', fresh: 1 },
|
|
112
|
-
'a run in flight across the deploy lost the state it already had'
|
|
113
|
-
)
|
|
114
|
-
})
|
|
115
|
-
})
|