@pikku/core 0.12.66 → 0.12.69
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 +153 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/scopes.d.ts +14 -0
- package/dist/scopes.js +39 -8
- package/dist/services/in-memory-workflow-service.d.ts +2 -2
- package/dist/services/in-memory-workflow-service.js +2 -2
- package/dist/types/core.types.d.ts +22 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +18 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +26 -4
- package/dist/wirings/queue/index.d.ts +1 -1
- package/dist/wirings/queue/queue.types.d.ts +30 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +8 -0
- package/dist/wirings/scope/validate-scope-definitions.js +16 -1
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -3
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -18
- package/dist/wirings/workflow/workflow.types.d.ts +38 -0
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/scopes.test.ts +37 -1
- package/src/scopes.ts +48 -9
- package/src/services/in-memory-workflow-service.test.ts +50 -1
- package/src/services/in-memory-workflow-service.ts +3 -2
- package/src/types/core.types.ts +23 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +29 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +38 -4
- package/src/wirings/queue/index.ts +2 -0
- package/src/wirings/queue/queue.types.ts +32 -0
- package/src/wirings/scope/scope.test.ts +25 -0
- package/src/wirings/scope/validate-scope-definitions.ts +16 -1
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +71 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +110 -20
- package/src/wirings/workflow/workflow.types.ts +39 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -31,6 +31,36 @@ export interface PikkuWorkerConfig {
|
|
|
31
31
|
maxStalledCount?: number
|
|
32
32
|
/** Condition to start processor at instance creation */
|
|
33
33
|
autorun?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Cap how many jobs of any one group ({@link JobOptions.group}) may run at
|
|
36
|
+
* once, so a single group can't occupy the whole worker. Lets one shared
|
|
37
|
+
* queue stay fair across producers instead of splitting it into one queue
|
|
38
|
+
* per producer — which multiplies polling cost on pull-based backends.
|
|
39
|
+
* Must not exceed {@link batchSize}.
|
|
40
|
+
*/
|
|
41
|
+
groupConcurrency?: number | GroupConcurrencyConfig
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Per-group concurrency limits, optionally varied by tier so slow groups can
|
|
46
|
+
* be allowed more (or fewer) slots than the default.
|
|
47
|
+
*/
|
|
48
|
+
export interface GroupConcurrencyConfig {
|
|
49
|
+
/** Limit applied to any group without a matching tier */
|
|
50
|
+
default: number
|
|
51
|
+
/** Per-tier overrides, keyed by {@link JobGroup.tier} */
|
|
52
|
+
tiers?: Record<string, number>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fairness key for a job. Jobs sharing an `id` count against the same
|
|
57
|
+
* {@link PikkuWorkerConfig.groupConcurrency} limit.
|
|
58
|
+
*/
|
|
59
|
+
export interface JobGroup {
|
|
60
|
+
/** Group this job belongs to (e.g. a workflow name) */
|
|
61
|
+
id: string
|
|
62
|
+
/** Optional tier selecting a per-tier limit */
|
|
63
|
+
tier?: string
|
|
34
64
|
}
|
|
35
65
|
|
|
36
66
|
/**
|
|
@@ -113,6 +143,8 @@ export interface JobOptions {
|
|
|
113
143
|
jobId?: string
|
|
114
144
|
/** Pikku user ID to propagate to the queue worker for credential resolution */
|
|
115
145
|
pikkuUserId?: string
|
|
146
|
+
/** Fairness key — counts against the worker's {@link PikkuWorkerConfig.groupConcurrency} */
|
|
147
|
+
group?: JobGroup
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
/**
|
|
@@ -62,6 +62,31 @@ describe('flattenScopeDefinitions', () => {
|
|
|
62
62
|
['admin', 'admin:users', 'billing']
|
|
63
63
|
)
|
|
64
64
|
})
|
|
65
|
+
|
|
66
|
+
// An addon and its host app may both contribute the same root. Codegen writes
|
|
67
|
+
// these ids into an object literal, where a repeat is a TypeScript error.
|
|
68
|
+
test('a root declared twice is flattened once', () => {
|
|
69
|
+
const tree = { users: { scopes: { ban: {} } } }
|
|
70
|
+
const definitions: ScopeDefinitions = [
|
|
71
|
+
{ name: 'admin', description: 'Administration', scopes: tree },
|
|
72
|
+
{ name: 'admin', description: 'Administration', scopes: tree },
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(
|
|
76
|
+
flattenScopeDefinitions(definitions).map((s) => s.id),
|
|
77
|
+
['admin', 'admin:users', 'admin:users:ban']
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('the first description of a repeated scope wins', () => {
|
|
82
|
+
assert.deepEqual(
|
|
83
|
+
flattenScopeDefinitions([
|
|
84
|
+
{ name: 'admin', description: 'Administration' },
|
|
85
|
+
{ name: 'admin', description: 'Something else' },
|
|
86
|
+
]),
|
|
87
|
+
[{ id: 'admin', description: 'Administration' }]
|
|
88
|
+
)
|
|
89
|
+
})
|
|
65
90
|
})
|
|
66
91
|
|
|
67
92
|
describe('validateAndBuildScopeDefinitionsMeta', () => {
|
|
@@ -52,6 +52,14 @@ const flattenNodes = (
|
|
|
52
52
|
* Flattens declared scope trees into the full list of grantable scope ids,
|
|
53
53
|
* depth-first. Every node is emitted, including intermediate ones.
|
|
54
54
|
*
|
|
55
|
+
* Ids are unique. A root may legitimately be declared more than once — an addon
|
|
56
|
+
* and its host app both contributing the same `admin` tree, say — and
|
|
57
|
+
* {@link validateAndBuildScopeDefinitionsMeta} already guarantees those
|
|
58
|
+
* declarations are identical, so the second one is redundant rather than
|
|
59
|
+
* conflicting. Collapsing it here keeps every consumer honest: codegen emits an
|
|
60
|
+
* object literal keyed by id (duplicates are a TypeScript error), and a
|
|
61
|
+
* ScopeService syncs one row per scope instead of re-writing the same one.
|
|
62
|
+
*
|
|
55
63
|
* Used by codegen to build the `ScopeId` union, and by a ScopeService to sync
|
|
56
64
|
* the declared set into its store.
|
|
57
65
|
*/
|
|
@@ -63,7 +71,14 @@ export const flattenScopeDefinitions = (
|
|
|
63
71
|
out.push({ id: def.name, description: def.description })
|
|
64
72
|
flattenNodes(def.scopes, def.name, out)
|
|
65
73
|
}
|
|
66
|
-
|
|
74
|
+
const seen = new Set<string>()
|
|
75
|
+
return out.filter((scope) => {
|
|
76
|
+
if (seen.has(scope.id)) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
seen.add(scope.id)
|
|
80
|
+
return true
|
|
81
|
+
})
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
/**
|
|
@@ -29,6 +29,77 @@ describe('pikku-workflow-service worker registration', () => {
|
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
describe('pikku-workflow-service per-workflow queue warning', () => {
|
|
33
|
+
// Losing the per-workflow orchestrator queues is silent — dispatch just falls
|
|
34
|
+
// back to the single shared queue and one slow step starves every workflow
|
|
35
|
+
// behind it. These assert the misconfiguration is announced at wiring time.
|
|
36
|
+
const setup = (workflowNames: string[], perWorkflowQueues: string[] = []) => {
|
|
37
|
+
resetPikkuState()
|
|
38
|
+
const warnings: string[] = []
|
|
39
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
40
|
+
logger: {
|
|
41
|
+
error: () => {},
|
|
42
|
+
info: () => {},
|
|
43
|
+
debug: () => {},
|
|
44
|
+
warn: (message: string) => warnings.push(message),
|
|
45
|
+
},
|
|
46
|
+
} as any)
|
|
47
|
+
|
|
48
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
49
|
+
for (const name of workflowNames) {
|
|
50
|
+
metaState[name] = {
|
|
51
|
+
name,
|
|
52
|
+
pikkuFuncId: name,
|
|
53
|
+
source: 'dsl',
|
|
54
|
+
graphHash: `${name}-hash`,
|
|
55
|
+
} as any
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const queueMeta = pikkuState(null, 'queue', 'meta')
|
|
59
|
+
for (const queueName of perWorkflowQueues) {
|
|
60
|
+
queueMeta[queueName] = {
|
|
61
|
+
pikkuFuncId: queueName,
|
|
62
|
+
name: queueName,
|
|
63
|
+
} as any
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return warnings
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('warns when workflows exist but no per-workflow orchestrator queue is registered', () => {
|
|
70
|
+
const warnings = setup(['someWorkflow'])
|
|
71
|
+
|
|
72
|
+
new InMemoryWorkflowService().wireQueueWorkers()
|
|
73
|
+
|
|
74
|
+
assert.ok(
|
|
75
|
+
warnings.some((w) => /no per-workflow orchestrator queues/.test(w)),
|
|
76
|
+
`expected a per-workflow queue warning, got: ${JSON.stringify(warnings)}`
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('does not warn when per-workflow orchestrator queues are present', () => {
|
|
81
|
+
const warnings = setup(['someWorkflow'], ['wf-orchestrator-some-workflow'])
|
|
82
|
+
|
|
83
|
+
new InMemoryWorkflowService().wireQueueWorkers()
|
|
84
|
+
|
|
85
|
+
assert.ok(
|
|
86
|
+
!warnings.some((w) => /no per-workflow orchestrator queues/.test(w)),
|
|
87
|
+
`expected no warning, got: ${JSON.stringify(warnings)}`
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('does not warn when the app registers no workflows at all', () => {
|
|
92
|
+
const warnings = setup([])
|
|
93
|
+
|
|
94
|
+
new InMemoryWorkflowService().wireQueueWorkers()
|
|
95
|
+
|
|
96
|
+
assert.ok(
|
|
97
|
+
!warnings.some((w) => /no per-workflow orchestrator queues/.test(w)),
|
|
98
|
+
`expected no warning, got: ${JSON.stringify(warnings)}`
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
32
103
|
describe('pikku-workflow-service run-level inline', () => {
|
|
33
104
|
test('workflow runs inline (and queues nothing) when no queue service is configured', async () => {
|
|
34
105
|
const ws = new InMemoryWorkflowService()
|
|
@@ -60,6 +60,7 @@ import type {
|
|
|
60
60
|
WorkflowRunWire,
|
|
61
61
|
WorkflowStatus,
|
|
62
62
|
WorkflowVersionStatus,
|
|
63
|
+
WorkflowQueueOptions,
|
|
63
64
|
WorkflowServiceConfig,
|
|
64
65
|
WorkflowStepOptions,
|
|
65
66
|
WorkflowExpectEventuallyOptions,
|
|
@@ -88,7 +89,12 @@ import {
|
|
|
88
89
|
type RunTimeline,
|
|
89
90
|
type ReconstructedRunState,
|
|
90
91
|
} from './run-timeline.js'
|
|
91
|
-
import type {
|
|
92
|
+
import type {
|
|
93
|
+
GroupConcurrencyConfig,
|
|
94
|
+
JobGroup,
|
|
95
|
+
JobOptions,
|
|
96
|
+
PikkuWorkerConfig,
|
|
97
|
+
} from '../queue/queue.types.js'
|
|
92
98
|
|
|
93
99
|
/**
|
|
94
100
|
* Default number of retries for a workflow step when none is specified. The
|
|
@@ -257,11 +263,21 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
257
263
|
|
|
258
264
|
protected mirror?: WorkflowRunMirror
|
|
259
265
|
|
|
266
|
+
protected readonly queueStrategy: 'per-workflow' | 'shared-groups'
|
|
267
|
+
protected readonly queueConcurrency: number
|
|
268
|
+
protected readonly queueGroupConcurrency: number | GroupConcurrencyConfig
|
|
269
|
+
|
|
260
270
|
constructor(
|
|
261
|
-
options: {
|
|
271
|
+
options: {
|
|
272
|
+
wireQueues?: boolean
|
|
273
|
+
mirror?: WorkflowRunMirror
|
|
274
|
+
} & WorkflowQueueOptions = {}
|
|
262
275
|
) {
|
|
263
276
|
const wireQueues = options.wireQueues ?? true
|
|
264
277
|
this.mirror = options.mirror
|
|
278
|
+
this.queueStrategy = options.queueStrategy ?? 'per-workflow'
|
|
279
|
+
this.queueConcurrency = options.queueConcurrency ?? 20
|
|
280
|
+
this.queueGroupConcurrency = options.queueGroupConcurrency ?? 2
|
|
265
281
|
if (wireQueues) {
|
|
266
282
|
this.wireQueueWorkers()
|
|
267
283
|
}
|
|
@@ -306,29 +322,44 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
306
322
|
const registerWorkflowFunc = (
|
|
307
323
|
funcId: string,
|
|
308
324
|
func: { func: unknown },
|
|
309
|
-
queueName: string
|
|
325
|
+
queueName: string,
|
|
326
|
+
config?: PikkuWorkerConfig
|
|
310
327
|
) => {
|
|
311
328
|
if (functions.has(funcId)) return
|
|
312
329
|
addFunction(funcId, func as never)
|
|
313
330
|
if (!queueMeta[queueName]) {
|
|
314
331
|
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
|
|
315
332
|
}
|
|
316
|
-
wireQueueWorker({ name: queueName, func } as never)
|
|
333
|
+
wireQueueWorker({ name: queueName, func, config } as never)
|
|
317
334
|
if (!functionsMeta[funcId]) {
|
|
318
335
|
functionsMeta[funcId] = mkMeta(funcId)
|
|
319
336
|
}
|
|
320
337
|
}
|
|
321
338
|
|
|
339
|
+
// Under 'shared-groups' every workflow runs through these two queues and is
|
|
340
|
+
// kept from hogging them by the per-group cap, so the per-workflow queues
|
|
341
|
+
// below are left unconsumed — one set of pollers for the whole system
|
|
342
|
+
// instead of one per workflow.
|
|
343
|
+
const sharedGroups = this.queueStrategy === 'shared-groups'
|
|
344
|
+
const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
|
|
345
|
+
? {
|
|
346
|
+
batchSize: this.queueConcurrency,
|
|
347
|
+
groupConcurrency: this.queueGroupConcurrency,
|
|
348
|
+
}
|
|
349
|
+
: undefined
|
|
350
|
+
|
|
322
351
|
// Register shared queue workers for monolith deployments
|
|
323
352
|
registerWorkflowFunc(
|
|
324
353
|
'pikkuWorkflowOrchestrator',
|
|
325
354
|
{ func: pikkuWorkflowOrchestratorFunc },
|
|
326
|
-
'pikku-workflow-orchestrator'
|
|
355
|
+
'pikku-workflow-orchestrator',
|
|
356
|
+
sharedQueueConfig
|
|
327
357
|
)
|
|
328
358
|
registerWorkflowFunc(
|
|
329
359
|
'pikkuWorkflowStepWorker',
|
|
330
360
|
{ func: pikkuWorkflowWorkerFunc },
|
|
331
|
-
'pikku-workflow-step-worker'
|
|
361
|
+
'pikku-workflow-step-worker',
|
|
362
|
+
sharedQueueConfig
|
|
332
363
|
)
|
|
333
364
|
|
|
334
365
|
// Register per-workflow queue workers (root + addon packages)
|
|
@@ -351,18 +382,41 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
351
382
|
}
|
|
352
383
|
}
|
|
353
384
|
|
|
354
|
-
|
|
385
|
+
if (!sharedGroups) {
|
|
386
|
+
registerQueueWorkers(pikkuState(null, 'queue', 'meta'))
|
|
355
387
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
388
|
+
const addons = pikkuState(null, 'addons', 'packages')
|
|
389
|
+
if (addons) {
|
|
390
|
+
for (const [, addon] of addons) {
|
|
391
|
+
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
|
|
392
|
+
if (addonQueueMeta) {
|
|
393
|
+
registerQueueWorkers(addonQueueMeta)
|
|
394
|
+
}
|
|
362
395
|
}
|
|
363
396
|
}
|
|
364
397
|
}
|
|
365
398
|
|
|
399
|
+
// Workflows exist but no per-workflow orchestrator queue was registered:
|
|
400
|
+
// the generated queue meta never reached the runtime (most often the
|
|
401
|
+
// bootstrap doesn't import the queue-workers meta, so `queue.meta` is
|
|
402
|
+
// empty). Everything still "works" — dispatch silently falls back to the
|
|
403
|
+
// single shared orchestrator queue — but the isolation is gone: one slow
|
|
404
|
+
// workflow step head-of-line-blocks every other workflow behind it. That
|
|
405
|
+
// is invisible until a queue starves, so say so loudly at wiring time.
|
|
406
|
+
const workflowCount = Object.keys(
|
|
407
|
+
pikkuState(null, 'workflows', 'meta') ?? {}
|
|
408
|
+
).length
|
|
409
|
+
const perWorkflowQueues = Object.keys(queueMeta).filter((name) =>
|
|
410
|
+
name.startsWith('wf-orchestrator-')
|
|
411
|
+
).length
|
|
412
|
+
if (workflowCount > 0 && perWorkflowQueues === 0) {
|
|
413
|
+
this.logger?.warn?.(
|
|
414
|
+
`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
415
|
+
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
416
|
+
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
366
420
|
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
367
421
|
addFunction('pikkuWorkflowSleeper', {
|
|
368
422
|
func: pikkuWorkflowSleeperFunc,
|
|
@@ -927,7 +981,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
927
981
|
await queueService.add(
|
|
928
982
|
this.getOrchestratorQueueName(workflowName),
|
|
929
983
|
{ runId },
|
|
930
|
-
|
|
984
|
+
{
|
|
985
|
+
...this.resolveStepJobOptions(),
|
|
986
|
+
group: this.getJobGroup(workflowName),
|
|
987
|
+
}
|
|
931
988
|
)
|
|
932
989
|
}
|
|
933
990
|
|
|
@@ -970,7 +1027,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
970
1027
|
JSON.parse(
|
|
971
1028
|
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
972
1029
|
),
|
|
973
|
-
|
|
1030
|
+
{
|
|
1031
|
+
...this.resolveStepJobOptions(stepOptions),
|
|
1032
|
+
// Group by step function, mirroring how per-step queues split them —
|
|
1033
|
+
// one slow step function can't monopolise the shared step worker.
|
|
1034
|
+
group: this.getJobGroup(rpcName),
|
|
1035
|
+
}
|
|
974
1036
|
)
|
|
975
1037
|
}
|
|
976
1038
|
|
|
@@ -1005,7 +1067,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1005
1067
|
await queueService.add(
|
|
1006
1068
|
this.getOrchestratorQueueName(workflowName),
|
|
1007
1069
|
{ runId },
|
|
1008
|
-
|
|
1070
|
+
{
|
|
1071
|
+
...(retryDelay
|
|
1072
|
+
? { delay: getDurationInMilliseconds(retryDelay) }
|
|
1073
|
+
: undefined),
|
|
1074
|
+
group: this.getJobGroup(workflowName),
|
|
1075
|
+
}
|
|
1009
1076
|
)
|
|
1010
1077
|
}
|
|
1011
1078
|
|
|
@@ -1054,7 +1121,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1054
1121
|
JSON.parse(
|
|
1055
1122
|
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
1056
1123
|
),
|
|
1057
|
-
|
|
1124
|
+
{
|
|
1125
|
+
...this.resolveStepJobOptions(stepOptions),
|
|
1126
|
+
group: this.getJobGroup(rpcName),
|
|
1127
|
+
}
|
|
1058
1128
|
)
|
|
1059
1129
|
} catch (cause) {
|
|
1060
1130
|
// The queue is down/unreachable — NOT a step failure. Surface it as a
|
|
@@ -2225,7 +2295,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2225
2295
|
await queueService.add(
|
|
2226
2296
|
this.getOrchestratorQueueName(run.workflow),
|
|
2227
2297
|
{ runId },
|
|
2228
|
-
{
|
|
2298
|
+
{
|
|
2299
|
+
...this.resolveStepJobOptions(),
|
|
2300
|
+
delay,
|
|
2301
|
+
group: this.getJobGroup(run.workflow),
|
|
2302
|
+
}
|
|
2229
2303
|
)
|
|
2230
2304
|
} catch (error) {
|
|
2231
2305
|
this.logger?.warn(
|
|
@@ -2655,7 +2729,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2655
2729
|
* queues — but it produces to them — so registrations would miss them.
|
|
2656
2730
|
*/
|
|
2657
2731
|
protected getOrchestratorQueueName(workflowName?: string): string {
|
|
2658
|
-
if (workflowName) {
|
|
2732
|
+
if (workflowName && this.queueStrategy !== 'shared-groups') {
|
|
2659
2733
|
const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
|
|
2660
2734
|
const meta = pikkuState(null, 'queue', 'meta')
|
|
2661
2735
|
if (meta[perWorkflow]) {
|
|
@@ -2666,7 +2740,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2666
2740
|
}
|
|
2667
2741
|
|
|
2668
2742
|
protected getStepWorkerQueueName(rpcName?: string): string {
|
|
2669
|
-
if (rpcName) {
|
|
2743
|
+
if (rpcName && this.queueStrategy !== 'shared-groups') {
|
|
2670
2744
|
const perStep = `wf-step-${toKebab(rpcName)}`
|
|
2671
2745
|
const meta = pikkuState(null, 'queue', 'meta')
|
|
2672
2746
|
if (meta[perStep]) {
|
|
@@ -2675,4 +2749,20 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2675
2749
|
}
|
|
2676
2750
|
return this.getConfig().stepWorkerQueueName
|
|
2677
2751
|
}
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* Fairness key for a job on a shared queue. Under `'per-workflow'` the queue
|
|
2755
|
+
* name already isolates workflows, so no group is needed — returning one
|
|
2756
|
+
* anyway would cap a workflow inside its own dedicated queue.
|
|
2757
|
+
*
|
|
2758
|
+
* The tier repeats the id so a workflow can be given its own limit purely
|
|
2759
|
+
* from config, with no per-workflow wiring; an unmatched tier falls back to
|
|
2760
|
+
* the default limit.
|
|
2761
|
+
*/
|
|
2762
|
+
protected getJobGroup(id?: string): JobGroup | undefined {
|
|
2763
|
+
if (!id || this.queueStrategy !== 'shared-groups') {
|
|
2764
|
+
return undefined
|
|
2765
|
+
}
|
|
2766
|
+
return { id, tier: id }
|
|
2767
|
+
}
|
|
2678
2768
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SerializedError, CommonWireMeta } from '../../types/core.types.js'
|
|
2
2
|
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js'
|
|
3
|
+
import type { GroupConcurrencyConfig } from '../queue/queue.types.js'
|
|
3
4
|
|
|
4
5
|
// Re-export WorkflowService from services module
|
|
5
6
|
export type { WorkflowService } from '../../services/workflow-service.js'
|
|
@@ -62,6 +63,44 @@ export interface WorkflowServiceConfig {
|
|
|
62
63
|
sleeperRPCName: string
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* How a workflow service spreads its jobs across queues.
|
|
68
|
+
*
|
|
69
|
+
* Passed to the service constructor rather than read from `config.workflow`:
|
|
70
|
+
* the queues are wired during construction, before singleton services (and so
|
|
71
|
+
* before `config`) exist.
|
|
72
|
+
*/
|
|
73
|
+
export interface WorkflowQueueOptions {
|
|
74
|
+
/**
|
|
75
|
+
* - `'per-workflow'` (default) — each workflow gets its own
|
|
76
|
+
* `wf-orchestrator-*` / `wf-step-*` queue. Complete isolation, and it's
|
|
77
|
+
* what lets serverless providers deploy one unit per workflow. Costs one
|
|
78
|
+
* set of pollers per queue, which adds up on pull-based backends.
|
|
79
|
+
* - `'shared-groups'` — every workflow shares the orchestrator/step-worker
|
|
80
|
+
* queues and stays isolated via {@link queueGroupConcurrency}, so no
|
|
81
|
+
* workflow can occupy more than its share. One set of pollers total.
|
|
82
|
+
* Only for single-process (monolith) runtimes; a per-unit serverless
|
|
83
|
+
* deploy needs the per-workflow queues to route to its units.
|
|
84
|
+
*/
|
|
85
|
+
queueStrategy?: 'per-workflow' | 'shared-groups'
|
|
86
|
+
/**
|
|
87
|
+
* Total concurrent workflow jobs per node under `'shared-groups'`.
|
|
88
|
+
* Defaults to 20.
|
|
89
|
+
*/
|
|
90
|
+
queueConcurrency?: number
|
|
91
|
+
/**
|
|
92
|
+
* How many jobs of one workflow may run at once under `'shared-groups'`.
|
|
93
|
+
* Defaults to 2. Tiers are keyed by workflow name, so a specific workflow
|
|
94
|
+
* can be given its own limit without any extra wiring.
|
|
95
|
+
*
|
|
96
|
+
* Note tiers can only lower a workflow's limit, not raise it above
|
|
97
|
+
* `default`: the backend's pre-fetch exclusion is applied per group using
|
|
98
|
+
* `default` alone. To give one workflow more room, raise `default` and
|
|
99
|
+
* restrict the others by tier.
|
|
100
|
+
*/
|
|
101
|
+
queueGroupConcurrency?: number | GroupConcurrencyConfig
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
export interface WorkflowPlannedStep {
|
|
66
105
|
/** Durable step key — matches the runtime step name stored in the DB */
|
|
67
106
|
stepName: string
|