feat(intake): run the drafting worker after a submission and on a sweep (#223)
Two drivers. The call after a successful submission means a draft is usually waiting by the time anybody looks; the five-minute sweep means a restart mid-draft is recoverable rather than a permanently stalled row, and picks up whatever the first call missed. Neither is awaited. A slow or failing model must not become a failed upload for someone who did nothing wrong, which is the whole reason drafting does not happen inline — the cost of a dropped call is a few minutes' delay, not a lost submission. Both catch for themselves. The comment beside the existing schedulers points out that `void` is only safe because those functions handle their own errors, and draftQueued does not: its first query can reject, and an escaping rejection would take the container down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { asyncRoute } from '../asyncRoute';
|
||||
import { hashToken } from '../uploadLinks';
|
||||
import { uploadImages, verifyUploadedImages, insertItemImages } from '../imageUpload';
|
||||
import { intakeViewLimiter, intakeSubmitLimiter } from '../rateLimit';
|
||||
import { draftQueued } from '../intake/draftingWorker';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -148,6 +149,14 @@ router.post(
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Deliberately not awaited, and catching for itself. A slow or failing
|
||||
// model must not become a failed upload for someone who did nothing
|
||||
// wrong, which is the whole reason drafting does not happen inline. The
|
||||
// sweeper picks up anything this misses, so the cost of it failing here
|
||||
// is a few minutes' delay rather than a lost submission.
|
||||
void draftQueued(1).catch((err) => console.error('[drafting] after submission:', err));
|
||||
|
||||
// No item id in the response: the sender has no business knowing about
|
||||
// the catalogue, and nothing they could do with it.
|
||||
res.status(201).json({ ok: true });
|
||||
|
||||
@@ -6,6 +6,7 @@ import { renderTemplate, greeting, formatDuration } from './emailTemplates';
|
||||
import { getSettings } from './adminSettings';
|
||||
import { loadStoredTemplate } from './routes/adminEmailTemplates';
|
||||
import { validateEnv } from './envValidation';
|
||||
import { draftQueued } from './intake/draftingWorker';
|
||||
|
||||
// Release cart holds whose expiry has passed.
|
||||
async function sweepExpiredCarts(): Promise<void> {
|
||||
@@ -96,6 +97,16 @@ interface ReminderRow {
|
||||
setInterval(() => void sweepExpiredCarts(), 5 * 60 * 1000);
|
||||
cron.schedule('0 9 * * *', () => void sendCartReminders());
|
||||
|
||||
// Every five minutes, in the same shape as the cart sweep. This is what makes a
|
||||
// restart mid-draft recoverable rather than a permanently stalled row, and what
|
||||
// picks up anything the post-submission call missed. Unlike the two above,
|
||||
// draftQueued does not catch at its own top level — the initial query can
|
||||
// reject — so it catches here instead, for the reason the comment above gives.
|
||||
setInterval(
|
||||
() => void draftQueued().catch((err) => console.error('[drafting] sweep:', err)),
|
||||
5 * 60 * 1000
|
||||
);
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||
|
||||
// Checked at boot rather than left to be discovered by the first request that
|
||||
|
||||
Reference in New Issue
Block a user