Skip to content

ADR-0011: single-home Claim eligibility (app.eligible_simulations)#8

Merged
ryanntannn merged 1 commit into
masterfrom
impl/0011-eligibility
Jun 25, 2026
Merged

ADR-0011: single-home Claim eligibility (app.eligible_simulations)#8
ryanntannn merged 1 commit into
masterfrom
impl/0011-eligibility

Conversation

@ryanntannn

Copy link
Copy Markdown
Contributor

Implements ADR-0011 — gives the per-User Claim-eligibility predicate a single home.

Change

  • New migration db/migrations/0006_eligible_simulations.sqlapp.eligible_simulations(version, cap) owns the predicate (state='queued' AND engine_version=p AND per-user running-count < cap) + ordering.
  • runner/synergy_runner/db.pyCLAIM_SQL now uses the function as a membership gate with FOR UPDATE OF s2 SKIP LOCKED on the base table; advisory-lock H-2 wrapper unchanged.
  • internal/controller/runnerpool_controller.go — KEDA eligibleQuerySELECT count(*) FROM app.eligible_simulations(...).
  • config/keda/scaledobject.yaml — doc sample updated.

Why the membership-gate form (not the obvious wrap)

Wrapping app.eligible_simulations(...) FOR UPDATE SKIP LOCKED silently drops the row lock on Postgres 16 (verified: no LockRows node, double-claims under concurrency). Locking + LIMIT stay on the base table.

Verification (docker PG16)

EXPLAIN shows LockRows preserved · two-session concurrency claims different rows · eligible_simulations returns correct depth · go build/test green. Python suite not run (no venv in worktree).

Design rationale: ADR-0011. Validated under worktree isolation in #7.

🤖 Generated with Claude Code

https://claude.ai/code/session_019p51SPdbrjmGXJVeZnwTrD

The per-User claim-eligibility predicate (state='queued', engine_version match,
per-user running-count < cap) plus the claim ordering (priority DESC,
created_at ASC) was duplicated across three runtimes that must agree: the Python
claim (runner db.py), the Go KEDA eligible-depth query, and the KEDA
scaledobject doc sample. Give it one home as a STABLE set-returning Postgres
function that owns the predicate + ordering but NOT row locking or the UPDATE.

- db/migrations/0006_eligible_simulations.sql: new
  app.eligible_simulations(engine_version, user_cap) RETURNS SETOF
  app.simulations (idempotent, CREATE OR REPLACE).
- runner/synergy_runner/db.py: rewrite CLAIM_SQL to a membership-gate form —
  JOIN the function to the base table and keep FOR UPDATE OF s2 SKIP LOCKED on
  app.simulations so the row lock stays on the base table (wrapping the function
  in FOR UPDATE silently drops the lock on PG16 -> double-claims). Advisory-lock
  H-2 logic untouched.
- internal/controller/runnerpool_controller.go: eligibleQuery becomes
  SELECT count(*) FROM app.eligible_simulations('<ver>', <cap>).
- config/keda/scaledobject.yaml: doc sample query + header updated to the
  function.

Verified on PG16: all migrations 0001-0006 apply in order; EXPLAIN of the new
claim shows a LockRows node on the base table; a two-session concurrency test
claims two different rows (no block/overwrite); eligible-depth count correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019p51SPdbrjmGXJVeZnwTrD
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.

1 participant