Skip to content

chore: run settlement/cache poll reads without stalling on DDL#1400

Merged
MicBun merged 1 commit into
mainfrom
refactor/settlement-cache-deadlock-safe-reads
Jul 1, 2026
Merged

chore: run settlement/cache poll reads without stalling on DDL#1400
MicBun merged 1 commit into
mainfrom
refactor/settlement-cache-deadlock-safe-reads

Conversation

@MicBun

@MicBun MicBun commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

resolves: #1401

Re-running a schema migration — or any DDL exec-sql — against a live node could deadlock the whole network. A background poller (the settlement scheduler on the leader, the cache refresher on every node) held the engine interpreter's read lock while its SELECT waited on a Postgres table lock. If a block was applying ALTER/DROP/CREATE on a table that poller reads, the block held that table's AccessExclusiveLock and needed the interpreter's write lock, while the poller held the interpreter read lock and needed the table lock. Neither could proceed and the chain stopped producing blocks.

There is a second, related variant: the settlement config reload runs inside EndBlock; if the same block altered settlement_config, that read self-blocks at the Postgres layer and the block can never commit.

This makes those poll reads unable to deadlock block production:

  • Settlement runs its four poll reads (config, unsettled markets, attestation existence, market components) on a dedicated read-only connection pool as plain SQL — off the engine interpreter entirely — so a settlement read can never hold the interpreter lock while waiting on a table lock.
  • Settlement and cache bound every connection with lock_timeout (3s). A read blocked by an in-block DDL lock now fails fast and retries on the next tick instead of hanging. Ordinary DML never conflicts with a plain SELECT, so this only ever trips during real DDL contention.
  • The settlement reads fail closed: if the dedicated pool can't be built, reads return an error (the scheduler skips and retries) rather than falling back to the interpreter.

The change is node-only and consensus-neutral — these reads are not part of any block's write set.

Verified

  • Real-Postgres liveness test: a read blocked by an ACCESS EXCLUSIVE lock fails fast in ~3s instead of hanging (it would hang → fail the test on the old path).
  • Settlement integration suite, tn_cache suite, and settlement/cache unit tests all pass; full build clean.
  • Added no-DB regression guards asserting both pools set lock_timeout, and that settlement reads fail closed when the pool is unavailable.

Follow-up (not in this PR)

  • tn_lp_rewards and tn_digest share the same EndBlock config-reload pattern. It is not reachable today (no migration alters their config tables), so it is tracked as a hardening item rather than changed here.

Summary by CodeRabbit

  • New Features

    • Added a dedicated read-only pool for settlement polling reads, improving reliability under database load.
    • Read operations now use a bounded lock wait to avoid long stalls.
  • Bug Fixes

    • Settlement reads now fail fast when the read connection isn’t available instead of falling back to slower or riskier paths.
    • Improved handling of locked tables so reads return sooner with a clear timeout error.

@holdex

holdex Bot commented Jul 1, 2026

Copy link
Copy Markdown

Time Submission Status

Member # Time Running Total Status Last Update
MicBun 4h ✅ Submitted Jul 1, 2026, 11:42 AM

Submit or update total time with:

@holdex pr submit-time 2h

Add time on top of previous submission with:

@holdex pr add-time 1h30m

See available commands to help comply with our Guidelines.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a bounded Postgres lock_timeout to the tn_cache connection pool via a new buildCachePoolConfig helper. Introduces a dedicated, lock-timeout-bounded ReadPool for tn_settlement scheduler polling reads, rewiring EngineOperations to query directly via this pool instead of the engine interpreter, with corresponding lifecycle wiring and tests.

Changes

tn_cache lock-timeout wiring

Layer / File(s) Summary
Cache pool config with lock_timeout
extensions/tn_cache/tn_cache.go, extensions/tn_cache/pool_config_test.go
Extracts pool config into buildCachePoolConfig, adds cacheReadLockTimeoutMillis constant, injects lock_timeout into pool runtime params, and adds a unit test verifying the value.

tn_settlement independent read pool

Layer / File(s) Summary
ReadPool implementation and tests
extensions/tn_settlement/internal/read_pool.go, extensions/tn_settlement/internal/read_pool_test.go, extensions/tn_settlement/internal/read_pool_pglive_test.go
Adds ReadPool type with Execute/BeginTx/Close, buildReadPoolConfig wiring lock_timeout, connectivity checks, and unit/integration tests including a blocked-read lock-timeout scenario.
EngineOperations read paths via readDB
extensions/tn_settlement/internal/engine_ops.go, extensions/tn_settlement/internal/engine_ops_test.go, extensions/tn_settlement/settlement_integration_test.go
Adds readDB field/param, toInt64 helper, and rewrites LoadSettlementConfig, FindUnsettledMarkets, AttestationExists, GetMarketQueryComponents to use direct SQL with nil checks; updates tests for fail-closed behavior and mocks.
Extension and startup wiring
extensions/tn_settlement/extension.go, extensions/tn_settlement/tn_settlement.go
Adds readPool field, SetReadPool method, closes it on shutdown, and creates/attaches the pool in engineReadyHook with fail-closed handling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant EngineOperations
  participant ReadPool
  participant Postgres

  Scheduler->>EngineOperations: LoadSettlementConfig / FindUnsettledMarkets / AttestationExists
  EngineOperations->>EngineOperations: check readDB != nil
  alt readDB is nil
    EngineOperations-->>Scheduler: error "settlement read handle not initialized"
  else readDB set
    EngineOperations->>ReadPool: Execute(SQL statement)
    ReadPool->>Postgres: acquire connection, run query (lock_timeout applied)
    Postgres-->>ReadPool: rows or lock timeout error
    ReadPool-->>EngineOperations: sql.ResultSet or error
    EngineOperations-->>Scheduler: parsed result or error
  end
Loading

Suggested labels: type: fix

Suggested reviewers: pr-time-tracker

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.00% which is sufficient. The required threshold is 80.00%.
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 matches the main change: moving settlement and cache poll reads to avoid stalling on DDL.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/settlement-cache-deadlock-safe-reads

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
extensions/tn_cache/tn_cache.go (1)

460-463: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Naming: constant says "Read" but comment/usage covers writes too.

cacheReadLockTimeoutMillis is applied to the whole pool (reads and writes per the comment at Lines 487-489). Consider renaming to something like cacheLockTimeoutMillis for accuracy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/tn_cache/tn_cache.go` around lines 460 - 463, The constant name in
tn_cache.go is misleading because its comment and pool usage cover both cache
reads and writes, not just reads. Rename cacheReadLockTimeoutMillis to a more
accurate symbol like cacheLockTimeoutMillis, and update any related
references/comments in the cache lock timeout setup so the name matches the
behavior in createIndependentConnectionPool and the pool configuration code.
extensions/tn_settlement/tn_settlement.go (1)

61-79: 🩺 Stability & Availability | 🔵 Trivial

No rebuild path if NewReadPool fails at startup.

If pool construction fails transiently at boot (e.g., DB not yet reachable), readDB stays nil for the rest of the process's lifetime — settlement reads are disabled "until restart," as the comment states. Since there's already a background retry worker (ext.startRetryWorker()) for config-reload resilience, consider whether it should also periodically attempt to (re)build the read pool, so a transient startup blip doesn't require a manual restart to recover.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/tn_settlement/tn_settlement.go` around lines 61 - 79,
`NewReadPool` only runs once during startup, so a transient failure leaves
`readDB` nil for the rest of the process and disables settlement reads until
restart. Update the startup flow in `tn_settlement.go` around
`internal.NewReadPool` and `internal.NewEngineOperations` to add a retry/rebuild
path, likely by extending the existing `ext.startRetryWorker()` logic or a
similar background worker, so it periodically re-attempts building the read pool
and rebinds `readDB` when the DB becomes available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extensions/tn_settlement/internal/read_pool.go`:
- Around line 45-55: NewReadPool is still building the PostgreSQL DSN by
concatenating dbConfig.Pass into the conn string, which breaks passwords
containing spaces or special characters. Update NewReadPool to stop appending
the password into connStr and instead let newReadPoolFromConnString or its
parsing path set the password on the parsed pgx ConnConfig after ParseConfig.
Keep the fix localized around NewReadPool and the connection-string parsing flow
so the password is handled via ConnConfig.Password rather than raw DSN
interpolation.

---

Nitpick comments:
In `@extensions/tn_cache/tn_cache.go`:
- Around line 460-463: The constant name in tn_cache.go is misleading because
its comment and pool usage cover both cache reads and writes, not just reads.
Rename cacheReadLockTimeoutMillis to a more accurate symbol like
cacheLockTimeoutMillis, and update any related references/comments in the cache
lock timeout setup so the name matches the behavior in
createIndependentConnectionPool and the pool configuration code.

In `@extensions/tn_settlement/tn_settlement.go`:
- Around line 61-79: `NewReadPool` only runs once during startup, so a transient
failure leaves `readDB` nil for the rest of the process and disables settlement
reads until restart. Update the startup flow in `tn_settlement.go` around
`internal.NewReadPool` and `internal.NewEngineOperations` to add a retry/rebuild
path, likely by extending the existing `ext.startRetryWorker()` logic or a
similar background worker, so it periodically re-attempts building the read pool
and rebinds `readDB` when the DB becomes available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d53b5df9-91a4-472f-8fb5-ed153640dff1

📥 Commits

Reviewing files that changed from the base of the PR and between d7334d2 and 53fdbcd.

📒 Files selected for processing (10)
  • extensions/tn_cache/pool_config_test.go
  • extensions/tn_cache/tn_cache.go
  • extensions/tn_settlement/extension.go
  • extensions/tn_settlement/internal/engine_ops.go
  • extensions/tn_settlement/internal/engine_ops_test.go
  • extensions/tn_settlement/internal/read_pool.go
  • extensions/tn_settlement/internal/read_pool_pglive_test.go
  • extensions/tn_settlement/internal/read_pool_test.go
  • extensions/tn_settlement/settlement_integration_test.go
  • extensions/tn_settlement/tn_settlement.go

Comment thread extensions/tn_settlement/internal/read_pool.go
@MicBun MicBun self-assigned this Jul 1, 2026
@MicBun

MicBun commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@holdex pr submit-time 4h

@MicBun MicBun force-pushed the refactor/settlement-cache-deadlock-safe-reads branch from 53fdbcd to 6c39173 Compare July 1, 2026 11:47
@MicBun MicBun changed the title refactor: run settlement/cache poll reads without stalling on DDL chore: run settlement/cache poll reads without stalling on DDL Jul 1, 2026
@MicBun MicBun merged commit be51cb8 into main Jul 1, 2026
9 of 10 checks passed
@MicBun MicBun deleted the refactor/settlement-cache-deadlock-safe-reads branch July 1, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Problem: operators can't run migrations on a live node safely

1 participant