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 * * *';