pg_pipeline 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +85 -0
- data/DESIGN.md +40 -0
- data/README.md +105 -1
- data/lib/pg_pipeline/bounded_queue.rb +10 -12
- data/lib/pg_pipeline/client.rb +14 -21
- data/lib/pg_pipeline/connection_driver.rb +215 -109
- data/lib/pg_pipeline/pool.rb +111 -61
- data/lib/pg_pipeline/prepared_statement.rb +1 -1
- data/lib/pg_pipeline/request.rb +69 -11
- data/lib/pg_pipeline/runtime/notification.rb +32 -0
- data/lib/pg_pipeline/runtime/queue.rb +31 -0
- data/lib/pg_pipeline/runtime/semaphore.rb +138 -0
- data/lib/pg_pipeline/runtime/task.rb +124 -0
- data/lib/pg_pipeline/runtime.rb +98 -0
- data/lib/pg_pipeline/session.rb +4 -2
- data/lib/pg_pipeline/session_guard.rb +40 -24
- data/lib/pg_pipeline/version.rb +1 -1
- data/lib/pg_pipeline.rb +1 -0
- metadata +26 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ebb76f18598913316b77fdc0fd0ab4aa411fbd014cf1f3f10e427c1a5709967
|
|
4
|
+
data.tar.gz: cd51b3fc6b5f20f5896969e8eb3a4263d999f2544239606944194192f894e850
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c1581a9ecc7f45b44342c49c529d12694c5d962808ef4ba7a0ec9d71aac5ae46852c99c242ec1b897c8c1b591d9ab66c55a24eacf0a4e6637de068624f36fc3
|
|
7
|
+
data.tar.gz: 3272e19424de3eed0f6c186b2bf14ac830ef226f9ce47207fd3c71fd38f622557a6542630c44934794cf9c650106a2e5a7e66ec475501220a685551799609111
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-08-07
|
|
4
|
+
|
|
5
|
+
Scheduler-agnostic control plane: the gem no longer depends on the `async` gem
|
|
6
|
+
at runtime. Any `Fiber::Scheduler` host works (Async::Scheduler, Itsi::Scheduler,
|
|
7
|
+
or another `Fiber.set_scheduler` implementation).
|
|
8
|
+
|
|
9
|
+
This unlocks hosts that were previously impossible. On our stand, Itsi with
|
|
10
|
+
0.3.0 serves ~26% more req/s than the 0.2.5 Falcon baseline (29350 vs 23254,
|
|
11
|
+
4 workers, `oha -z 60s -c 1000`) — a configuration 0.2.5 could not run at all,
|
|
12
|
+
since it required an Async reactor. Falcon throughput itself is unchanged
|
|
13
|
+
(23007 vs 23254, inside run-to-run spread): this is a portability change, and
|
|
14
|
+
the speedup comes from being free to pick the host.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- New `PgPipeline::Runtime` primitives (`Notification`, `Queue`, `Semaphore`,
|
|
19
|
+
`Task`, `spawn`, `with_timeout`, `Cancel`) built on `Fiber.scheduler`.
|
|
20
|
+
- Driver/pool background work uses `Runtime.spawn` instead of `parent.async`.
|
|
21
|
+
- Pinned-connection ownership keys on `Fiber.current` (not `Async::Task.current`).
|
|
22
|
+
- Watcher shutdown closes queues + connection, then joins tasks with a timeout.
|
|
23
|
+
- Timeouts go through `Timeout.timeout` (correct `timeout_after` arity), never a
|
|
24
|
+
direct one-arg `scheduler.timeout_after` call.
|
|
25
|
+
- Closed `Runtime::Queue#enqueue` is a no-op; supervisor sleep errors are recorded
|
|
26
|
+
and `stats` exposes `supervisor_alive`.
|
|
27
|
+
- `Client#start` / `Pool#start` / `ConnectionDriver#start` no longer take
|
|
28
|
+
`parent:` (no Async task-tree ownership; use `Runtime.spawn` on the active
|
|
29
|
+
scheduler). Call sites that passed `start(parent: task)` should use `#start`.
|
|
30
|
+
- Runtime dependency: only `pg`. `async` is a development dependency for the
|
|
31
|
+
existing Async-based test harness.
|
|
32
|
+
|
|
33
|
+
### Migration
|
|
34
|
+
|
|
35
|
+
Hosts must install a Fiber scheduler before `Client#start`. Under Async:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "async"
|
|
39
|
+
Sync { client.start; ... }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Under Itsi, the server installs `Itsi::Scheduler` for you — just `client.start`.
|
|
43
|
+
|
|
44
|
+
Apps that previously relied on `pg_pipeline` pulling in `async` transitively
|
|
45
|
+
should add `gem "async"` themselves if they still use Async.
|
|
46
|
+
|
|
47
|
+
## [0.2.5] - 2026-08-03
|
|
48
|
+
|
|
49
|
+
Control-plane cleanup on the multiplexed query path: fewer per-query allocations,
|
|
50
|
+
a cheaper session-guard fast path, slightly tighter result drain, and driver
|
|
51
|
+
fill metrics. Wire behaviour and the public API are unchanged.
|
|
52
|
+
|
|
53
|
+
### Performance
|
|
54
|
+
|
|
55
|
+
- `SessionGuard` skips comment/literal masking when the SQL has no quotes,
|
|
56
|
+
comments or dollar-quotes; skips the forbidden-pattern scan when the SQL has
|
|
57
|
+
neither `(` nor `into`; and uses a byte scan for multi-statement detection.
|
|
58
|
+
- `Request.build` / `PreparedQueryRequest.build` avoid keyword `Class#new` hash
|
|
59
|
+
allocation on the hot path.
|
|
60
|
+
- `RequestOps.snapshot_params` reuses a shared frozen empty array and skips
|
|
61
|
+
copying frozen immutable param arrays.
|
|
62
|
+
- Default query params use that empty frozen array instead of allocating `[]`
|
|
63
|
+
per call (`Client`, `Session`, `PreparedStatement`).
|
|
64
|
+
- `PoolOps.select_driver_into` writes the round-robin cursor into a pool-owned
|
|
65
|
+
slot instead of allocating a `[driver, cursor]` pair per selection.
|
|
66
|
+
- `drain_results` matches hot statuses (`TUPLES_OK`, `PIPELINE_SYNC`) first and
|
|
67
|
+
accumulates `results_read` once per drain.
|
|
68
|
+
|
|
69
|
+
### Observability
|
|
70
|
+
|
|
71
|
+
- Driver stats add `fast_sync`, `units_per_readable`, `results_per_readable`,
|
|
72
|
+
`flush_calls`, `flush_incomplete`, `dispatches`, and `flush_calls_per_unit`.
|
|
73
|
+
- One process-level warning on libpq older than 17 when Sync is coupled to flush
|
|
74
|
+
(`PG_PIPELINE_SILENCE_WARNINGS=1` to suppress).
|
|
75
|
+
|
|
76
|
+
### Reliability
|
|
77
|
+
|
|
78
|
+
- Round-robin cursor is normalised with `rr % size` when past the pool length.
|
|
79
|
+
- Driver metric readers default to zero if ivars are unset.
|
|
80
|
+
|
|
81
|
+
### Tests
|
|
82
|
+
|
|
83
|
+
- Guard fast-path / masking / cache eviction coverage.
|
|
84
|
+
- `Request.build` equivalence and prepared-query build coverage.
|
|
85
|
+
- Randomised driver-selection vs reference algorithm.
|
|
86
|
+
- Drain counter write-back when the loop raises.
|
|
87
|
+
|
|
3
88
|
## [0.2.4] - 2026-07-31
|
|
4
89
|
|
|
5
90
|
Request-completion allocation patch. It replaces the general-purpose
|
data/DESIGN.md
CHANGED
|
@@ -240,6 +240,46 @@ that the request-specific one-shot waiter deliberately does not provide.
|
|
|
240
240
|
> suite against the exact 2.42.0 floor so `Scheduler#block`/`#unblock` behavior is
|
|
241
241
|
> not merely assumed from a broad pessimistic dependency range.
|
|
242
242
|
|
|
243
|
+
## 10a. Guard evaluation shape
|
|
244
|
+
|
|
245
|
+
`SessionGuard` validates the session-neutral SQL contract of §6 before a query is
|
|
246
|
+
allowed onto the shared path, so its cost lands on the reactor thread and its
|
|
247
|
+
stalls are visible in every other fiber's latency, not just the caller's.
|
|
248
|
+
|
|
249
|
+
Evaluation is therefore staged so that the expensive stage runs only when it can
|
|
250
|
+
change the answer:
|
|
251
|
+
|
|
252
|
+
1. **Masking is skipped when it is provably an identity transform.** `code_only`
|
|
253
|
+
blanks string literals, quoted identifiers, comments and dollar-quoted bodies.
|
|
254
|
+
If none of `'`, `"`, `--`, `/*` or a `$tag$` delimiter appears in the SQL,
|
|
255
|
+
there is nothing to blank and the raw SQL is validated directly.
|
|
256
|
+
2. **The forbidden-pattern scan is skipped when no pattern can match.** Every
|
|
257
|
+
pattern in `FORBIDDEN_PATTERNS` and `STRICT_FORBIDDEN` is anchored on either a
|
|
258
|
+
parenthesis or the word `into`, so SQL containing neither cannot match any of
|
|
259
|
+
them.
|
|
260
|
+
3. **Verdicts are cached by SQL string**, and the cache evicts a single oldest
|
|
261
|
+
entry at its limit. Clearing the whole cache would turn one insertion into a
|
|
262
|
+
full recompute for every subsequent statement.
|
|
263
|
+
|
|
264
|
+
Stage 1 and stage 2 are correctness-preserving only as long as their premises
|
|
265
|
+
hold. Both are asserted in `spec/pg_pipeline/session_guard_fast_path_spec.rb`;
|
|
266
|
+
**adding a pattern that is not anchored on `(` or `into` requires updating the
|
|
267
|
+
prefilter in the same change.**
|
|
268
|
+
|
|
269
|
+
Masking is deliberately monotone in the wrong direction to be reordered: blanking
|
|
270
|
+
a comment can *create* a match (`nextval/* c */('s')` becomes `nextval ('s')`),
|
|
271
|
+
so the raw SQL can never be used as a negative filter for stage 2. Only the
|
|
272
|
+
absence of maskable syntax justifies skipping stage 1.
|
|
273
|
+
|
|
274
|
+
## 10b. Fill observability
|
|
275
|
+
|
|
276
|
+
Pipelining pays off only when one reactor wakeup is amortised over several units.
|
|
277
|
+
`ConnectionDriver#stats` therefore reports `units_per_readable` (and related
|
|
278
|
+
counters) alongside the queue depths. A value near 1.0 means each query costs a
|
|
279
|
+
full socket wait and a scheduler round trip, which bounds throughput
|
|
280
|
+
independently of control-plane work; values well above 1.0 mean the pipeline is
|
|
281
|
+
filling. Read this number before attributing throughput to Ruby-side cost.
|
|
282
|
+
|
|
243
283
|
## 11. Version policy
|
|
244
284
|
|
|
245
285
|
| Component | Minimum | Reason |
|
data/README.md
CHANGED
|
@@ -24,13 +24,66 @@ At 10 ms RTT a naïve pool does ~100 queries/s per connection.
|
|
|
24
24
|
A pipelined connection can do thousands — the wire stays full instead of sitting idle.
|
|
25
25
|
|
|
26
26
|
`pg_pipeline` is the Ruby control plane that does exactly this: it multiplexes
|
|
27
|
-
independent queries from many
|
|
27
|
+
independent queries from many fibers onto a small pool of libpq connections,
|
|
28
28
|
routes FIFO results back to the right fiber, and keeps transactional/session work
|
|
29
29
|
on separate pinned connections. All wire protocol work stays in libpq; zero C code here.
|
|
30
30
|
|
|
31
31
|
The single-owner connection model, FIFO result ownership, and lifecycle approach
|
|
32
32
|
are directly inspired by tokio-postgres.
|
|
33
33
|
|
|
34
|
+
## Any Fiber scheduler, not just Async
|
|
35
|
+
|
|
36
|
+
Up to 0.2.x the control plane was built on the `async` gem: `Async::Task`,
|
|
37
|
+
`Async::Queue`, `Async::Semaphore`, and a task tree rooted in Async's reactor.
|
|
38
|
+
That made Falcon the only realistic host.
|
|
39
|
+
|
|
40
|
+
0.3.0 removes that. The control plane is built on Ruby's `Fiber::Scheduler`
|
|
41
|
+
interface — `Fiber.schedule` plus the scheduler's `block` / `unblock` — and on
|
|
42
|
+
nothing else. The host installs whichever scheduler it likes; the gem never
|
|
43
|
+
installs one and never calls a scheduler hook directly. `async` is now a
|
|
44
|
+
development dependency only, and the sole runtime dependency is `pg`.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# Falcon / any Async host — unchanged, still works
|
|
48
|
+
Async do
|
|
49
|
+
client = PgPipeline::Client.open(ENV["DATABASE_URL"])
|
|
50
|
+
client.query("SELECT * FROM users WHERE id = $1", [id]).first
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Itsi, with its own scheduler — no Async anywhere
|
|
54
|
+
# Itsi.rb:
|
|
55
|
+
# fiber_scheduler "Itsi::Scheduler"
|
|
56
|
+
client = PgPipeline::Client.open(ENV["DATABASE_URL"])
|
|
57
|
+
client.query("SELECT * FROM users WHERE id = $1", [id]).first
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The call site does not change: `query` blocks the *fiber*, not the thread, so
|
|
61
|
+
application code reads synchronously with no `await` and no coloured functions.
|
|
62
|
+
The only hard requirement is that some scheduler is installed on the current
|
|
63
|
+
thread — under a web server running requests in `Fiber.schedule` that is free,
|
|
64
|
+
while a plain script or rake task must set one up itself.
|
|
65
|
+
|
|
66
|
+
### What it buys
|
|
67
|
+
|
|
68
|
+
On our benchmark stand (4 workers, `oha -z 60s -c 1000`, single-row lookup by
|
|
69
|
+
primary key, local PostgreSQL 16):
|
|
70
|
+
|
|
71
|
+
| gem | server | scheduler | req/s |
|
|
72
|
+
|---|---|---|---:|
|
|
73
|
+
| 0.2.5 | Falcon | Async | 23254 |
|
|
74
|
+
| 0.3.0 | Falcon | Async | 23007 |
|
|
75
|
+
| 0.3.0 | Itsi | Async | 29206 |
|
|
76
|
+
| 0.3.0 | Itsi | Itsi::Scheduler | 29350 |
|
|
77
|
+
|
|
78
|
+
Falcon throughput is unchanged — this was a portability change, not a
|
|
79
|
+
Falcon optimisation. The gain comes from being *able* to move: an Itsi host is
|
|
80
|
+
roughly **26% faster than the 0.2.5 Falcon baseline**, and that configuration
|
|
81
|
+
simply could not run before, because 0.2.5 required an Async reactor.
|
|
82
|
+
|
|
83
|
+
Numbers from one stand on one machine; treat them as a direction, not a
|
|
84
|
+
guarantee. Your own ratio depends on payload size, RTT, and how much of the
|
|
85
|
+
request is spent outside the database.
|
|
86
|
+
|
|
34
87
|
## Installation
|
|
35
88
|
|
|
36
89
|
```ruby
|
|
@@ -40,6 +93,14 @@ gem "pg_pipeline"
|
|
|
40
93
|
|
|
41
94
|
Requires Ruby ≥ 3.3, `async ~> 2.42`, `pg ≥ 1.5`, and **libpq ≥ 14** at runtime.
|
|
42
95
|
|
|
96
|
+
**libpq ≥ 17 is recommended for maximum local throughput.** Below 17 there is no
|
|
97
|
+
`PQsendPipelineSync`, so `PQpipelineSync` couples Sync with a flush and the driver
|
|
98
|
+
cannot batch writes across a dispatch burst. Correctness is identical and RTT
|
|
99
|
+
amortisation — the main reason to pipeline — still works on libpq 14; only local
|
|
100
|
+
throughput is capped. The driver detects this at connect time and warns once per
|
|
101
|
+
process; set `PG_PIPELINE_SILENCE_WARNINGS=1` to suppress it, and check
|
|
102
|
+
`db.stats[:pipeline][:drivers].first[:fast_sync]` to see which path is active.
|
|
103
|
+
|
|
43
104
|
## Usage
|
|
44
105
|
|
|
45
106
|
### Multiplexed queries
|
|
@@ -155,6 +216,49 @@ db.stats
|
|
|
155
216
|
|
|
156
217
|
`load` (pending + in-flight + submitting + dispatching) per driver is the routing/head-of-line signal.
|
|
157
218
|
|
|
219
|
+
Each driver also reports how well the pipeline is filling:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
db.stats[:pipeline][:drivers].first
|
|
223
|
+
# => { ..., fast_sync: true,
|
|
224
|
+
# units_per_readable: 7.9, results_per_readable: 22.2,
|
|
225
|
+
# flush_calls_per_unit: 0.17, flush_incomplete: 0 }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`units_per_readable` is the number to watch. Around `1.0` means every query pays a
|
|
229
|
+
full socket wait plus scheduler round trip and the pipeline is not filling — that is
|
|
230
|
+
the expected shape for a single fiber issuing one query at a time, and no amount of
|
|
231
|
+
Ruby-side optimisation will change it. Values well above `1.0` mean one reactor
|
|
232
|
+
wakeup is amortised over many queries, which is the regime pipelining is for. Check
|
|
233
|
+
this before attributing a throughput number to control-plane cost.
|
|
234
|
+
|
|
235
|
+
The numbers above are from a saturated HTTP benchmark (4 workers, 4 connections
|
|
236
|
+
each, ~20k requests/second against a local server): roughly eight queries per
|
|
237
|
+
reactor wakeup and one flush per six queries. `results_per_readable` runs at
|
|
238
|
+
three times `units_per_readable` because a completed unit yields three protocol
|
|
239
|
+
results -- the data, the query boundary and the Sync.
|
|
240
|
+
|
|
241
|
+
## Head-of-line blocking
|
|
242
|
+
|
|
243
|
+
Results on a pipelined connection arrive in FIFO order, and PostgreSQL offers no
|
|
244
|
+
safe way to cancel one request out of a multiplexed pipeline (see `DESIGN.md` §7).
|
|
245
|
+
Two consequences worth designing around:
|
|
246
|
+
|
|
247
|
+
- **A slow query delays everything behind it on the same connection.** With the
|
|
248
|
+
default `max_in_flight: 64`, one multi-second query can hold up to 63 unrelated
|
|
249
|
+
queries on that driver. Driver selection balances by queue depth, not by expected
|
|
250
|
+
cost, so a slow query counts the same as a fast one.
|
|
251
|
+
- **A timeout is not a cancellation.** Wrapping `db.query` in `with_timeout` returns
|
|
252
|
+
control to your fiber, but the unit stays in the pipeline until the server answers
|
|
253
|
+
it, and the requests behind it still wait.
|
|
254
|
+
|
|
255
|
+
If your workload mixes fast and slow queries, prefer one of:
|
|
256
|
+
|
|
257
|
+
- lower `max_in_flight` so a stall cannot capture a deep queue;
|
|
258
|
+
- a second `Client` with its own connections for the slow queries;
|
|
259
|
+
- `Client#session` for anything long-running, which uses an exclusive pinned
|
|
260
|
+
connection and cannot block multiplexed traffic.
|
|
261
|
+
|
|
158
262
|
## Failure model
|
|
159
263
|
|
|
160
264
|
| Error | Meaning | Retry safe? |
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "async/notification"
|
|
4
|
-
|
|
5
3
|
require_relative "errors"
|
|
4
|
+
require_relative "runtime"
|
|
6
5
|
|
|
7
6
|
module PgPipeline
|
|
8
7
|
class BoundedQueue
|
|
@@ -12,15 +11,14 @@ module PgPipeline
|
|
|
12
11
|
rescue ArgumentError, TypeError
|
|
13
12
|
raise ArgumentError, "limit must be an integer >= 1"
|
|
14
13
|
else
|
|
15
|
-
@items = []
|
|
16
|
-
|
|
17
|
-
@producers = []
|
|
14
|
+
@items, @consumers, @producers = [], [], []
|
|
15
|
+
|
|
18
16
|
@closed = false
|
|
19
17
|
@close_error = nil
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def enqueue(item)
|
|
23
|
-
|
|
21
|
+
while true
|
|
24
22
|
raise_close_error if @closed
|
|
25
23
|
|
|
26
24
|
if @items.size < @limit
|
|
@@ -34,7 +32,7 @@ module PgPipeline
|
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
def dequeue
|
|
37
|
-
|
|
35
|
+
while true
|
|
38
36
|
unless @items.empty?
|
|
39
37
|
item = @items.shift
|
|
40
38
|
wake_one(@producers)
|
|
@@ -72,13 +70,13 @@ module PgPipeline
|
|
|
72
70
|
private
|
|
73
71
|
|
|
74
72
|
def wait_on(list)
|
|
75
|
-
notification =
|
|
73
|
+
notification = Runtime::Notification.new
|
|
76
74
|
list << notification
|
|
77
75
|
completed = false
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
notification.wait
|
|
77
|
+
completed = true
|
|
78
|
+
ensure
|
|
79
|
+
if notification
|
|
82
80
|
still_queued = list.delete(notification)
|
|
83
81
|
wake_one(list) if !completed && still_queued.nil? && !@closed
|
|
84
82
|
end
|
data/lib/pg_pipeline/client.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "async"
|
|
4
|
-
|
|
5
3
|
require_relative "errors"
|
|
6
4
|
require_relative "pool"
|
|
7
5
|
require_relative "request"
|
|
@@ -15,19 +13,19 @@ module PgPipeline
|
|
|
15
13
|
attr_reader :guard
|
|
16
14
|
|
|
17
15
|
def initialize(connection_args = nil, guard: :default, **pool_opts)
|
|
16
|
+
@owner_thread, @scheduler = nil, nil
|
|
17
|
+
|
|
18
18
|
@guard = SessionGuard.normalize_mode!(guard)
|
|
19
19
|
@pool = Pool.new(connection_args, **pool_opts)
|
|
20
20
|
@started = false
|
|
21
|
-
@owner_thread = nil
|
|
22
|
-
@scheduler = nil
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def self.open(connection_args = nil, **opts, &block)
|
|
26
24
|
ClientOps.open(connection_args, opts, &block)
|
|
27
25
|
end
|
|
28
26
|
|
|
29
|
-
def start
|
|
30
|
-
def query(sql, params =
|
|
27
|
+
def start = ClientOps.start(self)
|
|
28
|
+
def query(sql, params = RequestOps::EMPTY_PARAMS) = ClientOps.query(self, sql, params)
|
|
31
29
|
def prepare(name, sql, param_types = nil) = ClientOps.prepare(self, name, sql, param_types)
|
|
32
30
|
def stats = ClientOps.stats(self)
|
|
33
31
|
|
|
@@ -54,17 +52,16 @@ module PgPipeline
|
|
|
54
52
|
|
|
55
53
|
def open(connection_args, opts)
|
|
56
54
|
client = Client.new(connection_args, **opts).start
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
client.close
|
|
61
|
-
end
|
|
55
|
+
yield client
|
|
56
|
+
ensure
|
|
57
|
+
client.close if client
|
|
62
58
|
end
|
|
63
59
|
|
|
64
|
-
def start(client
|
|
60
|
+
def start(client)
|
|
65
61
|
raise Error, "client already started" if started?(client)
|
|
62
|
+
raise Error, "client start requires an active Fiber scheduler" unless Fiber.scheduler
|
|
66
63
|
|
|
67
|
-
pool(client).start
|
|
64
|
+
pool(client).start
|
|
68
65
|
client.__send__(:owner_thread=, Thread.current)
|
|
69
66
|
client.__send__(:scheduler=, Fiber.scheduler)
|
|
70
67
|
client.__send__(:started=, true)
|
|
@@ -77,7 +74,7 @@ module PgPipeline
|
|
|
77
74
|
SessionGuard.assert_multiplexable_normalized!(sql, mode: client.guard)
|
|
78
75
|
|
|
79
76
|
wait_for_request do
|
|
80
|
-
submit_with_failover(client) { Request.
|
|
77
|
+
submit_with_failover(client) { Request.build(sql, params) }
|
|
81
78
|
end
|
|
82
79
|
end
|
|
83
80
|
|
|
@@ -98,15 +95,11 @@ module PgPipeline
|
|
|
98
95
|
|
|
99
96
|
def wait_for_request
|
|
100
97
|
request = yield
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
request.cancel! unless request.settled?
|
|
105
|
-
end
|
|
98
|
+
request.wait
|
|
99
|
+
ensure
|
|
100
|
+
request.cancel! if request && !request.settled?
|
|
106
101
|
end
|
|
107
102
|
|
|
108
|
-
# NotDispatchedError is safe to retry on a fresh Request by contract.
|
|
109
|
-
# ShutdownError is retried only while the current Request is still pre-dispatch.
|
|
110
103
|
def submit_with_failover(client)
|
|
111
104
|
attempts = 0
|
|
112
105
|
limit = [pool(client).pipeline_size, 1].max
|