Merge pull request #246 from MBombeck/fix/disable-dense-retention

v1.10.0.2 — disable intra-day retention drain (P2002 fold collision)
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-06-03 03:45:37 +02:00
commit e1ea0f6645
5 files changed
+52 -13

No files matched your search

+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [1.10.0.2] — 2026-06-03 — Disable the intra-day retention drain
### Fixed
- **Stops the post-deploy restart loop for good.** The intra-day retention drain (a storage optimisation behind the new stress reading) folded older samples into a daily summary, but on real data that summary collided with an existing daily row and the drain failed on every run, which kept the app cycling after a deploy. The drain is now switched off until it is reworked and tested against real data; the nightly pass is skipped, nothing is queued at start-up, and any work already queued completes harmlessly. No user-facing feature is lost — the stress reading simply falls back to its sparse-data behaviour.
## [1.10.0.1] — 2026-06-03 — Defer the intra-day retention drain past startup
### Fixed
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.10.0.1
version: 1.10.0.2
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.10.0.1",
"version": "1.10.0.2",
"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",
+25
View File
@@ -61,6 +61,22 @@ export const DENSE_INTRADAY_RETENTION_CRON = "50 3 * * *";
*/
export const DENSE_INTRADAY_RETENTION_BOOT_DELAY_SECONDS = 600;
/**
* Kill-switch, default OFF. The drain folds out-of-window per-sample rows
* into one daily-mean `stats:` row, but its canonical-timestamp upsert
* collides with an already-present daily row on the
* `(user_id, type, measured_at, source, sleep_stage)` unique constraint
* (`P2002`) on real data — a fold-coexistence bug the mocked unit tests
* could not surface. Until the fold is reworked + integration-tested
* against real rows, the drain is disabled: the boot-discovery enqueues
* nothing, the nightly walk is skipped, and the per-user handler no-ops so
* any already-queued backlog completes harmlessly instead of erroring.
* Re-enable with `DENSE_INTRADAY_RETENTION_ENABLED=true` once fixed. The
* Stress proxy degrades gracefully on sparse intra-day data meanwhile.
*/
export const DENSE_INTRADAY_RETENTION_ENABLED =
process.env.DENSE_INTRADAY_RETENTION_ENABLED === "true";
export interface DenseIntradayRetentionPayload {
userId: string;
enqueuedAt: string;
@@ -73,6 +89,11 @@ export interface DenseIntradayRetentionPayload {
export async function runDenseIntradayRetentionForUser(
userId: string,
): Promise<{ daysConsolidated: number; perSampleRowsSoftDeleted: number }> {
// Kill-switch: no-op so any already-queued backlog completes cleanly
// (drains the queue) instead of erroring on the P2002 fold collision.
if (!DENSE_INTRADAY_RETENTION_ENABLED) {
return { daysConsolidated: 0, perSampleRowsSoftDeleted: 0 };
}
const summary = await runDenseIntradayRetention(prisma, {
userId,
log: () => {
@@ -114,6 +135,10 @@ export async function enqueueBootTimeDenseIntradayRetention(): Promise<{
skipped: number;
error: string | null;
}> {
// Kill-switch: enqueue nothing while the drain is disabled.
if (!DENSE_INTRADAY_RETENTION_ENABLED) {
return { enqueued: 0, skipped: 0, error: null };
}
const boss = getGlobalBoss();
if (!boss) {
return { enqueued: 0, skipped: 0, error: null };
+19 -11
View File
@@ -88,6 +88,7 @@ import {
DENSE_INTRADAY_RETENTION_CONCURRENCY,
runDenseIntradayRetentionForUser,
enqueueBootTimeDenseIntradayRetention,
DENSE_INTRADAY_RETENTION_ENABLED,
type DenseIntradayRetentionPayload,
} from "@/lib/jobs/dense-intraday-retention";
import { runDenseIntradayRetention } from "@/lib/measurements/dense-intraday-retention";
@@ -2820,17 +2821,24 @@ export async function startReminderWorker() {
// accounts that accumulated out-of-window raw rows before this
// shipped.
try {
const denseSummary = await runDenseIntradayRetention(
getWorkerPrisma(),
{
dryRun: false,
log: (line) => workerLog("info", line),
},
);
workerLog(
"info",
`[dense-intraday-retention] triggeredAt=${job.data.triggeredAt} usersScanned=${denseSummary.totals.usersScanned} daysConsolidated=${denseSummary.totals.daysConsolidated} perSampleRowsSoftDeleted=${denseSummary.totals.perSampleRowsSoftDeleted} dailyRowsUpserted=${denseSummary.totals.dailyRowsUpserted}`,
);
if (!DENSE_INTRADAY_RETENTION_ENABLED) {
workerLog(
"info",
"[dense-intraday-retention] disabled — skipping the nightly walk (P2002 fold collision; re-enable via DENSE_INTRADAY_RETENTION_ENABLED once reworked)",
);
} else {
const denseSummary = await runDenseIntradayRetention(
getWorkerPrisma(),
{
dryRun: false,
log: (line) => workerLog("info", line),
},
);
workerLog(
"info",
`[dense-intraday-retention] triggeredAt=${job.data.triggeredAt} usersScanned=${denseSummary.totals.usersScanned} daysConsolidated=${denseSummary.totals.daysConsolidated} perSampleRowsSoftDeleted=${denseSummary.totals.perSampleRowsSoftDeleted} dailyRowsUpserted=${denseSummary.totals.dailyRowsUpserted}`,
);
}
} catch (err) {
recordError();
workerLog(