feat(results): made the results scheduler window configurable
This commit is contained in:
parent
f18779e182
commit
606583a1c5
@ -16,3 +16,8 @@ ORDER_CUTOFF=15:00
|
|||||||
ORDER_ASK_UNTIL=15:30
|
ORDER_ASK_UNTIL=15:30
|
||||||
ORDER_REMIND_MINUTES=10
|
ORDER_REMIND_MINUTES=10
|
||||||
ORDER_EXPIRE_MINUTES=30
|
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
|
||||||
|
|||||||
@ -21,6 +21,30 @@ describe('loadConfig', () => {
|
|||||||
expect(c.dbPath).toBe('/tmp/x.db');
|
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', () => {
|
describe('order settings', () => {
|
||||||
const base = { SRS_SECRET: 'a'.repeat(20) };
|
const base = { SRS_SECRET: 'a'.repeat(20) };
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,14 @@ const schema = z.object({
|
|||||||
ORDER_ASK_UNTIL: hhmm('15:30'),
|
ORDER_ASK_UNTIL: hhmm('15:30'),
|
||||||
ORDER_REMIND_MINUTES: z.coerce.number().int().min(1).max(1440).default(10),
|
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),
|
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), {
|
.refine((e) => toMinutes(e.ORDER_ASK_UNTIL) > toMinutes(e.ORDER_CUTOFF), {
|
||||||
path: ['ORDER_ASK_UNTIL'],
|
path: ['ORDER_ASK_UNTIL'],
|
||||||
message: 'must be later than ORDER_CUTOFF',
|
message: 'must be later than ORDER_CUTOFF',
|
||||||
@ -38,6 +45,12 @@ export interface OrderSettings {
|
|||||||
expireMin: number;
|
expireMin: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResultsSettings {
|
||||||
|
windowStartMin: number;
|
||||||
|
windowEndMin: number;
|
||||||
|
retryMin: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
host: string;
|
host: string;
|
||||||
@ -47,6 +60,7 @@ export interface Config {
|
|||||||
bridgeUrl: string;
|
bridgeUrl: string;
|
||||||
defaultCountryCode: string;
|
defaultCountryCode: string;
|
||||||
orders: OrderSettings;
|
orders: OrderSettings;
|
||||||
|
results: ResultsSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
|
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,
|
remindMin: e.ORDER_REMIND_MINUTES,
|
||||||
expireMin: e.ORDER_EXPIRE_MINUTES,
|
expireMin: e.ORDER_EXPIRE_MINUTES,
|
||||||
},
|
},
|
||||||
|
results: {
|
||||||
|
windowStartMin: toMinutes(e.RESULTS_WINDOW_START),
|
||||||
|
windowEndMin: toMinutes(e.RESULTS_WINDOW_END),
|
||||||
|
retryMin: e.RESULTS_RETRY_MINUTES,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,7 +60,34 @@ describe('results scheduler', () => {
|
|||||||
await x.s.tick();
|
await x.s.tick();
|
||||||
await x.s.tick();
|
await x.s.tick();
|
||||||
expect(x.calls()).toBe(0);
|
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 () => {
|
it('logs a held draw and survives a failing check', async () => {
|
||||||
|
|||||||
@ -3,13 +3,12 @@ import { runResultsJob, type DrawReport } from './job';
|
|||||||
import { istMinutes } from '../services/ist';
|
import { istMinutes } from '../services/ist';
|
||||||
import { todayIst } from './portal';
|
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 {
|
export interface SchedulerOptions {
|
||||||
db: Db;
|
db: Db;
|
||||||
log: (entry: Record<string, unknown>) => void;
|
log: (entry: Record<string, unknown>) => void;
|
||||||
|
windowStartMin?: number;
|
||||||
|
windowEndMin?: number;
|
||||||
|
retryMin?: number;
|
||||||
now?: () => Date;
|
now?: () => Date;
|
||||||
run?: (db: Db) => Promise<DrawReport[]>;
|
run?: (db: Db) => Promise<DrawReport[]>;
|
||||||
}
|
}
|
||||||
@ -19,6 +18,9 @@ function todaysDrawReady(db: Db, today: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createResultsScheduler(opts: SchedulerOptions) {
|
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 now = opts.now ?? (() => new Date());
|
||||||
const run = opts.run ?? ((db: Db) => runResultsJob({ db }));
|
const run = opts.run ?? ((db: Db) => runResultsJob({ db }));
|
||||||
let lastRunAt = 0;
|
let lastRunAt = 0;
|
||||||
@ -33,13 +35,13 @@ export function createResultsScheduler(opts: SchedulerOptions) {
|
|||||||
if (minutes >= WINDOW_END_MIN) {
|
if (minutes >= WINDOW_END_MIN) {
|
||||||
if (loggedGiveUpFor !== today && !todaysDrawReady(opts.db, today)) {
|
if (loggedGiveUpFor !== today && !todaysDrawReady(opts.db, today)) {
|
||||||
loggedGiveUpFor = 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;
|
return;
|
||||||
}
|
}
|
||||||
if (minutes < WINDOW_START_MIN || running) return;
|
if (minutes < WINDOW_START_MIN || running) return;
|
||||||
if (todaysDrawReady(opts.db, today)) 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;
|
running = true;
|
||||||
lastRunAt = t.getTime();
|
lastRunAt = t.getTime();
|
||||||
|
|||||||
@ -22,7 +22,11 @@ async function main(): Promise<void> {
|
|||||||
const app = createApp({ db, config, bridge, sendQueue }, { logger: true });
|
const app = createApp({ db, config, bridge, sendQueue }, { logger: true });
|
||||||
log = (e) => app.log.info(e);
|
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({
|
const stopSweeper = createConversationSweeper({
|
||||||
db,
|
db,
|
||||||
settings: config.orders,
|
settings: config.orders,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export function testConfig(overrides: Partial<Config> = {}): Config {
|
|||||||
bridgeUrl: 'http://127.0.0.1:3000',
|
bridgeUrl: 'http://127.0.0.1:3000',
|
||||||
defaultCountryCode: '91',
|
defaultCountryCode: '91',
|
||||||
orders: { cutoffMin: 15 * 60, askUntilMin: 15 * 60 + 30, remindMin: 10, expireMin: 30 },
|
orders: { cutoffMin: 15 * 60, askUntilMin: 15 * 60 + 30, remindMin: 10, expireMin: 30 },
|
||||||
|
results: { windowStartMin: 16 * 60, windowEndMin: 19 * 60, retryMin: 10 },
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user