Subject: [pgjdbc/pgjdbc] PR #4010: [#2325] Execute queries as pipelined when running on virtual thread

From: "beikov (@beikov)" <[email-masked]>
Date: Thu, 26 Mar 2026 17:35:30 +0000

Alternative design to https://github.com/pgjdbc/pgjdbc/pull/4009 that uses local queues per pipeline execution.

### All Submissions:

* [ ] Have you followed the guidelines in our [Contributing](https://github.com/pgjdbc/pgjdbc/blob/master/CONTRIBUTING.md) document?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change?

<!-- You can erase any parts of this template not applicable to your Pull Request. -->

### New Feature Submissions:

1. [ ] Does your submission pass tests?
2. [ ] Does `./gradlew styleCheck` pass ?
3. [ ] Have you added your new test classes to an existing test suite in alphabetical order?

### Changes to Existing Features:

* [ ] Does this break existing behaviour? If so please explain.
* [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
* [ ] Have you written new tests for your core changes, as applicable?
* [ ] Have you successfully run tests with your changes locally?

---

From: "beikov (@beikov)" <[email-masked]>
Date: Fri, 27 Mar 2026 11:59:49 +0000

The design is pretty simple. First, the code extracted the queues of the `QueryExecutor` into a wrapper object and passes that to all methods instead of relying on the shared state on the `QueryExecutor`.
That way, a high level operation can isolate all necessary queue objects when multiple threads call into the `QueryExecutor`.

Next, the new `executePipelined` method was introduced that creates isolated queues and splits its work into 3 phases:

1. While holding a write lock, acquire a "ticket" and send operations to the server
2. Lock and wait until it's the turn of the "ticket"
3. Process results

When done, it signals that the next "ticket" can start it's work.

This way, we can send multiple operations to the server in a pipelined fashion, but process results in FIFO order.
With e.g. virtual threads, when a read blocks a VT1, another virtual thread VT2 can send further commands before it enters Phase 2 and waits for its turn. As long as VT1 is waiting, further virtual threads can send even further commands.
Once VT1 is woken up, it needs to process the results and complete before further operations can be sent by other virtual threads.

This means that new operations can only be added while the thread whose turn it is waits for data in Phase 3. To achieve better pipeline usage, we'd need to defer the waking of the thread whose turn it is until all other virtual threads submitted their operations, but I don't know if there is a good way to do that.

Maybe someone has an idea how to achieve better utilization?

---

From: "beikov (@beikov)" <[email-masked]>
Date: Fri, 27 Mar 2026 12:25:46 +0000

Maybe someone here knows how virtual thread scheduling roughly works?

If e.g. VT1 was blocked and there is a not yet started VT2 in the queue - when VT1 is ready again because a poller thread got data, will the scheduler schedule VT2 or VT1 again? So the question is, does the scheduler try to let every virtual thread act at least once, before scheduling a previously blocked thread again? If it does that, then there should be no utilization problem, and all operations will be sent before any result processing happens.