nvent 0.5.3 → 0.5.5
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/dist/module.json +1 -1
- package/dist/module.mjs +110 -24
- package/dist/runtime/adapters/factory.js +8 -7
- package/dist/runtime/adapters/interfaces/queue.d.ts +5 -0
- package/dist/runtime/config/index.js +14 -1
- package/dist/runtime/config/types.d.ts +42 -0
- package/dist/runtime/events/types.d.ts +0 -1
- package/dist/runtime/events/utils/stallDetector.d.ts +13 -77
- package/dist/runtime/events/utils/stallDetector.js +8 -192
- package/dist/runtime/events/wiring/flowWiring.js +311 -107
- package/dist/runtime/events/wiring/triggerWiring.js +3 -1
- package/dist/runtime/nitro/plugins/02.workers.js +31 -2
- package/dist/runtime/nitro/routes/webhook.await.js +28 -6
- package/dist/runtime/nitro/utils/awaitPatterns/event.js +58 -50
- package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +6 -1
- package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +1 -1
- package/dist/runtime/nitro/utils/awaitPatterns/time.js +6 -2
- package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +53 -45
- package/dist/runtime/nitro/utils/defineFunction.d.ts +2 -9
- package/dist/runtime/nitro/utils/defineFunction.js +1 -14
- package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +84 -16
- package/dist/runtime/nitro/utils/defineHooks.d.ts +64 -10
- package/dist/runtime/nitro/utils/defineHooks.js +3 -0
- package/dist/runtime/nitro/utils/useAwait.d.ts +12 -0
- package/dist/runtime/nitro/utils/useAwait.js +34 -4
- package/dist/runtime/nitro/utils/useFlow.js +13 -2
- package/dist/runtime/nitro/utils/useHookRegistry.d.ts +10 -4
- package/dist/runtime/scheduler/scheduler.d.ts +19 -0
- package/dist/runtime/scheduler/scheduler.js +184 -8
- package/dist/runtime/scheduler/types.d.ts +6 -0
- package/dist/runtime/worker/node/runner.js +32 -91
- package/dist/runtime/worker/system/awaitHandlers.d.ts +27 -0
- package/dist/runtime/worker/system/awaitHandlers.js +230 -0
- package/dist/runtime/worker/system/index.d.ts +24 -0
- package/dist/runtime/worker/system/index.js +39 -0
- package/package.json +1 -1
|
@@ -10,7 +10,34 @@ import {
|
|
|
10
10
|
registerTimeAwait,
|
|
11
11
|
resolveTimeAwait
|
|
12
12
|
} from "./awaitPatterns/index.js";
|
|
13
|
-
import { useStoreAdapter, useStreamTopics, useNventLogger } from "#imports";
|
|
13
|
+
import { useStoreAdapter, useStreamTopics, useNventLogger, useRuntimeConfig } from "#imports";
|
|
14
|
+
export function useAwaitDefaults() {
|
|
15
|
+
try {
|
|
16
|
+
const config = useRuntimeConfig();
|
|
17
|
+
const awaitDefaults = config?.nvent?.flow?.awaitDefaults;
|
|
18
|
+
return {
|
|
19
|
+
webhookTimeout: awaitDefaults?.webhookTimeout ?? 24 * 60 * 60 * 1e3,
|
|
20
|
+
// 24 hours
|
|
21
|
+
eventTimeout: awaitDefaults?.eventTimeout ?? 24 * 60 * 60 * 1e3,
|
|
22
|
+
// 24 hours
|
|
23
|
+
timeTimeout: awaitDefaults?.timeTimeout,
|
|
24
|
+
// undefined by default
|
|
25
|
+
scheduleTimeout: awaitDefaults?.scheduleTimeout,
|
|
26
|
+
// undefined by default
|
|
27
|
+
timeoutAction: awaitDefaults?.timeoutAction ?? "fail"
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return {
|
|
31
|
+
webhookTimeout: 24 * 60 * 60 * 1e3,
|
|
32
|
+
// 24 hours
|
|
33
|
+
eventTimeout: 24 * 60 * 60 * 1e3,
|
|
34
|
+
// 24 hours
|
|
35
|
+
timeTimeout: void 0,
|
|
36
|
+
scheduleTimeout: void 0,
|
|
37
|
+
timeoutAction: "fail"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
14
41
|
export function useAwait() {
|
|
15
42
|
const logger = useNventLogger("await");
|
|
16
43
|
const store = useStoreAdapter();
|
|
@@ -62,7 +89,10 @@ export function useAwait() {
|
|
|
62
89
|
}
|
|
63
90
|
const awaitingSteps = entry.metadata.awaitingSteps || {};
|
|
64
91
|
if (stepName) {
|
|
65
|
-
|
|
92
|
+
const awaitKeyBefore = `${stepName}:before`;
|
|
93
|
+
const awaitKeyAfter = `${stepName}:after`;
|
|
94
|
+
const awaitState = awaitingSteps[awaitKeyBefore] || awaitingSteps[awaitKeyAfter] || awaitingSteps[stepName];
|
|
95
|
+
return awaitState || null;
|
|
66
96
|
}
|
|
67
97
|
return awaitingSteps;
|
|
68
98
|
},
|
|
@@ -78,8 +108,8 @@ export function useAwait() {
|
|
|
78
108
|
*/
|
|
79
109
|
async getAllActiveAwaits(flowName) {
|
|
80
110
|
const activeAwaits = [];
|
|
81
|
-
if (!store.
|
|
82
|
-
logger.warn("Store does not support
|
|
111
|
+
if (!store.index.read) {
|
|
112
|
+
logger.warn("Store does not support index read");
|
|
83
113
|
return activeAwaits;
|
|
84
114
|
}
|
|
85
115
|
if (flowName) {
|
|
@@ -20,9 +20,20 @@ export function useFlow() {
|
|
|
20
20
|
);
|
|
21
21
|
const opts = entryWorker?.queue?.defaultJobOptions || {};
|
|
22
22
|
const flowId = randomUUID();
|
|
23
|
-
const id = await queueAdapter.enqueue(queueName, {
|
|
23
|
+
const id = await queueAdapter.enqueue(queueName, {
|
|
24
|
+
name: flow.entry.step,
|
|
25
|
+
data: { ...payload, flowId, flowName },
|
|
26
|
+
opts
|
|
27
|
+
});
|
|
24
28
|
try {
|
|
25
|
-
await eventsManager.publishBus({
|
|
29
|
+
await eventsManager.publishBus({
|
|
30
|
+
type: "flow.start",
|
|
31
|
+
runId: flowId,
|
|
32
|
+
flowName,
|
|
33
|
+
data: {
|
|
34
|
+
input: payload
|
|
35
|
+
}
|
|
36
|
+
});
|
|
26
37
|
} catch {
|
|
27
38
|
}
|
|
28
39
|
return { id, queue: queueName, step: flow.entry.step, flowId };
|
|
@@ -5,18 +5,24 @@
|
|
|
5
5
|
export interface LifecycleHooks {
|
|
6
6
|
/**
|
|
7
7
|
* Called when await pattern is registered
|
|
8
|
-
* @param
|
|
8
|
+
* @param hookData - Type-safe data based on await type (webhookUrl, eventName, etc.)
|
|
9
9
|
* @param stepData - Current step data
|
|
10
|
-
* @param ctx - Worker context
|
|
10
|
+
* @param ctx - Worker context with awaitType and awaitConfig
|
|
11
11
|
*/
|
|
12
|
-
onAwaitRegister?: (
|
|
12
|
+
onAwaitRegister?: (hookData: any, stepData: any, ctx: any) => Promise<void>;
|
|
13
13
|
/**
|
|
14
14
|
* Called when await pattern is resolved
|
|
15
15
|
* @param resolvedData - Data from the trigger that resolved the await
|
|
16
16
|
* @param stepData - Current step data
|
|
17
|
-
* @param ctx - Worker context
|
|
17
|
+
* @param ctx - Worker context with awaitType
|
|
18
18
|
*/
|
|
19
19
|
onAwaitResolve?: (resolvedData: any, stepData: any, ctx: any) => Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Called when await pattern times out
|
|
22
|
+
* @param stepData - Current step data
|
|
23
|
+
* @param ctx - Worker context with awaitType and timeoutAction
|
|
24
|
+
*/
|
|
25
|
+
onAwaitTimeout?: (stepData: any, ctx: any) => Promise<void>;
|
|
20
26
|
}
|
|
21
27
|
export declare function useHookRegistry(): {
|
|
22
28
|
/**
|
|
@@ -44,6 +44,7 @@ export declare class Scheduler implements SchedulerAdapter {
|
|
|
44
44
|
private lockRenewalTimers;
|
|
45
45
|
private started;
|
|
46
46
|
private logger;
|
|
47
|
+
private syncSubscription?;
|
|
47
48
|
constructor(options: SchedulerOptions);
|
|
48
49
|
schedule(job: ScheduledJob): Promise<string>;
|
|
49
50
|
/**
|
|
@@ -60,6 +61,18 @@ export declare class Scheduler implements SchedulerAdapter {
|
|
|
60
61
|
* Release distributed lock
|
|
61
62
|
*/
|
|
62
63
|
private releaseLock;
|
|
64
|
+
/**
|
|
65
|
+
* Poll for new jobs created by other instances
|
|
66
|
+
* This ensures all instances have the same jobs in-memory
|
|
67
|
+
*
|
|
68
|
+
* NOTE: This is a simple but not optimal solution for distributed systems.
|
|
69
|
+
* TODO: For production multi-instance deployments, consider:
|
|
70
|
+
* - Pub/Sub notifications (Redis PUBSUB, Postgres NOTIFY/LISTEN)
|
|
71
|
+
* - Dedicated scheduler instance with queue-based execution
|
|
72
|
+
* - Service mesh with leader election
|
|
73
|
+
* See specs/distributed-scheduler.md for detailed architecture
|
|
74
|
+
*/
|
|
75
|
+
private startJobSyncPolling;
|
|
63
76
|
/**
|
|
64
77
|
* Start periodic lock renewal (for long-running jobs)
|
|
65
78
|
*/
|
|
@@ -104,6 +117,12 @@ export declare class Scheduler implements SchedulerAdapter {
|
|
|
104
117
|
start(): Promise<void>;
|
|
105
118
|
stop(): Promise<void>;
|
|
106
119
|
getScheduledJobs(): Promise<ScheduledJob[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Get jobs by pattern (e.g., by runId)
|
|
122
|
+
* This queries the persisted store, not just in-memory jobs
|
|
123
|
+
* Works across all instances in a distributed setup
|
|
124
|
+
*/
|
|
125
|
+
getJobsByPattern(pattern: string): Promise<ScheduledJob[]>;
|
|
107
126
|
/**
|
|
108
127
|
* Get all persisted jobs from store (for debugging/monitoring)
|
|
109
128
|
* This shows ALL jobs across ALL instances with their runtime stats
|
|
@@ -2,7 +2,8 @@ import { CronJob } from "cron";
|
|
|
2
2
|
import { getEventBus } from "../events/eventBus.js";
|
|
3
3
|
import { resolveTimeAwait } from "../nitro/utils/awaitPatterns/time.js";
|
|
4
4
|
import { resolveScheduleAwait } from "../nitro/utils/awaitPatterns/schedule.js";
|
|
5
|
-
import { useNventLogger } from "#imports";
|
|
5
|
+
import { useNventLogger, useStoreAdapter, useRuntimeConfig } from "#imports";
|
|
6
|
+
import { createStallDetector } from "../events/utils/stallDetector.js";
|
|
6
7
|
export class Scheduler {
|
|
7
8
|
store;
|
|
8
9
|
keyPrefix;
|
|
@@ -14,6 +15,7 @@ export class Scheduler {
|
|
|
14
15
|
lockRenewalTimers = /* @__PURE__ */ new Map();
|
|
15
16
|
started = false;
|
|
16
17
|
logger = useNventLogger("scheduler");
|
|
18
|
+
syncSubscription;
|
|
17
19
|
constructor(options) {
|
|
18
20
|
this.store = options.store;
|
|
19
21
|
this.keyPrefix = options.keyPrefix || "nvent:scheduler";
|
|
@@ -194,6 +196,54 @@ export class Scheduler {
|
|
|
194
196
|
await this.store.kv.delete(lockKey);
|
|
195
197
|
}
|
|
196
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Poll for new jobs created by other instances
|
|
201
|
+
* This ensures all instances have the same jobs in-memory
|
|
202
|
+
*
|
|
203
|
+
* NOTE: This is a simple but not optimal solution for distributed systems.
|
|
204
|
+
* TODO: For production multi-instance deployments, consider:
|
|
205
|
+
* - Pub/Sub notifications (Redis PUBSUB, Postgres NOTIFY/LISTEN)
|
|
206
|
+
* - Dedicated scheduler instance with queue-based execution
|
|
207
|
+
* - Service mesh with leader election
|
|
208
|
+
* See specs/distributed-scheduler.md for detailed architecture
|
|
209
|
+
*/
|
|
210
|
+
startJobSyncPolling() {
|
|
211
|
+
const pollInterval = setInterval(async () => {
|
|
212
|
+
if (!this.started) {
|
|
213
|
+
clearInterval(pollInterval);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
if (!this.store.index.read) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const jobIndex = `${this.keyPrefix}:jobs`;
|
|
221
|
+
const entries = await this.store.index.read(jobIndex, { limit: 1e4 });
|
|
222
|
+
let syncedCount = 0;
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const jobData = entry.metadata;
|
|
225
|
+
if (jobData && !this.jobs.has(jobData.id) && jobData.enabled !== false) {
|
|
226
|
+
this.logger.debug("Syncing job from another instance", {
|
|
227
|
+
jobId: jobData.id
|
|
228
|
+
});
|
|
229
|
+
await this.recoverJob(jobData);
|
|
230
|
+
syncedCount++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (syncedCount > 0) {
|
|
234
|
+
this.logger.info("Synced jobs from other instances", {
|
|
235
|
+
count: syncedCount,
|
|
236
|
+
totalJobs: this.jobs.size
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.logger.error("Error during job sync polling", {
|
|
241
|
+
error: error.message
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}, 3e4);
|
|
245
|
+
this.syncSubscription = () => clearInterval(pollInterval);
|
|
246
|
+
}
|
|
197
247
|
/**
|
|
198
248
|
* Start periodic lock renewal (for long-running jobs)
|
|
199
249
|
*/
|
|
@@ -350,6 +400,12 @@ export class Scheduler {
|
|
|
350
400
|
this.logger.debug("Skipping disabled job", { jobId: jobData.id });
|
|
351
401
|
return;
|
|
352
402
|
}
|
|
403
|
+
if (jobData.executeAt && typeof jobData.executeAt === "string") {
|
|
404
|
+
jobData.executeAt = new Date(jobData.executeAt).getTime();
|
|
405
|
+
}
|
|
406
|
+
if (jobData.interval && typeof jobData.interval === "string") {
|
|
407
|
+
jobData.interval = Number(jobData.interval);
|
|
408
|
+
}
|
|
353
409
|
if (jobData.metadata?.type === "schedule-trigger" && jobData.metadata?.triggerName) {
|
|
354
410
|
jobData.handler = async () => {
|
|
355
411
|
const logger = useNventLogger("scheduler");
|
|
@@ -442,6 +498,19 @@ export class Scheduler {
|
|
|
442
498
|
flowName,
|
|
443
499
|
runId
|
|
444
500
|
});
|
|
501
|
+
} else if (jobData.metadata?.component === "stall-detector") {
|
|
502
|
+
const { flowName, runId } = jobData.metadata;
|
|
503
|
+
jobData.handler = async () => {
|
|
504
|
+
const store = useStoreAdapter();
|
|
505
|
+
const config = useRuntimeConfig();
|
|
506
|
+
const stallDetector = createStallDetector(store, config.nvent?.flow?.stallDetection);
|
|
507
|
+
this.logger.info(`Per-flow stall timeout fired for '${flowName}' runId '${runId}'`);
|
|
508
|
+
await stallDetector.markAsStalled(flowName, runId, "Stall timeout reached");
|
|
509
|
+
};
|
|
510
|
+
this.logger.info("Reconstructed stall detector handler", {
|
|
511
|
+
flowName,
|
|
512
|
+
runId
|
|
513
|
+
});
|
|
445
514
|
} else if (!jobData.handler) {
|
|
446
515
|
this.logger.debug("Skipping job - no handler available, waiting for re-registration", {
|
|
447
516
|
jobId: jobData.id
|
|
@@ -472,9 +541,85 @@ export class Scheduler {
|
|
|
472
541
|
this.jobs.set(jobData.id, intervalId);
|
|
473
542
|
this.logger.info("Recovered interval job", { jobId: jobData.id });
|
|
474
543
|
} else if (jobData.type === "one-time" && jobData.executeAt) {
|
|
475
|
-
const
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const delay = jobData.executeAt - now;
|
|
476
546
|
const isAwaitPattern = jobData.metadata?.component === "await-pattern";
|
|
477
|
-
|
|
547
|
+
const isStallDetector = jobData.metadata?.component === "stall-detector";
|
|
548
|
+
const awaitType = jobData.metadata?.awaitType;
|
|
549
|
+
this.logger.debug("Recovering one-time job", {
|
|
550
|
+
jobId: jobData.id,
|
|
551
|
+
component: jobData.metadata?.component,
|
|
552
|
+
awaitType,
|
|
553
|
+
executeAt: jobData.executeAt,
|
|
554
|
+
executeAtISO: new Date(jobData.executeAt).toISOString(),
|
|
555
|
+
now,
|
|
556
|
+
nowISO: new Date(now).toISOString(),
|
|
557
|
+
delay,
|
|
558
|
+
delayHours: (delay / 1e3 / 60 / 60).toFixed(2)
|
|
559
|
+
});
|
|
560
|
+
if (isAwaitPattern && (awaitType === "webhook" || awaitType === "event")) {
|
|
561
|
+
if (delay > 0) {
|
|
562
|
+
const timeoutId = setTimeout(
|
|
563
|
+
async () => {
|
|
564
|
+
await this.executeWithLock(jobData);
|
|
565
|
+
await this.unschedule(jobData.id);
|
|
566
|
+
},
|
|
567
|
+
delay
|
|
568
|
+
);
|
|
569
|
+
this.jobs.set(jobData.id, timeoutId);
|
|
570
|
+
this.logger.info("Recovered webhook/event await timeout", {
|
|
571
|
+
jobId: jobData.id,
|
|
572
|
+
awaitType,
|
|
573
|
+
flowName: jobData.metadata?.flowName,
|
|
574
|
+
remainingMs: delay
|
|
575
|
+
});
|
|
576
|
+
} else {
|
|
577
|
+
this.logger.warn("Webhook/event await timeout is overdue - flow may be stalled or orphaned", {
|
|
578
|
+
jobId: jobData.id,
|
|
579
|
+
awaitType,
|
|
580
|
+
flowName: jobData.metadata?.flowName,
|
|
581
|
+
stepName: jobData.metadata?.stepName,
|
|
582
|
+
runId: jobData.metadata?.runId,
|
|
583
|
+
scheduledFor: new Date(jobData.executeAt).toISOString(),
|
|
584
|
+
overdueBy: Math.abs(delay)
|
|
585
|
+
});
|
|
586
|
+
await this.unschedule(jobData.id);
|
|
587
|
+
}
|
|
588
|
+
} else if (isStallDetector) {
|
|
589
|
+
if (delay > 0) {
|
|
590
|
+
const timeoutId = setTimeout(
|
|
591
|
+
async () => {
|
|
592
|
+
await this.executeWithLock(jobData);
|
|
593
|
+
await this.unschedule(jobData.id);
|
|
594
|
+
},
|
|
595
|
+
delay
|
|
596
|
+
);
|
|
597
|
+
this.jobs.set(jobData.id, timeoutId);
|
|
598
|
+
this.logger.info("Recovered stall detection timeout", {
|
|
599
|
+
jobId: jobData.id,
|
|
600
|
+
flowName: jobData.metadata?.flowName,
|
|
601
|
+
remainingMs: delay
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
this.logger.info("Executing overdue stall detection immediately", {
|
|
605
|
+
jobId: jobData.id,
|
|
606
|
+
flowName: jobData.metadata?.flowName,
|
|
607
|
+
runId: jobData.metadata?.runId,
|
|
608
|
+
overdueBy: Math.abs(delay)
|
|
609
|
+
});
|
|
610
|
+
setImmediate(async () => {
|
|
611
|
+
try {
|
|
612
|
+
await jobData.handler();
|
|
613
|
+
await this.unschedule(jobData.id);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.logger.error("Failed to execute overdue stall detection", {
|
|
616
|
+
jobId: jobData.id,
|
|
617
|
+
error: error.message
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
} else if (delay > 0) {
|
|
478
623
|
const timeoutId = setTimeout(
|
|
479
624
|
async () => {
|
|
480
625
|
await this.executeWithLock(jobData);
|
|
@@ -483,12 +628,13 @@ export class Scheduler {
|
|
|
483
628
|
delay
|
|
484
629
|
);
|
|
485
630
|
this.jobs.set(jobData.id, timeoutId);
|
|
486
|
-
this.logger.info("Recovered one-time job", { jobId: jobData.id });
|
|
487
|
-
} else if (isAwaitPattern) {
|
|
488
|
-
this.logger.info("Executing overdue await
|
|
631
|
+
this.logger.info("Recovered one-time job", { jobId: jobData.id, remainingMs: delay });
|
|
632
|
+
} else if (isAwaitPattern && (awaitType === "time" || awaitType === "schedule")) {
|
|
633
|
+
this.logger.info("Executing overdue time/schedule await immediately", {
|
|
489
634
|
jobId: jobData.id,
|
|
490
|
-
awaitType
|
|
491
|
-
flowName: jobData.metadata?.flowName
|
|
635
|
+
awaitType,
|
|
636
|
+
flowName: jobData.metadata?.flowName,
|
|
637
|
+
overdueBy: Math.abs(delay)
|
|
492
638
|
});
|
|
493
639
|
setImmediate(async () => {
|
|
494
640
|
try {
|
|
@@ -541,10 +687,15 @@ export class Scheduler {
|
|
|
541
687
|
if (this.started) return;
|
|
542
688
|
this.started = true;
|
|
543
689
|
await this.recoverJobs();
|
|
690
|
+
this.startJobSyncPolling();
|
|
544
691
|
this.logger.info("Started with active jobs", { count: this.jobs.size });
|
|
545
692
|
}
|
|
546
693
|
async stop() {
|
|
547
694
|
this.started = false;
|
|
695
|
+
if (this.syncSubscription) {
|
|
696
|
+
this.syncSubscription();
|
|
697
|
+
this.syncSubscription = void 0;
|
|
698
|
+
}
|
|
548
699
|
for (const job of this.jobs.values()) {
|
|
549
700
|
if (job instanceof CronJob) {
|
|
550
701
|
job.stop();
|
|
@@ -572,6 +723,31 @@ export class Scheduler {
|
|
|
572
723
|
async getScheduledJobs() {
|
|
573
724
|
return Array.from(this.jobConfigs.values());
|
|
574
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Get jobs by pattern (e.g., by runId)
|
|
728
|
+
* This queries the persisted store, not just in-memory jobs
|
|
729
|
+
* Works across all instances in a distributed setup
|
|
730
|
+
*/
|
|
731
|
+
async getJobsByPattern(pattern) {
|
|
732
|
+
const jobs = [];
|
|
733
|
+
try {
|
|
734
|
+
if (this.store.index.read) {
|
|
735
|
+
const jobIndex = `${this.keyPrefix}:jobs`;
|
|
736
|
+
const entries = await this.store.index.read(jobIndex, { limit: 1e4 });
|
|
737
|
+
for (const entry of entries) {
|
|
738
|
+
if (entry.metadata && entry.id.includes(pattern)) {
|
|
739
|
+
jobs.push(entry.metadata);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
this.logger.warn("Store does not support index read, falling back to in-memory jobs");
|
|
744
|
+
return Array.from(this.jobConfigs.values()).filter((job) => job.id.includes(pattern));
|
|
745
|
+
}
|
|
746
|
+
} catch (error) {
|
|
747
|
+
this.logger.error("Error getting jobs by pattern", { pattern, error: error.message });
|
|
748
|
+
}
|
|
749
|
+
return jobs;
|
|
750
|
+
}
|
|
575
751
|
/**
|
|
576
752
|
* Get all persisted jobs from store (for debugging/monitoring)
|
|
577
753
|
* This shows ALL jobs across ALL instances with their runtime stats
|
|
@@ -90,6 +90,12 @@ export interface SchedulerAdapter {
|
|
|
90
90
|
* Get all scheduled jobs (in-memory, for this instance)
|
|
91
91
|
*/
|
|
92
92
|
getScheduledJobs(): Promise<ScheduledJob[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Get jobs matching a pattern (e.g., by runId)
|
|
95
|
+
* Queries persisted store, works across all instances
|
|
96
|
+
* More efficient than getAllPersistedJobs() when filtering
|
|
97
|
+
*/
|
|
98
|
+
getJobsByPattern(pattern: string): Promise<ScheduledJob[]>;
|
|
93
99
|
/**
|
|
94
100
|
* Get all persisted jobs from store (across all instances)
|
|
95
101
|
* Useful for debugging and monitoring in horizontal setups
|
|
@@ -5,13 +5,11 @@ import {
|
|
|
5
5
|
useEventManager,
|
|
6
6
|
useNventLogger,
|
|
7
7
|
$useFunctionRegistry,
|
|
8
|
-
|
|
9
|
-
useHookRegistry,
|
|
10
|
-
useStreamTopics,
|
|
8
|
+
$useAnalyzedFlows,
|
|
11
9
|
useStateAdapter,
|
|
12
|
-
|
|
13
|
-
useRunContext
|
|
10
|
+
useQueueAdapter
|
|
14
11
|
} from "#imports";
|
|
12
|
+
import { SYSTEM_HANDLERS } from "../system/index.js";
|
|
15
13
|
const logger = useNventLogger("node-runner");
|
|
16
14
|
const defaultState = {
|
|
17
15
|
async get() {
|
|
@@ -136,49 +134,7 @@ export function createJobProcessor(handler, queueName) {
|
|
|
136
134
|
const awaitAfter = stepMeta?.awaitAfter;
|
|
137
135
|
const isAwaitResume = job.data?.awaitResolved === true;
|
|
138
136
|
const awaitData = job.data?.awaitData;
|
|
139
|
-
|
|
140
|
-
const awaitLogger = useNventLogger("await-before");
|
|
141
|
-
awaitLogger.info("Step has awaitBefore, registering await pattern", {
|
|
142
|
-
flowName,
|
|
143
|
-
runId: flowId,
|
|
144
|
-
stepName: job.name,
|
|
145
|
-
awaitType: awaitBefore.type
|
|
146
|
-
});
|
|
147
|
-
try {
|
|
148
|
-
const { register } = useAwait();
|
|
149
|
-
const awaitResult = await register(
|
|
150
|
-
flowId || "unknown",
|
|
151
|
-
job.name,
|
|
152
|
-
flowName,
|
|
153
|
-
awaitBefore,
|
|
154
|
-
"before"
|
|
155
|
-
// Position: awaitBefore means wait before execution
|
|
156
|
-
);
|
|
157
|
-
const hookRegistry = useHookRegistry();
|
|
158
|
-
const hooks = hookRegistry.load(flowName, job.name);
|
|
159
|
-
if (hooks?.onAwaitRegister) {
|
|
160
|
-
try {
|
|
161
|
-
await hooks.onAwaitRegister(
|
|
162
|
-
awaitResult.webhookUrl || awaitResult.eventName || "",
|
|
163
|
-
job.data,
|
|
164
|
-
useRunContext({ flowId, flowName, stepName: job.name })
|
|
165
|
-
);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
awaitLogger.error("onAwaitRegister hook failed", { error: err.message });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
awaiting: true,
|
|
172
|
-
awaitType: awaitBefore.type,
|
|
173
|
-
awaitConfig: awaitBefore
|
|
174
|
-
};
|
|
175
|
-
} catch (err) {
|
|
176
|
-
awaitLogger.error("Failed to register await pattern", {
|
|
177
|
-
error: err.message,
|
|
178
|
-
stack: err.stack
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
137
|
+
const awaitPosition = job.data?.awaitPosition;
|
|
182
138
|
const ctx = buildContext({
|
|
183
139
|
jobId: job.id,
|
|
184
140
|
queue: queueName,
|
|
@@ -204,7 +160,11 @@ export function createJobProcessor(handler, queueName) {
|
|
|
204
160
|
stepName: job.name,
|
|
205
161
|
stepId: stepRunId,
|
|
206
162
|
attempt,
|
|
207
|
-
data: {
|
|
163
|
+
data: {
|
|
164
|
+
jobId: job.id,
|
|
165
|
+
name: job.name,
|
|
166
|
+
queue: queueName
|
|
167
|
+
}
|
|
208
168
|
});
|
|
209
169
|
} catch {
|
|
210
170
|
}
|
|
@@ -275,50 +235,31 @@ export function createJobProcessor(handler, queueName) {
|
|
|
275
235
|
});
|
|
276
236
|
} catch {
|
|
277
237
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
awaitLogger.info("Step has awaitAfter, registering await pattern", {
|
|
281
|
-
flowName,
|
|
282
|
-
runId: flowId,
|
|
283
|
-
stepName: job.name,
|
|
284
|
-
awaitType: awaitAfter.type
|
|
285
|
-
});
|
|
238
|
+
const shouldRegisterAwaitAfter = awaitAfter && (!isAwaitResume || awaitPosition === "before");
|
|
239
|
+
if (shouldRegisterAwaitAfter) {
|
|
286
240
|
try {
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
const hookRegistry = useHookRegistry();
|
|
306
|
-
const hooks = hookRegistry.load(flowName, job.name);
|
|
307
|
-
if (hooks?.onAwaitRegister) {
|
|
308
|
-
try {
|
|
309
|
-
await hooks.onAwaitRegister(
|
|
310
|
-
awaitResult.webhookUrl || awaitResult.eventName || "",
|
|
311
|
-
{ ...job.data, result },
|
|
312
|
-
ctx
|
|
313
|
-
);
|
|
314
|
-
} catch (err) {
|
|
315
|
-
awaitLogger.error("onAwaitRegister hook failed", { error: err.message });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
241
|
+
const queue = useQueueAdapter();
|
|
242
|
+
const analyzedFlows = $useAnalyzedFlows();
|
|
243
|
+
const flowDef = analyzedFlows.find((f) => f.id === flowName);
|
|
244
|
+
const analyzedAwaitStep = flowDef?.analyzed?.steps?.[job.name];
|
|
245
|
+
const awaitStepTimeout = analyzedAwaitStep?.stepTimeout;
|
|
246
|
+
await queue.enqueue(queueName, {
|
|
247
|
+
name: SYSTEM_HANDLERS.AWAIT_REGISTER,
|
|
248
|
+
data: {
|
|
249
|
+
flowId: flowId || "unknown",
|
|
250
|
+
flowName,
|
|
251
|
+
stepName: job.name,
|
|
252
|
+
position: "after",
|
|
253
|
+
awaitConfig: awaitAfter,
|
|
254
|
+
input: { ...job.data, result }
|
|
255
|
+
},
|
|
256
|
+
opts: { jobId: `${flowId}__${job.name}__await-register-after`, timeout: awaitStepTimeout }
|
|
257
|
+
});
|
|
318
258
|
} catch (err) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
259
|
+
logger.error("Failed to register awaitAfter pattern", {
|
|
260
|
+
flowName,
|
|
261
|
+
stepName: job.name,
|
|
262
|
+
error: err.message
|
|
322
263
|
});
|
|
323
264
|
}
|
|
324
265
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal system handlers for await lifecycle events
|
|
3
|
+
* These handlers run in job context to handle await registration, resolution, and timeout
|
|
4
|
+
*/
|
|
5
|
+
import type { QueueJob } from '../node/runner.js';
|
|
6
|
+
/**
|
|
7
|
+
* System handler for await registration
|
|
8
|
+
* Registers await pattern and calls onAwaitRegister hook
|
|
9
|
+
*/
|
|
10
|
+
export declare function awaitRegisterHandler(job: QueueJob): Promise<{
|
|
11
|
+
success: boolean;
|
|
12
|
+
awaitResult: any;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* System handler for await resolution
|
|
16
|
+
* Calls onAwaitResolve hook and enqueues the actual step (for awaitBefore)
|
|
17
|
+
*/
|
|
18
|
+
export declare function awaitResolveHandler(job: QueueJob): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* System handler for await timeout
|
|
23
|
+
* Calls onAwaitTimeout hook
|
|
24
|
+
*/
|
|
25
|
+
export declare function awaitTimeoutHandler(job: QueueJob): Promise<{
|
|
26
|
+
success: boolean;
|
|
27
|
+
}>;
|