feat(results): made the results scheduler window configurable

This commit is contained in:
Aswin B. S 2026-11-03 19:03:02 +05:30
parent f18779e182
commit 606583a1c5
7 changed files with 90 additions and 8 deletions

View File

@ -16,3 +16,8 @@ ORDER_CUTOFF=15:00
ORDER_ASK_UNTIL=15:30
ORDER_REMIND_MINUTES=10
ORDER_EXPIRE_MINUTES=30
# Result PDF fetching (IST): check from START until a draw is stored, give up at END, retry every N minutes
RESULTS_WINDOW_START=16:00
RESULTS_WINDOW_END=19:00
RESULTS_RETRY_MINUTES=10

View File

@ -21,6 +21,30 @@ describe('loadConfig', () => {
expect(c.dbPath).toBe('/tmp/x.db');
});
describe('results settings', () => {
const base = { SRS_SECRET: 'a'.repeat(20) };
it('defaults to 16:00-19:00, retry 10', () => {
expect(loadConfig(base).results).toEqual({ windowStartMin: 960, windowEndMin: 1140, retryMin: 10 });
});
it('reads overrides', () => {
const c = loadConfig({
...base,
RESULTS_WINDOW_START: '16:30',
RESULTS_WINDOW_END: '20:00',
RESULTS_RETRY_MINUTES: '5',
});
expect(c.results).toEqual({ windowStartMin: 990, windowEndMin: 1200, retryMin: 5 });
});
it('rejects bad format, bad range and bad retry', () => {
expect(() => loadConfig({ ...base, RESULTS_WINDOW_START: '4pm' })).toThrow(/HH:MM/);
expect(() => loadConfig({ ...base, RESULTS_WINDOW_END: '16:00' })).toThrow(/later than RESULTS_WINDOW_START/);
expect(() => loadConfig({ ...base, RESULTS_RETRY_MINUTES: '0' })).toThrow(/RESULTS_RETRY_MINUTES/);
});
});
describe('order settings', () => {
const base = { SRS_SECRET: 'a'.repeat(20) };

View File

@ -21,7 +21,14 @@ const schema = z.object({
ORDER_ASK_UNTIL: hhmm('15:30'),
ORDER_REMIND_MINUTES: z.coerce.number().int().min(1).max(1440).default(10),
ORDER_EXPIRE_MINUTES: z.coerce.number().int().min(1).max(1440).default(30),
RESULTS_WINDOW_START: hhmm('16:00'),
RESULTS_WINDOW_END: hhmm('19:00'),
RESULTS_RETRY_MINUTES: z.coerce.number().int().min(1).max(120).default(10),
})
.refine((e) => toMinutes(e.RESULTS_WINDOW_END) > toMinutes(e.RESULTS_WINDOW_START), {
path: ['RESULTS_WINDOW_END'],
message: 'must be later than RESULTS_WINDOW_START',
})
.refine((e) => toMinutes(e.ORDER_ASK_UNTIL) > toMinutes(e.ORDER_CUTOFF), {
path: ['ORDER_ASK_UNTIL'],
message: 'must be later than ORDER_CUTOFF',
@ -38,6 +45,12 @@ export interface OrderSettings {
expireMin: number;
}
export interface ResultsSettings {
windowStartMin: number;
windowEndMin: number;
retryMin: number;
}
export interface Config {
port: number;
host: string;
@ -47,6 +60,7 @@ export interface Config {
bridgeUrl: string;
defaultCountryCode: string;
orders: OrderSettings;
results: ResultsSettings;
}
export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
@ -75,5 +89,10 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
remindMin: e.ORDER_REMIND_MINUTES,
expireMin: e.ORDER_EXPIRE_MINUTES,
},
results: {
windowStartMin: toMinutes(e.RESULTS_WINDOW_START),
windowEndMin: toMinutes(e.RESULTS_WINDOW_END),
retryMin: e.RESULTS_RETRY_MINUTES,
},
};
}

View File

@ -60,7 +60,34 @@ describe('results scheduler', () => {
await x.s.tick();
await x.s.tick();
expect(x.calls()).toBe(0);
expect(x.logs.filter((l) => String(l.msg).includes('by 7 PM'))).toHaveLength(1);
expect(x.logs.filter((l) => String(l.msg).includes('by end of window'))).toHaveLength(1);
});
it('honours a custom window and retry interval', async () => {
const db = makeDb();
let calls = 0;
let clock = z('09:59'); // 15:29 IST
const s = createResultsScheduler({
db,
log: () => {},
now: () => clock,
run: async () => {
calls++;
return [];
},
windowStartMin: 15 * 60 + 30,
windowEndMin: 15 * 60 + 50,
retryMin: 5,
});
await s.tick();
expect(calls).toBe(0);
clock = z('10:00'); // 15:30
await s.tick();
clock = z('10:05');
await s.tick();
clock = z('10:20'); // 15:50, window over
await s.tick();
expect(calls).toBe(2);
});
it('logs a held draw and survives a failing check', async () => {

View File

@ -3,13 +3,12 @@ import { runResultsJob, type DrawReport } from './job';
import { istMinutes } from '../services/ist';
import { todayIst } from './portal';
const WINDOW_START_MIN = 16 * 60; // 4 PM IST
const WINDOW_END_MIN = 19 * 60; // 7 PM IST
const EVERY_MS = 10 * 60_000;
export interface SchedulerOptions {
db: Db;
log: (entry: Record<string, unknown>) => void;
windowStartMin?: number;
windowEndMin?: number;
retryMin?: number;
now?: () => Date;
run?: (db: Db) => Promise<DrawReport[]>;
}
@ -19,6 +18,9 @@ function todaysDrawReady(db: Db, today: string): boolean {
}
export function createResultsScheduler(opts: SchedulerOptions) {
const WINDOW_START_MIN = opts.windowStartMin ?? 16 * 60;
const WINDOW_END_MIN = opts.windowEndMin ?? 19 * 60;
const EVERY_MS = (opts.retryMin ?? 10) * 60_000;
const now = opts.now ?? (() => new Date());
const run = opts.run ?? ((db: Db) => runResultsJob({ db }));
let lastRunAt = 0;
@ -33,13 +35,13 @@ export function createResultsScheduler(opts: SchedulerOptions) {
if (minutes >= WINDOW_END_MIN) {
if (loggedGiveUpFor !== today && !todaysDrawReady(opts.db, today)) {
loggedGiveUpFor = today;
opts.log({ level: 'warn', msg: 'results: no ready draw for today by 7 PM', date: today });
opts.log({ level: 'warn', msg: 'results: no ready draw for today by end of window', date: today });
}
return;
}
if (minutes < WINDOW_START_MIN || running) return;
if (todaysDrawReady(opts.db, today)) return;
if (t.getTime() - lastRunAt < EVERY_MS - 30_000) return;
if (t.getTime() - lastRunAt < Math.max(0, EVERY_MS - 30_000)) return;
running = true;
lastRunAt = t.getTime();

View File

@ -22,7 +22,11 @@ async function main(): Promise<void> {
const app = createApp({ db, config, bridge, sendQueue }, { logger: true });
log = (e) => app.log.info(e);
const stopScheduler = createResultsScheduler({ db, log: (e) => app.log.info(e) }).start();
const stopScheduler = createResultsScheduler({
db,
log: (e) => app.log.info(e),
...config.results,
}).start();
const stopSweeper = createConversationSweeper({
db,
settings: config.orders,

View File

@ -10,6 +10,7 @@ export function testConfig(overrides: Partial<Config> = {}): Config {
bridgeUrl: 'http://127.0.0.1:3000',
defaultCountryCode: '91',
orders: { cutoffMin: 15 * 60, askUntilMin: 15 * 60 + 30, remindMin: 10, expireMin: 30 },
results: { windowStartMin: 16 * 60, windowEndMin: 19 * 60, retryMin: 10 },
...overrides,
};
}