bulltrackers-module 1.0.302 → 1.0.304
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.
|
@@ -1,303 +1,149 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: computation-system/helpers/computation_dispatcher.js
|
|
3
|
-
* PURPOSE:
|
|
4
|
-
*
|
|
3
|
+
* PURPOSE: Sequential Cursor-Based Dispatcher.
|
|
4
|
+
* IMPLEMENTS: Dirty-Date Discovery, Forensics Rerouting, and Satiation Sweeps.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
|
|
8
|
-
const { groupByPass, analyzeDateExecution }
|
|
9
|
-
const { PubSubUtils }
|
|
10
|
-
const { fetchComputationStatus
|
|
11
|
-
const { checkRootDataAvailability }
|
|
12
|
-
const { generateCodeHash } = require('../topology/HashManager');
|
|
13
|
-
const pLimit = require('p-limit');
|
|
14
|
-
const crypto = require('crypto');
|
|
8
|
+
const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrator.js');
|
|
9
|
+
const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
10
|
+
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
11
|
+
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
15
12
|
|
|
16
|
-
const STATUS_IMPOSSIBLE = 'IMPOSSIBLE';
|
|
17
|
-
|
|
18
|
-
// Threshold to trigger high-mem routing (e.g., 1.5 GB for a 2GB worker)
|
|
19
13
|
const OOM_THRESHOLD_MB = 1500;
|
|
14
|
+
const SECONDS_PER_CALC_MARGIN = 25; // 20s base + safety margin
|
|
20
15
|
|
|
21
16
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* Checks if specific tasks on a date need a high-memory reroute.
|
|
18
|
+
* Returns only tasks that failed on 'standard' and haven't been tried on 'high-mem'.
|
|
24
19
|
*/
|
|
25
|
-
async function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!doc.exists) return 'standard';
|
|
32
|
-
|
|
33
|
-
const data = doc.data();
|
|
20
|
+
async function getHighMemReroutes(db, date, pass, tasks) {
|
|
21
|
+
const reroutes = [];
|
|
22
|
+
for (const task of tasks) {
|
|
23
|
+
const name = normalizeName(task.name);
|
|
24
|
+
const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks/${name}`;
|
|
25
|
+
const doc = await db.doc(ledgerPath).get();
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
if (doc.exists) {
|
|
28
|
+
const data = doc.data();
|
|
29
|
+
// Check if it failed due to memory and hasn't been attempted on high-mem yet
|
|
30
|
+
const isOOM = (data.status === 'FAILED' || data.status === 'CRASH') &&
|
|
31
|
+
(data.resourceTier !== 'high-mem') &&
|
|
32
|
+
((data.peakMemoryMB > OOM_THRESHOLD_MB) || (data.error && /memory/i.test(data.error.message)));
|
|
38
33
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
return 'high-mem';
|
|
34
|
+
if (isOOM) {
|
|
35
|
+
reroutes.push({ ...task, resources: 'high-mem' });
|
|
42
36
|
}
|
|
43
37
|
}
|
|
44
|
-
|
|
45
|
-
// Also check if it's explicitly marked FAILED with 'Memory' in error
|
|
46
|
-
if (data.status === 'FAILED' && data.error && /memory/i.test(data.error)) {
|
|
47
|
-
return 'high-mem';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
} catch (e) {
|
|
51
|
-
console.warn(`[Dispatcher] Forensics check failed for ${computationName}: ${e.message}`);
|
|
52
38
|
}
|
|
53
|
-
|
|
54
|
-
return 'standard';
|
|
39
|
+
return reroutes;
|
|
55
40
|
}
|
|
56
41
|
|
|
57
|
-
/**
|
|
58
|
-
* Dispatches computation tasks for a specific pass.
|
|
59
|
-
* @param {Object} config - System config (Injected with topics)
|
|
60
|
-
* @param {Object} dependencies - { db, logger, ... }
|
|
61
|
-
* @param {Array} computationManifest - List of calculations
|
|
62
|
-
* @param {Object} reqBody - (Optional) HTTP Body containing 'callbackUrl', 'date', and 'attempt'
|
|
63
|
-
*/
|
|
64
42
|
async function dispatchComputationPass(config, dependencies, computationManifest, reqBody = {}) {
|
|
65
43
|
const { logger, db } = dependencies;
|
|
66
44
|
const pubsubUtils = new PubSubUtils(dependencies);
|
|
67
|
-
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
68
45
|
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const attemptCount = reqBody.attempt ? parseInt(reqBody.attempt) : 1;
|
|
74
|
-
|
|
75
|
-
if (!passToRun) { return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.'); }
|
|
76
|
-
if (!dateStr) { return logger.log('ERROR', '[Dispatcher] No date defined. Aborting.'); }
|
|
77
|
-
|
|
78
|
-
const currentManifestHash = generateCodeHash( computationManifest.map(c => c.hash).sort().join('|') );
|
|
46
|
+
// Inputs from Workflow Cursor
|
|
47
|
+
const passToRun = String(reqBody.pass || config.COMPUTATION_PASS_TO_RUN);
|
|
48
|
+
const targetCursorN = parseInt(reqBody.cursorIndex || 1);
|
|
49
|
+
const dateLimitStr = reqBody.date || config.date;
|
|
79
50
|
|
|
51
|
+
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
80
52
|
const passes = groupByPass(computationManifest);
|
|
81
53
|
const calcsInThisPass = passes[passToRun] || [];
|
|
82
54
|
|
|
83
|
-
if (!calcsInThisPass.length) {
|
|
84
|
-
|
|
85
|
-
logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun} (Target: ${dateStr}) [Attempt ${attemptCount}]`);
|
|
86
|
-
|
|
87
|
-
// -- DATE ANALYSIS LOGIC (FIXED: RANGE SCAN) --
|
|
88
|
-
|
|
89
|
-
// 1. Determine the absolute start of data history
|
|
90
|
-
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
91
|
-
const startDate = earliestDates.absoluteEarliest;
|
|
92
|
-
const endDate = new Date(dateStr + 'T00:00:00Z');
|
|
93
|
-
|
|
94
|
-
// 2. Generate the full range of dates to check
|
|
95
|
-
let allExpectedDates = getExpectedDateStrings(startDate, endDate);
|
|
96
|
-
|
|
97
|
-
// Safety fallback
|
|
98
|
-
if (!allExpectedDates || allExpectedDates.length === 0) {
|
|
99
|
-
logger.log('WARN', `[Dispatcher] Date range calculation returned empty (Start: ${startDate.toISOString()} -> End: ${endDate.toISOString()}). Defaulting to single target date.`);
|
|
100
|
-
allExpectedDates = [dateStr];
|
|
101
|
-
} else {
|
|
102
|
-
logger.log('INFO', `[Dispatcher] 📅 Analysis Range: ${allExpectedDates.length} days (${allExpectedDates[0]} to ${allExpectedDates[allExpectedDates.length-1]})`);
|
|
55
|
+
if (!calcsInThisPass.length) {
|
|
56
|
+
return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
103
57
|
}
|
|
104
58
|
|
|
105
|
-
|
|
106
|
-
const
|
|
59
|
+
// 1. Discover all "Dirty" Dates (Dates needing work)
|
|
60
|
+
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
61
|
+
const allDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date(dateLimitStr + 'T00:00:00Z'));
|
|
107
62
|
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!shouldRunForensics) {
|
|
120
|
-
if (isBulkBackfill) logger.log('INFO', `[Dispatcher] ⏩ Bulk Backfill (${allExpectedDates.length} days). Skipping Forensics.`);
|
|
121
|
-
else logger.log('INFO', `[Dispatcher] ⏩ First Attempt. Skipping Forensics (Defaulting to Standard).`);
|
|
122
|
-
} else {
|
|
123
|
-
logger.log('WARN', `[Dispatcher] 🕵️♀️ Retry Detected (Attempt ${attemptCount}). Enabling Forensic Crash Analysis.`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const analysisPromises = allExpectedDates.map(d => dateLimit(async () => {
|
|
127
|
-
try {
|
|
128
|
-
const fetchPromises = [
|
|
129
|
-
fetchComputationStatus(d, config, dependencies),
|
|
130
|
-
checkRootDataAvailability(d, config, dependencies, DEFINITIVE_EARLIEST_DATES)
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
let prevDateStr = null;
|
|
134
|
-
if (calcsInThisPass.some(c => c.isHistorical)) {
|
|
135
|
-
const prevDate = new Date(d + 'T00:00:00Z');
|
|
136
|
-
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
137
|
-
prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
138
|
-
if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
139
|
-
fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const results = await Promise.all(fetchPromises);
|
|
144
|
-
const dailyStatus = results[0];
|
|
145
|
-
const availability = results[1];
|
|
146
|
-
const prevDailyStatus = (prevDateStr && results[2]) ? results[2] : (prevDateStr ? {} : null);
|
|
147
|
-
|
|
148
|
-
const rootDataStatus = availability ? availability.status : {
|
|
149
|
-
hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const report = analyzeDateExecution(d, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
153
|
-
|
|
154
|
-
const statusUpdates = {};
|
|
155
|
-
report.impossible.forEach(item => {
|
|
156
|
-
if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
|
|
157
|
-
statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown', reason: item.reason };
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
report.blocked.forEach(item => {
|
|
161
|
-
statusUpdates[item.name] = { hash: false, category: 'unknown', reason: item.reason };
|
|
162
|
-
});
|
|
163
|
-
report.failedDependency.forEach(item => {
|
|
164
|
-
const missingStr = item.missing ? item.missing.join(', ') : 'unknown';
|
|
165
|
-
statusUpdates[item.name] = { hash: false, category: 'unknown', reason: `Dependency Missing: ${missingStr}` };
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (Object.keys(statusUpdates).length > 0) {
|
|
169
|
-
await updateComputationStatus(d, statusUpdates, config, dependencies);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const validToRun = [...report.runnable, ...report.reRuns];
|
|
173
|
-
|
|
174
|
-
await Promise.all(validToRun.map(item => forensicsLimit(async () => {
|
|
175
|
-
const compName = normalizeName(item.name);
|
|
176
|
-
|
|
177
|
-
// [UPDATED] Conditional Forensics
|
|
178
|
-
let requiredResource = 'standard';
|
|
179
|
-
if (shouldRunForensics) {
|
|
180
|
-
requiredResource = await checkCrashForensics(db, d, passToRun, compName);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const uniqueDispatchId = crypto.randomUUID();
|
|
184
|
-
tasksToDispatch.push({
|
|
185
|
-
action: 'RUN_COMPUTATION_DATE',
|
|
186
|
-
dispatchId: uniqueDispatchId,
|
|
187
|
-
date: d,
|
|
188
|
-
pass: passToRun,
|
|
189
|
-
computation: compName,
|
|
190
|
-
hash: item.hash || item.newHash,
|
|
191
|
-
previousCategory: item.previousCategory || null,
|
|
192
|
-
triggerReason: item.reason || "Unknown",
|
|
193
|
-
dependencyResultHashes: item.dependencyResultHashes || {},
|
|
194
|
-
timestamp: Date.now(),
|
|
195
|
-
resources: requiredResource
|
|
196
|
-
});
|
|
197
|
-
})));
|
|
198
|
-
|
|
199
|
-
logger.log('INFO', `[Dispatcher] Analyzed ${d}: ${validToRun.length} tasks (Cumulative: ${tasksToDispatch.length})`);
|
|
200
|
-
|
|
201
|
-
} catch (e) {
|
|
202
|
-
logger.log('ERROR', `[Dispatcher] Failed analysis for ${d}: ${e.message}`);
|
|
63
|
+
const dirtyDates = [];
|
|
64
|
+
for (const d of allDates) {
|
|
65
|
+
const dailyStatus = await fetchComputationStatus(d, config, dependencies);
|
|
66
|
+
const availability = await checkRootDataAvailability(d, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
67
|
+
|
|
68
|
+
const report = analyzeDateExecution(d, calcsInThisPass, availability.status, dailyStatus, manifestMap, null);
|
|
69
|
+
const tasks = [...report.runnable, ...report.reRuns];
|
|
70
|
+
|
|
71
|
+
if (tasks.length > 0) {
|
|
72
|
+
dirtyDates.push({ date: d, tasks });
|
|
203
73
|
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
await Promise.all(analysisPromises);
|
|
74
|
+
}
|
|
207
75
|
|
|
208
|
-
|
|
76
|
+
let selectedDate = null;
|
|
77
|
+
let selectedTasks = [];
|
|
78
|
+
let isReroute = false;
|
|
79
|
+
let isSweep = false;
|
|
209
80
|
|
|
210
|
-
|
|
81
|
+
// 2. Logic: Prioritize Reroute for N-1
|
|
82
|
+
if (targetCursorN > 1 && (targetCursorN - 2) < dirtyDates.length) {
|
|
83
|
+
const prevEntry = dirtyDates[targetCursorN - 2];
|
|
84
|
+
const reroutes = await getHighMemReroutes(db, prevEntry.date, passToRun, prevEntry.tasks);
|
|
211
85
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
createdAt: new Date(),
|
|
218
|
-
date: dateStr,
|
|
219
|
-
pass: passToRun,
|
|
220
|
-
totalTasks: tasksToDispatch.length,
|
|
221
|
-
remainingTasks: tasksToDispatch.length,
|
|
222
|
-
callbackUrl: callbackUrl,
|
|
223
|
-
status: 'IN_PROGRESS'
|
|
224
|
-
});
|
|
225
|
-
logger.log('INFO', `[Dispatcher] 🏁 Run State Initialized: ${runId}. Tasks: ${tasksToDispatch.length}`);
|
|
86
|
+
if (reroutes.length > 0) {
|
|
87
|
+
selectedDate = prevEntry.date;
|
|
88
|
+
selectedTasks = reroutes;
|
|
89
|
+
isReroute = true;
|
|
90
|
+
logger.log('INFO', `[Dispatcher] Reroute detected for ${selectedDate}. Pausing N increment.`);
|
|
226
91
|
}
|
|
92
|
+
}
|
|
227
93
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const doc = await t.get(ledgerRef);
|
|
241
|
-
if (doc.exists) {
|
|
242
|
-
const data = doc.data();
|
|
243
|
-
if (data.status === 'COMPLETED') return false;
|
|
244
|
-
}
|
|
245
|
-
t.set(ledgerRef, {
|
|
246
|
-
status: 'PENDING',
|
|
247
|
-
dispatchId: task.dispatchId,
|
|
248
|
-
runId: task.runId,
|
|
249
|
-
computation: task.computation,
|
|
250
|
-
expectedHash: task.hash || 'unknown',
|
|
251
|
-
createdAt: new Date(),
|
|
252
|
-
dispatcherHash: currentManifestHash,
|
|
253
|
-
triggerReason: task.triggerReason,
|
|
254
|
-
resources: task.resources,
|
|
255
|
-
retries: 0
|
|
256
|
-
}, { merge: true });
|
|
257
|
-
return true;
|
|
258
|
-
});
|
|
259
|
-
finalDispatched.push(task);
|
|
260
|
-
} catch (txnErr) {
|
|
261
|
-
logger.log('WARN', `[Dispatcher] Transaction failed for ${task.computation}: ${txnErr.message}`);
|
|
94
|
+
// 3. Logic: N-th Dirty Date or Final Sweep
|
|
95
|
+
if (!selectedDate) {
|
|
96
|
+
if (targetCursorN <= dirtyDates.length) {
|
|
97
|
+
const entry = dirtyDates[targetCursorN - 1];
|
|
98
|
+
selectedDate = entry.date;
|
|
99
|
+
selectedTasks = entry.tasks;
|
|
100
|
+
} else {
|
|
101
|
+
// Final Satiation Sweep: Check if anything was missed (recovery)
|
|
102
|
+
if (dirtyDates.length > 0) {
|
|
103
|
+
isSweep = true;
|
|
104
|
+
selectedDate = dirtyDates[0].date;
|
|
105
|
+
selectedTasks = dirtyDates[0].tasks;
|
|
262
106
|
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
await Promise.all(txnPromises);
|
|
266
|
-
|
|
267
|
-
if (finalDispatched.length > 0) {
|
|
268
|
-
const standardTasks = finalDispatched.filter(t => t.resources !== 'high-mem');
|
|
269
|
-
const highMemTasks = finalDispatched.filter(t => t.resources === 'high-mem');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
270
109
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
tasks: standardTasks,
|
|
276
|
-
taskType: `computation-pass-${passToRun}-std`,
|
|
277
|
-
maxPubsubBatchSize: 100
|
|
278
|
-
});
|
|
279
|
-
}
|
|
110
|
+
// 4. Signal Pass Completion
|
|
111
|
+
if (!selectedDate) {
|
|
112
|
+
return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0, etaSeconds: 0 };
|
|
113
|
+
}
|
|
280
114
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
|
|
285
|
-
tasks: highMemTasks,
|
|
286
|
-
taskType: `computation-pass-${passToRun}-highmem`,
|
|
287
|
-
maxPubsubBatchSize: 100
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return { dispatched: finalDispatched.length, runId };
|
|
292
|
-
} else {
|
|
293
|
-
logger.log('INFO', `[Dispatcher] All tasks were already COMPLETED.`);
|
|
294
|
-
return { dispatched: 0 };
|
|
295
|
-
}
|
|
115
|
+
// 5. Dispatch to PubSub (Standard vs. High-Mem)
|
|
116
|
+
const standardTasks = selectedTasks.filter(t => t.resources !== 'high-mem').map(t => ({ ...t, date: selectedDate, pass: passToRun }));
|
|
117
|
+
const highMemTasks = selectedTasks.filter(t => t.resources === 'high-mem').map(t => ({ ...t, date: selectedDate, pass: passToRun }));
|
|
296
118
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
119
|
+
const pubPromises = [];
|
|
120
|
+
if (standardTasks.length > 0) {
|
|
121
|
+
pubPromises.push(pubsubUtils.batchPublishTasks(dependencies, {
|
|
122
|
+
topicName: config.computationTopicStandard || 'computation-tasks',
|
|
123
|
+
tasks : standardTasks,
|
|
124
|
+
taskType : `pass-${passToRun}-std`
|
|
125
|
+
}));
|
|
300
126
|
}
|
|
127
|
+
if (highMemTasks.length > 0) {
|
|
128
|
+
pubPromises.push(pubsubUtils.batchPublishTasks(dependencies, {
|
|
129
|
+
topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
|
|
130
|
+
tasks : highMemTasks,
|
|
131
|
+
taskType : `pass-${passToRun}-high`
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
await Promise.all(pubPromises);
|
|
135
|
+
|
|
136
|
+
const etaSeconds = Math.max(20, selectedTasks.length * SECONDS_PER_CALC_MARGIN);
|
|
137
|
+
|
|
138
|
+
logger.log('INFO', `[Dispatcher] ${isReroute ? 'Reroute' : (isSweep ? 'Sweep' : 'Standard')} Run: ${selectedDate}. Tasks: ${selectedTasks.length}. ETA: ${etaSeconds}s`);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
status : isSweep ? 'RECOVERY' : 'CONTINUE_PASS',
|
|
142
|
+
dateProcessed : selectedDate,
|
|
143
|
+
dispatched : selectedTasks.length,
|
|
144
|
+
n_cursor_ignored: isReroute, // Tell workflow to stay on same N
|
|
145
|
+
etaSeconds : etaSeconds
|
|
146
|
+
};
|
|
301
147
|
}
|
|
302
148
|
|
|
303
149
|
module.exports = { dispatchComputationPass };
|
|
@@ -1,148 +1,64 @@
|
|
|
1
|
-
# Cloud Workflows
|
|
2
|
-
# Orchestrates 5
|
|
3
|
-
# UPDATED: Passes 'attempt' count to Dispatcher to trigger Smart Forensics on retries.
|
|
1
|
+
# Cloud Workflows: Precision Cursor-Based Orchestrator
|
|
2
|
+
# PURPOSE: Orchestrates 5 passes with deterministic pacing and syntax fixes.
|
|
4
3
|
|
|
5
4
|
main:
|
|
6
5
|
params: [input]
|
|
7
6
|
steps:
|
|
8
7
|
- init:
|
|
9
8
|
assign:
|
|
10
|
-
- project: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
|
|
11
|
-
- location: "europe-west1"
|
|
12
|
-
|
|
13
|
-
# T-1 Date Logic (Process Yesterday)
|
|
14
|
-
- now: ${sys.now()}
|
|
15
|
-
- yesterday_timestamp: ${now - 86400}
|
|
16
|
-
- yesterday_str: ${text.substring(time.format(yesterday_timestamp), 0, 10)}
|
|
17
|
-
- date_to_run: ${default(map.get(input, "date"), yesterday_str)}
|
|
18
|
-
|
|
19
|
-
# Configuration Variables
|
|
9
|
+
- project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
|
|
20
10
|
- passes: ["1", "2", "3", "4", "5"]
|
|
21
|
-
-
|
|
11
|
+
- date_to_run: '${default(map.get(input, "date"), "2025-01-01")}'
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
# MAIN LOOP: Iterate through Passes 1 to 5
|
|
25
|
-
# ======================================================
|
|
26
|
-
- run_passes:
|
|
13
|
+
- run_sequential_passes:
|
|
27
14
|
for:
|
|
28
15
|
value: pass_id
|
|
29
16
|
in: ${passes}
|
|
30
17
|
steps:
|
|
31
|
-
-
|
|
18
|
+
- init_cursor:
|
|
32
19
|
assign:
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
# URL of the Cloud Function acting as the Dispatcher
|
|
36
|
-
- dispatcher_url: ${"https://europe-west1-" + project + ".cloudfunctions.net/computation-pass-" + pass_id}
|
|
20
|
+
- n_cursor: 1
|
|
21
|
+
- pass_complete: false
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
# RETRY LOOP: Try to complete the pass up to 3 times
|
|
40
|
-
# -----------------------------------------------
|
|
41
|
-
- pass_retry_loop:
|
|
23
|
+
- sequential_date_loop:
|
|
42
24
|
switch:
|
|
43
|
-
- condition: ${
|
|
25
|
+
- condition: ${not pass_complete}
|
|
44
26
|
steps:
|
|
45
|
-
-
|
|
46
|
-
assign:
|
|
47
|
-
- attempt_count: ${attempt_count + 1}
|
|
48
|
-
|
|
49
|
-
# 1. GENERATE CALLBACK ENDPOINT
|
|
50
|
-
- create_callback:
|
|
51
|
-
call: events.create_callback_endpoint
|
|
52
|
-
args:
|
|
53
|
-
http_callback_method: "POST"
|
|
54
|
-
result: callback_details
|
|
55
|
-
|
|
56
|
-
- extract_callback_url:
|
|
57
|
-
assign:
|
|
58
|
-
- callback_url: ${callback_details.url}
|
|
59
|
-
|
|
60
|
-
- log_start:
|
|
61
|
-
call: sys.log
|
|
62
|
-
args:
|
|
63
|
-
text: ${"Starting Pass " + pass_id + " (Attempt " + attempt_count + ") for " + date_to_run + ". Waiting for signal at " + callback_url}
|
|
64
|
-
severity: "INFO"
|
|
65
|
-
|
|
66
|
-
# 2. TRIGGER DISPATCHER
|
|
67
|
-
- trigger_dispatcher:
|
|
27
|
+
- call_dispatcher:
|
|
68
28
|
call: http.post
|
|
69
29
|
args:
|
|
70
|
-
url: ${
|
|
30
|
+
url: '${"https://europe-west1-" + project + ".cloudfunctions.net/computation-pass-" + pass_id}'
|
|
71
31
|
body:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
auth:
|
|
76
|
-
|
|
77
|
-
timeout: 1800 # 30 mins max for dispatch analysis
|
|
78
|
-
result: dispatch_response
|
|
32
|
+
pass: '${pass_id}'
|
|
33
|
+
cursorIndex: '${n_cursor}'
|
|
34
|
+
date: '${date_to_run}'
|
|
35
|
+
auth: { type: OIDC }
|
|
36
|
+
result: dispatch_res
|
|
79
37
|
|
|
80
|
-
|
|
81
|
-
- check_immediate_completion:
|
|
38
|
+
- evaluate_dispatch:
|
|
82
39
|
switch:
|
|
83
|
-
|
|
40
|
+
# State 1: Pass exhausted and Satiation Sweep complete
|
|
41
|
+
- condition: '${dispatch_res.body.status == "MOVE_TO_NEXT_PASS"}'
|
|
42
|
+
assign:
|
|
43
|
+
- pass_complete: true
|
|
44
|
+
|
|
45
|
+
# State 2: Tasks dispatched (Standard, Reroute, or Recovery Sweep)
|
|
46
|
+
- condition: '${dispatch_res.body.dispatched > 0}'
|
|
84
47
|
steps:
|
|
85
|
-
-
|
|
48
|
+
- log_dispatch:
|
|
86
49
|
call: sys.log
|
|
87
50
|
args:
|
|
88
|
-
text: ${"Pass " + pass_id + "
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
- pass_success: true
|
|
92
|
-
- next_pass_empty:
|
|
93
|
-
next: pass_retry_loop
|
|
94
|
-
|
|
95
|
-
# 4. WAIT FOR WORKER SIGNAL
|
|
96
|
-
- wait_for_completion:
|
|
97
|
-
call: events.await_callback
|
|
98
|
-
args:
|
|
99
|
-
callback: ${callback_details}
|
|
100
|
-
timeout: 10800 # Reduced to 3h to fail faster if stalled
|
|
101
|
-
result: callback_request
|
|
102
|
-
|
|
103
|
-
# 5. PROCESS SIGNAL
|
|
104
|
-
- parse_signal:
|
|
105
|
-
assign:
|
|
106
|
-
- signal_data: ${callback_request.http_request.body}
|
|
107
|
-
|
|
108
|
-
- evaluate_signal:
|
|
109
|
-
switch:
|
|
110
|
-
- condition: ${signal_data.status == "SUCCESS"}
|
|
111
|
-
steps:
|
|
112
|
-
- log_success:
|
|
113
|
-
call: sys.log
|
|
51
|
+
text: '${"Pass " + pass_id + " - Dispatching " + string(dispatch_res.body.dispatched) + " tasks for " + dispatch_res.body.dateProcessed + ". ETA: " + string(dispatch_res.body.etaSeconds) + "s."}'
|
|
52
|
+
- wait_for_completion:
|
|
53
|
+
call: sys.sleep
|
|
114
54
|
args:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- mark_success:
|
|
55
|
+
seconds: '${int(dispatch_res.body.etaSeconds)}'
|
|
56
|
+
- update_cursor:
|
|
118
57
|
assign:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
- condition: ${signal_data.status == "FAILURE"}
|
|
124
|
-
steps:
|
|
125
|
-
- log_failure:
|
|
126
|
-
call: sys.log
|
|
127
|
-
args:
|
|
128
|
-
text: ${"Pass " + pass_id + " signaled FAILURE (DLQ limit hit). Retrying pass."}
|
|
129
|
-
severity: "WARNING"
|
|
130
|
-
- retry_pass:
|
|
131
|
-
next: pass_retry_loop
|
|
132
|
-
|
|
133
|
-
# -----------------------------------------------
|
|
134
|
-
# END RETRY LOOP
|
|
135
|
-
# -----------------------------------------------
|
|
136
|
-
|
|
137
|
-
- check_final_status:
|
|
138
|
-
switch:
|
|
139
|
-
- condition: ${not pass_success}
|
|
140
|
-
steps:
|
|
141
|
-
- log_giving_up:
|
|
142
|
-
call: sys.log
|
|
143
|
-
args:
|
|
144
|
-
text: ${"Pass " + pass_id + " failed after " + max_retries + " attempts. Proceeding with potential gaps."}
|
|
145
|
-
severity: "ERROR"
|
|
58
|
+
# If n_cursor_ignored is true (Reroute or Sweep Recovery), we stay on same N
|
|
59
|
+
- n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
|
|
60
|
+
- next_loop:
|
|
61
|
+
next: sequential_date_loop
|
|
146
62
|
|
|
147
63
|
- finish:
|
|
148
|
-
return: "Pipeline Execution Complete"
|
|
64
|
+
return: "Pipeline Execution Satiated and Complete"
|