@zq-silk/yui 0.6.4 → 0.6.6
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/README.md +11 -3
- package/dist/cli/updateOrchestrator.js +12 -6
- package/dist/cli.js +12 -0
- package/dist/controller/agentRuntimeObserver.js +76 -16
- package/dist/controller/controller.js +398 -76
- package/dist/controller/fileSchedulerStoreAdapter.js +60 -51
- package/dist/controller/jobSupervisor.js +2 -16
- package/dist/controller/resourceInventoryLinux.js +73 -20
- package/dist/controller/runtime.js +1 -0
- package/dist/controller/runtimeEventProcessor.js +171 -31
- package/dist/coordination/keyedWorkQueue.js +189 -0
- package/dist/runtime/runtimeSessionCandidate.js +43 -0
- package/dist/scheduler/activeRoleRunDelivery.js +2 -4
- package/dist/scheduler/activeTaskProgress.js +2 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -1
- package/dist/scheduler/ports.js +35 -2
- package/dist/scheduler/roleRunLiveness.js +44 -22
- package/dist/scheduler/roleRunStall.js +49 -25
- package/dist/storage/sqliteSchema.js +363 -14
- package/dist/storage/sqliteStore.js +403 -30
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/taskStore.js +104 -6
- package/i18n/README.zh-CN.md +7 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
|
|
|
2
2
|
import { processLeaderWakeups } from "../scheduler/leaderWakeupProcessor.js";
|
|
3
3
|
import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
|
|
4
4
|
import { processActiveRoleRunDeliveries } from "../scheduler/activeRoleRunDelivery.js";
|
|
5
|
-
import { selectedSchedulerRoles, selectedSchedulerTasks } from "../scheduler/ports.js";
|
|
5
|
+
import { selectedActiveSchedulerTasks, selectedSchedulerRoles, selectedSchedulerTasks } from "../scheduler/ports.js";
|
|
6
6
|
import { reconcileExitedRoleRuns } from "../scheduler/roleRunLiveness.js";
|
|
7
7
|
import { DEFAULT_STALL_WINDOW_MS, reconcileStalledRoleRuns } from "../scheduler/roleRunStall.js";
|
|
8
8
|
import { repairOrphanedActiveTasks } from "../scheduler/activeTaskProgress.js";
|
|
@@ -10,6 +10,7 @@ import { processOperatorInputNotifications } from "../scheduler/operatorInputNot
|
|
|
10
10
|
import { startControllerServer } from "../core/controllerServer.js";
|
|
11
11
|
import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
12
12
|
import { isProjectMaintenanceFenced } from "../repository/projectMaintenanceLock.js";
|
|
13
|
+
import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
|
|
13
14
|
import { MailboxScheduler } from "../coordination/mailboxScheduler.js";
|
|
14
15
|
import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
|
|
15
16
|
import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
@@ -21,6 +22,8 @@ const DEFAULT_RUNTIME_OBSERVER_INTERVAL_MS = 1_000;
|
|
|
21
22
|
const DEFAULT_DELIVERY_RETRY_MS = 250;
|
|
22
23
|
const DEFAULT_DELIVERY_RETRY_LIMIT = 60;
|
|
23
24
|
const DEFAULT_TASK_ORCHESTRATION_RETRY_LIMIT = 2;
|
|
25
|
+
const DEFAULT_TASK_CONCURRENCY = 4;
|
|
26
|
+
const MAX_TASK_CONCURRENCY = 32;
|
|
24
27
|
const RUNTIME_RESERVATION_RECOVERY_AGE_MS = 120_000;
|
|
25
28
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
26
29
|
const CONTROLLER_LATENCY_BUCKETS_MS = [10, 50, 100, 250, 500, 1_000, 3_000];
|
|
@@ -40,18 +43,18 @@ const ZERO_DRAIN_METRICS = Object.freeze({
|
|
|
40
43
|
* Runs one lean scheduler pass. Due native Turn completions are folded before
|
|
41
44
|
* liveness, so a valid Hook boundary fences destructive process reconciliation.
|
|
42
45
|
*/
|
|
43
|
-
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer) {
|
|
46
|
+
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set()) {
|
|
44
47
|
const compiledSelection = compileReconcileSelection(scope);
|
|
45
48
|
const selection = includeOperator
|
|
46
|
-
? compiledSelection
|
|
47
|
-
: { ...compiledSelection, operator: false };
|
|
49
|
+
? { ...compiledSelection, blockedTaskIds }
|
|
50
|
+
: { ...compiledSelection, operator: false, blockedTaskIds };
|
|
48
51
|
queueSelectedCompletedTaskRuntimeCleanups(store, selection, now);
|
|
49
52
|
// A full-state Task projection can be individually bounded yet still starve
|
|
50
53
|
// control sockets when several scheduler phases repeat it in one native
|
|
51
54
|
// event-loop turn. Give already-written requests a poll boundary before the
|
|
52
55
|
// next durable phase; later phases retain their existing CAS fences.
|
|
53
56
|
await controlEventLoopTurn();
|
|
54
|
-
const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes);
|
|
57
|
+
const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds);
|
|
55
58
|
const roleSelection = selectionWithoutFailedCleanupRoles(store, selection, failedCleanupRoles);
|
|
56
59
|
const wakeupSelection = selectionWithoutFailedLeaderCleanupTasks(store, selection, failedCleanupRoles);
|
|
57
60
|
if (selection.full)
|
|
@@ -107,16 +110,17 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
107
110
|
await controlEventLoopTurn();
|
|
108
111
|
const liveStatuses = new Map();
|
|
109
112
|
const resourceEvidence = new Map();
|
|
110
|
-
const failedRunRefs = await reconcileExitedRoleRuns(store, delivery, now, roleSelection, unsettledRunRefs, liveStatuses, resourceEvidence);
|
|
113
|
+
const failedRunRefs = await reconcileExitedRoleRuns(store, delivery, now, roleSelection, unsettledRunRefs, liveStatuses, resourceEvidence, scope.kind === "dirty");
|
|
111
114
|
await controlEventLoopTurn();
|
|
112
115
|
await reconcileStalledRoleRuns(store, delivery, now, roleSelection, stallWindowMs, liveStatuses, resourceEvidence);
|
|
113
116
|
await controlEventLoopTurn();
|
|
114
|
-
await reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, scope, now);
|
|
115
|
-
const
|
|
117
|
+
await reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, scope, now, selection.blockedTaskIds);
|
|
118
|
+
const selectedInputTaskIds = selectedTaskIdsForBoundedPass(store, selection);
|
|
119
|
+
const autoResolvedInputs = selectedInputTaskIds === undefined
|
|
116
120
|
? store.resolveExpiredInputRecommendations(now)
|
|
117
|
-
:
|
|
121
|
+
: selectedInputTaskIds.size === 0
|
|
118
122
|
? []
|
|
119
|
-
: store.resolveExpiredInputRecommendations(now,
|
|
123
|
+
: store.resolveExpiredInputRecommendations(now, selectedInputTaskIds);
|
|
120
124
|
// Liveness and auto-resolution can durably queue new Leader work. Process
|
|
121
125
|
// only Tasks that were not part of phase one, except when a same-pass due
|
|
122
126
|
// completion made an initially busy Leader idle. Result order remains
|
|
@@ -160,8 +164,10 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
160
164
|
}
|
|
161
165
|
function selectedPendingWakeups(store, selection) {
|
|
162
166
|
const wakeups = selection.full
|
|
163
|
-
? store.listPendingWakeups()
|
|
167
|
+
? store.listPendingWakeups().filter((wakeup) => (!selection.blockedTaskIds?.has(wakeup.taskId)))
|
|
164
168
|
: [...selection.taskIds].flatMap((taskId) => {
|
|
169
|
+
if (selection.blockedTaskIds?.has(taskId))
|
|
170
|
+
return [];
|
|
165
171
|
const wakeup = store.getPendingWakeup(taskId);
|
|
166
172
|
return wakeup === null ? [] : [wakeup];
|
|
167
173
|
});
|
|
@@ -176,7 +182,8 @@ function exactTaskSelection(taskIds) {
|
|
|
176
182
|
taskIds,
|
|
177
183
|
allRoleTaskIds: new Set(),
|
|
178
184
|
rolesByTask: new Map(),
|
|
179
|
-
operator: false
|
|
185
|
+
operator: false,
|
|
186
|
+
blockedTaskIds: new Set()
|
|
180
187
|
};
|
|
181
188
|
}
|
|
182
189
|
function mergeWakeupPhaseResults(initial, later) {
|
|
@@ -197,6 +204,27 @@ function mergeWakeupPhaseResults(initial, later) {
|
|
|
197
204
|
function queueSelectedCompletedTaskRuntimeCleanups(store, selection, now) {
|
|
198
205
|
if (store.enqueueRuntimeCleanup === undefined)
|
|
199
206
|
return;
|
|
207
|
+
if (selection.full && store.listRuntimeSessionCandidates !== undefined) {
|
|
208
|
+
for (const candidate of store.listRuntimeSessionCandidates({
|
|
209
|
+
cleanupRequiredOnly: true
|
|
210
|
+
})) {
|
|
211
|
+
if (candidate.owner.scope !== "task"
|
|
212
|
+
|| !candidate.cleanupRequired
|
|
213
|
+
|| selection.blockedTaskIds?.has(candidate.owner.taskId)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const { taskId, roleName } = candidate.owner;
|
|
217
|
+
if (store.getTask(taskId)?.status !== "completed")
|
|
218
|
+
continue;
|
|
219
|
+
if (store.getActiveAgentRun(taskId, roleName) !== null)
|
|
220
|
+
continue;
|
|
221
|
+
const target = runtimeLifecycleTarget(candidate.owner);
|
|
222
|
+
if (hasRuntimeCleanupObligation(store.getWorkMailbox(target)))
|
|
223
|
+
continue;
|
|
224
|
+
store.enqueueRuntimeCleanup(candidate.owner, now);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
200
228
|
for (const task of selectedSchedulerTasks(store, selection)) {
|
|
201
229
|
if (task.status !== "completed")
|
|
202
230
|
continue;
|
|
@@ -227,8 +255,8 @@ function schedulerSessionRequiresRuntimeCleanup(session) {
|
|
|
227
255
|
}
|
|
228
256
|
return session.status === "running" || session.launchId !== undefined;
|
|
229
257
|
}
|
|
230
|
-
async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, outcomes) {
|
|
231
|
-
const targets = selectedRuntimeLifecycleTargets(store, scope);
|
|
258
|
+
async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, outcomes, blockedTaskIds = new Set()) {
|
|
259
|
+
const targets = selectedRuntimeLifecycleTargets(store, scope, blockedTaskIds);
|
|
232
260
|
const failedRoles = new Set();
|
|
233
261
|
for (const target of targets) {
|
|
234
262
|
const mailbox = store.getWorkMailbox(target);
|
|
@@ -315,14 +343,15 @@ async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost
|
|
|
315
343
|
}
|
|
316
344
|
return failedRoles;
|
|
317
345
|
}
|
|
318
|
-
async function reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, scope, now) {
|
|
346
|
+
async function reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, scope, now, blockedTaskIds = new Set()) {
|
|
319
347
|
if (scope.kind !== "full"
|
|
320
348
|
|| lifecycleHost === undefined
|
|
321
349
|
|| store.listDormantRuntimeOwners === undefined
|
|
322
350
|
|| store.markRuntimeOwnerStopped === undefined) {
|
|
323
351
|
return;
|
|
324
352
|
}
|
|
325
|
-
const candidates = store.listDormantRuntimeOwners()
|
|
353
|
+
const candidates = store.listDormantRuntimeOwners().filter((candidate) => (candidate.owner.scope !== "task"
|
|
354
|
+
|| !blockedTaskIds.has(candidate.owner.taskId)));
|
|
326
355
|
if (candidates.length === 0)
|
|
327
356
|
return;
|
|
328
357
|
const owners = candidates.map((candidate) => candidate.owner);
|
|
@@ -386,11 +415,13 @@ function runtimeOwnerIdentity(owner) {
|
|
|
386
415
|
function resolveDueRuntimeTurnCompletions(store, delivery, selection, now) {
|
|
387
416
|
if (typeof store.resolveDueRuntimeTurnCompletions !== "function")
|
|
388
417
|
return;
|
|
389
|
-
const selectedTaskIds =
|
|
418
|
+
const selectedTaskIds = selectedTaskIdsForBoundedPass(store, selection);
|
|
390
419
|
if (selectedTaskIds?.size === 0)
|
|
391
420
|
return;
|
|
392
|
-
const
|
|
393
|
-
|
|
421
|
+
const candidateTaskIds = selectedTaskIds === undefined
|
|
422
|
+
? undefined
|
|
423
|
+
: [...selectedTaskIds].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
|
|
424
|
+
const candidates = store.listPendingRuntimeTurnCompletions(candidateTaskIds);
|
|
394
425
|
const finalized = new Set(store.resolveDueRuntimeTurnCompletions(now, selectedTaskIds));
|
|
395
426
|
if (finalized.size === 0)
|
|
396
427
|
return;
|
|
@@ -413,22 +444,46 @@ function resolveDueRuntimeTurnCompletions(store, delivery, selection, now) {
|
|
|
413
444
|
function resolveDueProviderRetries(store, selection, now) {
|
|
414
445
|
if (typeof store.resolveDueProviderRetries !== "function")
|
|
415
446
|
return;
|
|
416
|
-
const selectedTaskIds =
|
|
447
|
+
const selectedTaskIds = selectedTaskIdsForBoundedPass(store, selection);
|
|
417
448
|
if (selectedTaskIds?.size === 0)
|
|
418
449
|
return;
|
|
419
450
|
store.resolveDueProviderRetries(now, selectedTaskIds);
|
|
420
451
|
}
|
|
421
|
-
function
|
|
452
|
+
function selectedTaskIdsForBoundedPass(store, selection) {
|
|
453
|
+
if (!selection.full) {
|
|
454
|
+
if ((selection.blockedTaskIds?.size ?? 0) === 0)
|
|
455
|
+
return selection.taskIds;
|
|
456
|
+
return new Set([...selection.taskIds].filter((taskId) => (!selection.blockedTaskIds.has(taskId))));
|
|
457
|
+
}
|
|
458
|
+
if ((selection.blockedTaskIds?.size ?? 0) === 0)
|
|
459
|
+
return undefined;
|
|
460
|
+
return new Set(selectedActiveSchedulerTasks(store, selection).map((task) => task.id));
|
|
461
|
+
}
|
|
462
|
+
function selectedReadyWorkMailboxes(store) {
|
|
463
|
+
return store.listReadyWorkMailboxes?.() ?? store.listWorkMailboxes();
|
|
464
|
+
}
|
|
465
|
+
function selectedRuntimeLifecycleTargets(store, scope, blockedTaskIds = new Set()) {
|
|
422
466
|
if (scope.kind === "full") {
|
|
423
|
-
return store
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
467
|
+
return selectedReadyWorkMailboxes(store).flatMap((mailbox) => {
|
|
468
|
+
if (mailbox.target.kind === "role-runtime") {
|
|
469
|
+
return blockedTaskIds.has(mailbox.target.taskId)
|
|
470
|
+
? []
|
|
471
|
+
: [mailbox.target];
|
|
472
|
+
}
|
|
473
|
+
return mailbox.target.kind === "global-role-runtime"
|
|
474
|
+
? [mailbox.target]
|
|
475
|
+
: [];
|
|
476
|
+
});
|
|
427
477
|
}
|
|
428
478
|
const targets = new Map();
|
|
429
479
|
for (const key of scope.keys) {
|
|
430
480
|
const parsed = parseMailboxKey(key);
|
|
431
481
|
if (parsed.kind === "task") {
|
|
482
|
+
if (blockedTaskIds.has(parsed.taskId))
|
|
483
|
+
continue;
|
|
484
|
+
const task = store.getTask(parsed.taskId);
|
|
485
|
+
if (task?.status !== "active" && task?.status !== "completed")
|
|
486
|
+
continue;
|
|
432
487
|
for (const role of store.listRoles(parsed.taskId)) {
|
|
433
488
|
const target = {
|
|
434
489
|
kind: "role-runtime",
|
|
@@ -439,6 +494,8 @@ function selectedRuntimeLifecycleTargets(store, scope) {
|
|
|
439
494
|
}
|
|
440
495
|
}
|
|
441
496
|
else if (parsed.kind === "role") {
|
|
497
|
+
if (blockedTaskIds.has(parsed.taskId))
|
|
498
|
+
continue;
|
|
442
499
|
const target = {
|
|
443
500
|
kind: "role-runtime",
|
|
444
501
|
taskId: parsed.taskId,
|
|
@@ -497,7 +554,7 @@ function selectionWithoutFailedCleanupRoles(store, selection, failedRoles) {
|
|
|
497
554
|
if (failedRoles.size === 0)
|
|
498
555
|
return selection;
|
|
499
556
|
const taskIds = selection.full
|
|
500
|
-
? new Set(store
|
|
557
|
+
? new Set(selectedActiveSchedulerTasks(store, selection).map((task) => task.id))
|
|
501
558
|
: new Set(selection.taskIds);
|
|
502
559
|
const rolesByTask = new Map();
|
|
503
560
|
for (const taskId of taskIds) {
|
|
@@ -511,7 +568,8 @@ function selectionWithoutFailedCleanupRoles(store, selection, failedRoles) {
|
|
|
511
568
|
taskIds,
|
|
512
569
|
allRoleTaskIds: new Set(),
|
|
513
570
|
rolesByTask,
|
|
514
|
-
operator: selection.operator
|
|
571
|
+
operator: selection.operator,
|
|
572
|
+
blockedTaskIds: selection.blockedTaskIds
|
|
515
573
|
};
|
|
516
574
|
}
|
|
517
575
|
function selectionWithoutFailedLeaderCleanupTasks(store, selection, failedRoles) {
|
|
@@ -519,7 +577,7 @@ function selectionWithoutFailedLeaderCleanupTasks(store, selection, failedRoles)
|
|
|
519
577
|
return selection;
|
|
520
578
|
}
|
|
521
579
|
const taskIds = selection.full
|
|
522
|
-
? new Set(store
|
|
580
|
+
? new Set(selectedActiveSchedulerTasks(store, selection).map((task) => task.id))
|
|
523
581
|
: new Set(selection.taskIds);
|
|
524
582
|
for (const taskId of taskIds) {
|
|
525
583
|
if (failedRoles.has(roleIdentity(taskId, "leader")))
|
|
@@ -530,7 +588,8 @@ function selectionWithoutFailedLeaderCleanupTasks(store, selection, failedRoles)
|
|
|
530
588
|
taskIds,
|
|
531
589
|
allRoleTaskIds: new Set(),
|
|
532
590
|
rolesByTask: new Map(),
|
|
533
|
-
operator: selection.operator
|
|
591
|
+
operator: selection.operator,
|
|
592
|
+
blockedTaskIds: selection.blockedTaskIds
|
|
534
593
|
};
|
|
535
594
|
}
|
|
536
595
|
function roleIdentity(taskId, roleName) {
|
|
@@ -538,8 +597,13 @@ function roleIdentity(taskId, roleName) {
|
|
|
538
597
|
}
|
|
539
598
|
function claimSelectedTaskMailboxes(store, selection, now) {
|
|
540
599
|
const targets = selection.full
|
|
541
|
-
? store
|
|
542
|
-
|
|
600
|
+
? selectedReadyWorkMailboxes(store).flatMap((mailbox) => (mailbox.target.kind === "task"
|
|
601
|
+
&& !selection.blockedTaskIds?.has(mailbox.target.taskId)
|
|
602
|
+
? [mailbox.target]
|
|
603
|
+
: []))
|
|
604
|
+
: [...selection.allRoleTaskIds]
|
|
605
|
+
.filter((taskId) => !selection.blockedTaskIds?.has(taskId))
|
|
606
|
+
.map((taskId) => ({ kind: "task", taskId }));
|
|
543
607
|
const claims = [];
|
|
544
608
|
for (const target of targets) {
|
|
545
609
|
const mailbox = store.getWorkMailbox(target);
|
|
@@ -565,7 +629,8 @@ export function compileReconcileSelection(scope) {
|
|
|
565
629
|
taskIds: new Set(),
|
|
566
630
|
allRoleTaskIds: new Set(),
|
|
567
631
|
rolesByTask: new Map(),
|
|
568
|
-
operator: true
|
|
632
|
+
operator: true,
|
|
633
|
+
blockedTaskIds: new Set()
|
|
569
634
|
};
|
|
570
635
|
}
|
|
571
636
|
const taskIds = new Set();
|
|
@@ -593,44 +658,46 @@ export function compileReconcileSelection(scope) {
|
|
|
593
658
|
taskIds,
|
|
594
659
|
allRoleTaskIds,
|
|
595
660
|
rolesByTask: mutableRoles,
|
|
596
|
-
operator
|
|
661
|
+
operator,
|
|
662
|
+
blockedTaskIds: new Set()
|
|
597
663
|
};
|
|
598
664
|
}
|
|
599
665
|
async function prepareActiveWorkspaces(store, workspace, selection, maintenanceFence, onMaintenanceFenceDefer) {
|
|
600
666
|
if (workspace === undefined)
|
|
601
667
|
return { failed: new Set(), ready: new Set() };
|
|
602
|
-
const
|
|
603
|
-
?
|
|
604
|
-
|
|
605
|
-
.
|
|
606
|
-
|
|
668
|
+
const tasks = selection.full
|
|
669
|
+
? selectedActiveSchedulerTasks(store, selection)
|
|
670
|
+
: [...selection.allRoleTaskIds].flatMap((taskId) => {
|
|
671
|
+
if (selection.blockedTaskIds?.has(taskId))
|
|
672
|
+
return [];
|
|
673
|
+
const task = store.getTask(taskId);
|
|
674
|
+
return task?.status === "active" ? [task] : [];
|
|
675
|
+
});
|
|
607
676
|
const failed = new Set();
|
|
608
677
|
const ready = new Set();
|
|
609
|
-
for (const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
try {
|
|
625
|
-
const result = await workspace.prepareTaskWorkspace(taskId);
|
|
626
|
-
if (result.status === "failed")
|
|
627
|
-
failed.add(taskId);
|
|
628
|
-
else
|
|
629
|
-
ready.add(taskId);
|
|
678
|
+
for (const task of tasks) {
|
|
679
|
+
const taskId = task.id;
|
|
680
|
+
// A Project under maintenance is fenced: defer this Task's preparation
|
|
681
|
+
// for the pass. The deferral is per-Project, never a Controller stop,
|
|
682
|
+
// and a deferred Task is not marked failed.
|
|
683
|
+
if (maintenanceFence !== undefined) {
|
|
684
|
+
const fencedProjects = task.projectBindings
|
|
685
|
+
.map(({ projectId }) => projectId)
|
|
686
|
+
.filter((projectId) => maintenanceFence(projectId));
|
|
687
|
+
if (fencedProjects.length > 0) {
|
|
688
|
+
onMaintenanceFenceDefer?.({ taskId, projectIds: fencedProjects });
|
|
689
|
+
continue;
|
|
630
690
|
}
|
|
631
|
-
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const result = await workspace.prepareTaskWorkspace(taskId);
|
|
694
|
+
if (result.status === "failed")
|
|
632
695
|
failed.add(taskId);
|
|
633
|
-
|
|
696
|
+
else
|
|
697
|
+
ready.add(taskId);
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
failed.add(taskId);
|
|
634
701
|
}
|
|
635
702
|
}
|
|
636
703
|
return { failed, ready };
|
|
@@ -675,10 +742,69 @@ function mailboxPart(value, key) {
|
|
|
675
742
|
throw new TypeError(`Controller mailbox key is invalid: ${key}.`);
|
|
676
743
|
}
|
|
677
744
|
}
|
|
745
|
+
function partitionDirtyScope(keys) {
|
|
746
|
+
const taskScopes = new Map();
|
|
747
|
+
const globalKeys = new Set();
|
|
748
|
+
for (const key of keys) {
|
|
749
|
+
const parsed = parseMailboxKey(key);
|
|
750
|
+
if (parsed.kind === "operator" || parsed.kind === "global-role") {
|
|
751
|
+
globalKeys.add(key);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const current = taskScopes.get(parsed.taskId) ?? {
|
|
755
|
+
taskKey: undefined,
|
|
756
|
+
roleKeys: new Map()
|
|
757
|
+
};
|
|
758
|
+
if (parsed.kind === "task") {
|
|
759
|
+
current.taskKey = key;
|
|
760
|
+
current.roleKeys.clear();
|
|
761
|
+
}
|
|
762
|
+
else if (current.taskKey === undefined) {
|
|
763
|
+
current.roleKeys.set(parsed.roleName, key);
|
|
764
|
+
}
|
|
765
|
+
taskScopes.set(parsed.taskId, current);
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
taskScopes: [...taskScopes].map(([taskId, scope]) => ({
|
|
769
|
+
taskId,
|
|
770
|
+
keys: scope.taskKey === undefined
|
|
771
|
+
? [...scope.roleKeys.values()]
|
|
772
|
+
: [scope.taskKey]
|
|
773
|
+
})),
|
|
774
|
+
globalKeys: [...globalKeys]
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function emptyControllerSchedulerResult() {
|
|
778
|
+
return {
|
|
779
|
+
activeRunDeliveries: [],
|
|
780
|
+
failedRunRefs: [],
|
|
781
|
+
wakeups: [],
|
|
782
|
+
inputNotifications: [],
|
|
783
|
+
autoResolvedInputs: []
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function runtimeTaskFailureIds(result) {
|
|
787
|
+
if (result === undefined)
|
|
788
|
+
return new Set();
|
|
789
|
+
return new Set(result.failed.flatMap((failure) => (failure.scope === "task"
|
|
790
|
+
&& typeof failure.taskId === "string"
|
|
791
|
+
&& failure.taskId.length > 0
|
|
792
|
+
? [failure.taskId]
|
|
793
|
+
: [])));
|
|
794
|
+
}
|
|
795
|
+
function mergeControllerSchedulerResults(results) {
|
|
796
|
+
return {
|
|
797
|
+
activeRunDeliveries: results.flatMap((result) => result.activeRunDeliveries),
|
|
798
|
+
failedRunRefs: results.flatMap((result) => result.failedRunRefs),
|
|
799
|
+
wakeups: results.flatMap((result) => result.wakeups),
|
|
800
|
+
inputNotifications: results.flatMap((result) => result.inputNotifications),
|
|
801
|
+
autoResolvedInputs: results.flatMap((result) => result.autoResolvedInputs)
|
|
802
|
+
};
|
|
803
|
+
}
|
|
678
804
|
/**
|
|
679
|
-
* Single-owner periodic runtime for FileTaskStore-backed scheduling.
|
|
680
|
-
*
|
|
681
|
-
*
|
|
805
|
+
* Single-owner periodic runtime for FileTaskStore-backed scheduling. Full pump
|
|
806
|
+
* requests remain exclusive, while dirty work is serialized per Task and may
|
|
807
|
+
* progress concurrently across different Tasks up to the configured bound.
|
|
682
808
|
*/
|
|
683
809
|
export class FileTaskController {
|
|
684
810
|
store;
|
|
@@ -690,6 +816,7 @@ export class FileTaskController {
|
|
|
690
816
|
#deliveryRetryMs;
|
|
691
817
|
#deliveryRetryLimit;
|
|
692
818
|
#taskOrchestrationRetryLimit;
|
|
819
|
+
#taskConcurrency;
|
|
693
820
|
#stallWindowMs;
|
|
694
821
|
#runtimeEventProcessor;
|
|
695
822
|
#runtimeObserver;
|
|
@@ -697,6 +824,9 @@ export class FileTaskController {
|
|
|
697
824
|
#lifecycleHost;
|
|
698
825
|
#deliveryRetryAttempts = new Map();
|
|
699
826
|
#deliveryRetryTimers = new Map();
|
|
827
|
+
#taskPassRetryAttempts = new Map();
|
|
828
|
+
#taskPassRetryTimers = new Map();
|
|
829
|
+
#dirtyTaskQueue;
|
|
700
830
|
#passRetryTimer;
|
|
701
831
|
#passRetryAttempt = 0;
|
|
702
832
|
#timer;
|
|
@@ -735,6 +865,7 @@ export class FileTaskController {
|
|
|
735
865
|
this.#deliveryRetryMs = positiveInteger(options.deliveryRetryMs, DEFAULT_DELIVERY_RETRY_MS, "Controller delivery retry delay");
|
|
736
866
|
this.#deliveryRetryLimit = positiveInteger(options.deliveryRetryLimit, DEFAULT_DELIVERY_RETRY_LIMIT, "Controller delivery retry limit");
|
|
737
867
|
this.#taskOrchestrationRetryLimit = positiveInteger(options.taskOrchestrationRetryLimit, DEFAULT_TASK_ORCHESTRATION_RETRY_LIMIT, "Controller Task orchestration retry limit");
|
|
868
|
+
this.#taskConcurrency = boundedPositiveInteger(options.taskConcurrency, DEFAULT_TASK_CONCURRENCY, MAX_TASK_CONCURRENCY, "Controller Task concurrency");
|
|
738
869
|
this.#stallWindowMs = positiveInteger(options.stallWindowMs, DEFAULT_STALL_WINDOW_MS, "Controller Run stall window");
|
|
739
870
|
this.#runtimeEventProcessor = options.runtimeEventProcessor;
|
|
740
871
|
this.#runtimeObserver = options.runtimeObserver;
|
|
@@ -862,6 +993,8 @@ export class FileTaskController {
|
|
|
862
993
|
clearTimeout(timer);
|
|
863
994
|
this.#deliveryRetryTimers.clear();
|
|
864
995
|
this.#deliveryRetryAttempts.clear();
|
|
996
|
+
this.#clearAllTaskPassRetries();
|
|
997
|
+
void this.#dirtyTaskQueue?.abortPending();
|
|
865
998
|
if (this.#passRetryTimer !== undefined) {
|
|
866
999
|
clearTimeout(this.#passRetryTimer);
|
|
867
1000
|
this.#passRetryTimer = undefined;
|
|
@@ -924,6 +1057,8 @@ export class FileTaskController {
|
|
|
924
1057
|
if (scope.kind === "full") {
|
|
925
1058
|
this.#pendingFull = true;
|
|
926
1059
|
this.#pendingKeys.clear();
|
|
1060
|
+
this.#clearAllTaskPassRetries();
|
|
1061
|
+
void this.#dirtyTaskQueue?.abortPending();
|
|
927
1062
|
}
|
|
928
1063
|
else if (!this.#pendingFull) {
|
|
929
1064
|
for (const key of scope.keys)
|
|
@@ -944,13 +1079,7 @@ export class FileTaskController {
|
|
|
944
1079
|
void this.pump().catch(this.#onError);
|
|
945
1080
|
}
|
|
946
1081
|
async #runCoalesced() {
|
|
947
|
-
let result =
|
|
948
|
-
activeRunDeliveries: [],
|
|
949
|
-
failedRunRefs: [],
|
|
950
|
-
wakeups: [],
|
|
951
|
-
inputNotifications: [],
|
|
952
|
-
autoResolvedInputs: []
|
|
953
|
-
};
|
|
1082
|
+
let result = emptyControllerSchedulerResult();
|
|
954
1083
|
let pendingRuntimeDrain = false;
|
|
955
1084
|
try {
|
|
956
1085
|
while (this.#pendingFull || this.#pendingKeys.size > 0 || pendingRuntimeDrain) {
|
|
@@ -958,6 +1087,8 @@ export class FileTaskController {
|
|
|
958
1087
|
? { kind: "full" }
|
|
959
1088
|
: { kind: "dirty", keys: [...this.#pendingKeys] };
|
|
960
1089
|
const runtimeCleanupOutcomes = [];
|
|
1090
|
+
const runtimeFailedTaskIds = new Set();
|
|
1091
|
+
let failedTaskScopes = [];
|
|
961
1092
|
pendingRuntimeDrain = false;
|
|
962
1093
|
this.#pendingFull = false;
|
|
963
1094
|
this.#pendingKeys.clear();
|
|
@@ -982,12 +1113,25 @@ export class FileTaskController {
|
|
|
982
1113
|
}
|
|
983
1114
|
}
|
|
984
1115
|
const firstRuntimeDrain = await this.#drainRuntimeEvents();
|
|
1116
|
+
for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
|
|
1117
|
+
runtimeFailedTaskIds.add(taskId);
|
|
1118
|
+
}
|
|
985
1119
|
// DurableJob reconciliation runs before the scheduler pass so a
|
|
986
1120
|
// terminal job's Leader wakeup is enqueued in the same pass that
|
|
987
1121
|
// processes Leader wakeups.
|
|
988
1122
|
this.#jobSupervisor?.reconcile(this.#now());
|
|
989
|
-
|
|
1123
|
+
if (scope.kind === "full") {
|
|
1124
|
+
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds);
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
const dirtyPass = await this.#runDirtySchedulerPass(scope, runtimeCleanupOutcomes, runtimeFailedTaskIds);
|
|
1128
|
+
result = dirtyPass.result;
|
|
1129
|
+
failedTaskScopes = dirtyPass.failedTaskScopes;
|
|
1130
|
+
}
|
|
990
1131
|
const secondRuntimeDrain = await this.#drainRuntimeEvents();
|
|
1132
|
+
for (const taskId of runtimeTaskFailureIds(secondRuntimeDrain)) {
|
|
1133
|
+
runtimeFailedTaskIds.add(taskId);
|
|
1134
|
+
}
|
|
991
1135
|
pendingRuntimeDrain = ((secondRuntimeDrain?.remainingEventCount ?? 0) > 0
|
|
992
1136
|
&& ((firstRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0
|
|
993
1137
|
|| (secondRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0));
|
|
@@ -1009,9 +1153,34 @@ export class FileTaskController {
|
|
|
1009
1153
|
}
|
|
1010
1154
|
}
|
|
1011
1155
|
this.#clearPassRetry();
|
|
1156
|
+
if (scope.kind === "full")
|
|
1157
|
+
this.#clearAllTaskPassRetries();
|
|
1012
1158
|
this.#scheduleRuntimeCleanupRetries(runtimeCleanupOutcomes);
|
|
1013
1159
|
this.#scheduleDeliveryRetries(result);
|
|
1014
|
-
|
|
1160
|
+
const failedTaskIds = new Set([
|
|
1161
|
+
...failedTaskScopes.map((failedScope) => failedScope.taskId),
|
|
1162
|
+
...runtimeFailedTaskIds
|
|
1163
|
+
]);
|
|
1164
|
+
this.#scheduleTaskMailboxRetries(scope.kind === "dirty" && failedTaskIds.size > 0
|
|
1165
|
+
? {
|
|
1166
|
+
kind: "dirty",
|
|
1167
|
+
keys: scope.keys.filter((key) => {
|
|
1168
|
+
const target = parseMailboxKey(key);
|
|
1169
|
+
return (target.kind === "operator"
|
|
1170
|
+
|| target.kind === "global-role"
|
|
1171
|
+
|| !failedTaskIds.has(target.taskId));
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
: scope);
|
|
1175
|
+
for (const failedScope of failedTaskScopes) {
|
|
1176
|
+
this.#clearDeliveryRetry(`task:${encodeURIComponent(failedScope.taskId)}`);
|
|
1177
|
+
this.#scheduleTaskPassRetry(failedScope);
|
|
1178
|
+
}
|
|
1179
|
+
for (const taskId of runtimeFailedTaskIds) {
|
|
1180
|
+
const key = `task:${encodeURIComponent(taskId)}`;
|
|
1181
|
+
this.#clearDeliveryRetry(key);
|
|
1182
|
+
this.#scheduleTaskPassRetry({ taskId, keys: [key] });
|
|
1183
|
+
}
|
|
1015
1184
|
if (scope.kind === "full"
|
|
1016
1185
|
|| scope.keys.some((key) => key !== "operator")
|
|
1017
1186
|
|| (firstRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0
|
|
@@ -1045,6 +1214,87 @@ export class FileTaskController {
|
|
|
1045
1214
|
this.#scheduleNextInputDeadline();
|
|
1046
1215
|
}
|
|
1047
1216
|
}
|
|
1217
|
+
async #runDirtySchedulerPass(scope, runtimeCleanupOutcomes, blockedTaskIds = new Set()) {
|
|
1218
|
+
const partition = partitionDirtyScope(scope.keys);
|
|
1219
|
+
const taskScopes = partition.taskScopes.filter((taskScope) => (!blockedTaskIds.has(taskScope.taskId)));
|
|
1220
|
+
const orderedResults = [];
|
|
1221
|
+
if (partition.globalKeys.length > 0) {
|
|
1222
|
+
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds));
|
|
1223
|
+
}
|
|
1224
|
+
if (taskScopes.length === 0
|
|
1225
|
+
|| this.#stopped
|
|
1226
|
+
|| this.#pendingFull) {
|
|
1227
|
+
return {
|
|
1228
|
+
result: mergeControllerSchedulerResults(orderedResults),
|
|
1229
|
+
failedTaskScopes: []
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
const queue = new KeyedWorkQueue();
|
|
1233
|
+
this.#dirtyTaskQueue = queue;
|
|
1234
|
+
const scopesByTask = new Map(taskScopes.map((taskScope, index) => [
|
|
1235
|
+
taskScope.taskId,
|
|
1236
|
+
{ taskScope, index }
|
|
1237
|
+
]));
|
|
1238
|
+
const taskResults = Array(taskScopes.length);
|
|
1239
|
+
const taskCleanupOutcomes = Array.from({ length: taskScopes.length }, () => []);
|
|
1240
|
+
const failedTaskScopes = Array(taskScopes.length);
|
|
1241
|
+
const consume = async () => {
|
|
1242
|
+
while (true) {
|
|
1243
|
+
const item = await queue.take();
|
|
1244
|
+
if (item === undefined)
|
|
1245
|
+
return;
|
|
1246
|
+
const selected = scopesByTask.get(item.key);
|
|
1247
|
+
if (selected === undefined) {
|
|
1248
|
+
item.done();
|
|
1249
|
+
throw new Error(`Dirty Task queue returned an unknown Task: ${item.key}.`);
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
if (this.#stopped || this.#pendingFull)
|
|
1253
|
+
continue;
|
|
1254
|
+
try {
|
|
1255
|
+
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds);
|
|
1256
|
+
this.#clearTaskPassRetry(selected.taskScope.taskId);
|
|
1257
|
+
}
|
|
1258
|
+
catch (error) {
|
|
1259
|
+
failedTaskScopes[selected.index] = selected.taskScope;
|
|
1260
|
+
this.#onError(error);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
finally {
|
|
1264
|
+
item.done();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
const consumers = Array.from({
|
|
1269
|
+
length: Math.min(this.#taskConcurrency, taskScopes.length)
|
|
1270
|
+
}, () => consume());
|
|
1271
|
+
for (const taskScope of taskScopes)
|
|
1272
|
+
queue.signal(taskScope.taskId);
|
|
1273
|
+
const stopped = queue.shutdown();
|
|
1274
|
+
if (this.#stopped || this.#pendingFull)
|
|
1275
|
+
void queue.abortPending();
|
|
1276
|
+
try {
|
|
1277
|
+
await Promise.all(consumers);
|
|
1278
|
+
await stopped;
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
await queue.abortPending();
|
|
1282
|
+
await Promise.allSettled(consumers);
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
finally {
|
|
1286
|
+
if (this.#dirtyTaskQueue === queue)
|
|
1287
|
+
this.#dirtyTaskQueue = undefined;
|
|
1288
|
+
}
|
|
1289
|
+
for (const outcomes of taskCleanupOutcomes) {
|
|
1290
|
+
runtimeCleanupOutcomes.push(...outcomes);
|
|
1291
|
+
}
|
|
1292
|
+
orderedResults.push(...taskResults.filter((taskResult) => taskResult !== undefined));
|
|
1293
|
+
return {
|
|
1294
|
+
result: mergeControllerSchedulerResults(orderedResults),
|
|
1295
|
+
failedTaskScopes: failedTaskScopes.filter((failedScope) => failedScope !== undefined)
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1048
1298
|
async #runOperatorPass() {
|
|
1049
1299
|
const result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), undefined, { kind: "dirty", keys: ["operator"] });
|
|
1050
1300
|
if (this.#operatorStartupRetryArmed && result.inputNotifications.length === 0) {
|
|
@@ -1087,8 +1337,16 @@ export class FileTaskController {
|
|
|
1087
1337
|
this.#runtimeSelectedEvents += metrics.selectedEventCount;
|
|
1088
1338
|
this.#runtimeProgressEventsCoalesced += metrics.progressEventsCoalesced;
|
|
1089
1339
|
this.#runtimeStateTransactions += metrics.stateTransactions;
|
|
1090
|
-
|
|
1091
|
-
|
|
1340
|
+
const taskFailures = result.failed.filter((failure) => (failure.scope === "task"
|
|
1341
|
+
&& typeof failure.taskId === "string"
|
|
1342
|
+
&& failure.taskId.length > 0));
|
|
1343
|
+
const invalidFailures = result.failed.filter((failure) => !(failure.scope === "task"
|
|
1344
|
+
&& typeof failure.taskId === "string"
|
|
1345
|
+
&& failure.taskId.length > 0));
|
|
1346
|
+
for (const failure of taskFailures)
|
|
1347
|
+
this.#onError(failure.error);
|
|
1348
|
+
if (invalidFailures.length > 0) {
|
|
1349
|
+
throw new RuntimeEventApplyError(invalidFailures.map((failure) => failure.error), "One or more native Turn events could not be applied.");
|
|
1092
1350
|
}
|
|
1093
1351
|
return result;
|
|
1094
1352
|
}
|
|
@@ -1113,6 +1371,63 @@ export class FileTaskController {
|
|
|
1113
1371
|
this.#passRetryTimer = undefined;
|
|
1114
1372
|
this.#passRetryAttempt = 0;
|
|
1115
1373
|
}
|
|
1374
|
+
#scheduleTaskPassRetry(scope) {
|
|
1375
|
+
if (this.#stopped || this.#pendingFull)
|
|
1376
|
+
return;
|
|
1377
|
+
if (this.store.getTask(scope.taskId)?.status !== "active") {
|
|
1378
|
+
this.#clearTaskPassRetry(scope.taskId);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
const identity = JSON.stringify([...scope.keys].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true }))));
|
|
1382
|
+
let previous = this.#taskPassRetryAttempts.get(scope.taskId);
|
|
1383
|
+
if (previous !== undefined && previous.identity !== identity) {
|
|
1384
|
+
this.#clearTaskPassRetry(scope.taskId);
|
|
1385
|
+
previous = undefined;
|
|
1386
|
+
}
|
|
1387
|
+
if (this.#taskPassRetryTimers.has(scope.taskId))
|
|
1388
|
+
return;
|
|
1389
|
+
const attempts = previous?.attempts ?? 0;
|
|
1390
|
+
if (attempts >= this.#taskOrchestrationRetryLimit) {
|
|
1391
|
+
this.#clearTaskPassRetry(scope.taskId);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
this.#taskPassRetryAttempts.set(scope.taskId, {
|
|
1395
|
+
identity,
|
|
1396
|
+
attempts: attempts + 1
|
|
1397
|
+
});
|
|
1398
|
+
const delayMs = Math.min(2_000, this.#deliveryRetryMs * (2 ** Math.min(attempts, 3)));
|
|
1399
|
+
const timer = setTimeout(() => {
|
|
1400
|
+
this.#taskPassRetryTimers.delete(scope.taskId);
|
|
1401
|
+
if (this.#stopped || this.#pendingFull)
|
|
1402
|
+
return;
|
|
1403
|
+
try {
|
|
1404
|
+
if (this.store.getTask(scope.taskId)?.status !== "active") {
|
|
1405
|
+
this.#clearTaskPassRetry(scope.taskId);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
void this.#requestPass({ kind: "dirty", keys: scope.keys }).catch(this.#onError);
|
|
1409
|
+
}
|
|
1410
|
+
catch (error) {
|
|
1411
|
+
this.#clearTaskPassRetry(scope.taskId);
|
|
1412
|
+
this.#onError(error);
|
|
1413
|
+
}
|
|
1414
|
+
}, delayMs);
|
|
1415
|
+
timer.unref();
|
|
1416
|
+
this.#taskPassRetryTimers.set(scope.taskId, timer);
|
|
1417
|
+
}
|
|
1418
|
+
#clearTaskPassRetry(taskId) {
|
|
1419
|
+
const timer = this.#taskPassRetryTimers.get(taskId);
|
|
1420
|
+
if (timer !== undefined)
|
|
1421
|
+
clearTimeout(timer);
|
|
1422
|
+
this.#taskPassRetryTimers.delete(taskId);
|
|
1423
|
+
this.#taskPassRetryAttempts.delete(taskId);
|
|
1424
|
+
}
|
|
1425
|
+
#clearAllTaskPassRetries() {
|
|
1426
|
+
for (const timer of this.#taskPassRetryTimers.values())
|
|
1427
|
+
clearTimeout(timer);
|
|
1428
|
+
this.#taskPassRetryTimers.clear();
|
|
1429
|
+
this.#taskPassRetryAttempts.clear();
|
|
1430
|
+
}
|
|
1116
1431
|
#scheduleNextInputDeadline() {
|
|
1117
1432
|
if (this.#deadlineTimer !== undefined) {
|
|
1118
1433
|
clearTimeout(this.#deadlineTimer);
|
|
@@ -1247,7 +1562,7 @@ export class FileTaskController {
|
|
|
1247
1562
|
#scheduleTaskMailboxRetries(scope) {
|
|
1248
1563
|
const selection = compileReconcileSelection(scope);
|
|
1249
1564
|
const targets = selection.full
|
|
1250
|
-
? this.store
|
|
1565
|
+
? selectedReadyWorkMailboxes(this.store).flatMap((mailbox) => (mailbox.target.kind === "task" ? [mailbox.target] : []))
|
|
1251
1566
|
: [...selection.allRoleTaskIds].map((taskId) => ({ kind: "task", taskId }));
|
|
1252
1567
|
for (const target of targets) {
|
|
1253
1568
|
const key = `task:${encodeURIComponent(target.taskId)}`;
|
|
@@ -1563,6 +1878,13 @@ function positiveInteger(value, fallback, label) {
|
|
|
1563
1878
|
}
|
|
1564
1879
|
return resolved;
|
|
1565
1880
|
}
|
|
1881
|
+
function boundedPositiveInteger(value, fallback, maximum, label) {
|
|
1882
|
+
const resolved = positiveInteger(value, fallback, label);
|
|
1883
|
+
if (resolved > maximum) {
|
|
1884
|
+
throw new TypeError(`${label} must be at most ${maximum}`);
|
|
1885
|
+
}
|
|
1886
|
+
return resolved;
|
|
1887
|
+
}
|
|
1566
1888
|
function operatorMailboxBatchIdentity(mailbox) {
|
|
1567
1889
|
const batch = mailbox?.pending ?? mailbox?.processing?.batch;
|
|
1568
1890
|
if (batch === null || batch === undefined)
|