Skip to content

Commit 0b9a7e7

Browse files
feat(ado-scheduler): timestamped, change-only logging with heartbeat
The poll loop re-logged every session's placement decision each pass (~every 5s), flooding the log with identical "skip … colocate (lease ok)" lines. Now: - every line is prefixed with an ISO-8601 timestamp; - a session's decision is logged only when it CHANGES vs the prior pass; - a periodic heartbeat (SCHED_HEARTBEAT_MS, default 60s) re-logs the full steady state so liveness stays visible; - SCHED_VERBOSE=1 restores per-pass logging of every decision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26f310eb-4680-4102-a8aa-f8d041940ab4
1 parent 8eb63b4 commit 0b9a7e7

1 file changed

Lines changed: 39 additions & 5 deletions

File tree

packages/sdk/examples/ado_scheduler.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,21 @@ const cfg = {
126126
onlySessionId: process.env.SCHED_SESSION_ID || null,
127127
};
128128

129-
const log = (msg) => console.log(`[ado-scheduler] ${msg}`);
130-
const errlog = (msg) => console.error(`[ado-scheduler] ${msg}`);
129+
const iso = () => new Date().toISOString();
130+
const log = (msg) => console.log(`${iso()} [ado-scheduler] ${msg}`);
131+
const errlog = (msg) => console.error(`${iso()} [ado-scheduler] ${msg}`);
132+
133+
// ─── steady-state log suppression ────────────────────────────────────────────
134+
// The poll loop re-evaluates every session each pass (~every pollMs). Logging
135+
// every session's placement decision each pass floods the log with identical
136+
// "skip … colocate (lease ok)" lines. Instead we log a session's decision only
137+
// when it CHANGES, and emit a periodic heartbeat so liveness is still visible.
138+
// Set SCHED_VERBOSE=1 to restore per-pass logging of every decision.
139+
const VERBOSE = truthy(process.env.SCHED_VERBOSE);
140+
const HEARTBEAT_MS = intEnv("SCHED_HEARTBEAT_MS", 60000);
141+
const lastDecisionSig = new Map(); // sessionId -> "action:reason"
142+
let lastObservedSig = null;
143+
let lastHeartbeatMs = 0;
131144

132145
// ─── credentials ─────────────────────────────────────────────────────────────
133146
const credential = new DefaultAzureCredential();
@@ -288,15 +301,35 @@ async function pass(mgmt, pool) {
288301
!s.parentSessionId && !s.isSystem && s.status === cfg.targetStatus);
289302
if (cfg.onlySessionId) candidates = candidates.filter((s) => s.sessionId === cfg.onlySessionId);
290303

291-
log(`observed ${sessions.length} sessions; ${candidates.length} runnable root candidate(s)` +
292-
(cfg.onlySessionId ? ` (filtered to ${cfg.onlySessionId})` : ""));
304+
// Heartbeat: force a full re-log of the observed line + every decision on a
305+
// fixed cadence so the log periodically shows the current steady state.
306+
const heartbeat = VERBOSE || (nowMs - lastHeartbeatMs >= HEARTBEAT_MS);
307+
if (heartbeat) lastHeartbeatMs = nowMs;
308+
309+
// Prune decision signatures for sessions no longer observed as candidates so
310+
// a re-appearing session logs its decision afresh (and the map stays bounded).
311+
const candidateIds = new Set(candidates.map((s) => s.sessionId));
312+
for (const id of lastDecisionSig.keys()) {
313+
if (!candidateIds.has(id)) lastDecisionSig.delete(id);
314+
}
315+
316+
const observedSig = `${sessions.length}/${candidates.length}`;
317+
if (heartbeat || observedSig !== lastObservedSig) {
318+
log(`observed ${sessions.length} sessions; ${candidates.length} runnable root candidate(s)` +
319+
(cfg.onlySessionId ? ` (filtered to ${cfg.onlySessionId})` : ""));
320+
lastObservedSig = observedSig;
321+
}
293322

294323
let queued = 0;
295324
for (const s of candidates) {
296325
const affinity = await getAffinity(pool, s.sessionId);
297326
const decision = decidePlacement(s, affinity, nowMs);
298327
if (decision.action !== "queue-ado-run") {
299-
log(` skip ${s.sessionId}${decision.action}: ${decision.reason}`);
328+
const sig = `${decision.action}:${decision.reason}`;
329+
if (VERBOSE || heartbeat || lastDecisionSig.get(s.sessionId) !== sig) {
330+
log(` skip ${s.sessionId}${decision.action}: ${decision.reason}`);
331+
}
332+
lastDecisionSig.set(s.sessionId, sig);
300333
continue;
301334
}
302335
if (queued >= cfg.maxQueuePerPass) {
@@ -334,6 +367,7 @@ try {
334367
log(`ADO: org=${cfg.adoOrg} project="${cfg.adoProject}" pipeline=${cfg.pipelineId} ref=${cfg.branchRef} pilotswarmRef=${cfg.pilotswarmRef}`);
335368
log(`store: ${cfg.host}/${cfg.database} user=${cfg.user} mi=${cfg.useMi} cmsSchema=${cfg.cmsSchema} affinitySchema=${cfg.affinitySchema}`);
336369
log(`loop: pollMs=${cfg.pollMs} once=${cfg.once} dryRun=${cfg.dryRun} maxQueue/pass=${cfg.maxQueuePerPass} targetStatus=${cfg.targetStatus} retryOnLeaseExpiry=${cfg.retryOnLeaseExpiry}`);
370+
log(`logging: verbose=${VERBOSE} heartbeatMs=${HEARTBEAT_MS} (change-only per-session logging; set SCHED_VERBOSE=1 for per-pass)`);
337371

338372
// Load the ADO PAT from Key Vault (workload identity) before any queue call.
339373
await loadAdoPatFromKeyVault();

0 commit comments

Comments
 (0)