nvent 0.5.6 → 0.5.8
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 +1 -0
- package/dist/runtime/events/utils/scheduleTrigger.js +3 -1
- package/dist/runtime/events/utils/stallDetector.d.ts +7 -0
- package/dist/runtime/events/utils/stallDetector.js +67 -1
- package/dist/runtime/nitro/utils/useFlow.d.ts +7 -0
- package/dist/runtime/nitro/utils/useFlow.js +45 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export async function scheduleTrigger(triggerName, scheduleConfig, triggerStatus
|
|
|
6
6
|
const eventBus = getEventBus();
|
|
7
7
|
try {
|
|
8
8
|
const jobId = `trigger:${triggerName}`;
|
|
9
|
+
const isEnabled = triggerStatus === "active";
|
|
9
10
|
const handler = async () => {
|
|
10
11
|
logger.debug("Schedule trigger fired", { trigger: triggerName });
|
|
11
12
|
await eventBus.publish({
|
|
@@ -28,7 +29,7 @@ export async function scheduleTrigger(triggerName, scheduleConfig, triggerStatus
|
|
|
28
29
|
type: "schedule-trigger",
|
|
29
30
|
scheduleConfig
|
|
30
31
|
},
|
|
31
|
-
enabled:
|
|
32
|
+
enabled: isEnabled
|
|
32
33
|
};
|
|
33
34
|
if (scheduleConfig.cron) {
|
|
34
35
|
jobConfig.cron = scheduleConfig.cron;
|
|
@@ -39,6 +40,7 @@ export async function scheduleTrigger(triggerName, scheduleConfig, triggerStatus
|
|
|
39
40
|
await scheduler.schedule(jobConfig);
|
|
40
41
|
logger.info("Scheduled trigger", {
|
|
41
42
|
trigger: triggerName,
|
|
43
|
+
enabled: isEnabled,
|
|
42
44
|
cron: scheduleConfig.cron,
|
|
43
45
|
interval: scheduleConfig.interval,
|
|
44
46
|
timezone: scheduleConfig.timezone
|
|
@@ -39,8 +39,15 @@ export declare class FlowStallDetector {
|
|
|
39
39
|
/**
|
|
40
40
|
* Mark a flow as stalled
|
|
41
41
|
* Emits a flow.stalled event and updates the flow status
|
|
42
|
+
* Also emits step.stalled events for any steps that were running when the flow stalled
|
|
42
43
|
*/
|
|
43
44
|
markAsStalled(flowName: string, runId: string, reason?: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Find steps that were running when the flow stalled
|
|
47
|
+
* A step is considered "running" if it has step.started but no terminal event
|
|
48
|
+
* (step.completed, step.failed, step.stalled)
|
|
49
|
+
*/
|
|
50
|
+
private findStalledSteps;
|
|
44
51
|
/**
|
|
45
52
|
* Run startup recovery to clean up flows left in running state from previous server instance
|
|
46
53
|
* This marks all running flows as stalled since their in-memory state is lost
|
|
@@ -34,6 +34,7 @@ export class FlowStallDetector {
|
|
|
34
34
|
/**
|
|
35
35
|
* Mark a flow as stalled
|
|
36
36
|
* Emits a flow.stalled event and updates the flow status
|
|
37
|
+
* Also emits step.stalled events for any steps that were running when the flow stalled
|
|
37
38
|
*/
|
|
38
39
|
async markAsStalled(flowName, runId, reason = "No activity timeout") {
|
|
39
40
|
const { StoreSubjects } = useStreamTopics();
|
|
@@ -57,14 +58,46 @@ export class FlowStallDetector {
|
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
const streamName = StoreSubjects.flowRun(runId);
|
|
61
|
+
const stalledAt = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
const allEvents = await this.store.stream.read(streamName);
|
|
64
|
+
const stalledSteps = this.findStalledSteps(allEvents);
|
|
65
|
+
for (const stepName of stalledSteps) {
|
|
66
|
+
await this.store.stream.append(streamName, {
|
|
67
|
+
type: "step.stalled",
|
|
68
|
+
runId,
|
|
69
|
+
flowName,
|
|
70
|
+
stepName,
|
|
71
|
+
data: {
|
|
72
|
+
reason: "Flow stalled - step execution interrupted",
|
|
73
|
+
stalledAt
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
this.logger.debug(`Emitted step.stalled for step '${stepName}'`, { flowName, runId });
|
|
77
|
+
}
|
|
78
|
+
if (stalledSteps.length > 0) {
|
|
79
|
+
this.logger.info(`Marked ${stalledSteps.length} step(s) as stalled`, {
|
|
80
|
+
flowName,
|
|
81
|
+
runId,
|
|
82
|
+
steps: stalledSteps
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.logger.warn("Failed to emit step.stalled events", {
|
|
87
|
+
flowName,
|
|
88
|
+
runId,
|
|
89
|
+
error: err.message
|
|
90
|
+
});
|
|
91
|
+
}
|
|
60
92
|
await this.store.stream.append(streamName, {
|
|
61
93
|
type: "flow.stalled",
|
|
62
94
|
runId,
|
|
63
95
|
flowName,
|
|
64
96
|
data: {
|
|
65
97
|
reason,
|
|
66
|
-
previousStatus
|
|
98
|
+
previousStatus,
|
|
67
99
|
// Include previous status so stats handler knows which counter to decrement
|
|
100
|
+
stalledAt
|
|
68
101
|
}
|
|
69
102
|
});
|
|
70
103
|
this.logger.info(`Marked flow as stalled - '${flowName}' runId '${runId}': ${reason}`);
|
|
@@ -72,6 +105,39 @@ export class FlowStallDetector {
|
|
|
72
105
|
this.logger.error(`Failed to mark flow as stalled for '${flowName}' runId '${runId}': ${error.message}`);
|
|
73
106
|
}
|
|
74
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Find steps that were running when the flow stalled
|
|
110
|
+
* A step is considered "running" if it has step.started but no terminal event
|
|
111
|
+
* (step.completed, step.failed, step.stalled)
|
|
112
|
+
*/
|
|
113
|
+
findStalledSteps(events) {
|
|
114
|
+
const stepStates = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const e of events) {
|
|
116
|
+
const stepName = e.stepName;
|
|
117
|
+
if (!stepName) continue;
|
|
118
|
+
if (e.type === "step.started") {
|
|
119
|
+
if (!stepStates.has(stepName) || stepStates.get(stepName) === "started") {
|
|
120
|
+
stepStates.set(stepName, "started");
|
|
121
|
+
}
|
|
122
|
+
} else if (e.type === "step.completed") {
|
|
123
|
+
stepStates.set(stepName, "completed");
|
|
124
|
+
} else if (e.type === "step.failed") {
|
|
125
|
+
const willRetry = e.data?.willRetry || e.data?.retry;
|
|
126
|
+
if (!willRetry) {
|
|
127
|
+
stepStates.set(stepName, "failed");
|
|
128
|
+
}
|
|
129
|
+
} else if (e.type === "step.stalled") {
|
|
130
|
+
stepStates.set(stepName, "stalled");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const stalledSteps = [];
|
|
134
|
+
for (const [stepName, state] of stepStates) {
|
|
135
|
+
if (state === "started") {
|
|
136
|
+
stalledSteps.push(stepName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return stalledSteps;
|
|
140
|
+
}
|
|
75
141
|
/**
|
|
76
142
|
* Run startup recovery to clean up flows left in running state from previous server instance
|
|
77
143
|
* This marks all running flows as stalled since their in-memory state is lost
|
|
@@ -25,6 +25,12 @@ export interface CancelFlowResult {
|
|
|
25
25
|
runId: string;
|
|
26
26
|
flowName: string;
|
|
27
27
|
}
|
|
28
|
+
export interface RestartFlowResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
oldRunId: string;
|
|
31
|
+
newRunId: string;
|
|
32
|
+
flowName: string;
|
|
33
|
+
}
|
|
28
34
|
export interface RunningFlow {
|
|
29
35
|
id: string;
|
|
30
36
|
flowName: string;
|
|
@@ -37,6 +43,7 @@ export interface FlowComposable {
|
|
|
37
43
|
startFlow: (flowName: string, payload?: any) => Promise<StartFlowResult>;
|
|
38
44
|
emit: (trigger: string, payload?: any) => Promise<any[]>;
|
|
39
45
|
cancelFlow: (flowName: string, runId: string) => Promise<CancelFlowResult>;
|
|
46
|
+
restartFlow: (flowName: string, runId: string) => Promise<RestartFlowResult>;
|
|
40
47
|
isRunning: (flowName: string, runId?: string, options?: {
|
|
41
48
|
excludeRunIds?: string[];
|
|
42
49
|
}) => Promise<boolean>;
|
|
@@ -94,6 +94,51 @@ export function useFlow() {
|
|
|
94
94
|
throw err;
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
|
+
/**
|
|
98
|
+
* Restart a flow by canceling the current run and starting a new one with the same input
|
|
99
|
+
* @param flowName - The name of the flow
|
|
100
|
+
* @param runId - The run ID to restart
|
|
101
|
+
*/
|
|
102
|
+
async restartFlow(flowName, runId) {
|
|
103
|
+
try {
|
|
104
|
+
const streamName = StoreSubjects.flowRun(runId);
|
|
105
|
+
const events = await store.stream.read(streamName);
|
|
106
|
+
const startEvent = events.find((e) => e.type === "flow.start" || e.type === "flow.started");
|
|
107
|
+
const originalInput = startEvent?.data?.input || {};
|
|
108
|
+
logger.debug("Found original input for restart", { flowName, runId, hasInput: !!startEvent });
|
|
109
|
+
const runIndexKey = StoreSubjects.flowRunIndex(flowName);
|
|
110
|
+
const entry = await store.index.get(runIndexKey, runId);
|
|
111
|
+
const currentStatus = entry?.metadata?.status;
|
|
112
|
+
if (currentStatus === "running" || currentStatus === "awaiting") {
|
|
113
|
+
await eventsManager.publishBus({
|
|
114
|
+
type: "flow.cancel",
|
|
115
|
+
runId,
|
|
116
|
+
flowName,
|
|
117
|
+
data: {
|
|
118
|
+
canceledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
119
|
+
previousStatus: currentStatus,
|
|
120
|
+
reason: "Restarted"
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
logger.info("Canceled flow for restart", { flowName, runId, previousStatus: currentStatus });
|
|
124
|
+
}
|
|
125
|
+
const newResult = await this.startFlow(flowName, originalInput);
|
|
126
|
+
logger.info("Flow restarted", {
|
|
127
|
+
flowName,
|
|
128
|
+
oldRunId: runId,
|
|
129
|
+
newRunId: newResult.flowId
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
oldRunId: runId,
|
|
134
|
+
newRunId: newResult.flowId,
|
|
135
|
+
flowName
|
|
136
|
+
};
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger.error("Failed to restart flow", { flowName, runId, error: err });
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
97
142
|
/**
|
|
98
143
|
* Check if a flow is currently running (includes 'running' and 'awaiting' status)
|
|
99
144
|
* @param flowName - The name of the flow to check
|