Skip to content

Support Yugabyte as a Postgres target - #1347

Merged
brandur merged 1 commit into
masterfrom
brandur-yugabyte-support
Sep 28, 2026
Merged

brandur merged 1 commit into
masterfrom
brandur-yugabyte-support

Conversation

@brandur

@brandur brandur commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

This one's aimed at #1346, in which it might be possible for us to
support Yugabyte as a database target without a hugely inordinate amount
of work.

Yugabyte is currently targeting compatibility against Postgres 15 [1].
It doesn't support xmax which is what #1346 is about, but somewhat
surprisingly, we only use xmax in one place and don't use any other
Postgres 16+ features (as Postgres 15 is still a valid target in the
CI matrix).

The xmax trick to determine whether an upserted row is new or existing
is a little outdated anyway because Postgres 18 added the capability to
detect an existing row with OLD.id IS NOT NULL [2].

Long run, we should switch to that for everything. Shorter term,
Postgres 18 is still quite new, so I propose we do something like this:

  • If on Postgres 18+ (we should be getting Postgres 19 soon), use
    OLD.id IS NOT NULL.

  • If on Yugabyte, fall back to the same trick we use in SQLite by
    upserting rows with a unique nonce and checking whether the nonce was
    the one we inserted or not.

  • Otherwise, use the existing approach with xmax.

We do have to check which database we're on, but only once, after which
we can cache that information forever, so it shouldn't have any impact
on performance.

Fixes #1346.

[1] https://docs.yugabyte.com/stable/faq/compatibility/#what-is-the-extent-of-compatibility-with-postgresql
[2] https://www.crunchydata.com/blog/postgres-18-old-and-new-in-the-returning-clause


// UniqueInsertMetadataWithNonce returns metadata with nonce set under
// UniqueInsertMetadataKey.
func UniqueInsertMetadataWithNonce(metadata []byte, nonce string) ([]byte, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This approach originally comes from SQLite, but extracted out to here to be reusable.

@brandur

brandur commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@bgentry Thoughts on this?

I'm kind of thinking that we wouldn't be able to officially support Yugabyte since I really don't want to be testing against it, but we could have soft support that works as long as they commit to their stated PG 15 contract with a few very minor deviations (like the xmax thing which admittedly is a little bit abusive of Postgres anyway).

@brandur
brandur requested a review from bgentry August 11, 2026 20:21
@jqueuniet

Copy link
Copy Markdown

I tested this branch in our test environment and our service managed to both enqueue asynchronous tasks and run scheduled ones without any other error.

@brandur

brandur commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@jqueuniet Excellent! Thanks for checking. Next we'll see if it can stand the stress of a DB-based job queue ...

@brandur
brandur force-pushed the brandur-yugabyte-support branch 3 times, most recently from 79e78e8 to e166e64 Compare August 31, 2026 22:14
Comment thread client.go Outdated
// non-functional. Here we try to make an initial assessment of health and
// return quickly in case of an apparent problem.
if err := c.driver.GetExecutor().Exec(fetchCtx, "SELECT 1"); err != nil {
if err := c.driver.GetExecutor().Ping(fetchCtx); err != nil {

@bgentry bgentry Sep 1, 2026 •

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.

🤖 via Codex: This changes the startup health check into a one-time capability initialization. Once uniqueInsertMode has been cached, normally by initialPing, Ping returns successfully without performing any database I/O.

A later Start or restart can therefore succeed while PostgreSQL is unavailable, which is the exact case this block is intended to reject, especially for poll-only and database/sql clients.

Could we keep a real SELECT 1 or pool ping on every Start and initialize the insert mode separately, or make Ping always reach the database?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So both comments here were on code that I'd brought in near the last second — previously, I'd had the drivers lazily check whether a database is Yugabyte by doing it on first insert, and the idea here was to do an aggressive database check on client start. As uncovered by these issues though, doing so leads to added complexity and a number of possible races, so I think the whole scheme was more trouble than it was worth, and have now removed it.

return mode, nil
}

e.driver.uniqueInsertModeInitMu.Lock()

@bgentry bgentry Sep 1, 2026 •

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.

🤖 via Codex: Holding this non-context-aware mutex across PGGetProductAndVersion can make inserts wait for the full 10-second initialPing timeout even after their own context is canceled.

There is also a MaxConns=1 cycle: an existing transaction holds the sole connection, initialPing takes this mutex and waits for the pool, and InsertTx uses the existing transaction but blocks on this mutex. Progress resumes only when initialPing times out. The new test pauses before the real Ping takes this lock, so it does not cover the cycle.

Could we avoid background initialization and resolve lazily, or permit redundant detection queries or context-aware waiting instead of holding a mutex across database I/O? The database/sql implementation has the same issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See comment above.

@brandur
brandur force-pushed the brandur-yugabyte-support branch 2 times, most recently from a098ddf to 2b9a430 Compare September 2, 2026 23:02
@brandur
brandur force-pushed the brandur-yugabyte-support branch from 2b9a430 to e32e5dc Compare September 21, 2026 21:18
@brandur

brandur commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

@bgentry Pushed a new version of this. Mind taking a look?

@bgentry

bgentry commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

The reporter is using Yugabyte 2025.2.1, where LISTEN/NOTIFY does not deliver events. River's PostgreSQL driver still reports listener support, so a client with the default PollOnly: false creates a notifier. The producer then skips pollForSettingChanges, even though it will receive no queue-control notifications. Pause, resume, and queue metadata changes can remain unapplied by running workers until restart; the enqueue and scheduled-job smoke test would not catch this.

Could we make Yugabyte use polling when notification support is unavailable, and add coverage for queue-control changes? If support requires a Yugabyte version and flags that enable LISTEN/NOTIFY, that requirement should also be explicit.

@bgentry bgentry 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.

Leaving just a single bot-written finding that I didn't have time to fully review before heading to dinner, but otherwise LGTM and ship it when ready ✌️

@brandur
brandur force-pushed the brandur-yugabyte-support branch 2 times, most recently from 853ecdc to afd40a7 Compare September 28, 2026 05:36
@jqueuniet

Copy link
Copy Markdown

Yugabyte supports LISTEN/NOTIFY with v2025.2.3 and later, but it also requires enabling a feature flag for now as the feature is still in early access mode.

https://docs.yugabyte.com/stable/api/ysql/the-sql-language/statements/cmd_listen_notify/

@brandur

brandur commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Nice. Okay, I modified this to also check for listen/notify and fall back to poll only where not available.

Yugabyte supports LISTEN/NOTIFY with v2025.2.3 and later, but it also requires enabling a feature flag for now as the feature is still in early access mode.

https://docs.yugabyte.com/stable/api/ysql/the-sql-language/statements/cmd_listen_notify/

Yep, good catch. My implementation here will check for the listen/notify setting specifically:

coalesce(current_setting('yb_enable_listen_notify', true), 'off')::boolean

@brandur

brandur commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T12:10:14.753496Z f0247c2 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: afd40a7d84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ID: params.ID,
CancelAttemptedAt: cancelledAt,
ControlTopic: params.ControlTopic,
Notify: capabilities.SupportsListenNotify,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Poll for remote cancellations when notifications are disabled

When YugabyteDB reports LISTEN/NOTIFY disabled, this sets Notify to false, so cancelling a running job only records cancel_attempted_at while deliberately leaving the job in running. For a job executing in another process, the only prompt path to producer.maybeCancelJob is the control notification, while poll mode only polls queue settings; consequently JobCancel returns success but the worker context is never cancelled and may continue indefinitely. Add a cancellation polling or equivalent cross-process fallback, including coverage in the shared driver behavior suite.

AGENTS.md reference: AGENTS.md:L25-L26

Useful? React with 👍 / 👎.

@brandur
brandur force-pushed the brandur-yugabyte-support branch from afd40a7 to 7c0c1ed Compare September 28, 2026 11:34
@brandur

brandur commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c0c1ed27e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +939 to +940
if !capabilities.SupportsListenNotify {
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve leader resignation in no-notify mode

When Yugabyte reports LISTEN/NOTIFY unavailable, both PostgreSQL executors return success here without delivering leadership messages. After QueueMaintainerLeader.tryStart exhausts its three startup attempts, it relies exclusively on ClientNotifyBundle.RequestResign; that call funnels through this no-op, so the elector never receives pendingRequestResign, continues renewing its term, and maintenance such as scheduling, rescue, periodic enqueueing, and cleanup can remain stopped indefinitely instead of another client taking over. Add a pollable or direct local resignation path for this recovery flow, with shared driver coverage.

AGENTS.md reference: AGENTS.md:L25-L26

Useful? React with 👍 / 👎.

@brandur
brandur force-pushed the brandur-yugabyte-support branch from 7c0c1ed to f0247c2 Compare September 28, 2026 12:01
@brandur

brandur commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: f0247c20ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@brandur
brandur force-pushed the brandur-yugabyte-support branch from f0247c2 to 43f2ec6 Compare September 28, 2026 12:17
This one's aimed at #1346, in which it might be possible for us to
support Yugabyte as a database target without a hugely inordinate amount
of work.

Yugabyte is currently targeting compatibility against Postgres 15 [1].
It doesn't support `xmax` which is what #1346 is about, but somewhat
surprisingly, we only use `xmax` in one place and don't use any other
Postgres 16+ features (as Postgres 15 is still a valid target in the
CI matrix).

The `xmax` trick to determine whether an upserted row is new or existing
is a little outdated anyway because Postgres 18 added the capability to
detect an existing row with `OLD.id IS NOT NULL` [2].

Long run, we should switch to that for everything. Shorter term,
Postgres 18 is still quite new, so I propose we do something like this:

* If on Postgres 18+ (we should be getting Postgres 19 soon), use
  `OLD.id IS NOT NULL`.

* If on Yugabyte, fall back to the same trick we use in SQLite by
  upserting rows with a unique nonce and checking whether the nonce was
  the one we inserted or not.

* Otherwise, use the existing approach with `xmax`.

We do have to check which database we're on, but only once, after which
we can cache that information forever, so it shouldn't have any impact
on performance.

Fixes #1346.

[1] https://docs.yugabyte.com/stable/faq/compatibility/#what-is-the-extent-of-compatibility-with-postgresql
[2] https://www.crunchydata.com/blog/postgres-18-old-and-new-in-the-returning-clause
@brandur
brandur force-pushed the brandur-yugabyte-support branch from 43f2ec6 to b182275 Compare September 28, 2026 12:18
@brandur

brandur commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Thx! Adding the listen/notify stuff definitely led Codex to find some knock on bugs, but they should be all fixed up now.

@brandur
brandur merged commit 3f43e6c into master Sep 28, 2026
15 checks passed
@brandur
brandur deleted the brandur-yugabyte-support branch September 28, 2026 14:14
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.

Yugabyte compatibility issues when inserting new tasks

3 participants