mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
chore(release): v1.4.37.1 — event-loop unblock on the read path
Hotfix on top of v1.4.37. ensureUserRollupsFresh fire-and-forget on /api/analytics, comprehensive aggregator, summaries slice. The synchronous variant was blocking the Node event loop for ~60 s per cache miss on power-user accounts whose iOS step samples kept the 90-day rollup window slightly stale, which starved /api/health, /api/version and concurrent iOS requests. The downstream coverage probe preserves correctness via the live-SQL fallback; the only user-observable change is one cache window of staleness on the very first request after a new measurement lands.
This commit is contained in:
1 parent
c4f8e0c8af
commit
374c1d3b06
5 files changed
+67
-20
No files matched your search
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## [1.4.37.1] — 2026-05-17 — Event-loop unblock on the read path
|
||||
|
||||
Hotfix on top of v1.4.37. Post-deploy verification surfaced live
|
||||
`/api/analytics` cold-mount hits of ~62 s and parallel
|
||||
`/api/health` checks of ~58 s on the live instance — far above the
|
||||
v1.4.37 perf claim of 1.5–3 s. Root cause: the synchronous
|
||||
`ensureUserRollupsFresh(userId)` call at the top of each heavy
|
||||
read path (analytics route, comprehensive aggregator, summaries
|
||||
slice) folds the trailing 90-day DAY window when the rollup
|
||||
watermark trails the newest measurement. On an account with
|
||||
high-frequency Apple Health step ingest the watermark falls behind
|
||||
every few minutes, so each cache miss synchronously paid the full
|
||||
refresh cost on a Node.js worker that also serves the rest of the
|
||||
request fan-out — including `/api/health`, `/api/version`, and
|
||||
concurrent iOS calls — and the event loop was blocked for the
|
||||
duration of the recompute.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `ensureUserRollupsFresh` is now fired-and-forgotten on all three
|
||||
read-path call sites (`src/app/api/analytics/route.ts`,
|
||||
`src/lib/insights/comprehensive-aggregator.ts`,
|
||||
`src/lib/analytics/summaries-slice.ts`). The function already
|
||||
carries its own try/catch envelope, so the `void`-call cannot
|
||||
reject. Correctness is preserved by the downstream
|
||||
`probeRollupCoverage` check: when a type is partial the live
|
||||
fallback fires; the only user-observable change is that the very
|
||||
first request after a fresh measurement landed may serve data up
|
||||
to ~60 s old, and the next request returns the up-to-date value
|
||||
once the background refresh completes.
|
||||
|
||||
### Operator notes
|
||||
|
||||
- No schema change. No env-var change.
|
||||
- Coolify auto-deploys main on tag push; first webhook may pull
|
||||
stale `:latest`, redeploy after the docker-publish workflow
|
||||
completes.
|
||||
|
||||
## [1.4.37] — 2026-05-17 — Final web polish before iOS focus
|
||||
|
||||
The closing web release before the v1.5 iOS-app sprint. Carries the
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.4.37",
|
||||
"version": "1.4.37.1",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
@@ -112,14 +112,18 @@ type AuthedUser = Awaited<ReturnType<typeof requireAuth>>["user"];
|
||||
* `buildCoachSnapshotImpl` shape.
|
||||
*/
|
||||
async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
// v1.5.0 — warm the persistent rollup table as a side effect of
|
||||
// the analytics fan-out. No-op when rollups are already ahead of
|
||||
// the newest measurement; on first cold-mount after a process
|
||||
// restart it folds the trailing 90-day window so downstream
|
||||
// consumers (Coach drawer, weekly report, admin analytics) see a
|
||||
// warm rollup table on their next read. The route's response
|
||||
// shape is untouched.
|
||||
await ensureUserRollupsFresh(user.id);
|
||||
// v1.4.37.1 hotfix — fire-and-forget the rollup-fresh check.
|
||||
// Awaiting it on the read path can stall the Node event loop for
|
||||
// 30–60 s on a power-user account whose iOS step samples keep the
|
||||
// 90-day DAY window slightly stale, which starves /api/health,
|
||||
// /api/version, and concurrent iOS calls. The downstream coverage
|
||||
// probe falls back to live SQL when a type is uncovered, so
|
||||
// correctness is preserved; the worst the user sees is data from
|
||||
// up to ~60 s ago on the very first request after a fresh
|
||||
// measurement lands, and the next request returns the up-to-date
|
||||
// value once the background refresh completes. `ensureUserRollupsFresh`
|
||||
// wraps its own try/catch, so the void call cannot reject.
|
||||
void ensureUserRollupsFresh(user.id);
|
||||
|
||||
// v1.4.37 W2 — single per-type coverage probe shared by the
|
||||
// bp_in_target / healthScore / correlations branches below. The
|
||||
|
||||
@@ -179,11 +179,14 @@ export interface SummariesSlice {
|
||||
export async function computeSummariesSlice(
|
||||
userId: string,
|
||||
): Promise<SummariesSlice> {
|
||||
// Keep the persistent rollup table current before the bucket read
|
||||
// fires. On a warm process this is a single watermark query; on a
|
||||
// cold mount it folds the trailing 90-day window into the DAY
|
||||
// rollup so the bucket read below has something to compose.
|
||||
await ensureUserRollupsFresh(userId);
|
||||
// v1.4.37.1 hotfix — fire-and-forget. See `src/app/api/analytics/route.ts`
|
||||
// for the full rationale: awaiting this on the read path can stall
|
||||
// the event loop for 30–60 s on power-user accounts whose iOS step
|
||||
// samples keep the 90-day window slightly stale. The downstream
|
||||
// coverage probe falls back to live SQL for uncovered types, so
|
||||
// correctness is preserved; the read just doesn't block waiting
|
||||
// for the background refresh.
|
||||
void ensureUserRollupsFresh(userId);
|
||||
|
||||
// v1.4.36 QA C1 — per-type coverage probe replaces the legacy global
|
||||
// COUNT. The previous gate returned true as soon as ANY type had at
|
||||
|
||||
@@ -233,12 +233,14 @@ export async function buildComprehensiveAggregate(
|
||||
): Promise<ComprehensiveAggregate> {
|
||||
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Keep the persistent rollup table current before any read fires.
|
||||
// After this returns the DAY-granularity buckets for the trailing
|
||||
// 90 days reflect every measurement that landed before this call
|
||||
// (the per-write hook is synchronous on the DAY bucket, and
|
||||
// `ensureUserRollupsFresh` covers cold-mount / process-restart).
|
||||
await ensureUserRollupsFresh(userId);
|
||||
// v1.4.37.1 hotfix — fire-and-forget. See `src/app/api/analytics/route.ts`
|
||||
// for the full rationale: awaiting this on the read path can stall
|
||||
// the event loop for 30–60 s on power-user accounts whose iOS step
|
||||
// samples keep the 90-day window slightly stale. The downstream
|
||||
// coverage probe falls back to live SQL for uncovered types, so
|
||||
// correctness is preserved; the read just doesn't block waiting
|
||||
// for the background refresh.
|
||||
void ensureUserRollupsFresh(userId);
|
||||
|
||||
// v1.4.36 QA C1 — per-type coverage probe. The legacy global COUNT
|
||||
// returned true as soon as any one type had a DAY bucket, which made
|
||||
|
||||
Reference in new issue
Block a user