From 1db89e1dc4ee8142bd460dbe2595dea31ccb770e Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Mon, 5 Oct 2026 02:44:05 +0800 Subject: [PATCH] Fix(datasets): size the Sentry monitor's maxRuntime to the scheduled rows Rows update one after another, each with up to 30 minutes, so four datasets can legitimately take two hours while the monitor failed a run after one. maxRuntime is now every scheduled row's timeout plus a 10-minute margin. Co-Authored-By: Claude Opus 5.5 --- common/dataset-updater.js | 14 +++++++++++++- tests/dataset-updater.test.js | 12 +++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/common/dataset-updater.js b/common/dataset-updater.js index 03b5e25f..91bb37d3 100644 --- a/common/dataset-updater.js +++ b/common/dataset-updater.js @@ -341,6 +341,18 @@ export const rowsMissingACheck = async (rows, { pattern, now = new Date() }) => return stale; }; +/** + * Sentry Crons settings for a run updating `rowCount` datasets in turn: its + * longest legitimate run is every row at its timeout, plus a margin, so a + * healthy run is never marked failed. Exported for tests. + */ +export const monitorConfigFor = (pattern, rowCount, timezone) => ({ + schedule: { type: 'crontab', value: pattern }, + timezone, + checkinMargin: 60, + maxRuntime: Math.ceil((rowCount * UPDATE_TIMEOUT_MS) / 60000) + 10, +}); + let scheduler = null; /** @@ -360,7 +372,7 @@ export const startDatasetScheduler = (rows) => { scheduler = new Cron(pattern, { protect: true, unref: true }, () => withCronMonitor( 'dataset-update', () => updateDatasets(scheduled), - { schedule: { type: 'crontab', value: pattern }, timezone, checkinMargin: 60, maxRuntime: 60 }, + monitorConfigFor(pattern, scheduled.length, timezone), ).catch((error) => logger.error({ err: error }, 'dataset update run failed'))); const next = scheduler.nextRun()?.toLocaleString('en-US', { hour12: false }); logger.info(`🗓️ Dataset auto update: ${scheduled.map((row) => row.id).join(', ')} at cron ${pattern} (next ${next})`); diff --git a/tests/dataset-updater.test.js b/tests/dataset-updater.test.js index f7b5a97f..1c1bf1c3 100644 --- a/tests/dataset-updater.test.js +++ b/tests/dataset-updater.test.js @@ -15,7 +15,7 @@ import lockfile from 'proper-lockfile'; import { STATE_FILE, LOCK_FILE, updateDataset, bootstrapDataset, readState, isPresent, isAutoUpdateEnabled, startDatasetScheduler, DEFAULT_UPDATE_CRON, rowsMissingACheck, watchDatasets, - updateDatasets, downloadToFile, + updateDatasets, downloadToFile, monitorConfigFor, } from '../common/dataset-updater.js'; import logger from '../common/logger.js'; import { setUpstreamUserAgent } from '../common/fetch-with-timeout.js'; @@ -302,6 +302,16 @@ describe('startDatasetScheduler', () => { }); }); +describe('monitorConfigFor', () => { + it('allows every scheduled row its full timeout, plus a margin', () => { + const config = monitorConfigFor('30 4 * * *', 4, 'Asia/Singapore'); + assert.equal(config.maxRuntime, 4 * 30 + 10); + assert.deepEqual(config.schedule, { type: 'crontab', value: '30 4 * * *' }); + assert.equal(config.timezone, 'Asia/Singapore'); + assert.equal(monitorConfigFor('30 4 * * *', 1).maxRuntime, 40); + }); +}); + describe('rowsMissingACheck', () => { const at = (day, hour, minute) => new Date(2026, 9, day, hour, minute); // local time, like the cron const pattern = '30 4 * * *';