Skip to content

Add opt-in LISTEN/NOTIFY long polling - #41

Open
Thom Chiovoloni (thomcc-work) wants to merge 12 commits into
mainfrom
thomcc/long-polling
Open

Thom Chiovoloni (thomcc-work) wants to merge 12 commits into
mainfrom
thomcc/long-polling

Conversation

@thomcc-work

@thomcc-work Thom Chiovoloni (thomcc-work) commented Oct 20, 2026 •

Copy link
Copy Markdown

Summary

Adds opt-in PostgreSQL LISTEN/NOTIFY long polling for orchestration and activity work, adapted from duroxide-pg-opt. The goal is to reduce empty-queue queries and polling-induced latency while preserving existing queue eligibility, locking, and acknowledgment rules. Existing constructors continue to use short polling by default.

let mut config = ProviderConfig::url(database_url);
config.long_poll.enabled = true;
let provider = PostgresProvider::new_with_config(config).await?;

The same configuration is supported for Entra-authenticated connections. SQLx 0.8.3 or later is required for explicit listener reconnect handling; native-TLS configuration is unchanged.

Waiting and recovery

  • Register notification waiters before checking the queue, avoiding lost wakeups. Notifications wake all waiters for the affected queue so tag and session filters do not starve matching consumers.
  • Track delayed visibility and orchestration, activity, and session lease expiry. Future-dated lease hints allow recovery after an owner crashes without waiting for the full fetch poll timeout.
  • Notify after orchestration acknowledgment releases the instance lock. Delayed abandonment also sends an immediate release hint so other already-visible messages need not wait for the abandoned message’s delay.
  • Keep timer storage bounded and rescan after timers fire. Periodic recovery scans default to 60 seconds. The first availability wake is immediate; repeated unsuccessful wakes and timer catch-up scans are coalesced to a 100 ms cadence.
  • Run timer scans in READ ONLY transactions with 200 ms server-side lock and statement timeouts. Server-cancelled scans roll back and return their connections to the pool instead of accumulating blocked backends.
  • Keep at most one timer scan in flight, retaining it across watchdog wakeups and listener reconnects. A one-second polling fallback continues independently of stalled scans or reconnect attempts.
  • After resubscribing, recover deadlines with a fresh scan of both queues. If a scan from before the new LISTEN is still running, retain it and schedule the fresh catch-up scan after it completes.
  • Preserve zero-timeout fetch behavior. The poll deadline bounds notification waiting; it never cancels a potentially mutating database fetch.

Notifications remain hints: database predicates and locks determine whether work can actually be claimed.

Migration and activation

Do not release this PR before #32 and #51. A release containing migration 0030 must also contain #32’s migrations 0027–0028 and #51’s migration 0029. Likewise, #51 must not be released before #32. Publishing these out of order creates a migration ledger that the later combined build rejects; coordinate releases rather than editing migration records.

Migration 0030 installs six disabled notification triggers covering both queues and session leases. Notifications are activated schema-wide only by construction with both MigrationPolicy::ApplyAll and long polling enabled. VerifyOnly performs no DDL and requires prior activation. Disabling an individual provider’s listener does not disable shared triggers.

Migration and activation transactions use 250 ms lock timeouts and jittered retries for deadlocks and lock contention, with a ten-second retry budget. Failed attempts roll back without advancing the ledger. Migration errors retain the database message and SQLSTATE source. Connections are preserved for reuse only after successful migration and confirmed advisory-lock release; errors and cancellation close them.

The cleanup function preserves migration 0028’s batched-pruning cleanup. Disposable databases that ran an earlier draft of this PR as migration 0027 must be recreated. Older binaries still reject unknown migration versions even when notifications are disabled. This does not convert databases using the incompatible duroxide-pg-opt migration history.

Operational considerations

  • Long polling reserves one connection within the configured pool and requires at least two. It warms another connection for fetches. LISTEN requires a session-preserving connection; transaction-pooling proxies are not a substitute.
  • Activation adds notification costs to successful orchestration and activity claims, lease-changing renewals, and session-lease writes, not just enqueue and acknowledgment. Empty fetches do not notify. These costs also affect short-polling callers of an activated schema.
  • PostgreSQL serializes notification-emitting commits through a server-wide lock held through commit. WAL or synchronous-replication stalls can delay other notification writers, including in other databases. Evaluate representative write and renewal workloads before activation.
  • Transactions that emit notifications cannot use PREPARE TRANSACTION. Activated queue and session-lease writes are therefore incompatible with PostgreSQL two-phase commit.
  • Detected disconnects recover automatically, but silent network partitions are not actively probed. SQLx listener cleanup can still wait indefinitely on a wedged socket, so shutdown has no hard cleanup deadline in that case.
  • Providers must be recreated if their queue tables are dropped and recreated, because notification channels identify the original tables.

Copilot AI 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.

🔵 Needs a closer look

Unresolved wakeup gaps and schema-wide listener recovery require fixes and PostgreSQL-backed human validation.

2 open issues
What changed in this PR

Adds opt-in notification-driven long polling to the PostgreSQL provider while preserving short polling by default.

Changes:

  • Adds listener recovery, bounded timer tracking, and long-poll configuration.
  • Installs disabled notification triggers with explicit schema-wide activation.
  • Adds integration coverage, CI validation, and operational documentation.
File Description
tests/​postgres_provider_test.rs Enables validation in either polling mode.
tests/​long_poll_tests.rs Adds integration tests and an idle-query benchmark.
src/​provider.rs Integrates configuration and long-poll fetches.
src/​notifier.rs Implements listeners, timers, recovery, and waiting.
src/​migrations.rs Adds notification activation and verification.
src/​lib.rs Exports and documents long-poll configuration.
README.md Documents usage and operational limitations.
migrations/​README.md Documents activation and test commands.
migrations/​0027_diff.md Records migration changes.
migrations/​0027_add_long_poll_notifications.sql Installs disabled triggers and updates cleanup.
docs/​migration-policy.md Explains activation and compatibility.
CHANGELOG.md Records the unreleased feature.
Cargo.toml Raises the SQLx minimum to 0.8.3.
.github/​workflows/​test.yml Runs validation with long polling enabled.

Review effort: Balanced

Comment thread src/notifier.rs Resolved

Copilot AI 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.

🔵 Needs a closer look

Schema-wide trigger activation and concurrency-sensitive listener, timer, and cancellation behavior warrant human review backed by PostgreSQL integration results.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

- Future-dated lease hints, including session expiry, plus immediate release hints for delayed abandonment.
- Warm fetch connections, bounded timer scans, and independent degraded-mode fallback.
- Bounded, jittered migration/activation retries with rollback and deadlock regressions.
- Updated trigger inventory, deterministic catch-up coverage, and a reproducible benchmark driver.
Run timer reads in transactions with server-side lock and statement timeouts. Retain one owned scan across client watchdog wakeups and listener reconnects instead of cancelling and replacing an in-flight query.

Add the supplied backend-accumulation regression, connection-reuse and single-flight coverage, and a timer wake deadline that cannot pass through the polling fallback. Addresses review items 41-N1 and 41-N5.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Place long-poll notification DDL after the pending 0027-0029 migrations, preserving migration 0028's prune_executions_batch cleanup and companion diff baseline. Update upgrade tests and documentation without stacking the prerequisite PRs.

Include SQLSTATE and the full context chain in DDL retry warnings, test those diagnostics, and document draining writers and retrying ApplyAll when the fixed contention budget expires. Validated the intended predecessor sequence in an isolated source copy. Addresses 41-C1, 41-N3, and 41-N4.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a 16+16 dispatcher, 64-client profile with sampled notification-commit and synchronous-replication wait time. Exercise workflow claims and saturated orchestration, activity, and session renewals with verified synchronous-standby and pool-capacity preconditions.

Document notification-bearing operations and reproducible benchmark methodology without publishing performance results from uncontrolled runs. Include timer transaction control in idle SQL counts. Addresses review item 41-N2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the database message in ordinary migration error display while retaining the SQLx source for SQLSTATE retry classification. Add a red/green regression that also proves failed migration statements leave the ledger, stored function, and durable rows unchanged.

State that migration 0030 must not ship before the 0027-0029 prerequisites, including the dependency of #51 on #32. Addresses review items 41-N6 and 41-N7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep timer scans single-flight, but queue a fresh catch-up for both queues when a reconnect adopts an older snapshot. Run scan transactions READ ONLY and verify rejected writes preserve durable state and connection defaults.

Replace SQL-lock timing gates with controlled acquisition gates, verify successful commits rather than idle backends, and wait through reconnect re-entry before checking the scan count. Add the disconnect-gap regression and benchmark statistics-role preflight/documentation. Addresses 41-N8 and all 41-N9 nits; no performance figures are published.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep close-on-drop armed for migration failures, unconfirmed unlocks, and cancellation, but explicitly return the healthy connection to SQLx after success and confirmed advisory-lock release. This leaves short-poll startup with its original warm connection.

Add backend-identity and immediate-idle assertions for migration/no-op/activation success, plus failed-unlock and cancellation safety regressions. Update the migration lifecycle documentation. Addresses review item 41-N10.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match #51's associated load_migrations and parse_version functions, including the failed-unlock regression added by #41. This keeps the standalone branch buildable and avoids E0599 when the prerequisite changes are combined.

Retain b33a3f2's connection-reuse behavior without changing pool sizing or tuning for the upstream barrier-based test. Addresses review item 41-N11.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Guard migration connections with RAII: unsuccessful exits mark the connection for closure before its field is dropped, while successful migration and confirmed unlock allow SQLx's normal asynchronous pool return.

Keep the same-backend and advisory-lock regressions, remove the immediate-idle assertion, and document asynchronous return. No pool sizing changes or timing-test workarounds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep both delayed items enqueued up front, but spawn the second fetch without asserting that it is still waiting. A slow dispatcher may start that fetch after the second item is already eligible.

Exercise both queues with immediate and deliberately late second fetches. The added late-start case reproduces the old assertion failure and passes after the fix; the shared waiting helper and production code remain unchanged. Addresses review item 41-N12.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@tjgreen42 Todd J. Green (tjgreen42) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review by GPT-6.1 Sol.

No actionable findings in this revision. Reviewed listener startup/reconnection, wakeup registration and deferred-work deadlines, schema-wide trigger activation, polling fallback, shutdown, and migration compatibility. The earlier lost-wakeup/recovery concerns are addressed. The documented SQLx limitation around unbounded graceful pool.close() on a wedged listener is not a hang in provider/runtime drop, which aborts the listener without awaiting cleanup.

Scope: static review at 20dffa7, including the relevant SQLx cleanup behavior. No local PostgreSQL integration tests were run; local nextest compilation was blocked by the available Rust 1.88 toolchain versus a resolved dependency requiring Rust 1.89.

@tjgreen42

Copy link
Copy Markdown
Contributor

GPT-6.1 Sol: Please refresh the PR description to match the current branch. It still says the PR is a draft, but it is marked ready for review. Please also mention the later hardening: lease-expiry hints; read-only timer scans with 200 ms server-side timeouts; single-flight scans retained across watchdog wakeups and reconnects; and fresh catch-up scans of both queues after reconnect. The operational warning should explicitly include notification costs for successful claims, lease-changing renewals, and session-lease writes. The migration 0030 and prerequisite release-order notes still appear current.

@tjgreen42

Copy link
Copy Markdown
Contributor

Thom Chiovoloni (@thomcc-work) Are we planning to add the capability to migrate from a pre-existing duroxide-pg-opt installation?

@tjgreen42 Todd J. Green (tjgreen42) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good, strictly an improvement on duroxide-pg-opt in several respects. Nice job. What's left before we can turn it on by default? (Stress-testing to make sure notifications don't get out of hand under high load?)

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