Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions apps/worker/src/scheduler/scheduled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,37 @@ const PERSIST_BATCH_SIZE = 25;
// Look back a bit so maintenance start/end notifications are not missed if a tick is delayed.
const MAINTENANCE_EVENT_LOOKBACK_SECONDS = 10 * 60;

async function refreshHomepageSnapshotViaService(env: Env): Promise<void> {
if (!env.SELF) {
throw new Error('SELF service binding missing');
}
if (!env.ADMIN_TOKEN) {
throw new Error('ADMIN_TOKEN missing');
}

const res = await env.SELF.fetch(
new Request('http://internal/api/v1/internal/refresh/homepage', {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
body: env.ADMIN_TOKEN,
}),
);

if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`service refresh failed: HTTP ${res.status} ${text}`.trim());
}
}

async function refreshHomepageSnapshotInline(env: Env, now: number): Promise<void> {
const [{ computePublicHomepagePayload }, { refreshPublicHomepageSnapshotIfNeeded }] =
await Promise.all([
const [{ computePublicHomepagePayload }, { refreshPublicHomepageSnapshot }] = await Promise.all([
import('../public/homepage'),
import('../snapshots/public-homepage'),
import('../snapshots'),
]);

await refreshPublicHomepageSnapshotIfNeeded({
await refreshPublicHomepageSnapshot({
db: env.DB,
now,
compute: () => computePublicHomepagePayload(env.DB, now),
Expand Down Expand Up @@ -682,9 +705,16 @@ export async function runScheduledTick(env: Env, ctx: ExecutionContext): Promise
const now = Math.floor(Date.now() / 1000);
const checkedAt = Math.floor(now / 60) * 60;
const queueHomepageRefresh = () =>
refreshHomepageSnapshotInline(env, now).catch((err) => {
console.warn('homepage snapshot: refresh failed', err);
});
env.SELF
? refreshHomepageSnapshotViaService(env).catch(async (err) => {
console.warn('homepage snapshot: service refresh failed', err);
await refreshHomepageSnapshotInline(env, now).catch((fallbackErr) => {
console.warn('homepage snapshot: refresh failed', fallbackErr);
});
})
: refreshHomepageSnapshotInline(env, now).catch((err) => {
console.warn('homepage snapshot: refresh failed', err);
});

const acquired = await acquireLease(env.DB, LOCK_NAME, now, LOCK_LEASE_SECONDS);
if (!acquired) {
Expand Down
37 changes: 29 additions & 8 deletions apps/worker/test/scheduled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ vi.mock('../src/notify/webhook', () => ({
vi.mock('../src/public/homepage', () => ({
computePublicHomepagePayload: vi.fn(),
}));
vi.mock('../src/snapshots/public-homepage', () => ({
refreshPublicHomepageSnapshotIfNeeded: vi.fn(),
vi.mock('../src/snapshots', () => ({
refreshPublicHomepageSnapshot: vi.fn(),
}));

import type { Env } from '../src/env';
Expand All @@ -29,7 +29,7 @@ import { dispatchWebhookToChannels } from '../src/notify/webhook';
import { computePublicHomepagePayload } from '../src/public/homepage';
import { runScheduledTick } from '../src/scheduler/scheduled';
import { acquireLease } from '../src/scheduler/lock';
import { refreshPublicHomepageSnapshotIfNeeded } from '../src/snapshots/public-homepage';
import { refreshPublicHomepageSnapshot } from '../src/snapshots';
import { readSettings } from '../src/settings';
import { createFakeD1Database, type FakeD1QueryHandler } from './helpers/fake-d1';

Expand Down Expand Up @@ -160,7 +160,7 @@ describe('scheduler/scheduled regression', () => {
resolved_incident_preview: null,
maintenance_history_preview: null,
} as never);
vi.mocked(refreshPublicHomepageSnapshotIfNeeded).mockResolvedValue(false);
vi.mocked(refreshPublicHomepageSnapshot).mockResolvedValue(undefined);
vi.mocked(runHttpCheck).mockResolvedValue({
status: 'up',
latencyMs: 21,
Expand Down Expand Up @@ -205,19 +205,40 @@ describe('scheduler/scheduled regression', () => {
expect(readSettings).toHaveBeenCalledTimes(1);
expect(waitUntil).toHaveBeenCalledTimes(1);
await Promise.all(waitUntil.mock.calls.map((call) => call[0] as Promise<unknown>));
expect(refreshPublicHomepageSnapshotIfNeeded).toHaveBeenCalledWith({
expect(refreshPublicHomepageSnapshot).toHaveBeenCalledWith({
db: env.DB,
now: expectedNow,
compute: expect.any(Function),
});
const refreshArgs = vi.mocked(refreshPublicHomepageSnapshotIfNeeded).mock.calls[0]?.[0];
const refreshArgs = vi.mocked(refreshPublicHomepageSnapshot).mock.calls[0]?.[0];
expect(refreshArgs).toBeDefined();
await refreshArgs?.compute();
expect(computePublicHomepagePayload).toHaveBeenCalledWith(env.DB, expectedNow);
});

it('self-invokes homepage refresh via service binding when SELF is configured', async () => {
const env = createEnv({ dueRows: [] }) as unknown as Env;
env.ADMIN_TOKEN = 'test-admin-token';
const selfFetch = vi.fn().mockResolvedValueOnce(new Response('ok', { status: 200 }));
env.SELF = { fetch: selfFetch } as unknown as Fetcher;
const waitUntil = vi.fn();

await runScheduledTick(env, { waitUntil } as unknown as ExecutionContext);

expect(waitUntil).toHaveBeenCalledTimes(1);
await Promise.all(waitUntil.mock.calls.map((call) => call[0] as Promise<unknown>));

expect(selfFetch).toHaveBeenCalledTimes(1);
const req = selfFetch.mock.calls[0]?.[0] as Request;
expect(req).toBeInstanceOf(Request);
expect(req.method).toBe('POST');
expect(new URL(req.url).pathname).toBe('/api/v1/internal/refresh/homepage');
expect(await req.text()).toBe('test-admin-token');
expect(refreshPublicHomepageSnapshot).not.toHaveBeenCalled();
});

it('logs homepage snapshot refresh failures without breaking the tick', async () => {
vi.mocked(refreshPublicHomepageSnapshotIfNeeded).mockRejectedValueOnce(
vi.mocked(refreshPublicHomepageSnapshot).mockRejectedValueOnce(
new Error('snapshot refresh failed'),
);

Expand Down Expand Up @@ -312,7 +333,7 @@ describe('scheduler/scheduled regression', () => {

expect(waitUntil).toHaveBeenCalledTimes(1);
await Promise.all(waitUntil.mock.calls.map((call) => call[0] as Promise<unknown>));
expect(refreshPublicHomepageSnapshotIfNeeded).toHaveBeenCalledTimes(1);
expect(refreshPublicHomepageSnapshot).toHaveBeenCalledTimes(1);
});

it('passes explicit response assertion modes through scheduled HTTP checks', async () => {
Expand Down
Loading