mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(measurements): fall back to live SQL when the rollup read throws
The daily-series reader only fell back to live SQL on a coverage MISS (zero rollup rows); a transient rollup-table error (statement_timeout, deadlock, connection reset) was unwrapped and 500'd the tile on the single-type route even though live SQL could serve it — and diverged from the batched series route, which already caught. Wrap the primary daily read and the long-range tiered read: treat a throw as a coverage miss and fall through to the live date_trunc aggregate.
This commit is contained in:
1 parent
62c7278275
commit
982fe53b51
2 files changed
+96
-12
No files matched your search
@@ -158,3 +158,54 @@ describe("readDailySeries — long-range tier step-up", () => {
|
||||
expect(mocks.findMany).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readDailySeries — rollup read throw falls back to live SQL (F-DB-2)", () => {
|
||||
it("a rollup-table throw on a normal window degrades to the live date_trunc aggregate, not a 500", async () => {
|
||||
const to = new Date("2026-06-21T00:00:00.000Z");
|
||||
const from = new Date(to.getTime() - 90 * DAY_MS);
|
||||
|
||||
// The primary rollup read throws (statement_timeout / deadlock / reset).
|
||||
mocks.findMany.mockReset().mockRejectedValue(new Error("statement timeout"));
|
||||
// Live fallback serves the tile.
|
||||
mocks.queryRaw.mockResolvedValueOnce([
|
||||
{
|
||||
type: "WEIGHT",
|
||||
bucket_start: new Date("2026-06-10T00:00:00.000Z"),
|
||||
avg: 80.5,
|
||||
cnt: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await readDailySeries({
|
||||
userId: "u",
|
||||
type: "WEIGHT",
|
||||
from,
|
||||
to,
|
||||
priorityJson: null,
|
||||
});
|
||||
|
||||
// The read did NOT throw — it fell through to live SQL and served a row.
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({ type: "WEIGHT", value: 80.5, count: 2 });
|
||||
expect(mocks.queryRaw).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a tiered-reader throw on a long window falls through to the daily path", async () => {
|
||||
const to = new Date("2026-06-21T00:00:00.000Z");
|
||||
const from = new Date(to.getTime() - 3650 * DAY_MS);
|
||||
mocks.readTieredRollupSeries.mockRejectedValueOnce(new Error("deadlock"));
|
||||
|
||||
const result = await readDailySeries({
|
||||
userId: "u",
|
||||
type: "WEIGHT",
|
||||
from,
|
||||
to,
|
||||
priorityJson: null,
|
||||
});
|
||||
|
||||
// The daily DAY-rollup read still ran as the fallback (the default findMany
|
||||
// returns 3 rows), so the throw did not 500 the read.
|
||||
expect(mocks.findMany).toHaveBeenCalled();
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -76,17 +76,32 @@ export async function readDailySeries(opts: {
|
||||
(to.getTime() - from.getTime()) / 86_400_000,
|
||||
);
|
||||
if (windowDaysFull > cap) {
|
||||
const tiered = await readTieredRollupSeries({
|
||||
userId,
|
||||
type,
|
||||
// v1.26.0 SEAM-N2 — pass the resolved `[from, to]` bounds so the tier
|
||||
// reads the REQUESTED window (which may be entirely historic), not a
|
||||
// trailing "to now" slice. `windowDaysFull` still gates entry above and
|
||||
// drives the tier width inside the reader.
|
||||
from,
|
||||
to,
|
||||
priorityJson,
|
||||
});
|
||||
// A rollup-table throw here (statement_timeout, deadlock, connection reset)
|
||||
// must not 500 the whole read — treat it as a coverage miss and fall
|
||||
// through to the daily path (which itself falls back to live SQL).
|
||||
let tiered: Awaited<ReturnType<typeof readTieredRollupSeries>> = null;
|
||||
try {
|
||||
tiered = await readTieredRollupSeries({
|
||||
userId,
|
||||
type,
|
||||
// v1.26.0 SEAM-N2 — pass the resolved `[from, to]` bounds so the tier
|
||||
// reads the REQUESTED window (which may be entirely historic), not a
|
||||
// trailing "to now" slice. `windowDaysFull` still gates entry above and
|
||||
// drives the tier width inside the reader.
|
||||
from,
|
||||
to,
|
||||
priorityJson,
|
||||
});
|
||||
} catch (err) {
|
||||
annotate({
|
||||
meta: {
|
||||
tiered_series_read_threw: true,
|
||||
tiered_series_read_error:
|
||||
err instanceof Error ? err.message : String(err),
|
||||
type,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (tiered && tiered.rows.length > 0) {
|
||||
return tiered.rows;
|
||||
}
|
||||
@@ -129,7 +144,25 @@ export async function readDailySeries(opts: {
|
||||
priorityJson,
|
||||
).slice(0, cap);
|
||||
|
||||
const rollupRows = await readRollup();
|
||||
// F-DB-2 — the primary rollup read is wrapped: a transient rollup-table error
|
||||
// (statement_timeout, deadlock, connection reset) must NOT 500 the tile when
|
||||
// the live table can still serve it. Treat a throw as a coverage miss and fall
|
||||
// straight through to the live `date_trunc` aggregate — the same posture the
|
||||
// batched series reader already takes, so the single-type route and the batch
|
||||
// route no longer diverge on a rollup hiccup.
|
||||
let rollupRows: Awaited<ReturnType<typeof readRollup>>;
|
||||
try {
|
||||
rollupRows = await readRollup();
|
||||
} catch (err) {
|
||||
annotate({
|
||||
meta: {
|
||||
rollup_read_threw: true,
|
||||
rollup_read_error: err instanceof Error ? err.message : String(err),
|
||||
type,
|
||||
},
|
||||
});
|
||||
return readLiveDaily({ userId, type, from, to, cap, priorityJson });
|
||||
}
|
||||
|
||||
// Coverage-mismatch probe + inline fold (identical to the route).
|
||||
const windowMs = to.getTime() - from.getTime();
|
||||
|
||||
Reference in new issue
Block a user