@rex0220/ksql-flownet 1.0.0

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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +81 -0
  3. package/README.md +117 -0
  4. package/dist/bundle/bundle-builder.d.ts +49 -0
  5. package/dist/bundle/bundle-builder.js +208 -0
  6. package/dist/bundle/index.d.ts +4 -0
  7. package/dist/bundle/index.js +4 -0
  8. package/dist/bundle/upload.d.ts +28 -0
  9. package/dist/bundle/upload.js +72 -0
  10. package/dist/bundle/zip-store.d.ts +17 -0
  11. package/dist/bundle/zip-store.js +175 -0
  12. package/dist/cli/archive-run-command.d.ts +12 -0
  13. package/dist/cli/archive-run-command.js +131 -0
  14. package/dist/cli/cancel-run-command.d.ts +9 -0
  15. package/dist/cli/cancel-run-command.js +77 -0
  16. package/dist/cli/force-unlock-network-command.d.ts +8 -0
  17. package/dist/cli/force-unlock-network-command.js +129 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.js +132 -0
  20. package/dist/cli/plan-command.d.ts +2 -0
  21. package/dist/cli/plan-command.js +100 -0
  22. package/dist/cli/poll-requests-command.d.ts +3 -0
  23. package/dist/cli/poll-requests-command.js +74 -0
  24. package/dist/cli/record-job-unlock-command.d.ts +11 -0
  25. package/dist/cli/record-job-unlock-command.js +78 -0
  26. package/dist/cli/resolve-node-command.d.ts +13 -0
  27. package/dist/cli/resolve-node-command.js +174 -0
  28. package/dist/cli/run-network-command.d.ts +14 -0
  29. package/dist/cli/run-network-command.js +418 -0
  30. package/dist/cli/status-command.d.ts +5 -0
  31. package/dist/cli/status-command.js +137 -0
  32. package/dist/cli/validate-command.d.ts +6 -0
  33. package/dist/cli/validate-command.js +17 -0
  34. package/dist/dag/descendants.d.ts +9 -0
  35. package/dist/dag/descendants.js +67 -0
  36. package/dist/dag/topological-sort.d.ts +7 -0
  37. package/dist/dag/topological-sort.js +32 -0
  38. package/dist/domain/business-key.d.ts +15 -0
  39. package/dist/domain/business-key.js +249 -0
  40. package/dist/domain/canonical-lock-key.d.ts +17 -0
  41. package/dist/domain/canonical-lock-key.js +60 -0
  42. package/dist/domain/canonical-record-key.d.ts +18 -0
  43. package/dist/domain/canonical-record-key.js +70 -0
  44. package/dist/domain/load-network.d.ts +4 -0
  45. package/dist/domain/load-network.js +47 -0
  46. package/dist/domain/network-definition.d.ts +47 -0
  47. package/dist/domain/network-definition.js +7 -0
  48. package/dist/domain/persistence-model.d.ts +198 -0
  49. package/dist/domain/persistence-model.js +2 -0
  50. package/dist/domain/run-aggregate.d.ts +3 -0
  51. package/dist/domain/run-aggregate.js +15 -0
  52. package/dist/domain/validate-network-path.d.ts +11 -0
  53. package/dist/domain/validate-network-path.js +33 -0
  54. package/dist/domain/validate-network.d.ts +3 -0
  55. package/dist/domain/validate-network.js +180 -0
  56. package/dist/executor/attempt-executor.d.ts +43 -0
  57. package/dist/executor/attempt-executor.js +236 -0
  58. package/dist/executor/job-log-reader.d.ts +37 -0
  59. package/dist/executor/job-log-reader.js +91 -0
  60. package/dist/executor/ksql-flow-cli.d.ts +86 -0
  61. package/dist/executor/ksql-flow-cli.js +161 -0
  62. package/dist/executor/preflight.d.ts +41 -0
  63. package/dist/executor/preflight.js +150 -0
  64. package/dist/executor/result-classifier.d.ts +69 -0
  65. package/dist/executor/result-classifier.js +237 -0
  66. package/dist/executor/run-subprocess.d.ts +64 -0
  67. package/dist/executor/run-subprocess.js +175 -0
  68. package/dist/io/input-baseline.d.ts +19 -0
  69. package/dist/io/input-baseline.js +214 -0
  70. package/dist/io/io-config.d.ts +6 -0
  71. package/dist/io/io-config.js +33 -0
  72. package/dist/io/io-path.d.ts +37 -0
  73. package/dist/io/io-path.js +279 -0
  74. package/dist/orchestration/archive-run.d.ts +53 -0
  75. package/dist/orchestration/archive-run.js +323 -0
  76. package/dist/orchestration/cancel-request.d.ts +18 -0
  77. package/dist/orchestration/cancel-request.js +67 -0
  78. package/dist/orchestration/ensure-run.d.ts +83 -0
  79. package/dist/orchestration/ensure-run.js +632 -0
  80. package/dist/orchestration/job-lock-audit.d.ts +24 -0
  81. package/dist/orchestration/job-lock-audit.js +123 -0
  82. package/dist/orchestration/orphan-attempt-adjudication.d.ts +39 -0
  83. package/dist/orchestration/orphan-attempt-adjudication.js +167 -0
  84. package/dist/orchestration/reconciliation.d.ts +41 -0
  85. package/dist/orchestration/reconciliation.js +526 -0
  86. package/dist/orchestration/resolve-node.d.ts +29 -0
  87. package/dist/orchestration/resolve-node.js +105 -0
  88. package/dist/orchestration/run-activity.d.ts +13 -0
  89. package/dist/orchestration/run-activity.js +14 -0
  90. package/dist/orchestration/sequential-scheduler.d.ts +79 -0
  91. package/dist/orchestration/sequential-scheduler.js +706 -0
  92. package/dist/orchestration/status.d.ts +91 -0
  93. package/dist/orchestration/status.js +171 -0
  94. package/dist/persistence/in-memory-repository.d.ts +41 -0
  95. package/dist/persistence/in-memory-repository.js +333 -0
  96. package/dist/persistence/kintone/client.d.ts +36 -0
  97. package/dist/persistence/kintone/client.js +93 -0
  98. package/dist/persistence/kintone/design-notes.d.ts +38 -0
  99. package/dist/persistence/kintone/design-notes.js +38 -0
  100. package/dist/persistence/kintone/repository.d.ts +44 -0
  101. package/dist/persistence/kintone/repository.js +836 -0
  102. package/dist/persistence/kintone/schema.d.ts +116 -0
  103. package/dist/persistence/kintone/schema.js +115 -0
  104. package/dist/persistence/network-lock-reader.d.ts +24 -0
  105. package/dist/persistence/network-lock-reader.js +43 -0
  106. package/dist/persistence/network-lock-recovery.d.ts +66 -0
  107. package/dist/persistence/network-lock-recovery.js +392 -0
  108. package/dist/persistence/network-lock.d.ts +97 -0
  109. package/dist/persistence/network-lock.js +408 -0
  110. package/dist/persistence/repository.d.ts +101 -0
  111. package/dist/persistence/repository.js +11 -0
  112. package/dist/persistence/state-transition.d.ts +4 -0
  113. package/dist/persistence/state-transition.js +25 -0
  114. package/dist/requests/flownet-child-client.d.ts +78 -0
  115. package/dist/requests/flownet-child-client.js +243 -0
  116. package/dist/requests/kintone-request-store.d.ts +44 -0
  117. package/dist/requests/kintone-request-store.js +202 -0
  118. package/dist/requests/poll-requests-config.d.ts +26 -0
  119. package/dist/requests/poll-requests-config.js +105 -0
  120. package/dist/requests/request-model.d.ts +57 -0
  121. package/dist/requests/request-model.js +305 -0
  122. package/dist/requests/request-poller.d.ts +33 -0
  123. package/dist/requests/request-poller.js +353 -0
  124. package/dist/requests/request-result.d.ts +18 -0
  125. package/dist/requests/request-result.js +215 -0
  126. package/dist/requests/start-request.d.ts +26 -0
  127. package/dist/requests/start-request.js +83 -0
  128. package/package.json +63 -0
  129. package/schemas/.gitkeep +1 -0
  130. package/schemas/network-definition.schema.json +121 -0
@@ -0,0 +1,706 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { readStoreZip, verifyBundle } from "../bundle/index.js";
6
+ import { stableTopologicalSort } from "../dag/topological-sort.js";
7
+ import { loadNetworkDefinitionSource } from "../domain/load-network.js";
8
+ import { InputPathError, OutputPathError, resolveNodeInputs, resolveNodeOutputs, } from "../io/io-path.js";
9
+ import { inputBaselinesEqual, parseInputBaseline, serializeInputBaseline, } from "../io/input-baseline.js";
10
+ import { computeRunAggregateStatus } from "../domain/run-aggregate.js";
11
+ import { RepositoryError } from "../persistence/repository.js";
12
+ import { KintoneTransportError } from "../persistence/kintone/client.js";
13
+ import { adjudicateOrphanRunningAttempts, finalizeAbandonedInvocations, orphanAdjudicationReason, } from "./orphan-attempt-adjudication.js";
14
+ import { isCancelHold } from "./cancel-request.js";
15
+ export class SchedulerLeaseInterruptedError extends Error {
16
+ reconciliationRequired;
17
+ code = "NETWORK_LEASE_INTERRUPTED";
18
+ constructor(reconciliationRequired, options) {
19
+ super(reconciliationRequired
20
+ ? "network lease could not be confirmed; execution evidence requires reconciliation"
21
+ : "network lease was interrupted", options);
22
+ this.reconciliationRequired = reconciliationRequired;
23
+ this.name = "SchedulerLeaseInterruptedError";
24
+ }
25
+ }
26
+ /** FN-10 Phase 1 scheduler: one invocation, one node at a time. */
27
+ export async function runSequentialScheduler(input) {
28
+ const now = input.now ?? (() => new Date());
29
+ const uuid = input.uuid ?? randomUUID;
30
+ let extracted = null;
31
+ const selected = new Set(input.invocation.value.mode === "RERUN_FROM"
32
+ ? input.invocation.value.selected_node_ids
33
+ : []);
34
+ const preserved = new Set();
35
+ const blocked = new Set();
36
+ const results = new Map();
37
+ let aggregateStatus = input.run.value.status;
38
+ let closeInput = null;
39
+ let reconciliationRequired = false;
40
+ let adjudicationReason;
41
+ let ioFailureResultCode = null;
42
+ input.leaseMonitor.start?.();
43
+ try {
44
+ extracted = await extractBundle(input.bundleBytes, input.executionRoot);
45
+ const runControlPlaneOperation = controlPlaneDrainRunner(input.leaseMonitor, extracted.definition.network_lock.lease_duration_sec * 1000, input.controlPlaneDrain);
46
+ let run = await ensureRunStarted(input, now);
47
+ let attempts = [];
48
+ if (input.invocation.value.mode !== "NEW") {
49
+ attempts = await runControlPlaneOperation(() => input.repository.getAttempts(input.run.value.run_id));
50
+ const adjudications = await adjudicateOrphanRunningAttempts({
51
+ runId: input.run.value.run_id,
52
+ invocationId: input.invocation.value.invocation_id,
53
+ repository: input.repository,
54
+ jobLogReader: input.jobLogReader ?? missingJobLogReader,
55
+ confirmWrite: () => confirmOrdinaryWrite(input.leaseMonitor),
56
+ now: () => now().toISOString(),
57
+ attempts,
58
+ });
59
+ if (adjudications.length > 0) {
60
+ const byId = new Map(adjudications.map((item) => [item.attemptId, item]));
61
+ attempts = attempts.map((attempt) => {
62
+ const adjudicated = byId.get(attempt.value.node_attempt_id);
63
+ return adjudicated === undefined
64
+ ? attempt
65
+ : {
66
+ value: {
67
+ ...attempt.value,
68
+ status: adjudicated.status,
69
+ result_code: adjudicated.resultCode,
70
+ },
71
+ revision: attempt.revision + 1,
72
+ };
73
+ });
74
+ }
75
+ adjudicationReason = orphanAdjudicationReason(adjudications);
76
+ await finalizeAbandonedInvocations({
77
+ runId: input.run.value.run_id,
78
+ invocationId: input.invocation.value.invocation_id,
79
+ repository: input.repository,
80
+ confirmWrite: () => confirmOrdinaryWrite(input.leaseMonitor),
81
+ now: () => now().toISOString(),
82
+ });
83
+ }
84
+ const states = stateMap(await input.repository.getNodeStates(input.run.value.run_id));
85
+ const excluded = await prepareResumeStates(input, extracted.definition, states, preserved, blocked, now, attempts);
86
+ const retryBrakeNodeIds = [...blocked].filter((nodeId) => states.get(nodeId)?.value.status_reason?.startsWith("RETRY_BRAKE:"));
87
+ const brakeReasons = retryBrakeNodeIds
88
+ .map((nodeId) => states.get(nodeId)?.value.status_reason)
89
+ .filter((reason) => reason?.startsWith("RETRY_BRAKE:"));
90
+ if (brakeReasons.length > 0)
91
+ adjudicationReason = [adjudicationReason, ...brakeReasons]
92
+ .filter((value) => value !== undefined)
93
+ .join("; ");
94
+ const order = stableTopologicalSort(extracted.definition.nodes).order;
95
+ const nodes = new Map(extracted.definition.nodes.map((node) => [node.id, node]));
96
+ for (const nodeId of order) {
97
+ const node = nodes.get(nodeId);
98
+ let state = requiredState(states, nodeId);
99
+ if (input.invocation.value.mode === "RERUN_FROM" &&
100
+ !selected.has(nodeId)) {
101
+ if (state.value.status === "SUCCESS") {
102
+ preserved.add(nodeId);
103
+ results.set(nodeId, nodeResult(nodeId, "PRESERVED", state));
104
+ }
105
+ else {
106
+ results.set(nodeId, nodeResult(nodeId, "EXCLUDED", state));
107
+ }
108
+ continue;
109
+ }
110
+ if (state.value.status === "SUCCESS") {
111
+ preserved.add(nodeId);
112
+ results.set(nodeId, nodeResult(nodeId, "PRESERVED", state));
113
+ continue;
114
+ }
115
+ if (excluded.has(nodeId) && !isDescendantThatMustBlock(nodeId, state)) {
116
+ blocked.add(nodeId);
117
+ results.set(nodeId, nodeResult(nodeId, "EXCLUDED", state));
118
+ continue;
119
+ }
120
+ const dependencies = node.depends_on.map((id) => requiredState(states, id));
121
+ const failedDependencies = dependencies
122
+ .filter(({ value }) => ["FAILED", "BLOCKED", "CANCELLED", "UNKNOWN"].includes(value.status))
123
+ .map(({ value }) => value.node_id);
124
+ if (failedDependencies.length > 0 || excluded.has(nodeId)) {
125
+ if (!(await confirmOrdinaryWrite(input.leaseMonitor))) {
126
+ reconciliationRequired = true;
127
+ throw new SchedulerLeaseInterruptedError(true);
128
+ }
129
+ state = await writeNodeState(input.repository, state, {
130
+ status: "BLOCKED",
131
+ blocked_by: failedDependencies,
132
+ status_reason: "ALL_SUCCESS_NOT_SATISFIED",
133
+ finished_at: now().toISOString(),
134
+ updated_at: now().toISOString(),
135
+ });
136
+ states.set(nodeId, state);
137
+ blocked.add(nodeId);
138
+ results.set(nodeId, nodeResult(nodeId, "BLOCKED", state));
139
+ run = await updateAggregate(input, run, states, now, runControlPlaneOperation);
140
+ aggregateStatus = run.value.status;
141
+ continue;
142
+ }
143
+ if (dependencies.some(({ value }) => value.status === "WAITING" || value.status === "RUNNING")) {
144
+ selected.add(nodeId);
145
+ results.set(nodeId, nodeResult(nodeId, "DEFERRED", state));
146
+ continue;
147
+ }
148
+ if (state.value.status !== "WAITING") {
149
+ blocked.add(nodeId);
150
+ results.set(nodeId, nodeResult(nodeId, "EXCLUDED", state));
151
+ continue;
152
+ }
153
+ selected.add(nodeId);
154
+ const cancelRequest = await runControlPlaneOperation(() => input.repository.getCancelRequest(input.run.value.run_id));
155
+ if (isCancelHold(cancelRequest)) {
156
+ let effective = cancelRequest;
157
+ if (cancelRequest.value.state === "REQUESTED") {
158
+ try {
159
+ effective = await runControlPlaneOperation(() => input.repository.updateCancelRequest(input.run.value.run_id, cancelRequest.revision, {
160
+ ...cancelRequest.value,
161
+ state: "ACCEPTED",
162
+ accepted_at: now().toISOString(),
163
+ }));
164
+ }
165
+ catch (error) {
166
+ if (!(error instanceof RepositoryError) ||
167
+ error.code !== "REVISION_CONFLICT")
168
+ throw error;
169
+ effective = await runControlPlaneOperation(() => input.repository.getCancelRequest(input.run.value.run_id));
170
+ }
171
+ }
172
+ if (isCancelHold(effective)) {
173
+ results.set(nodeId, nodeResult(nodeId, "DEFERRED", state));
174
+ closeInput = finalization("CANCELLED", "STOP_REQUESTED", selected, preserved, blocked, `cancel request CANCEL:${input.run.value.run_id} accepted`);
175
+ break;
176
+ }
177
+ }
178
+ if (!input.leaseMonitor.canStartNewNode()) {
179
+ results.set(nodeId, nodeResult(nodeId, "DEFERRED", state));
180
+ break;
181
+ }
182
+ if (!(await confirmOrdinaryWrite(input.leaseMonitor))) {
183
+ reconciliationRequired = true;
184
+ throw new SchedulerLeaseInterruptedError(true);
185
+ }
186
+ const attemptId = `attempt_${uuid()}`;
187
+ let attempt = await input.repository.createAttempt({
188
+ node_state: state,
189
+ node_attempt_id: attemptId,
190
+ invocation_id: input.invocation.value.invocation_id,
191
+ });
192
+ const startedAt = now().toISOString();
193
+ state = await writeNodeState(input.repository, state, {
194
+ status: "RUNNING",
195
+ latest_attempt_no: attempt.value.attempt_no,
196
+ active_attempt_id: attemptId,
197
+ blocked_by: [],
198
+ status_reason: null,
199
+ started_at: startedAt,
200
+ finished_at: null,
201
+ updated_at: startedAt,
202
+ });
203
+ states.set(nodeId, state);
204
+ let outcome;
205
+ try {
206
+ const inputPatterns = node.inputs ?? {};
207
+ const imports = Object.keys(inputPatterns).length === 0
208
+ ? []
209
+ : await resolveNodeInputs({
210
+ ioRoot: requiredIoRoot(input.ioRoot),
211
+ patterns: inputPatterns,
212
+ businessKey: input.run.value.business_key,
213
+ profile: input.profile,
214
+ });
215
+ const outputPatterns = node.outputs ?? {};
216
+ const exports = Object.keys(outputPatterns).length === 0
217
+ ? []
218
+ : await resolveNodeOutputs({
219
+ ioRoot: requiredOutputIoRoot(input.ioRoot),
220
+ patterns: outputPatterns,
221
+ businessKey: input.run.value.business_key,
222
+ profile: input.profile,
223
+ runId: input.run.value.run_id,
224
+ nodeId,
225
+ });
226
+ if (imports.length > 0 &&
227
+ shouldComparePreviousBaseline(input.invocation.value.mode, selected, nodeId, attempts)) {
228
+ const previous = latestInputBaseline(attempts, nodeId);
229
+ if (previous !== null && !inputBaselinesEqual(previous, imports)) {
230
+ throw new InputPathError("INPUT_FILE_MUTATED", `input baseline changed for node '${nodeId}'; restore the original file or create a new correction business key`);
231
+ }
232
+ }
233
+ if (imports.length > 0) {
234
+ attempt = await persistInputBaseline(input.repository, attempt, serializeInputBaseline(imports), runControlPlaneOperation);
235
+ }
236
+ outcome = await input.attemptExecutor.execute({
237
+ attempt,
238
+ nodeState: state,
239
+ sqlPath: extracted.sqlPaths.get(nodeId),
240
+ profile: input.profile,
241
+ configPath: input.configPath,
242
+ asOf: input.run.value.as_of ?? input.run.value.created_at,
243
+ correlationId: input.run.value.run_id,
244
+ attemptId,
245
+ expectedJobId: node.job_id,
246
+ imports,
247
+ exports,
248
+ executionStartedAt: startedAt,
249
+ authorizeResultPersistence: async () => {
250
+ if (input.leaseMonitor.canPersistResults())
251
+ return true;
252
+ return input.leaseMonitor.confirmLeaseForFinalWrite();
253
+ },
254
+ runControlPlaneOperation,
255
+ });
256
+ }
257
+ catch (error) {
258
+ if (!(error instanceof InputPathError) &&
259
+ !(error instanceof OutputPathError))
260
+ throw error;
261
+ const authorized = input.leaseMonitor.canPersistResults() ||
262
+ (await input.leaseMonitor.confirmLeaseForFinalWrite());
263
+ if (!authorized) {
264
+ reconciliationRequired = true;
265
+ throw new SchedulerLeaseInterruptedError(true);
266
+ }
267
+ outcome = await finalizeIoPathFailure(input.repository, attempt, state, error, now().toISOString(), runControlPlaneOperation);
268
+ }
269
+ state = outcome.nodeState;
270
+ if (outcome.invocationResultCode === "INPUT_FILE_MISSING" ||
271
+ outcome.invocationResultCode === "INPUT_FILE_MUTATED" ||
272
+ outcome.invocationResultCode === "OUTPUT_PATH_REJECTED")
273
+ ioFailureResultCode = outcome.invocationResultCode;
274
+ states.set(nodeId, state);
275
+ results.set(nodeId, nodeResult(nodeId, outcome.nodeState.value.status === "WAITING"
276
+ ? "DEFERRED"
277
+ : "EXECUTED", outcome.nodeState, outcome.attempt.value.attempt_no, outcome.attempt.value.result_code));
278
+ run = await updateAggregate(input, run, states, now, runControlPlaneOperation);
279
+ aggregateStatus = run.value.status;
280
+ if (input.leaseMonitor.requiresNetworkLeaseInterruptedFinalization()) {
281
+ closeInput = finalization("CANCELLED", "NETWORK_LEASE_INTERRUPTED", selected, preserved, blocked, adjudicationReason);
282
+ break;
283
+ }
284
+ }
285
+ if (closeInput === null) {
286
+ run = await updateAggregate(input, run, states, now, runControlPlaneOperation);
287
+ aggregateStatus = run.value.status;
288
+ const terminal = invocationOutcome(aggregateStatus);
289
+ closeInput = finalization(terminal.status, terminal.status === "FAILED" && ioFailureResultCode !== null
290
+ ? ioFailureResultCode
291
+ : terminal.resultCode, selected, preserved, blocked, adjudicationReason);
292
+ }
293
+ input.leaseMonitor.stop?.();
294
+ await input.close(closeInput);
295
+ return {
296
+ nodeResults: order.map((nodeId) => results.get(nodeId) ??
297
+ nodeResult(nodeId, "DEFERRED", requiredState(states, nodeId))),
298
+ aggregateStatus,
299
+ invocationStatus: closeInput.status,
300
+ invocationResultCode: closeInput.resultCode,
301
+ selectedNodeIds: [...selected],
302
+ preservedNodeIds: [...preserved],
303
+ blockedNodeIds: [...blocked],
304
+ retryBrakeNodeIds,
305
+ reconciliationRequired,
306
+ };
307
+ }
308
+ catch (error) {
309
+ if (closeInput === null) {
310
+ const leaseLost = (error instanceof SchedulerLeaseInterruptedError &&
311
+ error.reconciliationRequired) ||
312
+ (typeof error === "object" &&
313
+ error !== null &&
314
+ "code" in error &&
315
+ error.code === "NETWORK_LEASE_INTERRUPTED");
316
+ closeInput = leaseLost
317
+ ? {
318
+ ...finalization("CANCELLED", "NETWORK_LEASE_INTERRUPTED", selected, preserved, blocked, adjudicationReason),
319
+ persistInvocation: false,
320
+ }
321
+ : finalization("FAILED", "SCHEDULER_FAILED", selected, preserved, blocked, adjudicationReason);
322
+ try {
323
+ input.leaseMonitor.stop?.();
324
+ await input.close(closeInput);
325
+ }
326
+ catch (closeError) {
327
+ throw new AggregateError([error, closeError], "scheduler and close failed", {
328
+ cause: closeError,
329
+ });
330
+ }
331
+ }
332
+ throw error;
333
+ }
334
+ finally {
335
+ input.leaseMonitor.stop?.();
336
+ if (extracted !== null)
337
+ await rm(extracted.directory, { recursive: true, force: true });
338
+ }
339
+ }
340
+ function shouldComparePreviousBaseline(mode, selected, nodeId, attempts) {
341
+ if (mode === "RERUN_FROM")
342
+ return selected.has(nodeId);
343
+ if (mode !== "RESUME")
344
+ return false;
345
+ const latest = attempts
346
+ .filter(({ value }) => value.node_id === nodeId)
347
+ .sort((left, right) => right.value.attempt_no - left.value.attempt_no)[0];
348
+ return (latest?.value.status === "FAILED" || latest?.value.status === "CANCELLED");
349
+ }
350
+ function latestInputBaseline(attempts, nodeId) {
351
+ for (const attempt of attempts
352
+ .filter(({ value }) => value.node_id === nodeId)
353
+ .sort((left, right) => right.value.attempt_no - left.value.attempt_no)) {
354
+ const baseline = parseInputBaseline(attempt.value.error_message);
355
+ if (baseline !== null)
356
+ return baseline;
357
+ }
358
+ return null;
359
+ }
360
+ async function persistInputBaseline(repository, attempt, summary, controlPlane) {
361
+ try {
362
+ return await controlPlane(() => repository.setAttemptInputBaseline(attempt.value.node_attempt_id, attempt.revision, { error_message: summary }));
363
+ }
364
+ catch (error) {
365
+ if (!(error instanceof RepositoryError) ||
366
+ (error.code !== "REVISION_CONFLICT" && error.code !== "AMBIGUOUS_WRITE"))
367
+ throw error;
368
+ const matches = (await controlPlane(() => repository.getAttempts(attempt.value.run_id))).filter(({ value }) => value.node_attempt_id === attempt.value.node_attempt_id);
369
+ if (matches.length === 1 &&
370
+ matches[0].value.status === "RUNNING" &&
371
+ matches[0].value.execution_started_at === null &&
372
+ matches[0].value.error_message === summary)
373
+ return matches[0];
374
+ throw new RepositoryError("AMBIGUOUS_WRITE", "input baseline write could not be uniquely confirmed", error);
375
+ }
376
+ }
377
+ function requiredIoRoot(ioRoot) {
378
+ if (ioRoot === undefined) {
379
+ throw new InputPathError("INPUT_PATH_REJECTED", "IO root is not configured for a node with inputs");
380
+ }
381
+ return ioRoot;
382
+ }
383
+ function requiredOutputIoRoot(ioRoot) {
384
+ if (ioRoot === undefined) {
385
+ throw new OutputPathError("IO root is not configured for a node with outputs");
386
+ }
387
+ return ioRoot;
388
+ }
389
+ async function finalizeIoPathFailure(repository, attempt, nodeState, error, finishedAt, controlPlane) {
390
+ const finalizedAttempt = await controlPlane(() => repository.finalizeAttempt(attempt.value.node_attempt_id, attempt.revision, {
391
+ status: "FAILED",
392
+ result_code: error.code,
393
+ runner_execution_started_at: null,
394
+ execution_id: null,
395
+ finished_at: finishedAt,
396
+ duration_sec: null,
397
+ error_message: error.message,
398
+ read_count: 0,
399
+ written_count: 0,
400
+ last_successful_chunk_no: null,
401
+ last_written_key: null,
402
+ }));
403
+ const finalizedState = await controlPlane(() => repository.upsertNodeState({
404
+ expected_revision: nodeState.revision,
405
+ value: {
406
+ ...nodeState.value,
407
+ status: "FAILED",
408
+ active_attempt_id: null,
409
+ status_reason: error.code,
410
+ finished_at: finishedAt,
411
+ updated_at: finishedAt,
412
+ },
413
+ }));
414
+ return {
415
+ classification: {
416
+ kind: "NO_RESULT",
417
+ attemptOutcome: "FAILED",
418
+ resultCode: error.code,
419
+ details: [error.message],
420
+ result: null,
421
+ invocationResultCode: error.code,
422
+ },
423
+ process: {
424
+ exitCode: null,
425
+ stdout: "",
426
+ stderr: "",
427
+ resultJsonPath: "",
428
+ timedOut: false,
429
+ forced: false,
430
+ launchFailureConfirmed: true,
431
+ },
432
+ attempt: finalizedAttempt,
433
+ nodeState: finalizedState,
434
+ invocationResultCode: error.code,
435
+ };
436
+ }
437
+ async function prepareResumeStates(input, definition, states, preserved, blocked, now, attempts) {
438
+ const excludedRoots = new Set();
439
+ for (const node of definition.nodes) {
440
+ let state = requiredState(states, node.id);
441
+ if (state.value.status === "SUCCESS") {
442
+ preserved.add(node.id);
443
+ continue;
444
+ }
445
+ if (state.value.status === "UNKNOWN")
446
+ excludedRoots.add(node.id);
447
+ if ((state.value.status === "FAILED" || state.value.status === "CANCELLED") &&
448
+ !state.value.idempotent) {
449
+ excludedRoots.add(node.id);
450
+ }
451
+ const resetBlocked = input.invocation.value.mode === "RESUME" &&
452
+ state.value.status === "BLOCKED";
453
+ const resetRetryable = input.invocation.value.mode === "RESUME" &&
454
+ (state.value.status === "FAILED" || state.value.status === "CANCELLED") &&
455
+ state.value.idempotent;
456
+ if (resetRetryable) {
457
+ const brake = retryBrakeForNode(attempts, node.id);
458
+ if (brake !== null) {
459
+ if (!(await confirmOrdinaryWrite(input.leaseMonitor)))
460
+ throw new SchedulerLeaseInterruptedError(true);
461
+ state = await writeNodeState(input.repository, state, {
462
+ status_reason: `RETRY_BRAKE:${brake.failureKind}x${brake.count}`,
463
+ updated_at: now().toISOString(),
464
+ });
465
+ states.set(node.id, state);
466
+ excludedRoots.add(node.id);
467
+ blocked.add(node.id);
468
+ continue;
469
+ }
470
+ }
471
+ if (resetBlocked || resetRetryable) {
472
+ if (!(await confirmOrdinaryWrite(input.leaseMonitor)))
473
+ throw new SchedulerLeaseInterruptedError(true);
474
+ state = await writeNodeState(input.repository, state, {
475
+ status: "WAITING",
476
+ active_attempt_id: null,
477
+ blocked_by: [],
478
+ status_reason: null,
479
+ finished_at: null,
480
+ updated_at: now().toISOString(),
481
+ });
482
+ states.set(node.id, state);
483
+ }
484
+ }
485
+ const excluded = descendantsIncludingRoots(definition, excludedRoots);
486
+ for (const id of excludedRoots)
487
+ blocked.add(id);
488
+ return excluded;
489
+ }
490
+ export function retryBrakeForNode(attempts, nodeId) {
491
+ const history = attempts
492
+ .filter(({ value }) => value.node_id === nodeId)
493
+ .slice()
494
+ .sort((left, right) => right.value.attempt_no - left.value.attempt_no);
495
+ let failureKind = null;
496
+ let count = 0;
497
+ for (const { value } of history) {
498
+ if (value.status === "CANCELLED" && value.result_code === "PREPARE_FAILED")
499
+ continue;
500
+ if (value.status !== "FAILED")
501
+ break;
502
+ const current = value.result_code.trim();
503
+ if (current === "")
504
+ break;
505
+ failureKind ??= current;
506
+ if (current !== failureKind)
507
+ break;
508
+ count += 1;
509
+ }
510
+ return failureKind !== null && count >= 3 ? { failureKind, count } : null;
511
+ }
512
+ function descendantsIncludingRoots(definition, roots) {
513
+ const result = new Set(roots);
514
+ let changed = true;
515
+ while (changed) {
516
+ changed = false;
517
+ for (const node of definition.nodes) {
518
+ if (!result.has(node.id) &&
519
+ node.depends_on.some((dependency) => result.has(dependency))) {
520
+ result.add(node.id);
521
+ changed = true;
522
+ }
523
+ }
524
+ }
525
+ return result;
526
+ }
527
+ function isDescendantThatMustBlock(nodeId, state) {
528
+ return state.value.node_id === nodeId && state.value.status === "WAITING";
529
+ }
530
+ async function ensureRunStarted(input, now) {
531
+ if (input.run.value.started_at !== null)
532
+ return input.run;
533
+ if (!(await confirmOrdinaryWrite(input.leaseMonitor)))
534
+ throw new SchedulerLeaseInterruptedError(true);
535
+ const at = now().toISOString();
536
+ return input.repository.updateRunAggregate(input.run.value.run_id, input.run.revision, { status: "RUNNING", started_at: at, finished_at: null, updated_at: at });
537
+ }
538
+ async function updateAggregate(input, run, states, now, runControlPlaneOperation = (operation) => operation()) {
539
+ if (!(await confirmAggregateWrite(input.leaseMonitor)))
540
+ throw new SchedulerLeaseInterruptedError(true);
541
+ const status = computeRunAggregateStatus([...states.values()].map(({ value }) => value.status), run.value.started_at);
542
+ const at = now().toISOString();
543
+ return runControlPlaneOperation(() => input.repository.updateRunAggregate(run.value.run_id, run.revision, {
544
+ status,
545
+ started_at: run.value.started_at,
546
+ finished_at: isTerminalAggregate(status) ? at : null,
547
+ updated_at: at,
548
+ }));
549
+ }
550
+ function controlPlaneDrainRunner(monitor, maxDurationMs, options) {
551
+ const nowMs = options?.nowMs ?? Date.now;
552
+ const retryDelayMs = options?.retryDelayMs ?? 2_000;
553
+ const sleep = options?.sleep ??
554
+ ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
555
+ let deadline = null;
556
+ return async (operation) => {
557
+ let lastError;
558
+ try {
559
+ return await operation();
560
+ }
561
+ catch (error) {
562
+ if (!isKintoneTransportFailure(error))
563
+ throw error;
564
+ lastError = error;
565
+ deadline ??= nowMs() + maxDurationMs;
566
+ monitor.markControlPlaneUnreachable?.();
567
+ }
568
+ while (nowMs() < deadline) {
569
+ const remaining = deadline - nowMs();
570
+ await sleep(Math.min(retryDelayMs, Math.max(0, remaining)));
571
+ if (nowMs() >= deadline)
572
+ break;
573
+ if (!(await monitor.confirmLeaseForFinalWrite()))
574
+ continue;
575
+ try {
576
+ return await operation();
577
+ }
578
+ catch (error) {
579
+ if (!isKintoneTransportFailure(error))
580
+ throw error;
581
+ lastError = error;
582
+ monitor.markControlPlaneUnreachable?.();
583
+ }
584
+ }
585
+ throw new SchedulerLeaseInterruptedError(true, { cause: lastError });
586
+ };
587
+ }
588
+ function isKintoneTransportFailure(error) {
589
+ if (error instanceof KintoneTransportError)
590
+ return true;
591
+ if (error instanceof RepositoryError) {
592
+ if (error.code !== "AMBIGUOUS_WRITE" && error.code !== "REMOTE_ERROR")
593
+ return false;
594
+ return isKintoneTransportFailure(error.causeDetail);
595
+ }
596
+ if (error instanceof AggregateError)
597
+ return error.errors.some(isKintoneTransportFailure);
598
+ if (typeof error === "object" && error !== null && "cause" in error)
599
+ return isKintoneTransportFailure(error.cause);
600
+ return false;
601
+ }
602
+ async function confirmOrdinaryWrite(monitor) {
603
+ if (!monitor.canStartNewNode())
604
+ return false;
605
+ return monitor.tick === undefined ? true : monitor.tick();
606
+ }
607
+ async function confirmAggregateWrite(monitor) {
608
+ if (!monitor.canPersistResults())
609
+ return false;
610
+ if (monitor.requiresNetworkLeaseInterruptedFinalization())
611
+ return true;
612
+ return monitor.tick === undefined ? true : monitor.tick();
613
+ }
614
+ async function writeNodeState(repository, current, update) {
615
+ return repository.upsertNodeState({
616
+ expected_revision: current.revision,
617
+ value: { ...current.value, ...update },
618
+ });
619
+ }
620
+ function invocationOutcome(status) {
621
+ switch (status) {
622
+ case "SUCCESS":
623
+ return { status: "SUCCESS", resultCode: "OK" };
624
+ case "UNKNOWN":
625
+ return { status: "UNKNOWN", resultCode: "NODE_RESULT_UNKNOWN" };
626
+ case "FAILED":
627
+ return { status: "FAILED", resultCode: "NODE_FAILED_OR_BLOCKED" };
628
+ case "CANCELLED":
629
+ return { status: "CANCELLED", resultCode: "NODE_CANCELLED" };
630
+ case "CREATED":
631
+ case "RUNNING":
632
+ return { status: "CANCELLED", resultCode: "NODES_DEFERRED" };
633
+ }
634
+ }
635
+ function finalization(status, resultCode, selected, preserved, blocked, reason) {
636
+ return {
637
+ status,
638
+ resultCode,
639
+ selectedNodeIds: [...selected],
640
+ preservedNodeIds: [...preserved],
641
+ blockedNodeIds: [...blocked],
642
+ ...(reason === undefined ? {} : { reason }),
643
+ };
644
+ }
645
+ const missingJobLogReader = {
646
+ async findAttemptResult() {
647
+ throw new Error("job log reader is required to adjudicate orphan RUNNING attempts");
648
+ },
649
+ };
650
+ function isTerminalAggregate(status) {
651
+ return status !== "CREATED" && status !== "RUNNING";
652
+ }
653
+ function stateMap(states) {
654
+ return new Map(states.map((state) => [state.value.node_id, state]));
655
+ }
656
+ function requiredState(states, nodeId) {
657
+ const state = states.get(nodeId);
658
+ if (state === undefined)
659
+ throw new Error(`Node State '${nodeId}' is missing`);
660
+ return state;
661
+ }
662
+ function nodeResult(nodeId, disposition, state, attemptNo = null, resultCode = state.value.status_reason) {
663
+ return {
664
+ nodeId,
665
+ disposition,
666
+ status: state.value.status,
667
+ attemptNo,
668
+ resultCode,
669
+ };
670
+ }
671
+ async function extractBundle(bundleBytes, executionRoot) {
672
+ const verified = verifyBundle(bundleBytes);
673
+ const entries = new Map(readStoreZip(bundleBytes).map((entry) => [entry.name, entry.data]));
674
+ const networkBytes = entries.get("network.yaml");
675
+ if (networkBytes === undefined)
676
+ throw new Error("bundle has no network.yaml");
677
+ const loaded = loadNetworkDefinitionSource(networkBytes.toString("utf8"));
678
+ if (loaded.definition === undefined || loaded.errors.length > 0)
679
+ throw new Error("bundle network definition is invalid");
680
+ const directory = await mkdtemp(join(resolve(executionRoot ?? tmpdir()), "ksql-flownet-run-"));
681
+ const sqlPaths = new Map();
682
+ try {
683
+ for (const file of verified.manifest.files) {
684
+ if (file.nodeId === undefined)
685
+ continue;
686
+ if (!/^jobs\/[A-Za-z0-9._-]+\.sql$/u.test(file.path))
687
+ throw new Error(`unsafe bundle SQL path '${file.path}'`);
688
+ const bytes = entries.get(file.path);
689
+ if (bytes === undefined)
690
+ throw new Error(`bundle SQL '${file.path}' is missing`);
691
+ const path = join(directory, ...file.path.split("/"));
692
+ await mkdir(dirname(path), { recursive: true });
693
+ await writeFile(path, bytes, { flag: "wx" });
694
+ sqlPaths.set(file.nodeId, resolve(path));
695
+ }
696
+ for (const node of loaded.definition.nodes)
697
+ if (!sqlPaths.has(node.id))
698
+ throw new Error(`bundle SQL for node '${node.id}' is missing`);
699
+ return { directory, definition: loaded.definition, sqlPaths };
700
+ }
701
+ catch (error) {
702
+ await rm(directory, { recursive: true, force: true });
703
+ throw error;
704
+ }
705
+ }
706
+ //# sourceMappingURL=sequential-scheduler.js.map