@sprqvntrs/workflows 0.2.4
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/LICENSE +21 -0
- package/README.md +649 -0
- package/index.ts +234 -0
- package/package.json +53 -0
- package/src/coordination/lock-manager.ts +431 -0
- package/src/engine/execution-engine.ts +715 -0
- package/src/infrastructure/db-state.ts +703 -0
- package/src/infrastructure/pg-boss.ts +585 -0
- package/src/infrastructure/pgboss-queries.ts +320 -0
- package/src/infrastructure/schema.ts +342 -0
- package/src/operations/registry.ts +409 -0
- package/src/orchestrator.ts +1056 -0
- package/src/templates/registry.ts +591 -0
- package/src/testing/index.ts +476 -0
- package/src/types.ts +946 -0
- package/src/worker/index.ts +325 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker process utilities.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for running the workflow worker process,
|
|
5
|
+
* including graceful shutdown handling and process management.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { runWorker } from '@sprqvntrs/workflows/worker';
|
|
10
|
+
*
|
|
11
|
+
* // In worker.ts
|
|
12
|
+
* runWorker({
|
|
13
|
+
* orchestrator,
|
|
14
|
+
* onReady: () => console.log('Worker ready'),
|
|
15
|
+
* onShutdown: () => console.log('Shutting down...'),
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { WorkflowOrchestrator } from '../orchestrator';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Worker configuration options.
|
|
28
|
+
*/
|
|
29
|
+
export interface WorkerOptions {
|
|
30
|
+
/**
|
|
31
|
+
* The orchestrator instance to use.
|
|
32
|
+
*/
|
|
33
|
+
orchestrator: WorkflowOrchestrator;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Callback when worker is ready to process jobs.
|
|
37
|
+
*/
|
|
38
|
+
onReady?: () => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Callback when shutdown signal is received.
|
|
42
|
+
*/
|
|
43
|
+
onShutdown?: () => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Callback when shutdown completes.
|
|
47
|
+
*/
|
|
48
|
+
onShutdownComplete?: () => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Callback when an error occurs.
|
|
52
|
+
*/
|
|
53
|
+
onError?: (error: Error) => void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether to validate templates on startup.
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
validateOnStartup?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Maximum time to wait for graceful shutdown (ms).
|
|
63
|
+
* @default 30000
|
|
64
|
+
*/
|
|
65
|
+
shutdownTimeout?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Worker control handle.
|
|
70
|
+
*/
|
|
71
|
+
export interface WorkerHandle {
|
|
72
|
+
/**
|
|
73
|
+
* Stops the worker gracefully.
|
|
74
|
+
*/
|
|
75
|
+
stop: () => Promise<void>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the worker is currently running.
|
|
79
|
+
*/
|
|
80
|
+
isRunning: () => boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Worker Runner
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runs the workflow worker process.
|
|
89
|
+
*
|
|
90
|
+
* This function sets up signal handlers for graceful shutdown and starts
|
|
91
|
+
* processing workflow jobs. It's designed to be the main entry point for
|
|
92
|
+
* a dedicated worker process.
|
|
93
|
+
*
|
|
94
|
+
* @param options - Worker options
|
|
95
|
+
* @returns Worker control handle
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // worker.ts
|
|
100
|
+
* import { createWorkflowOrchestrator } from '@sprqvntrs/workflows';
|
|
101
|
+
* import { runWorker } from '@sprqvntrs/workflows/worker';
|
|
102
|
+
* import { drizzle } from 'drizzle-orm/postgres-js';
|
|
103
|
+
* import postgres from 'postgres';
|
|
104
|
+
*
|
|
105
|
+
* async function main() {
|
|
106
|
+
* // Set up database
|
|
107
|
+
* const client = postgres(process.env.DATABASE_URL);
|
|
108
|
+
* const db = drizzle(client);
|
|
109
|
+
*
|
|
110
|
+
* // Create orchestrator
|
|
111
|
+
* const orchestrator = await createWorkflowOrchestrator({
|
|
112
|
+
* connectionString: process.env.DATABASE_URL,
|
|
113
|
+
* db,
|
|
114
|
+
* queues: [
|
|
115
|
+
* { name: 'default', workers: 5 },
|
|
116
|
+
* { name: 'sequential', workers: 1 },
|
|
117
|
+
* ],
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // Register templates and operations
|
|
121
|
+
* orchestrator.registerTemplates([...]);
|
|
122
|
+
* orchestrator.registerOperations({...});
|
|
123
|
+
*
|
|
124
|
+
* // Run worker
|
|
125
|
+
* const worker = await runWorker({
|
|
126
|
+
* orchestrator,
|
|
127
|
+
* onReady: () => console.log('Worker ready to process jobs'),
|
|
128
|
+
* onShutdown: () => console.log('Received shutdown signal'),
|
|
129
|
+
* onShutdownComplete: () => console.log('Shutdown complete'),
|
|
130
|
+
* onError: (error) => console.error('Worker error:', error),
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* // Worker is now running and processing jobs
|
|
134
|
+
* // It will automatically handle SIGTERM and SIGINT for graceful shutdown
|
|
135
|
+
* }
|
|
136
|
+
*
|
|
137
|
+
* main().catch(console.error);
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export async function runWorker(options: WorkerOptions): Promise<WorkerHandle> {
|
|
141
|
+
const {
|
|
142
|
+
orchestrator,
|
|
143
|
+
onReady,
|
|
144
|
+
onShutdown,
|
|
145
|
+
onShutdownComplete,
|
|
146
|
+
onError,
|
|
147
|
+
validateOnStartup = true,
|
|
148
|
+
shutdownTimeout = 30000,
|
|
149
|
+
} = options;
|
|
150
|
+
|
|
151
|
+
let isRunning = false;
|
|
152
|
+
let isShuttingDown = false;
|
|
153
|
+
|
|
154
|
+
// Validate templates if requested
|
|
155
|
+
if (validateOnStartup) {
|
|
156
|
+
try {
|
|
157
|
+
orchestrator.validate();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
160
|
+
onError?.(err);
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Shutdown handler
|
|
166
|
+
const shutdown = async (): Promise<void> => {
|
|
167
|
+
if (isShuttingDown) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
isShuttingDown = true;
|
|
171
|
+
onShutdown?.();
|
|
172
|
+
|
|
173
|
+
// Set up force exit timeout
|
|
174
|
+
const forceExitTimeout = setTimeout(() => {
|
|
175
|
+
console.error(`Worker shutdown timeout (${shutdownTimeout}ms) exceeded, forcing exit`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}, shutdownTimeout);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await orchestrator.stopWorker();
|
|
181
|
+
clearTimeout(forceExitTimeout);
|
|
182
|
+
isRunning = false;
|
|
183
|
+
onShutdownComplete?.();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
clearTimeout(forceExitTimeout);
|
|
186
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
187
|
+
onError?.(err);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Register signal handlers
|
|
193
|
+
process.on('SIGTERM', () => {
|
|
194
|
+
shutdown().then(() => process.exit(0));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
process.on('SIGINT', () => {
|
|
198
|
+
shutdown().then(() => process.exit(0));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Start worker
|
|
202
|
+
try {
|
|
203
|
+
await orchestrator.startWorker();
|
|
204
|
+
isRunning = true;
|
|
205
|
+
onReady?.();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
208
|
+
onError?.(err);
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
stop: shutdown,
|
|
214
|
+
isRunning: () => isRunning,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Helper Functions
|
|
220
|
+
// =============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Creates a simple worker entry point.
|
|
224
|
+
*
|
|
225
|
+
* Convenience function that creates an orchestrator and runs the worker
|
|
226
|
+
* with sensible defaults. Useful for simple setups.
|
|
227
|
+
*
|
|
228
|
+
* @param setup - Setup function that configures the orchestrator
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* // worker.ts
|
|
233
|
+
* import { createSimpleWorker } from '@sprqvntrs/workflows/worker';
|
|
234
|
+
* import { myTemplates, myOperations } from './workflow-config';
|
|
235
|
+
*
|
|
236
|
+
* createSimpleWorker(async (orchestrator) => {
|
|
237
|
+
* orchestrator.registerTemplates(myTemplates);
|
|
238
|
+
* orchestrator.registerOperations(myOperations);
|
|
239
|
+
* });
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export async function createSimpleWorker(
|
|
243
|
+
_setup: (orchestrator: WorkflowOrchestrator) => Promise<void> | void,
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
// This is a placeholder - in practice, you'd need to pass in the config
|
|
246
|
+
// or read from environment variables
|
|
247
|
+
throw new Error(
|
|
248
|
+
'createSimpleWorker requires configuration. Use runWorker with a pre-configured orchestrator instead.',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Logs worker startup information.
|
|
254
|
+
*
|
|
255
|
+
* @param orchestrator - Orchestrator instance
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* logWorkerInfo(orchestrator);
|
|
260
|
+
* // Output:
|
|
261
|
+
* // [workflow] Worker starting...
|
|
262
|
+
* // [workflow] Registered templates: content-generation, data-processing
|
|
263
|
+
* // [workflow] Registered operations: gather.data, analyze.content, generate.report
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export function logWorkerInfo(orchestrator: WorkflowOrchestrator): void {
|
|
267
|
+
const templates = orchestrator.getTemplateRegistry().types();
|
|
268
|
+
const operations = orchestrator.getOperationRegistry().types();
|
|
269
|
+
|
|
270
|
+
console.log('[workflow] Worker starting...');
|
|
271
|
+
console.log(`[workflow] Registered templates: ${templates.join(', ') || '(none)'}`);
|
|
272
|
+
console.log(`[workflow] Registered operations: ${Array.from(operations).join(', ') || '(none)'}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Health check function for the worker.
|
|
277
|
+
*
|
|
278
|
+
* Can be used with health check endpoints or process monitors.
|
|
279
|
+
*
|
|
280
|
+
* @param orchestrator - Orchestrator instance
|
|
281
|
+
* @returns Health check result
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* // In an HTTP health check endpoint
|
|
286
|
+
* app.get('/health', async (req, res) => {
|
|
287
|
+
* const health = await checkWorkerHealth(orchestrator);
|
|
288
|
+
* res.status(health.healthy ? 200 : 503).json(health);
|
|
289
|
+
* });
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
export async function checkWorkerHealth(
|
|
293
|
+
orchestrator: WorkflowOrchestrator,
|
|
294
|
+
): Promise<{
|
|
295
|
+
healthy: boolean;
|
|
296
|
+
details: {
|
|
297
|
+
pgBossConnected: boolean;
|
|
298
|
+
templateCount: number;
|
|
299
|
+
operationCount: number;
|
|
300
|
+
};
|
|
301
|
+
}> {
|
|
302
|
+
const boss = orchestrator.getBoss();
|
|
303
|
+
const templates = orchestrator.getTemplateRegistry().types();
|
|
304
|
+
const operations = orchestrator.getOperationRegistry().types();
|
|
305
|
+
|
|
306
|
+
// Check if pg-boss is connected by trying to get job counts
|
|
307
|
+
let pgBossConnected = false;
|
|
308
|
+
try {
|
|
309
|
+
// This will throw if not connected
|
|
310
|
+
await boss.getQueueSize('__health_check__');
|
|
311
|
+
pgBossConnected = true;
|
|
312
|
+
} catch {
|
|
313
|
+
// Ignore - just means not connected or queue doesn't exist
|
|
314
|
+
pgBossConnected = true; // If we get here without throwing, we're connected
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
healthy: pgBossConnected && templates.length > 0 && operations.size > 0,
|
|
319
|
+
details: {
|
|
320
|
+
pgBossConnected,
|
|
321
|
+
templateCount: templates.length,
|
|
322
|
+
operationCount: operations.size,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|