pg_pipeline 0.2.5 → 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 +44 -0
- data/README.md +54 -1
- data/lib/pg_pipeline/bounded_queue.rb +10 -12
- data/lib/pg_pipeline/client.rb +12 -17
- data/lib/pg_pipeline/connection_driver.rb +128 -71
- data/lib/pg_pipeline/pool.rb +82 -48
- data/lib/pg_pipeline/request.rb +5 -7
- 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/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,49 @@
|
|
|
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
|
+
|
|
3
47
|
## [0.2.5] - 2026-08-03
|
|
4
48
|
|
|
5
49
|
Control-plane cleanup on the multiplexed query path: fewer per-query allocations,
|
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
|
|
@@ -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,18 +13,18 @@ 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
|
|
27
|
+
def start = ClientOps.start(self)
|
|
30
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)
|
|
@@ -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)
|
|
@@ -98,11 +95,9 @@ 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
103
|
def submit_with_failover(client)
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pg"
|
|
4
|
-
require "async"
|
|
5
|
-
require "async/queue"
|
|
6
4
|
|
|
7
5
|
require_relative "errors"
|
|
6
|
+
require_relative "runtime"
|
|
8
7
|
require_relative "bounded_queue"
|
|
9
8
|
require_relative "server_caps"
|
|
10
9
|
require_relative "request"
|
|
@@ -21,7 +20,7 @@ module PgPipeline
|
|
|
21
20
|
:owner_task, :reader_task, :writer_task
|
|
22
21
|
|
|
23
22
|
attr_writer :readable_events, :results_read, :units_completed, :flush_calls,
|
|
24
|
-
:flush_incomplete, :dispatches
|
|
23
|
+
:flush_incomplete, :dispatches, :leaked_watchers
|
|
25
24
|
|
|
26
25
|
def readable_events = @readable_events || 0
|
|
27
26
|
def results_read = @results_read || 0
|
|
@@ -29,6 +28,7 @@ module PgPipeline
|
|
|
29
28
|
def flush_calls = @flush_calls || 0
|
|
30
29
|
def flush_incomplete = @flush_incomplete || 0
|
|
31
30
|
def dispatches = @dispatches || 0
|
|
31
|
+
def leaked_watchers = @leaked_watchers || 0
|
|
32
32
|
|
|
33
33
|
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT)
|
|
34
34
|
@max_pending = DriverOps.positive_integer!(max_pending, :max_pending)
|
|
@@ -40,35 +40,19 @@ module PgPipeline
|
|
|
40
40
|
DriverOps.warn_flush_coupling_once(@caps)
|
|
41
41
|
|
|
42
42
|
@requests = BoundedQueue.new(@max_pending)
|
|
43
|
-
@events =
|
|
44
|
-
@reader_rearm =
|
|
45
|
-
@writer_commands =
|
|
43
|
+
@events = Runtime::Queue.new
|
|
44
|
+
@reader_rearm = Runtime::Queue.new
|
|
45
|
+
@writer_commands = Runtime::Queue.new
|
|
46
|
+
|
|
47
|
+
@accepting = @running = @draining = @needs_flush = @writer_armed = @request_event_pending = false
|
|
48
|
+
@readable_events = @results_read = @units_completed = @flush_calls =
|
|
49
|
+
@flush_incomplete = @dispatches = @leaked_watchers = @submitting = 0
|
|
50
|
+
@socket = @owner_task = @reader_task = @writer_task = @dispatching = nil
|
|
46
51
|
|
|
47
52
|
@inflight = []
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@accepting = false
|
|
52
|
-
@running = false
|
|
53
|
-
@draining = false
|
|
54
|
-
@needs_flush = false
|
|
55
|
-
@writer_armed = false
|
|
56
|
-
@request_event_pending = false
|
|
57
|
-
|
|
58
|
-
@readable_events = 0
|
|
59
|
-
@results_read = 0
|
|
60
|
-
@units_completed = 0
|
|
61
|
-
@flush_calls = 0
|
|
62
|
-
@flush_incomplete = 0
|
|
63
|
-
@dispatches = 0
|
|
64
|
-
|
|
65
|
-
@socket = nil
|
|
66
|
-
@owner_task = nil
|
|
67
|
-
@reader_task = nil
|
|
68
|
-
@writer_task = nil
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def start(parent: Async::Task.current) = DriverOps.start(self, parent)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start = DriverOps.start(self)
|
|
72
56
|
def submit(request) = DriverOps.submit(self, request)
|
|
73
57
|
def load = @requests.size + @inflight.size + @submitting + (@dispatching ? 1 : 0)
|
|
74
58
|
def available? = @accepting && @running
|
|
@@ -91,7 +75,8 @@ module PgPipeline
|
|
|
91
75
|
dispatches: dispatches,
|
|
92
76
|
units_per_readable: DriverOps.ratio(units_completed, readable_events),
|
|
93
77
|
results_per_readable: DriverOps.ratio(results_read, readable_events),
|
|
94
|
-
flush_calls_per_unit: DriverOps.ratio(flush_calls, units_completed)
|
|
78
|
+
flush_calls_per_unit: DriverOps.ratio(flush_calls, units_completed),
|
|
79
|
+
leaked_watchers: leaked_watchers
|
|
95
80
|
}
|
|
96
81
|
end
|
|
97
82
|
|
|
@@ -99,21 +84,19 @@ module PgPipeline
|
|
|
99
84
|
return true unless available?
|
|
100
85
|
|
|
101
86
|
probe = Request.build("SELECT 1", nil)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
probe.cancel! unless probe.settled?
|
|
116
|
-
end
|
|
87
|
+
submit(probe)
|
|
88
|
+
Runtime.with_timeout(timeout) { probe.wait }
|
|
89
|
+
true
|
|
90
|
+
rescue Runtime::TimeoutError
|
|
91
|
+
DriverOps.abort_timed_out_health_probe(self, probe)
|
|
92
|
+
rescue QueryError, PipelineAbortedError
|
|
93
|
+
true
|
|
94
|
+
rescue ShutdownError, NotDispatchedError
|
|
95
|
+
true
|
|
96
|
+
rescue ConnectionLostError, PG::Error
|
|
97
|
+
false
|
|
98
|
+
ensure
|
|
99
|
+
probe.cancel! if probe && !probe.settled?
|
|
117
100
|
end
|
|
118
101
|
|
|
119
102
|
def graceful_close = DriverOps.graceful_close(self)
|
|
@@ -124,6 +107,9 @@ module PgPipeline
|
|
|
124
107
|
end
|
|
125
108
|
|
|
126
109
|
module DriverOps
|
|
110
|
+
WATCHER_JOIN_TIMEOUT = 2.0
|
|
111
|
+
OWNER_JOIN_TIMEOUT = 5.0
|
|
112
|
+
|
|
127
113
|
module_function
|
|
128
114
|
|
|
129
115
|
def ratio(numerator, denominator)
|
|
@@ -155,8 +141,9 @@ module PgPipeline
|
|
|
155
141
|
raise ArgumentError, "#{name} must be an integer >= 1"
|
|
156
142
|
end
|
|
157
143
|
|
|
158
|
-
def start(d
|
|
144
|
+
def start(d)
|
|
159
145
|
raise Error, "driver already started" if d.running
|
|
146
|
+
raise Error, "driver start requires an active Fiber scheduler" unless Fiber.scheduler
|
|
160
147
|
|
|
161
148
|
d.conn.setnonblocking(true)
|
|
162
149
|
d.conn.enter_pipeline_mode
|
|
@@ -165,15 +152,15 @@ module PgPipeline
|
|
|
165
152
|
d.accepting = true
|
|
166
153
|
d.running = true
|
|
167
154
|
|
|
168
|
-
d.reader_task =
|
|
169
|
-
d.writer_task =
|
|
170
|
-
d.owner_task =
|
|
155
|
+
d.reader_task = Runtime.spawn(name: :reader) { reader_watcher(d) }
|
|
156
|
+
d.writer_task = Runtime.spawn(name: :writer) { writer_watcher(d) }
|
|
157
|
+
d.owner_task = Runtime.spawn(name: :owner) { owner_loop(d) }
|
|
171
158
|
d
|
|
172
159
|
rescue Exception
|
|
173
160
|
d.accepting = false
|
|
174
161
|
d.running = false
|
|
175
|
-
|
|
176
|
-
|
|
162
|
+
teardown_watchers(d)
|
|
163
|
+
join_owner(d)
|
|
177
164
|
raise
|
|
178
165
|
end
|
|
179
166
|
|
|
@@ -200,7 +187,7 @@ module PgPipeline
|
|
|
200
187
|
d.accepting = false
|
|
201
188
|
d.requests.close(ShutdownError.new("driver is closing"))
|
|
202
189
|
d.events.enqueue(:close)
|
|
203
|
-
d
|
|
190
|
+
join_owner(d)
|
|
204
191
|
nil
|
|
205
192
|
end
|
|
206
193
|
|
|
@@ -210,7 +197,7 @@ module PgPipeline
|
|
|
210
197
|
d.accepting = false
|
|
211
198
|
d.requests.close(not_dispatched_error(error))
|
|
212
199
|
d.events.enqueue([:abort, error])
|
|
213
|
-
|
|
200
|
+
join_owner(d)
|
|
214
201
|
nil
|
|
215
202
|
end
|
|
216
203
|
|
|
@@ -232,7 +219,12 @@ module PgPipeline
|
|
|
232
219
|
end
|
|
233
220
|
|
|
234
221
|
def owner_loop(d)
|
|
235
|
-
|
|
222
|
+
while d.running
|
|
223
|
+
event = d.events.dequeue
|
|
224
|
+
break if event.nil?
|
|
225
|
+
|
|
226
|
+
process_event(d, event)
|
|
227
|
+
end
|
|
236
228
|
rescue StandardError => e
|
|
237
229
|
fatal_close(d, ConnectionLostError.new("driver crashed: #{e.class}: #{e.message}"))
|
|
238
230
|
ensure
|
|
@@ -275,6 +267,7 @@ module PgPipeline
|
|
|
275
267
|
|
|
276
268
|
def notify_requests(d)
|
|
277
269
|
return if d.request_event_pending
|
|
270
|
+
return unless d.running
|
|
278
271
|
|
|
279
272
|
d.request_event_pending = true
|
|
280
273
|
d.events.enqueue(:requests)
|
|
@@ -377,6 +370,7 @@ module PgPipeline
|
|
|
377
370
|
|
|
378
371
|
def arm_writer(d)
|
|
379
372
|
return if d.writer_armed
|
|
373
|
+
return unless d.running
|
|
380
374
|
|
|
381
375
|
d.writer_armed = true
|
|
382
376
|
d.writer_commands.enqueue(:wait_writable)
|
|
@@ -466,14 +460,11 @@ module PgPipeline
|
|
|
466
460
|
d.accepting = false
|
|
467
461
|
d.running = false
|
|
468
462
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
stop_watchers(d)
|
|
475
|
-
safe_close_conn(d)
|
|
476
|
-
end
|
|
463
|
+
d.conn.exit_pipeline_mode
|
|
464
|
+
rescue PG::Error => e
|
|
465
|
+
fail_all(d, ConnectionLostError.new("failed to exit pipeline mode: #{e.message}"))
|
|
466
|
+
ensure
|
|
467
|
+
teardown_watchers(d)
|
|
477
468
|
end
|
|
478
469
|
|
|
479
470
|
def fatal_close(d, error)
|
|
@@ -483,8 +474,8 @@ module PgPipeline
|
|
|
483
474
|
d.running = false
|
|
484
475
|
d.requests.close(not_dispatched_error(error))
|
|
485
476
|
fail_all(d, error)
|
|
486
|
-
|
|
487
|
-
|
|
477
|
+
|
|
478
|
+
teardown_watchers(d)
|
|
488
479
|
end
|
|
489
480
|
|
|
490
481
|
def fail_all(d, error)
|
|
@@ -549,21 +540,87 @@ module PgPipeline
|
|
|
549
540
|
end
|
|
550
541
|
end
|
|
551
542
|
|
|
552
|
-
def
|
|
553
|
-
|
|
554
|
-
|
|
543
|
+
def teardown_watchers(d)
|
|
544
|
+
tasks = release_watchers(d)
|
|
545
|
+
close_wait_points(d)
|
|
546
|
+
begin
|
|
547
|
+
d.socket&.close
|
|
548
|
+
rescue StandardError
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
stop_watchers(tasks)
|
|
552
|
+
join_watchers(d, tasks)
|
|
553
|
+
d.socket = nil
|
|
554
|
+
safe_close_conn(d)
|
|
555
|
+
nil
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def release_watchers(d)
|
|
559
|
+
tasks = [d.reader_task, d.writer_task].compact
|
|
555
560
|
d.reader_task = nil
|
|
556
561
|
d.writer_task = nil
|
|
562
|
+
tasks
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def close_wait_points(d)
|
|
566
|
+
[d.reader_rearm, d.writer_commands, d.events].each do |queue|
|
|
567
|
+
queue&.close
|
|
568
|
+
rescue StandardError
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def stop_watchers(tasks)
|
|
574
|
+
Array(tasks).each do |task|
|
|
575
|
+
task.stop
|
|
576
|
+
rescue Runtime::Cancel, StandardError
|
|
577
|
+
nil
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
nil
|
|
581
|
+
end
|
|
557
582
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
583
|
+
def join_watchers(d, tasks)
|
|
584
|
+
Array(tasks).each do |task|
|
|
585
|
+
task.wait(WATCHER_JOIN_TIMEOUT)
|
|
586
|
+
rescue Runtime::TimeoutError
|
|
587
|
+
d.leaked_watchers += 1
|
|
588
|
+
warn_leaked_task(d, task, WATCHER_JOIN_TIMEOUT)
|
|
589
|
+
rescue Runtime::Cancel, StandardError
|
|
561
590
|
nil
|
|
562
591
|
end
|
|
563
592
|
|
|
564
593
|
nil
|
|
565
594
|
end
|
|
566
595
|
|
|
596
|
+
def join_owner(d)
|
|
597
|
+
owner = d.owner_task
|
|
598
|
+
return if owner.nil?
|
|
599
|
+
return if Fiber.current.equal?(owner.fiber)
|
|
600
|
+
|
|
601
|
+
begin
|
|
602
|
+
owner.wait(OWNER_JOIN_TIMEOUT)
|
|
603
|
+
rescue Runtime::TimeoutError
|
|
604
|
+
d.leaked_watchers += 1
|
|
605
|
+
warn_leaked_task(d, owner, OWNER_JOIN_TIMEOUT)
|
|
606
|
+
rescue Runtime::Cancel, StandardError
|
|
607
|
+
nil
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
nil
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def warn_leaked_task(d, task, timeout)
|
|
614
|
+
return if ENV["PG_PIPELINE_SILENCE_WARNINGS"]
|
|
615
|
+
|
|
616
|
+
warn(
|
|
617
|
+
"pg_pipeline: task #{task.name.inspect} did not exit within " \
|
|
618
|
+
"#{timeout}s and has been leaked (total #{d.leaked_watchers}). " \
|
|
619
|
+
"Closing the duplexed socket_io / connection did not release the fiber " \
|
|
620
|
+
"(often parked in wait_readable/wait_writable)."
|
|
621
|
+
)
|
|
622
|
+
end
|
|
623
|
+
|
|
567
624
|
def safe_close_conn(d)
|
|
568
625
|
d.conn.close unless d.conn.finished?
|
|
569
626
|
rescue StandardError
|