Skip to content

coordinator: harden worker payout-lock lifecycle (surface unbind failures, idle GC, bounded load, bulk unbind) - #27

Merged
jokeez merged 2 commits into
jokeez:mainfrom
bobbyning:fix/payout-lock-lifecycle
Oct 5, 2026
Merged

jokeez merged 2 commits into
jokeez:mainfrom
bobbyning:fix/payout-lock-lifecycle

Conversation

@bobbyning

@bobbyning bobbyning commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up hardening of the worker_payout_lock durable binding introduced for report #27 - four lifecycle gaps, all in one table code path. Review findings from the first bot pass are addressed in the second commit (5c61110).

What

  1. Admin unbind could answer ok:true while the durable DELETE silently failed. clearPayoutLock swallowed the DELETE error, cleared the in-memory map, and the endpoint always wrote ok:true. If the DELETE fails (e.g. sqlite busy), the binding survives on disk and the worker is silently re-bound to its old address after the next coordinator restart - the coordinator: harden worker payout-lock lifecycle (surface unbind failures, idle GC, bounded load, bulk unbind) #27 recovery path quietly no-ops. The DELETE now runs first and its failure is surfaced: the endpoint answers 500 + ok:false. Unbind (DELETE + memory clear) is serialized against persistPayoutLock with a dedicated mutex, so an in-flight claim cannot re-insert the binding between the two steps.

  2. The table grows forever. One row per worker_id ever seen, no TTL/GC - the sibling dedup tables got a TTL after the multi-million-entry starvation incident, this table got nothing. Adds a seen_at column (old installs are upgraded and backfilled on first boot, so idle-GC never reaps pre-upgrade rows), refreshed on every persist while the first-bound address is never rewritten (report coordinator: harden worker payout-lock lifecycle (surface unbind failures, idle GC, bounded load, bulk unbind) #27 semantics preserved). Idle rows are reaped on attach (default 30d, HACKME_PAYOUT_LOCK_IDLE_SEC, 0 disables), with an index on seen_at for the GC cutoff and the newest-first load (created after the column upgrade so old-schema installs migrate cleanly). Active bindings are kept fresh by a throttled liveness touch (at most 1/h per worker) on the lock reads every submit path already performs - the touch is UPDATE-only and never inserts, so it cannot resurrect a row an in-flight admin unbind just deleted; the in-memory throttle advances only after the UPDATE succeeds, and it is seeded from the durable seen_at at load so a restart does not trigger a per-worker write burst.

  3. Startup loaded the whole table into memory. loadPayoutLocks had no LIMIT. It is now newest-first bounded by the existing dedup memory cap and the configured worker cap - correctness is unchanged because lockedPayoutAddress falls back to the durable row per worker, so evicted bindings stay enforced. That fallback fails closed: a durable-store error returns a sentinel that fails identity checks instead of reading as no binding; the first read of an evicted binding also refreshes its seen_at.

  4. Fleet retirement needed N calls. The unbind endpoint additionally accepts worker_ids[] (cap 256, same per-id validation), returning the count of ids whose live binding was removed plus per-id failures.

Evidence

  • On main (57d3b7b): TestUnbindPayoutLockSurfacesDurableDeleteFailure FAILS with code=200 ok:true cleared:true while the durable row survives (closed-DB failure injection); TestUnbindPayoutLockBulk FAILS (worker_ids ignored -> 400). The coordinator: harden worker payout-lock lifecycle (surface unbind failures, idle GC, bounded load, bulk unbind) #27 happy path still passes on main.
  • On this branch: 13/13 new tests green (failure surfacing, happy-path regression, upgrade+backfill, first-bind-wins + liveness refresh, idle-GC on/off, active-binding liveness touch survives restart, touch throttling + retry-on-failed-touch, bulk + cap + validation, bounded newest-first load, fail-closed sentinel on store errors, evicted-read refresh, maxWorkers cap); full ./cmd/coordinator package green, go vet clean, suite stable across repeated runs.

Notes

  • Migration is additive (ALTER + backfill + index): existing rows and first-bound addresses are preserved exactly; the idle horizon starts counting from the upgrade moment.
  • Single-worker unbind response shape is unchanged (ok/cleared/worker_id/previous_address); bulk adds a separate worker_ids request shape.

…e GC, bounded load, bulk unbind)

- clearPayoutLock now runs the durable DELETE first and surfaces its
  failure: admin unbind answered ok:true while the row could survive
  (error was swallowed), silently re-binding the worker after restart.
- worker_payout_lock grows one row per worker_id ever seen; add seen_at
  (backfilled on upgrade, first-bound address never rewritten), idle-GC
  on attach (HACKME_PAYOUT_LOCK_IDLE_SEC, default 30d, 0 disables).
- startup load is bounded newest-first (reuse the dedup memory cap);
  lockedPayoutAddress already falls back per-worker, so evicted
  bindings stay enforced.
- admin unbind accepts worker_ids[] (cap 256) for fleet retirement.

Tests: zz_payout_lock_lifecycle_test.go (red on main for the silent
ok:true and the missing bulk path; green + full package green).
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: jokeez/hackme/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 4d9f255c-ed07-455d-8c47-8d99b9af6eba
📥 Commits

Reviewing files that changed from the base of the PR and between 57d3b7b and 3b382b6.

📒 Files selected for processing (3)
  • cmd/coordinator/work.go
  • cmd/coordinator/work_dedup.go
  • cmd/coordinator/zz_payout_lock_lifecycle_test.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The coordinator now tracks payout-lock activity in durable storage, removes idle lock rows, and bounds in-memory loading. The admin unbind endpoint accepts single-worker or bulk requests and reports durable deletion failures.

Changes

Payout-lock lifecycle

Layer / File(s) Summary
Durable lock retention and loading
cmd/coordinator/work_dedup.go, cmd/coordinator/zz_payout_lock_lifecycle_test.go
Migration adds and backfills seen_at. Startup runs configured idle-row cleanup and loads the newest locks up to the in-memory limit. Tests cover migration, cleanup settings, and bounded loading with durable fallback.
Lock activity and durable deletion
cmd/coordinator/work.go, cmd/coordinator/work_dedup.go, cmd/coordinator/zz_payout_lock_lifecycle_test.go
workerPayoutStat gains a JSON-excluded activity timestamp. Lock reads refresh durable activity at most hourly. Persistence refreshes activity without replacing the first address. Durable deletion errors prevent the in-memory binding from being cleared. Tests cover persistence, refresh throttling, and unbind outcomes.
Admin unbind requests
cmd/coordinator/work.go, cmd/coordinator/zz_payout_lock_lifecycle_test.go
The endpoint accepts one worker ID or up to 256 worker IDs. Bulk requests validate all IDs before clearing locks and report the number cleared and any failures. Tests cover bulk success, invalid IDs, and the size limit.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Suggested reviewers: jokeez

Merge Risk: ⚪ Minimal · up to 3b382

No actionable issue is established for the payout-lock lifecycle changes; merge after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 3b382

The changes improve administrative recovery and bound startup loading, but identity enforcement becomes more dependent on successful database access. Database read failures can permit rebinding of unloaded workers, and failed activity updates can leave live bindings eligible for deletion after restart.

Retained concerns

  • Medium · security · inferred: Bounded restoration expands fail-open identity enforcement. Locks outside the startup limit now depend on a durable lookup, but lockedPayoutAddress returns an empty binding for any query error. During a database read failure, an authenticated worker-token holder can claim an omitted worker ID using another public key; the claim path treats the ID as unlocked and can establish that address in memory. Subsequent signed submissions then check that in-memory address. The base restored the entire durable table, so successfully loaded locks did not require this read to enforce ownership. Successful durable lookups reject the foreign key; the concern requires an omitted binding and a read failure.
  • Medium · security · inferred: Activity refresh failure can become an unintended ownership release. The memory throttle advances before the durable UPDATE, whose errors are ignored, suppressing further touch attempts for an hour. If the durable timestamp crosses the idle horizon and the coordinator restarts before a successful refresh, startup GC deletes the binding before restoration. Another permitted caller can then bind that worker ID without its previous key or an admin unbind. The base had no automatic expiry. Successful persistence can refresh the timestamp independently, so this is conditional on stale durable activity surviving until restart, not a consequence of every transient write failure.
Security review details

Security Blast Radius

  • inferred — The identified attack paths concern worker IDs managed by the affected coordinator: unloaded durable bindings during lookup failures, or bindings removed after stale activity reaches the expiry horizon. A caller needs access permitted by the work authentication gate and can supply its own public key; admin authority is not needed for claims. Wider tenant or deployment exposure is not established.

Security Findings and Attack Paths

  • inferred — The security-sensitive transition is from unavailable or expired durable authority to treating a caller-selected worker ID as unbound. An alternate key can then become the memory binding used for later signed submissions. These are conditional architecture concerns derived from source, not demonstrated production exploits.

Trust Boundaries and Controls

  • observed — Administrative unbinding uses the admin authentication gate, while work claims and submits accept admin or worker-scoped credentials. The bulk extension does not replace the admin gate. Public-entrypoint routing references in the lifecycle test file resolve to test helpers and tests, not new production HTTP routes.

Resilience and Maintainability Implications

  • inferred — Binding creation and administrative cleanup are not one serialized ownership transition: memory mutation and durable writes occur separately. Concurrent creators can interfere with unbind recovery, but this class of exposure predates the PR and is not counted as a new concern merely because deletion order changed.

Hardening Proposals

  • proposed — Distinguish an absent durable binding from an unavailable lookup and reject ownership transitions when authority cannot be established. Advance the activity throttle only after successful persistence, keeping failed refreshes retryable. If idle expiry is intended to reclaim storage without releasing identity ownership, retain a separate ownership tombstone.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the coordinator payout-lock lifecycle changes. It is specific and related to the main changes, though the parenthetical list makes it longer than necessary.
Description check ✅ Passed The description explains the changes, rationale, migration behavior, and test results. It is mostly complete, but it does not use the template’s Summary and Test plan sections or state how admin endpo…
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden coordinator payout-lock lifecycle and bulk unbind

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Surface durable unbind failures and support validated bulk retirement of worker bindings.
• Reap idle locks, refresh active bindings, and bound startup loading without changing first-address
 enforcement.
• Add lifecycle tests covering migration, garbage collection, loading, and unbind behavior.
Diagram

graph TD
  Attach["Coordinator attach"] --> Migrate["Upgrade timestamps"] --> DB[("Payout lock DB")]
  Attach --> GC["Reap idle locks"] --> DB
  DB --> Load["Bounded warm-up"] --> Cache["Worker cache"]
  Submit["Worker submit"] --> Cache --> DB
  Admin["Admin unbind"] --> DB
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Periodic idle-lock GC
  • ➕ Bounds table growth even when a coordinator runs for longer than the idle horizon.
  • ➖ Adds a background lifecycle and requires coordination with concurrent lock activity.

Recommendation: The attach-time GC and bounded warm-up keep this change focused and preserve durable fallback enforcement. Consider periodic GC separately if coordinators commonly run for long periods without restarting; attach-time GC alone cannot bound growth during such a run.

Files changed (3) +522 / -34

Enhancement (1) +56 / -5
work.goReport unbind failures and accept bulk worker IDs +56/-5

Report unbind failures and accept bulk worker IDs

• Adds a non-JSON timestamp to throttle payout-lock liveness writes. The admin endpoint accepts up to 256 validated worker IDs and reports per-ID failures; single-worker unbind now returns HTTP 500 when the durable delete fails.

cmd/coordinator/work.go

Bug fix (1) +165 / -29
work_dedup.goManage durable payout-lock age, loading, and deletion +165/-29

Manage durable payout-lock age, loading, and deletion

• Adds and backfills seen_at, reaps idle rows on attach, and loads the newest bindings up to the existing memory cap. Throttled lock-read touches refresh active rows without inserting them, while unbind deletes the durable row before clearing memory and surfaces delete errors. First-bound payout addresses remain unchanged.

cmd/coordinator/work_dedup.go

Tests (1) +301 / -0
zz_payout_lock_lifecycle_test.goCover payout-lock lifecycle and admin unbind regressions +301/-0

Cover payout-lock lifecycle and admin unbind regressions

• Adds nine tests for durable-delete failure reporting, successful and bulk unbind, migration backfill, first-address preservation, idle GC, bounded loading with durable fallback, and liveness-touch behavior.

cmd/coordinator/zz_payout_lock_lifecycle_test.go

Comment thread cmd/coordinator/work_dedup.go Fixed
@qodo-code-review

qodo-code-review Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. An evicted lock can fail open ✓ Resolved
Description
lockedPayoutAddress treats every error from its bounded-load fallback SELECT as an absent binding
rather than distinguishing sql.ErrNoRows from a database failure. When an evicted worker's row
cannot be read, identity checks can accept a different payout address even though the durable lock
still exists.
Code

cmd/coordinator/work_dedup.go[R299-301]

+	var seen int64
+	if err := db.QueryRow(`SELECT payout_address, seen_at FROM worker_payout_lock WHERE worker_id=?`, workerID).Scan(&addr, &seen); err != nil {
		return ""
Relevance

●●● Strong

Database failures are indistinguishable from missing rows, creating a security-relevant fail-open
path.

PR-#20

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new bounded warm-up deliberately leaves older rows dependent on the fallback. That fallback
returns an empty address for any scan error, and the identity checks interpret an empty address as
unlocked.

cmd/coordinator/work_dedup.go[218-225]
cmd/coordinator/work_dedup.go[294-301]
cmd/coordinator/work.go[1525-1564]
cmd/coordinator/fuzz_pool.go[766-799]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A database error while fetching an evicted payout lock is interpreted as no lock.
## Fix Focus Areas
- cmd/coordinator/work_dedup.go[276-321]
- cmd/coordinator/work.go[1525-1564]
- cmd/coordinator/fuzz_pool.go[766-799]
## Recommended Fix
Distinguish a missing row from operational SELECT errors and propagate the latter to claim and submit handlers as a rejection or retryable failure. Test an evicted binding with an injected read error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. An in-flight claim can undo an unbind ✓ Resolved
Description
clearPayoutLock deletes the durable row before clearing the in-memory address, leaving a window in
which notePayoutLock can insert the same binding again. If a claim persists that address between
the DELETE and the memory clear, the admin receives success while the row survives for the next
restart.
Code

cmd/coordinator/work_dedup.go[R370-371]

+	if db != nil {
+		res, derr := db.Exec(`DELETE FROM worker_payout_lock WHERE worker_id=?`, workerID)
Relevance

●●● Strong

Unlocked DELETE and concurrent persistence can resurrect a binding after successful administrative
unbind.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Unbind releases the mutex for the DELETE and reacquires it to clear memory. Claim-side binding also
releases the mutex before its insert/upsert, so its insert can run after that DELETE and survive the
final memory clear.

cmd/coordinator/work_dedup.go[366-389]
cmd/coordinator/work.go[1569-1595]
cmd/coordinator/work_dedup.go[337-350]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A claim can reinsert a payout-lock row after admin unbind deletes it but before unbind clears memory.
## Fix Focus Areas
- cmd/coordinator/work_dedup.go[366-389]
- cmd/coordinator/work.go[1569-1595]
## Recommended Fix
Serialize the durable delete and in-memory clear with claim-side binding and persistence for the same worker. Add a concurrent claim/unbind regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. An active worker can lose its payout lock ✓ Resolved
Description
lockedPayoutAddress caches an evicted durable binding and its old seen_at value without
refreshing that value on the first read. If the worker uses its lock shortly before the idle cutoff
and the coordinator restarts before another lock read, startup GC deletes the row despite that use.
Code

cmd/coordinator/work_dedup.go[R316-319]

+	if st := m.worker[workerID]; seen > 0 && st.LockSeenUnix < seen {
+		st.LockSeenUnix = seen
+		m.worker[workerID] = st
+	}
Relevance

●● Moderate

Subtle restart and fallback-liveness interaction; plausible correctness issue but no close
historical precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The touch runs only before the fallback, for an address already present in memory. The fallback
merely copies the durable timestamp, while attach-time GC runs before loading bindings; a fuzz
submit can use the fallback without subsequently persisting the lock.

cmd/coordinator/work_dedup.go[278-320]
cmd/coordinator/work_dedup.go[88-94]
cmd/coordinator/work_dedup.go[149-168]
cmd/coordinator/fuzz_pool.go[766-784]
cmd/coordinator/fuzz_pool.go[825-850]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The first read of an evicted payout lock does not refresh its durable liveness timestamp.
## Fix Focus Areas
- cmd/coordinator/work_dedup.go[299-320]
- cmd/coordinator/work_dedup.go[282-293]
## Recommended Fix
When the durable fallback finds a row whose timestamp is due for refresh, update its seen_at before treating that read as a successful liveness touch. Cover a single read near the GC cutoff followed by restart.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Failed touches can expire live locks ✓ Resolved
Description
lockedPayoutAddress advances LockSeenUnix before touchPayoutLockSeenAt runs, and the UPDATE
discards its error. A transient database failure therefore suppresses another attempt for an hour;
if startup GC runs after the stored timestamp crosses the idle cutoff, it deletes a binding the
worker was using.
Code

cmd/coordinator/work_dedup.go[R333-334]

+	_, _ = m.dedupDB.Exec(`UPDATE worker_payout_lock SET seen_at=? WHERE worker_id=?`,
+		time.Now().Unix(), workerID)
Relevance

●●● Strong

Ignored UPDATE failures combined with preemptive throttling can leave durable liveness stale.

PR-#20

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The throttle is set before the database call, whose result is ignored. GC uses only the durable
timestamp, so the in-memory advance cannot protect the row.

cmd/coordinator/work_dedup.go[282-294]
cmd/coordinator/work_dedup.go[329-334]
cmd/coordinator/work_dedup.go[149-168]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed durable liveness update still advances the in-memory throttle, allowing a live binding's timestamp to expire.
## Fix Focus Areas
- cmd/coordinator/work_dedup.go[285-294]
- cmd/coordinator/work_dedup.go[329-335]
## Recommended Fix
Return and handle UPDATE errors, advancing the successful-touch timestamp only after the update succeeds. Permit a later read to retry a failed touch and log failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Restart can exceed the worker limit ✓ Resolved
Description
loadPayoutLocks loads up to workDedupMaxInMemory() rows into m.worker without applying
m.maxWorkers. With more than the configured worker limit of recently seen locks, startup fills the
map beyond that limit, and the normal claim path rejects new worker IDs as too_many_workers.
Code

cmd/coordinator/work_dedup.go[R223-225]

+	rows, err := m.dedupDB.Query(
+		`SELECT worker_id, payout_address, seen_at FROM worker_payout_lock ORDER BY seen_at DESC LIMIT ?`,
+		workDedupMaxInMemory())
Relevance

●●● Strong

Load cap exceeds worker admission cap, allowing startup state to violate configured worker limits.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The dedup memory limit defaults to 500,000, whereas the worker limit defaults to 200,000. Every
queried payout-lock row is inserted without a capacity check, and claim admission checks the
resulting map size.

cmd/coordinator/work_dedup.go[113-123]
cmd/coordinator/work_dedup.go[223-265]
cmd/coordinator/work.go[332-335]
cmd/coordinator/work.go[1695-1706]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The payout-lock warm-up uses the dedup limit rather than the worker-map capacity.
## Fix Focus Areas
- cmd/coordinator/work_dedup.go[218-265]
- cmd/coordinator/work.go[332-335]
## Recommended Fix
Limit warm-up inserts to available worker-map capacity while retaining durable lookup for unloaded bindings. Test startup with more recent locks than maxWorkers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Large lock tables slow startup ✓ Resolved
Description
migrateWorkDedupTables adds seen_at but creates no index for it, while loadPayoutLocks orders
the entire table by that column and idle GC filters on it. As the table grows, each startup must
scan rows for GC and scan and sort rows for the bounded warm-up, despite the LIMIT.
Code

cmd/coordinator/work_dedup.go[R32-34]

+			payout_address TEXT NOT NULL,
+			seen_at INTEGER NOT NULL DEFAULT 0
		)`,
Relevance

●●● Strong

Missing index directly undermines the PR’s idle-GC and newest-first startup performance goals.

PR-#20

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The schema creates indexes for both dedup timestamp columns but none for worker_payout_lock.seen_at.
The added DELETE predicate and ORDER BY both use that unindexed column.

cmd/coordinator/work_dedup.go[17-41]
cmd/coordinator/work_dedup.go[71-79]
cmd/coordinator/work_dedup.go[149-160]
cmd/coordinator/work_dedup.go[218-225]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new payout-lock timestamp is used for startup deletion and newest-first loading without an index.
## Fix Focus Areas
- cmd/coordinator/work_dedup.go[17-41]
- cmd/coordinator/work_dedup.go[149-160]
- cmd/coordinator/work_dedup.go[218-225]
## Recommended Fix
Create an index on worker_payout_lock(seen_at) after the column upgrade so both existing and fresh installations receive it. Verify migration and query plans in tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Cross-repo context — repo relationships
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 13/18, lines 556/200; both must reach the floor). Router rationale: This is a behaviorally dense lifecycle change spanning migration, durable persistence, GC, bounded loading, concurrency-sensitive unbind/touch ordering, and bulk API handling, creating multiple independent defect opportunities.

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread cmd/coordinator/work_dedup.go
Comment thread cmd/coordinator/work_dedup.go
Comment thread cmd/coordinator/work_dedup.go Outdated
Comment thread cmd/coordinator/work_dedup.go
Comment thread cmd/coordinator/work_dedup.go
Comment thread cmd/coordinator/work_dedup.go
…h-on-success, warm-up caps, seen_at index

- serialize clearPayoutLock (DELETE + memory clear) against persistPayoutLock
  with a dedicated mutex, so an in-flight claim cannot re-insert a binding an
  admin unbind just deleted
- lockedPayoutAddress fails closed when the durable store errors (sentinel
  instead of an empty no-binding read)
- the throttled liveness touch advances only after the UPDATE succeeds, so a
  transient DB failure retries on the next lock read
- first read of an evicted binding refreshes its seen_at immediately
- warm-up load respects maxWorkers, newest-first; capped bindings fall back
  to the durable row per read
- seen_at index is created after the column upgrade, so old-schema installs
  migrate cleanly (index-on-missing-column would fail the migration)
- log lines quote worker ids (CodeQL log-injection hygiene)

Tests: +4 (fail-closed sentinel, evicted-read refresh, retry-on-failed-touch,
maxWorkers cap); full package green.
@jokeez
jokeez merged commit d27e576 into jokeez:main Oct 5, 2026
6 checks passed
@jokeez

jokeez commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Merged as d27e576 — thanks Bobby.

Payout-lock lifecycle hardening (unbind fail-closed, idle GC, bulk unbind, seen_at) is on main and deploying to the hub coordinator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants