pg_pipeline 0.3.2 → 0.3.4
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 +45 -0
- data/README.md +21 -0
- data/lib/pg_pipeline/bounded_queue.rb +8 -6
- data/lib/pg_pipeline/client.rb +65 -41
- data/lib/pg_pipeline/connection_driver.rb +390 -286
- data/lib/pg_pipeline/pool.rb +325 -206
- data/lib/pg_pipeline/prepared_statement.rb +11 -8
- data/lib/pg_pipeline/request.rb +21 -18
- data/lib/pg_pipeline/runtime/notification.rb +2 -1
- data/lib/pg_pipeline/runtime/semaphore.rb +3 -3
- data/lib/pg_pipeline/runtime/task.rb +14 -8
- data/lib/pg_pipeline/runtime.rb +17 -18
- data/lib/pg_pipeline/server_caps.rb +3 -3
- data/lib/pg_pipeline/session.rb +9 -7
- data/lib/pg_pipeline/session_guard.rb +108 -73
- data/lib/pg_pipeline/transaction.rb +33 -34
- data/lib/pg_pipeline/type_maps.rb +104 -0
- data/lib/pg_pipeline/version.rb +1 -1
- data/lib/pg_pipeline.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c47c7cb9266d0541b6d57aeba3c25f311f1e8ba6b661c2b79ddc0aea6fe5fe24
|
|
4
|
+
data.tar.gz: 8c34eae2688944b938ee4adfe2c07ae2730571536ff328dc41368add95667410
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a869a0dcfb3a2c8f0eaa9f7f4979ebf71f8af893b3792de15a4db6c8a2f9852f9f7a42474580468cdb3b9cac50f08ec0a2525627551fc731d715b70ac1c3d03c
|
|
7
|
+
data.tar.gz: ca9179c01ab0a824b07be2923578b51489d91ccd2e9076ba2da44d789f2aef666cffd77d481b85577c0265f8f7bc0f0b33fe028ba535007928bb7489b21c504c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.4] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
Internal refactoring of the control plane. No intended public API or wire
|
|
6
|
+
behaviour changes.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Centralized pool option defaults and validation.
|
|
11
|
+
- Split driver, pool, request, SQL-guard, and type-map operations into smaller
|
|
12
|
+
focused methods and removed redundant repeated lookups.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Readable driver events now trigger result draining.
|
|
17
|
+
|
|
18
|
+
## [0.3.3] - 2026-08-19
|
|
19
|
+
|
|
20
|
+
Opt-in typed result decoding for multiplexed prepared statements. Default
|
|
21
|
+
behaviour, Sync-per-unit, and the failure model are unchanged.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `Client#prepare(..., typed: true)` — text format. The first `TUPLES_OK`
|
|
26
|
+
result builds a `TypeMapByColumn`; later dispatches reuse that map from the
|
|
27
|
+
`Request` (resolved at submit, never re-read from handle state at drain).
|
|
28
|
+
Column values arrive as typed Ruby objects (Integer, Time, BigDecimal, …)
|
|
29
|
+
instead of strings, for every PostgreSQL type ruby-pg covers in text mode.
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
|
|
33
|
+
- This is an API feature, not a throughput lever. Typed columns that become
|
|
34
|
+
immediates (`int`, `bool`) drop one heap object each; the rest of the
|
|
35
|
+
per-query allocation is the control plane (`Request`, fibers, `PG::Result`).
|
|
36
|
+
- `CoderMapsBundle` is built on a throwaway connection at the first
|
|
37
|
+
`prepare(..., typed: true)`, never at `Client.start` and never on a live
|
|
38
|
+
pipeline FIFO (a catalog query there desyncs Sync). Untyped `query` /
|
|
39
|
+
`prepare` do not touch type maps on the drain path.
|
|
40
|
+
- A failed type-map apply is a request-local `QueryError`. `typed: true`
|
|
41
|
+
promised a typed value; the handle does not silently fall back to strings.
|
|
42
|
+
- `bigdecimal` is an optional dependency of ruby-pg for `numeric` text
|
|
43
|
+
decoding (required as a gem on Ruby 3.4+). Without it, other typed columns
|
|
44
|
+
still work; `numeric` follows the registry.
|
|
45
|
+
- Default path (no `typed:`): `TypeMapAllStrings`, text format, string keys.
|
|
46
|
+
- Binary result format is not part of this release.
|
|
47
|
+
|
|
3
48
|
## [0.3.2] - 2026-08-17
|
|
4
49
|
|
|
5
50
|
Runtime correctness. Public SQL, Sync-per-unit, failure model, and the
|
data/README.md
CHANGED
|
@@ -143,6 +143,27 @@ ensure
|
|
|
143
143
|
end
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
Pass `typed: true` to decode cells through ruby-pg's text type map (`Integer`,
|
|
147
|
+
`Time`, `BigDecimal`, …) instead of strings. The map is built from the first
|
|
148
|
+
result and reused. This does not change the wire format or the Sync-per-unit
|
|
149
|
+
model; it is opt-in convenience, not a faster path.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
by_id = db.prepare(
|
|
153
|
+
"user_by_id",
|
|
154
|
+
"SELECT id, email FROM users WHERE id = $1",
|
|
155
|
+
[23],
|
|
156
|
+
typed: true
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result = by_id.query([42])
|
|
160
|
+
begin
|
|
161
|
+
result.first["id"] # => 42 (Integer)
|
|
162
|
+
ensure
|
|
163
|
+
result.clear
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
146
167
|
`Client#prepare` returns only after the statement is ready on every currently
|
|
147
168
|
live pipeline connection. Replacement connections automatically prepare the
|
|
148
169
|
registered catalog before they begin accepting requests. The logical name is
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'runtime'
|
|
5
5
|
|
|
6
6
|
module PgPipeline
|
|
7
7
|
class BoundedQueue
|
|
8
8
|
def initialize(limit)
|
|
9
9
|
@limit = Integer(limit)
|
|
10
|
-
raise ArgumentError,
|
|
10
|
+
raise ArgumentError, 'limit must be >= 1' if @limit < 1
|
|
11
11
|
rescue ArgumentError, TypeError
|
|
12
|
-
raise ArgumentError,
|
|
12
|
+
raise ArgumentError, 'limit must be an integer >= 1'
|
|
13
13
|
else
|
|
14
|
-
@items
|
|
14
|
+
@items = []
|
|
15
|
+
@consumers = []
|
|
16
|
+
@producers = []
|
|
15
17
|
|
|
16
18
|
@closed = false
|
|
17
19
|
@close_error = nil
|
|
@@ -93,7 +95,7 @@ module PgPipeline
|
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def raise_close_error
|
|
96
|
-
error = @close_error || ShutdownError.new(
|
|
98
|
+
error = @close_error || ShutdownError.new('queue closed')
|
|
97
99
|
raise error.class, error.message
|
|
98
100
|
end
|
|
99
101
|
end
|
data/lib/pg_pipeline/client.rb
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'pool'
|
|
5
|
+
require_relative 'request'
|
|
6
|
+
require_relative 'prepared_statement'
|
|
7
|
+
require_relative 'session_guard'
|
|
8
|
+
require_relative 'session'
|
|
9
|
+
require_relative 'transaction'
|
|
10
10
|
|
|
11
11
|
module PgPipeline
|
|
12
12
|
class Client
|
|
13
13
|
attr_reader :guard
|
|
14
14
|
|
|
15
15
|
def initialize(connection_args = nil, guard: :default, **pool_opts)
|
|
16
|
-
@owner_thread
|
|
16
|
+
@owner_thread = nil
|
|
17
|
+
@scheduler = nil
|
|
17
18
|
|
|
18
19
|
@guard = SessionGuard.normalize_mode!(guard)
|
|
19
20
|
@pool = Pool.new(connection_args, **pool_opts)
|
|
@@ -26,7 +27,11 @@ module PgPipeline
|
|
|
26
27
|
|
|
27
28
|
def start = ClientOps.start(self)
|
|
28
29
|
def query(sql, params = RequestOps::EMPTY_PARAMS) = ClientOps.query(self, sql, params)
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
def prepare(name, sql, param_types = nil, typed: false)
|
|
32
|
+
ClientOps.prepare(self, name, sql, param_types, typed: typed)
|
|
33
|
+
end
|
|
34
|
+
|
|
30
35
|
def stats = ClientOps.stats(self)
|
|
31
36
|
|
|
32
37
|
def session(&block)
|
|
@@ -42,9 +47,8 @@ module PgPipeline
|
|
|
42
47
|
|
|
43
48
|
private
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
attr_writer :owner_thread, :scheduler
|
|
50
|
+
attr_accessor :owner_thread, :scheduler, :started
|
|
51
|
+
attr_reader :pool
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
module ClientOps
|
|
@@ -54,16 +58,18 @@ module PgPipeline
|
|
|
54
58
|
client = Client.new(connection_args, **opts).start
|
|
55
59
|
yield client
|
|
56
60
|
ensure
|
|
57
|
-
client
|
|
61
|
+
client&.close
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def start(client)
|
|
61
|
-
raise Error,
|
|
62
|
-
|
|
65
|
+
raise Error, 'client already started' if started?(client)
|
|
66
|
+
|
|
67
|
+
scheduler = Fiber.scheduler
|
|
68
|
+
raise Error, 'client start requires an active Fiber scheduler' unless scheduler
|
|
63
69
|
|
|
64
70
|
pool(client).start
|
|
65
71
|
client.__send__(:owner_thread=, Thread.current)
|
|
66
|
-
client.__send__(:scheduler=,
|
|
72
|
+
client.__send__(:scheduler=, scheduler)
|
|
67
73
|
client.__send__(:started=, true)
|
|
68
74
|
client
|
|
69
75
|
end
|
|
@@ -78,18 +84,22 @@ module PgPipeline
|
|
|
78
84
|
end
|
|
79
85
|
end
|
|
80
86
|
|
|
81
|
-
def prepare(client, name, sql, param_types)
|
|
87
|
+
def prepare(client, name, sql, param_types, typed: false)
|
|
82
88
|
ensure_started!(client)
|
|
83
89
|
sql = RequestOps.snapshot_sql(sql)
|
|
84
90
|
SessionGuard.assert_multiplexable_normalized!(sql, mode: client.guard)
|
|
85
|
-
pool(client).__send__(:prepare_statement, client, name, sql, param_types)
|
|
91
|
+
pool(client).__send__(:prepare_statement, client, name, sql, param_types, typed: typed)
|
|
86
92
|
end
|
|
87
93
|
|
|
88
94
|
def query_prepared(client, statement, params)
|
|
89
95
|
ensure_started!(client)
|
|
90
96
|
|
|
91
97
|
wait_for_request do
|
|
92
|
-
submit_with_failover(client)
|
|
98
|
+
submit_with_failover(client) do
|
|
99
|
+
request = Request.prepared_query(statement, params: params)
|
|
100
|
+
pool(client).bind_type_map(request, statement) if statement.typed?
|
|
101
|
+
request
|
|
102
|
+
end
|
|
93
103
|
end
|
|
94
104
|
end
|
|
95
105
|
|
|
@@ -102,26 +112,38 @@ module PgPipeline
|
|
|
102
112
|
|
|
103
113
|
def submit_with_failover(client)
|
|
104
114
|
attempts = 0
|
|
105
|
-
|
|
115
|
+
client_pool = pool(client)
|
|
116
|
+
limit = [client_pool.pipeline_size, 1].max
|
|
106
117
|
last_error = nil
|
|
107
118
|
|
|
108
119
|
while attempts < limit
|
|
109
120
|
request = yield
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
attempts += 1
|
|
116
|
-
rescue ShutdownError => e
|
|
117
|
-
last_error = e
|
|
118
|
-
attempts += 1
|
|
119
|
-
break unless request.state == :new && !request.settled?
|
|
120
|
-
end
|
|
121
|
+
last_error = submit_attempt(client_pool, request)
|
|
122
|
+
return request unless last_error
|
|
123
|
+
|
|
124
|
+
attempts += 1
|
|
125
|
+
break if stop_retrying?(request, last_error)
|
|
121
126
|
end
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
raise_submission_error(last_error)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def submit_attempt(client_pool, request)
|
|
132
|
+
client_pool.__send__(:pipeline_driver).submit(request)
|
|
133
|
+
nil
|
|
134
|
+
rescue NotDispatchedError, ShutdownError => e
|
|
135
|
+
e
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def stop_retrying?(request, error)
|
|
139
|
+
error.is_a?(ShutdownError) && (request.state != :new || request.settled?)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def raise_submission_error(error)
|
|
143
|
+
error ||= NotDispatchedError.new('no live pipeline connections; request was not dispatched')
|
|
144
|
+
raise error if error.is_a?(NotDispatchedError)
|
|
145
|
+
|
|
146
|
+
raise NotDispatchedError, error.message
|
|
125
147
|
end
|
|
126
148
|
|
|
127
149
|
def session(client)
|
|
@@ -137,13 +159,13 @@ module PgPipeline
|
|
|
137
159
|
end
|
|
138
160
|
end
|
|
139
161
|
|
|
140
|
-
def transaction(client)
|
|
162
|
+
def transaction(client, &block)
|
|
141
163
|
ensure_started!(client)
|
|
142
164
|
|
|
143
165
|
pool(client).__send__(:with_pinned) do |conn|
|
|
144
166
|
tx = Transaction.new(conn)
|
|
145
167
|
begin
|
|
146
|
-
TransactionOps.run(tx)
|
|
168
|
+
TransactionOps.run(tx, &block)
|
|
147
169
|
ensure
|
|
148
170
|
SessionOps.close!(tx)
|
|
149
171
|
end
|
|
@@ -159,10 +181,11 @@ module PgPipeline
|
|
|
159
181
|
return unless started?(client)
|
|
160
182
|
|
|
161
183
|
ensure_context!(client)
|
|
184
|
+
client_pool = pool(client)
|
|
162
185
|
begin
|
|
163
|
-
|
|
186
|
+
client_pool.graceful_close
|
|
164
187
|
ensure
|
|
165
|
-
client.__send__(:started=, false) if
|
|
188
|
+
client.__send__(:started=, false) if client_pool.closing?
|
|
166
189
|
end
|
|
167
190
|
end
|
|
168
191
|
|
|
@@ -170,10 +193,11 @@ module PgPipeline
|
|
|
170
193
|
return unless started?(client)
|
|
171
194
|
|
|
172
195
|
ensure_context!(client)
|
|
196
|
+
client_pool = pool(client)
|
|
173
197
|
begin
|
|
174
|
-
|
|
198
|
+
client_pool.abort!
|
|
175
199
|
ensure
|
|
176
|
-
client.__send__(:started=, false) if
|
|
200
|
+
client.__send__(:started=, false) if client_pool.closing?
|
|
177
201
|
end
|
|
178
202
|
end
|
|
179
203
|
|
|
@@ -182,7 +206,7 @@ module PgPipeline
|
|
|
182
206
|
end
|
|
183
207
|
|
|
184
208
|
def ensure_started!(client)
|
|
185
|
-
raise Error,
|
|
209
|
+
raise Error, 'client not started; call #start or use Client.open' unless started?(client)
|
|
186
210
|
|
|
187
211
|
ensure_context!(client)
|
|
188
212
|
end
|
|
@@ -191,7 +215,7 @@ module PgPipeline
|
|
|
191
215
|
return if Thread.current.equal?(client.__send__(:owner_thread)) &&
|
|
192
216
|
Fiber.scheduler.equal?(client.__send__(:scheduler))
|
|
193
217
|
|
|
194
|
-
raise Error,
|
|
218
|
+
raise Error, 'client is reactor-local and cannot be used from another thread/scheduler'
|
|
195
219
|
end
|
|
196
220
|
|
|
197
221
|
def started?(client)
|