Skip to content

Reap-settle sweep: settled rows consume the batch — no true drainage under sustained >batch/min load #97

Description

@guohai

Context

Follow-up from PR #96 (the #90 grace-period fix). Both CI reviewers (Codex, Claude) independently flagged this; it is a pre-existing limitation of the reap-settle sweep design, not introduced by #96. #96 correctly closes the actual #90 High (the sub-second finalize→result window, which bites at normal volume). This issue is the separate, higher-load drainage gap.

The problem

storage.getReapableSharedJobs selects terminal targeted jobs whose snapshot->settlementContext IS NOT NULL, ordered oldest-first, LIMIT 200. It does not filter by the plugin's settlement status, and Core never clears settlementContext after a settlement terminalizes. So an already-settled job stays query-eligible forever (until it ages out of the 15-min lookback).

Consequence under sustained load: if more than ~200 terminal targeted jobs sit in the lookback window, every tick re-returns the same oldest 200 (mostly already-settled → settle() no-ops), and a genuinely-unsettled job sitting behind the batch cap can never be reached within the window. It then falls to the 26h leak-reaper, which refunds — the same money-loss class #90 was about, but gated behind throughput instead of a sub-second window.

  • Trigger regime: >200 terminal targeted (paid shared) jobs / ~14-min window, with those overflow jobs' complete-route settle() having failed (so they truly depend on the sweep).
  • Current exposure: none in practice — no production paid shared-dispatch volume yet (Phase B merged 2026-08-17). This is pre-production hardening for when marketplace volume exists.

What #96 already did (not a fix for this, but related)

  • Ascending (oldest-first) order + asc(id) tiebreaker → favors the rows nearest aging out, deterministic batch boundary.
  • Honest saturation log: warns only when the batch is full and its oldest row is within 2 min of the lookback edge (a real "about to age out" signal), so operators get a heads-up before this bites.

Neither actually drains the backlog — they surface it and order it well.

Options for the real fix

  1. Settled-marker (preferred): a Core↔plugin signal so a terminalized settlement's job is excluded from the candidate set — e.g. plugin reports back, Core stamps snapshot.settlement.done (or clears settlementContext), and the query filters it out. Keeps LIMIT honest.
  2. Settlement-status join: query excludes jobs whose plugin settlement row is already terminal (couples Core query to plugin schema — weaker boundary).
  3. Stable paging cursor: page through the full eligible window by (completed_at, id) cursor instead of a fixed head-of-window LIMIT, so settled rows don't permanently occupy the budget.

Option 1 fits the Core-money-agnostic boundary best (Core carries an opaque marker; only the plugin interprets settlement).

Acceptance

  • Under a synthetic backlog of >batch terminal targeted jobs/min with some unsettled overflow, every unsettled job is settled within the lookback window (none reaches the 26h leak-reaper for lack of a sweep slot).
  • Core stays money-agnostic (no plugin-settlement-schema knowledge in Core queries), per the cycle-2 boundary.

Refs: PR #96, GitHub #90.

Generated with SMT smt@agora.build

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions