mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Migration 0434 turns the route opt-in on for every account that already holds a stored workout track, so its tracks keep arriving after the upgrade; only accounts without one start off. Workouts stored before this release with a track but no elevation get their metres climbed written into Workout.elevationM by a one-time background pass. It runs detached at worker boot, reads one track at a time with a pause between batches of 100, and keeps its position in app_settings, so a restart resumes and a finished pass ends at once. The "starting" health reason is bounded: after five minutes the probe says plain degraded and the process logs one line, and the time the background role took to come up is logged either way. The compose healthcheck's ten-minute start period already keeps those probes from counting; its comment now says so.
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
/**
|
||
* Where this process is in bringing up its background role.
|
||
*
|
||
* The pg-boss worker (or, in a web-only process, the send-only producer)
|
||
* starts after the HTTP server is already serving: starting it inside
|
||
* `register()` held every request, `/api/health` included, for the seconds
|
||
* pg-boss needs to start, provision its queues and run the boot discovery.
|
||
* The probe reads this to say `starting` while that is under way rather than
|
||
* hanging, and the operator sees a 503 that names a transient state.
|
||
*
|
||
* The window is bounded. A start that has not finished after
|
||
* `STARTING_MAX_MS` is no longer reported as `starting`: it reads `stalled`,
|
||
* the probe drops the reason and says plainly `degraded`, and the process
|
||
* logs one line saying so. An uptime monitor therefore never sees the
|
||
* "transient" label for longer than a real boot takes.
|
||
*
|
||
* `null` means no background role was started in this process at all.
|
||
*/
|
||
export type StartupPhase = "starting" | "ready" | "failed" | "stalled";
|
||
|
||
/** Longest a start is still called `starting`. Prod boots take 10–60 s. */
|
||
export const STARTING_MAX_MS = 5 * 60_000;
|
||
|
||
const KEY = Symbol.for("healthlog.boot.startupState");
|
||
|
||
interface State {
|
||
phase: Exclude<StartupPhase, "stalled">;
|
||
since: number;
|
||
}
|
||
|
||
function state(): State | undefined {
|
||
return (globalThis as Record<symbol, unknown>)[KEY] as State | undefined;
|
||
}
|
||
|
||
export function markStartup(
|
||
phase: Exclude<StartupPhase, "stalled">,
|
||
now: number = Date.now(),
|
||
): void {
|
||
(globalThis as Record<symbol, unknown>)[KEY] = { phase, since: now };
|
||
}
|
||
|
||
export function startupPhase(now: number = Date.now()): StartupPhase | null {
|
||
const current = state();
|
||
if (!current) return null;
|
||
if (current.phase === "starting" && now - current.since > STARTING_MAX_MS) {
|
||
return "stalled";
|
||
}
|
||
return current.phase;
|
||
}
|
||
|
||
/** Milliseconds since the current phase began, or null before any start. */
|
||
export function startupElapsedMs(now: number = Date.now()): number | null {
|
||
const current = state();
|
||
return current ? now - current.since : null;
|
||
}
|