Files
Marc-André Bombeck 0aa5cec17d feat(jobs): bind every queue through createAndWork and ban the bare call
All 104 bindings across the five registrars now go through createAndWork,
whose handler type is (jobs) => Promise<JobOutcome>. The forty-two inline
handlers return what they did. A handler that falls off the end no longer
compiles, and the healthlog/job-handler-outcome rule refuses a bare
boss.work( anywhere under src/lib/jobs except the one module that owns
the wrapper. The rule's header states its own limit: it catches the
omission, not a handler that claims ok:true after a swallow.

Retry policy, reviewed queue by queue because the rethrow changes what
runs, not only what is visible:

  - Twenty-one cron passes carry cronIsTheRetry (retryLimit 0). They are
    the retention DELETEs, the OAuth-state prunes, the hourly maintenance
    sweeps and the nightly per-sample drain — every one of them a
    statement that will fail identically a second later. pg-boss defaults
    to two retries with no delay, so converting them without this would
    have run the same doomed statement three times a night. The next tick
    is the retry and it arrives with the whole gap as backoff.
  - The policy rides on the schedule, not on createQueue: create_queue()
    ends in ON CONFLICT DO NOTHING, so a queue-level default would be
    inert on every already-running instance.
  - Everything else keeps its retries deliberately. A TLS probe, an
    off-host upload, a restore drill and a provider poll all fail
    transiently, and the reminder ticks only fail at job level when the
    pass itself could not run.
2026-07-27 23:08:50 +02:00

108 lines
4.0 KiB
JavaScript

/**
* @fileoverview ESLint rule — a pg-boss queue binding must go through
* `createAndWork`, never a bare `boss.work(`.
*
* `boss.work(queue, options, handler)` completes a job whenever the handler's
* promise resolves, whatever it resolves to. Every binding in this tree used
* to be written that way, so a handler that caught an error, logged a warning
* and returned looked exactly like one that did its work. `createAndWork`
* (`src/lib/jobs/reminder/registrar-shared.ts`) types its handler as
* `(jobs) => Promise<JobOutcome>` and wraps it in `runJob`, which fails the
* pg-boss job on `ok: false`. Routing every binding through it is what makes
* "the handler returned nothing" a compile error rather than a silent pass.
*
* WHAT THIS RULE PROVES, AND WHAT IT DOES NOT
*
* It proves that no queue in this tree is bound by a path that accepts a
* void handler. That is all it proves. It does NOT detect a handler that
* returns `{ ok: true }` after swallowing an exception — that compiles, it
* passes this rule, and it is exactly as invisible as the defect the outcome
* type was written for. The rule converts an omission (forgetting to signal
* failure) into a commission (claiming a success that did not happen). The
* second is a smaller class and a reviewable one; it is not zero.
*
* Two further evasions are known and unhandled: aliasing the method
* (`const w = boss.work.bind(boss)`, `const { work } = boss`) and calling
* `work` on an object not named `boss`. Both are syntactic dodges a reviewer
* would see in the diff; neither is defended against here.
*
* Scope: `src/lib/jobs/`, excluding the registrar module that owns the one
* legal `boss.work` call and excluding test files, which mock `boss.work`
* and assert against it directly.
*
* @see src/lib/jobs/run-job.ts
* @see src/lib/jobs/job-outcome.ts
* @see src/lib/jobs/reminder/registrar-shared.ts
*/
"use strict";
// The one module allowed to call `boss.work` — `createAndWork` lives here
// and is the wrapper every other binding has to use.
const EXEMPT_FILES = ["src/lib/jobs/reminder/registrar-shared.ts"];
// The worker tree. A `boss.work` outside it would not be a queue binding.
const ENFORCED_ROOTS = ["src/lib/jobs/"];
function toPosix(filename) {
return filename.replace(/\\/g, "/");
}
function isTestFile(posix) {
return (
/\.test\.[cm]?[jt]sx?$/.test(posix) ||
/\.spec\.[cm]?[jt]sx?$/.test(posix) ||
posix.includes("/__tests__/") ||
posix.includes("/__mocks__/")
);
}
function isEnforced(filename) {
const posix = toPosix(filename);
if (!ENFORCED_ROOTS.some((root) => posix.includes(root))) return false;
if (EXEMPT_FILES.some((f) => posix.includes(f))) return false;
if (isTestFile(posix)) return false;
return true;
}
/** @type {import("eslint").Rule.RuleModule} */
const jobHandlerOutcomeRule = {
meta: {
type: "problem",
docs: {
description:
"pg-boss queue bindings must go through createAndWork so the handler returns a JobOutcome.",
},
schema: [],
messages: {
bareBossWork:
'boss.work() completes a job whenever the handler resolves, so a swallowed failure reads as success. Bind the queue with createAndWork(boss, QUEUE, options, handler) from "./registrar-shared" — its handler type is (jobs) => Promise<JobOutcome> and runJob fails the job on ok:false.',
},
},
create(context) {
const filename = context.filename ?? context.getFilename?.();
if (!filename || !isEnforced(filename)) {
return {};
}
return {
CallExpression(node) {
const callee = node.callee;
if (
callee.type !== "MemberExpression" ||
callee.computed ||
callee.property.type !== "Identifier" ||
callee.property.name !== "work" ||
callee.object.type !== "Identifier" ||
callee.object.name !== "boss"
) {
return;
}
context.report({ node, messageId: "bareBossWork" });
},
};
},
};
module.exports = jobHandlerOutcomeRule;