bulltrackers-module 1.0.342 → 1.0.343
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,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: computation-system/helpers/computation_dispatcher.js
|
|
3
3
|
* PURPOSE: Sequential Cursor-Based Dispatcher.
|
|
4
|
+
* UPDATED: Implemented "Fast-Forward" Scanning Loop to skip empty dates efficiently.
|
|
4
5
|
* UPDATED: Enforces Strict One-Shot Policy (Standard -> HighMem -> Dead Letter).
|
|
5
|
-
* UPDATED: Prevents infinite loops by permanently ignoring deterministic failures.
|
|
6
6
|
* UPDATED: Generates Google Cloud Trace Context (traceId/spanId) for end-to-end monitoring.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
|
12
12
|
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
13
13
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
14
14
|
const crypto = require('crypto');
|
|
15
|
-
const monConfig = require('../config/monitoring_config');
|
|
15
|
+
const monConfig = require('../config/monitoring_config');
|
|
16
16
|
|
|
17
17
|
const OOM_THRESHOLD_MB = 1500;
|
|
18
18
|
const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
|
|
@@ -281,7 +281,6 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
|
|
|
281
281
|
const currentDispatchId = crypto.randomUUID();
|
|
282
282
|
|
|
283
283
|
const tasksPayload = validTasks.map(t => {
|
|
284
|
-
// [NEW] Generate Eyeball Trace
|
|
285
284
|
const traceId = crypto.randomBytes(16).toString('hex');
|
|
286
285
|
const spanId = crypto.randomBytes(8).toString('hex');
|
|
287
286
|
|
|
@@ -294,7 +293,6 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
|
|
|
294
293
|
dispatchId: currentDispatchId,
|
|
295
294
|
triggerReason: 'SWEEP_RECOVERY',
|
|
296
295
|
resources: 'high-mem', // FORCE
|
|
297
|
-
// [NEW] Attach the eyeball
|
|
298
296
|
traceContext: {
|
|
299
297
|
traceId: traceId,
|
|
300
298
|
spanId: spanId,
|
|
@@ -315,7 +313,7 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
|
|
|
315
313
|
}
|
|
316
314
|
|
|
317
315
|
// =============================================================================
|
|
318
|
-
// LOGIC: Standard Dispatch (
|
|
316
|
+
// LOGIC: Standard Dispatch (Fast-Forward Enabled)
|
|
319
317
|
// =============================================================================
|
|
320
318
|
async function handleStandardDispatch(config, dependencies, computationManifest, reqBody) {
|
|
321
319
|
const { logger, db } = dependencies;
|
|
@@ -336,16 +334,29 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
336
334
|
const sessionDates = await getStableDateSession(config, dependencies, passToRun, dateLimitStr, forceRebuild);
|
|
337
335
|
if (!sessionDates || sessionDates.length === 0) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
338
336
|
|
|
339
|
-
|
|
337
|
+
// --- Fast-Forward Loop Configuration ---
|
|
338
|
+
// Scans up to 50 dates or 40 seconds to find work, avoiding empty "wait loops"
|
|
339
|
+
const MAX_SCAN_DEPTH = 50;
|
|
340
|
+
const TIME_LIMIT_MS = 40000;
|
|
341
|
+
const startTime = Date.now();
|
|
342
|
+
|
|
343
|
+
let currentCursor = targetCursorN;
|
|
340
344
|
let selectedTasks = [];
|
|
345
|
+
let selectedDate = null;
|
|
346
|
+
let datesScanned = 0;
|
|
341
347
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
// Loop until work is found, end is reached, or safety limits hit
|
|
349
|
+
while (currentCursor <= sessionDates.length) {
|
|
350
|
+
datesScanned++;
|
|
351
|
+
selectedDate = sessionDates[currentCursor - 1]; // 0-indexed array
|
|
352
|
+
|
|
353
|
+
// 1. Safety Break (Prevent Timeout)
|
|
354
|
+
if ((Date.now() - startTime) > TIME_LIMIT_MS || datesScanned > MAX_SCAN_DEPTH) {
|
|
355
|
+
logger.log('INFO', `[Dispatcher] ⏩ Fast-forward paused at ${selectedDate} after scanning ${datesScanned} dates.`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
347
358
|
|
|
348
|
-
|
|
359
|
+
// 2. Analyze Date
|
|
349
360
|
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
350
361
|
let prevDailyStatusPromise = Promise.resolve(null);
|
|
351
362
|
if (calcsInThisPass.some(c => c.isHistorical)) {
|
|
@@ -362,48 +373,70 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
362
373
|
checkRootDataAvailability(selectedDate, config, dependencies, DEFINITIVE_EARLIEST_DATES)
|
|
363
374
|
]);
|
|
364
375
|
|
|
365
|
-
if (availability && availability.status) {
|
|
376
|
+
if (availability && availability.status) {
|
|
366
377
|
const report = analyzeDateExecution(selectedDate, calcsInThisPass, availability.status, dailyStatus, manifestMap, prevDailyStatus);
|
|
367
378
|
let rawTasks = [...report.runnable, ...report.reRuns];
|
|
368
379
|
|
|
369
380
|
if (rawTasks.length > 0) {
|
|
370
381
|
rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
382
|
+
const activeTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
|
|
383
|
+
|
|
384
|
+
if (activeTasks.length > 0) {
|
|
385
|
+
const { standard, highMem } = await splitRoutes(db, selectedDate, passToRun, activeTasks, logger);
|
|
386
|
+
selectedTasks = [...standard, ...highMem];
|
|
387
|
+
|
|
388
|
+
if (selectedTasks.length > 0) {
|
|
389
|
+
// Found work! Break loop to dispatch.
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
378
393
|
}
|
|
379
394
|
}
|
|
395
|
+
|
|
396
|
+
// No work found for this date. Fast-forward to next.
|
|
397
|
+
currentCursor++;
|
|
380
398
|
}
|
|
381
399
|
|
|
400
|
+
// --- Result Handling ---
|
|
401
|
+
|
|
402
|
+
// Case 1: Satiated (Scanned to end of session with no work)
|
|
403
|
+
if (currentCursor > sessionDates.length && selectedTasks.length === 0) {
|
|
404
|
+
return {
|
|
405
|
+
status: 'CONTINUE_PASS',
|
|
406
|
+
dateProcessed: selectedDate,
|
|
407
|
+
dispatched: 0,
|
|
408
|
+
n_cursor_ignored: false,
|
|
409
|
+
remainingDates: 0,
|
|
410
|
+
nextCursor: currentCursor // Matches length + 1
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Case 2: Paused by Limit (No work found yet, but more dates remain)
|
|
382
415
|
if (selectedTasks.length === 0) {
|
|
383
416
|
return {
|
|
384
417
|
status: 'CONTINUE_PASS',
|
|
385
418
|
dateProcessed: selectedDate,
|
|
386
419
|
dispatched: 0,
|
|
387
420
|
n_cursor_ignored: false,
|
|
388
|
-
|
|
389
|
-
|
|
421
|
+
remainingDates: sessionDates.length - currentCursor + 1,
|
|
422
|
+
nextCursor: currentCursor // Resume from here
|
|
390
423
|
};
|
|
391
424
|
}
|
|
392
425
|
|
|
426
|
+
// Case 3: Work Found (Dispatching)
|
|
393
427
|
const totalweight = selectedTasks.reduce((sum, t) => sum + (manifestWeightMap.get(normalizeName(t.name)) || 1.0), 0);
|
|
394
428
|
const currentDispatchId = crypto.randomUUID();
|
|
395
429
|
const etaSeconds = Math.max(20, Math.ceil(totalweight * BASE_SECONDS_PER_WEIGHT_UNIT));
|
|
396
430
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
431
|
+
if (datesScanned > 1) {
|
|
432
|
+
logger.log('INFO', `[Dispatcher] ⏩ Fast-forwarded ${datesScanned - 1} empty dates. Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`);
|
|
433
|
+
} else {
|
|
434
|
+
logger.log('INFO', `[Dispatcher] ✅ Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`);
|
|
435
|
+
}
|
|
401
436
|
|
|
402
437
|
const mapToTaskPayload = (t) => {
|
|
403
|
-
// [NEW] Generate Eyeball Trace
|
|
404
438
|
const traceId = crypto.randomBytes(16).toString('hex');
|
|
405
439
|
const spanId = crypto.randomBytes(8).toString('hex');
|
|
406
|
-
|
|
407
440
|
return {
|
|
408
441
|
...t,
|
|
409
442
|
action: 'RUN_COMPUTATION_DATE',
|
|
@@ -413,7 +446,6 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
413
446
|
dispatchId: currentDispatchId,
|
|
414
447
|
triggerReason: t.reason,
|
|
415
448
|
resources: t.resources || 'standard',
|
|
416
|
-
// [NEW] Attach the eyeball
|
|
417
449
|
traceContext: {
|
|
418
450
|
traceId: traceId,
|
|
419
451
|
spanId: spanId,
|
|
@@ -448,7 +480,8 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
448
480
|
dispatched: selectedTasks.length,
|
|
449
481
|
n_cursor_ignored: false,
|
|
450
482
|
etaSeconds: etaSeconds,
|
|
451
|
-
remainingDates: sessionDates.length - targetCursorN
|
|
483
|
+
remainingDates: sessionDates.length - targetCursorN,
|
|
484
|
+
nextCursor: currentCursor + 1 // Start next scan AFTER this date
|
|
452
485
|
};
|
|
453
486
|
}
|
|
454
487
|
|
|
@@ -465,32 +498,25 @@ async function splitRoutes(db, date, pass, tasks, logger) {
|
|
|
465
498
|
const doc = await db.doc(ledgerPath).get();
|
|
466
499
|
|
|
467
500
|
if (!doc.exists) {
|
|
468
|
-
// New task -> Standard
|
|
469
501
|
standard.push(task);
|
|
470
502
|
continue;
|
|
471
503
|
}
|
|
472
504
|
|
|
473
505
|
const data = doc.data();
|
|
474
506
|
|
|
475
|
-
// If it FAILED, we check if we can escalate it.
|
|
476
507
|
if (data.status === 'FAILED') {
|
|
477
508
|
const stage = data.error?.stage;
|
|
478
509
|
|
|
479
|
-
// 1. QUALITY / LOGIC FAIL: Dead Letter (Drop it)
|
|
480
510
|
if (['QUALITY_CIRCUIT_BREAKER', 'SEMANTIC_GATE'].includes(stage)) {
|
|
481
511
|
logger.log('WARN', `[Dispatcher] 🛑 Dropping ${name} - Deterministic Failure (${stage}).`);
|
|
482
512
|
continue;
|
|
483
513
|
}
|
|
484
514
|
|
|
485
|
-
// 2. PREVIOUSLY HIGH MEM FAIL: Dead Letter (Drop it)
|
|
486
515
|
if (data.resourceTier === 'high-mem') {
|
|
487
516
|
logger.log('WARN', `[Dispatcher] 🛑 Dropping ${name} - Failed on High-Mem already.`);
|
|
488
517
|
continue;
|
|
489
518
|
}
|
|
490
519
|
|
|
491
|
-
// 3. STANDARD FAIL (Crash/OOM): Promote to High Mem (Retry)
|
|
492
|
-
// If it failed standard, we give it ONE shot on high-mem.
|
|
493
|
-
// Note: Even if it was an "Unknown" error, we promote to High-Mem to cover OOMs that looked like crashes.
|
|
494
520
|
highMem.push({
|
|
495
521
|
...task,
|
|
496
522
|
resources: 'high-mem',
|
|
@@ -498,7 +524,6 @@ async function splitRoutes(db, date, pass, tasks, logger) {
|
|
|
498
524
|
});
|
|
499
525
|
|
|
500
526
|
} else {
|
|
501
|
-
// If status is not FAILED (e.g. was Zombie and filterActiveTasks passed it), retry Standard.
|
|
502
527
|
standard.push(task);
|
|
503
528
|
}
|
|
504
529
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cloud Workflows: Precision Cursor-Based Orchestrator
|
|
2
|
-
#
|
|
3
|
-
# UPGRADED:
|
|
2
|
+
# FAST-FORWARD MODE: Dispatch -> (Internal Loop) -> Wait ETA -> Jump to Next Valid Cursor
|
|
3
|
+
# UPGRADED: Supports 'nextCursor' to skip empty date ranges instantly.
|
|
4
4
|
|
|
5
5
|
main:
|
|
6
6
|
params: [input]
|
|
@@ -39,12 +39,12 @@ main:
|
|
|
39
39
|
|
|
40
40
|
- evaluate_dispatch:
|
|
41
41
|
switch:
|
|
42
|
-
# 1. End of Session (Dispatcher reached end of date list)
|
|
42
|
+
# 1. End of Session (Dispatcher reached absolute end of date list)
|
|
43
43
|
- condition: '${dispatch_res.body.status == "MOVE_TO_NEXT_PASS"}'
|
|
44
44
|
assign:
|
|
45
45
|
- pass_complete: true
|
|
46
46
|
|
|
47
|
-
# 2. Satiation Check (
|
|
47
|
+
# 2. Satiation Check (Dispatcher scanned to end, found nothing)
|
|
48
48
|
- condition: '${dispatch_res.body.status == "CONTINUE_PASS" and dispatch_res.body.remainingDates == 0 and dispatch_res.body.dispatched == 0}'
|
|
49
49
|
steps:
|
|
50
50
|
- log_satiation:
|
|
@@ -55,7 +55,7 @@ main:
|
|
|
55
55
|
assign:
|
|
56
56
|
- pass_complete: true
|
|
57
57
|
|
|
58
|
-
# 3. Work Dispatched: Wait ETA ->
|
|
58
|
+
# 3. Work Dispatched: Wait ETA -> Jump to 'nextCursor'
|
|
59
59
|
- condition: '${dispatch_res.body.dispatched > 0}'
|
|
60
60
|
steps:
|
|
61
61
|
- wait_for_completion:
|
|
@@ -64,25 +64,26 @@ main:
|
|
|
64
64
|
seconds: '${int(dispatch_res.body.etaSeconds)}'
|
|
65
65
|
- update_cursor:
|
|
66
66
|
assign:
|
|
67
|
-
|
|
67
|
+
# CRITICAL CHANGE: Use dispatcher's specific jump target
|
|
68
|
+
- n_cursor: '${default(dispatch_res.body.nextCursor, n_cursor + 1)}'
|
|
68
69
|
- next_loop_work:
|
|
69
70
|
next: sequential_date_loop
|
|
70
71
|
|
|
71
|
-
# 4. No Work (
|
|
72
|
+
# 4. No Work (Fast-Forward limit reached or Single Check): Jump to 'nextCursor'
|
|
72
73
|
- condition: '${dispatch_res.body.dispatched == 0}'
|
|
73
74
|
steps:
|
|
74
75
|
- wait_short:
|
|
75
76
|
call: sys.sleep
|
|
76
77
|
args:
|
|
77
|
-
seconds: 2 # Tiny debounce
|
|
78
|
+
seconds: 2 # Tiny debounce for API limits
|
|
78
79
|
- update_cursor_retry:
|
|
79
80
|
assign:
|
|
80
|
-
#
|
|
81
|
-
- n_cursor: '${
|
|
81
|
+
# CRITICAL CHANGE: Resume exactly where the Fast-Forward loop stopped
|
|
82
|
+
- n_cursor: '${default(dispatch_res.body.nextCursor, n_cursor + 1)}'
|
|
82
83
|
- next_loop_retry:
|
|
83
84
|
next: sequential_date_loop
|
|
84
85
|
|
|
85
|
-
# ---
|
|
86
|
+
# --- VERIFICATION & SWEEP (No changes needed here) ---
|
|
86
87
|
- verify_pass_completion:
|
|
87
88
|
call: http.post
|
|
88
89
|
args:
|