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
9 changes: 5 additions & 4 deletions src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,10 @@ const PRIOR_CONTRIBUTIONS_LIMIT = 5;
* lock_expires_at has passed is reclaimable by design, but the claim queries
* guard on status='open', so until expire() runs the task is invisible and its
* reservation blocks the volunteer's budget. The cron trigger runs expire()
* every 5 minutes; this makes the pool-facing reads self-healing too, so a
* lapsed lock never gates on the next cron tick. Best-effort: a failure here
* must never fail the read/checkout it piggybacks on.
* hourly (a low-frequency backstop so Neon's compute can autosuspend when idle
* — see wrangler.toml); this lazy path makes the pool-facing reads self-healing
* too, so an active pool never waits on the cron tick to reclaim a lapsed lock.
* Best-effort: a failure here must never fail the read/checkout it piggybacks on.
*/
async function reclaimLapsedLocks(): Promise<void> {
try {
Expand Down Expand Up @@ -2494,7 +2495,7 @@ function pendingDecompositionSql(alias: string): string {
export async function listOpenTasks(filter: OpenTaskFilter = {}): Promise<TaskRow[]> {
// A task under a lapsed lock belongs in this listing — reclaim before
// reading so stranded work is visible to the next poll, not just to the
// 5-minute cron sweep.
// hourly cron sweep.
await reclaimLapsedLocks();
const conditions: string[] = [`status = 'open'`, `NOT ${pendingDecompositionSql('tasks')}`];
const params: unknown[] = [];
Expand Down
12 changes: 7 additions & 5 deletions src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ export default {
return app.fetch(req, env as any, ctx as any);
},
email: emailHandler,
// Cron trigger (wrangler.toml [triggers]): the lease-expiry sweep. A crashed
// runner never calls /release; without this, its task is stranded out of the
// pool and its reservation blocks the volunteer's budget until someone
// remembers to POST /admin/expire. Thin shim by design — all the logic (and
// its tests) live in operations.expire().
// Cron trigger (wrangler.toml [triggers]): the hourly lease-expiry sweep. A
// crashed runner never calls /release; without this, its task is stranded out
// of the pool and its reservation blocks the volunteer's budget until someone
// remembers to POST /admin/expire. Runs hourly, not every few minutes: the
// hot path (checkout/listing) already reclaims lazily, so a frequent tick
// would buy nothing but keep Neon's compute from ever autosuspending. Thin
// shim by design — all the logic (and its tests) live in operations.expire().
scheduled: async (_controller: unknown, _env: unknown, _ctx: unknown) => {
const r = await expire();
if (r.expired_count > 0) {
Expand Down
23 changes: 17 additions & 6 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,24 @@ enabled = true

# The lease-expiry sweep must run on its own: a crashed or stranded runner never
# calls /release, and without this sweep its task stays invisible to the pool
# and its reservation blocks the volunteer's budget INDEFINITELY. Every 5
# minutes the Worker's `scheduled` handler (src/worker.ts) runs expire(), which
# returns lapsed-locked tasks to the pool and refunds their reservations.
# Checkout/listing also reclaim lazily (see operations.ts), so the cron is the
# backstop, not the only line of defense.
# and its reservation blocks the volunteer's budget INDEFINITELY. The Worker's
# `scheduled` handler (src/worker.ts) runs expire(), which returns lapsed-locked
# tasks to the pool and refunds their reservations.
#
# Cadence is HOURLY, not every-5-minutes, and that's deliberate: checkout and
# listing already reclaim lapsed locks lazily on the hot path (see
# reclaimLapsedLocks in operations.ts), so any active use of the pool self-heals
# a stranded reservation within one request. The cron only matters when the
# system is otherwise idle — and a query every 5 minutes is exactly what defeats
# Neon's autosuspend (default 300s idle), pinning the serverless compute on 24/7
# and burning through the monthly CU-hour allowance for no work. An hourly tick
# lets the compute scale to zero between bursts while still capping how long a
# crashed runner's reservation can linger in a fully idle system at ~1h (the
# lease itself is 10 min; the delay is only the budget-refund backstop). If you
# ever need faster idle reclaim, lower this AND raise the Neon autosuspend
# window so the two don't fight.
[triggers]
crons = ["*/5 * * * *"]
crons = ["0 * * * *"]

# Outbound transactional email via Cloudflare Email Sending (the domain
# givework.dev is onboarded there, so SPF/DKIM are aligned). The Worker sends the
Expand Down
Loading