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
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'pg'
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'runtime'
|
|
7
|
+
require_relative 'bounded_queue'
|
|
8
|
+
require_relative 'server_caps'
|
|
9
|
+
require_relative 'request'
|
|
10
10
|
|
|
11
11
|
module PgPipeline
|
|
12
12
|
class ConnectionDriver
|
|
13
13
|
DEFAULT_MAX_PENDING = 256
|
|
14
14
|
DEFAULT_MAX_IN_FLIGHT = 64
|
|
15
15
|
|
|
16
|
-
attr_reader :conn, :caps, :max_pending, :max_in_flight
|
|
16
|
+
attr_reader :conn, :caps, :max_pending, :max_in_flight, :type_maps
|
|
17
17
|
attr_accessor :socket, :requests, :events, :reader_rearm, :writer_commands,
|
|
18
18
|
:inflight, :dispatching, :submitting, :accepting, :running,
|
|
19
19
|
:draining, :needs_flush, :writer_armed, :request_event_pending,
|
|
@@ -30,11 +30,13 @@ module PgPipeline
|
|
|
30
30
|
def dispatches = @dispatches || 0
|
|
31
31
|
def leaked_watchers = @leaked_watchers || 0
|
|
32
32
|
|
|
33
|
-
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT
|
|
33
|
+
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT,
|
|
34
|
+
type_maps: nil)
|
|
34
35
|
@max_pending = DriverOps.positive_integer!(max_pending, :max_pending)
|
|
35
36
|
@max_in_flight = DriverOps.positive_integer!(max_in_flight, :max_in_flight)
|
|
36
37
|
|
|
37
38
|
@conn = conn
|
|
39
|
+
@type_maps = type_maps
|
|
38
40
|
@caps = ServerCaps.from_connection(conn)
|
|
39
41
|
@caps.assert_supported!
|
|
40
42
|
DriverOps.warn_flush_coupling_once(@caps)
|
|
@@ -45,7 +47,7 @@ module PgPipeline
|
|
|
45
47
|
|
|
46
48
|
@accepting = @running = @draining = @needs_flush = @writer_armed = @request_event_pending = false
|
|
47
49
|
@readable_events = @results_read = @units_completed = @flush_calls =
|
|
48
|
-
|
|
50
|
+
@flush_incomplete = @dispatches = @leaked_watchers = @submitting = 0
|
|
49
51
|
@socket = @owner_task = @reader_task = @writer_task = @dispatching = nil
|
|
50
52
|
@inflight = []
|
|
51
53
|
end
|
|
@@ -64,7 +66,14 @@ module PgPipeline
|
|
|
64
66
|
in_flight: @inflight.size,
|
|
65
67
|
submitting: @submitting,
|
|
66
68
|
needs_flush: @needs_flush,
|
|
67
|
-
fast_sync: @caps.fast_sync
|
|
69
|
+
fast_sync: @caps.fast_sync?
|
|
70
|
+
}.merge(counter_stats)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def counter_stats
|
|
76
|
+
{
|
|
68
77
|
readable_events: readable_events,
|
|
69
78
|
results_read: results_read,
|
|
70
79
|
units_completed: units_completed,
|
|
@@ -78,10 +87,12 @@ module PgPipeline
|
|
|
78
87
|
}
|
|
79
88
|
end
|
|
80
89
|
|
|
90
|
+
public
|
|
91
|
+
|
|
81
92
|
def health_check(timeout)
|
|
82
93
|
return true unless available?
|
|
83
94
|
|
|
84
|
-
probe = Request.build(
|
|
95
|
+
probe = Request.build('SELECT 1', nil)
|
|
85
96
|
submit(probe)
|
|
86
97
|
Runtime.with_timeout(timeout) { probe.wait }
|
|
87
98
|
true
|
|
@@ -99,18 +110,18 @@ module PgPipeline
|
|
|
99
110
|
|
|
100
111
|
def graceful_close = DriverOps.graceful_close(self)
|
|
101
112
|
|
|
102
|
-
def abort!(error = ConnectionLostError.new(
|
|
113
|
+
def abort!(error = ConnectionLostError.new('connection aborted'))
|
|
103
114
|
DriverOps.abort!(self, error)
|
|
104
115
|
end
|
|
105
116
|
end
|
|
106
117
|
|
|
107
118
|
module DriverOps
|
|
119
|
+
module_function
|
|
120
|
+
|
|
108
121
|
WATCHER_JOIN_TIMEOUT = 2.0
|
|
109
122
|
OWNER_JOIN_TIMEOUT = 5.0
|
|
110
123
|
WATCHER_POLL_INTERVAL = 0.25
|
|
111
124
|
|
|
112
|
-
module_function
|
|
113
|
-
|
|
114
125
|
def ratio(numerator, denominator)
|
|
115
126
|
return 0.0 if denominator.zero?
|
|
116
127
|
|
|
@@ -120,14 +131,14 @@ module PgPipeline
|
|
|
120
131
|
def warn_flush_coupling_once(caps)
|
|
121
132
|
return if @flush_coupling_warned
|
|
122
133
|
return if caps.fast_sync?
|
|
123
|
-
return if ENV[
|
|
134
|
+
return if ENV['PG_PIPELINE_SILENCE_WARNINGS']
|
|
124
135
|
|
|
125
136
|
@flush_coupling_warned = true
|
|
126
137
|
warn(
|
|
127
138
|
"pg_pipeline: libpq #{caps.libpq_version} couples pipeline Sync with flush " \
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
'(no PQsendPipelineSync). Queries still pipeline and still amortise RTT, but ' \
|
|
140
|
+
'one flush per unit caps local throughput; libpq >= 17 is recommended for ' \
|
|
141
|
+
'maximum throughput. Set PG_PIPELINE_SILENCE_WARNINGS=1 to silence this.'
|
|
131
142
|
)
|
|
132
143
|
end
|
|
133
144
|
|
|
@@ -140,290 +151,373 @@ module PgPipeline
|
|
|
140
151
|
raise ArgumentError, "#{name} must be an integer >= 1"
|
|
141
152
|
end
|
|
142
153
|
|
|
143
|
-
def start(
|
|
144
|
-
raise Error,
|
|
145
|
-
raise Error,
|
|
154
|
+
def start(driver)
|
|
155
|
+
raise Error, 'driver already started' if driver.running
|
|
156
|
+
raise Error, 'driver start requires an active Fiber scheduler' unless Fiber.scheduler
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
d.owner_task = Runtime.spawn(name: :owner) { owner_loop(d) }
|
|
156
|
-
d
|
|
158
|
+
conn = driver.conn
|
|
159
|
+
conn.setnonblocking(true)
|
|
160
|
+
conn.enter_pipeline_mode
|
|
161
|
+
driver.socket = conn.socket_io
|
|
162
|
+
driver.accepting = true
|
|
163
|
+
driver.running = true
|
|
164
|
+
start_tasks(driver)
|
|
165
|
+
driver
|
|
157
166
|
rescue Exception
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
teardown_watchers(
|
|
161
|
-
join_owner(
|
|
167
|
+
driver.accepting = false
|
|
168
|
+
driver.running = false
|
|
169
|
+
teardown_watchers(driver)
|
|
170
|
+
join_owner(driver)
|
|
162
171
|
raise
|
|
163
172
|
end
|
|
164
173
|
|
|
165
|
-
def
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
def start_tasks(driver)
|
|
175
|
+
driver.reader_task = Runtime.spawn(name: :reader) { reader_watcher(driver) }
|
|
176
|
+
driver.writer_task = Runtime.spawn(name: :writer) { writer_watcher(driver) }
|
|
177
|
+
driver.owner_task = Runtime.spawn(name: :owner) { owner_loop(driver) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def submit(driver, request)
|
|
181
|
+
raise ShutdownError, 'driver is not accepting work' unless driver.accepting
|
|
182
|
+
raise ProtocolError, 'request must be new before submit' unless request.state == :new
|
|
168
183
|
|
|
169
|
-
|
|
184
|
+
driver.submitting += 1
|
|
170
185
|
begin
|
|
171
|
-
|
|
186
|
+
driver.requests.enqueue(request)
|
|
172
187
|
request.queued!
|
|
173
|
-
notify_requests(
|
|
188
|
+
notify_requests(driver)
|
|
174
189
|
ensure
|
|
175
|
-
|
|
176
|
-
d.events.enqueue(:submission_finished) if d.draining && d.running && d.submitting.zero?
|
|
190
|
+
finish_submission(driver)
|
|
177
191
|
end
|
|
178
192
|
|
|
179
193
|
request
|
|
180
194
|
end
|
|
181
195
|
|
|
182
|
-
def
|
|
183
|
-
|
|
196
|
+
def finish_submission(driver)
|
|
197
|
+
submitting = driver.submitting - 1
|
|
198
|
+
driver.submitting = submitting
|
|
199
|
+
return unless driver.draining && driver.running && submitting.zero?
|
|
200
|
+
|
|
201
|
+
driver.events.enqueue(:submission_finished)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def graceful_close(driver)
|
|
205
|
+
return unless driver.running
|
|
184
206
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
join_owner(
|
|
207
|
+
driver.accepting = false
|
|
208
|
+
driver.requests.close(ShutdownError.new('driver is closing'))
|
|
209
|
+
driver.events.enqueue(:close)
|
|
210
|
+
join_owner(driver)
|
|
189
211
|
nil
|
|
190
212
|
end
|
|
191
213
|
|
|
192
|
-
def abort!(
|
|
193
|
-
return unless
|
|
214
|
+
def abort!(driver, error)
|
|
215
|
+
return unless driver.running
|
|
194
216
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
join_owner(
|
|
217
|
+
driver.accepting = false
|
|
218
|
+
driver.requests.close(not_dispatched_error(error))
|
|
219
|
+
driver.events.enqueue([:abort, error])
|
|
220
|
+
join_owner(driver)
|
|
199
221
|
nil
|
|
200
222
|
end
|
|
201
223
|
|
|
202
|
-
def abort_timed_out_health_probe(
|
|
203
|
-
return true if probe.settled? || !
|
|
204
|
-
return true unless exclusive_health_probe?(
|
|
224
|
+
def abort_timed_out_health_probe(driver, probe)
|
|
225
|
+
return true if probe.settled? || !driver.running
|
|
226
|
+
return true unless exclusive_health_probe?(driver, probe)
|
|
205
227
|
|
|
206
|
-
abort!(
|
|
228
|
+
abort!(driver, ConnectionLostError.new('idle health check timed out'))
|
|
207
229
|
false
|
|
208
230
|
end
|
|
209
231
|
|
|
210
|
-
def exclusive_health_probe?(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
def exclusive_health_probe?(driver, probe)
|
|
233
|
+
inflight = driver.inflight
|
|
234
|
+
driver.accepting &&
|
|
235
|
+
driver.dispatching.nil? &&
|
|
236
|
+
driver.submitting.zero? &&
|
|
237
|
+
driver.requests.empty? &&
|
|
238
|
+
inflight.length == 1 &&
|
|
239
|
+
inflight.first.equal?(probe)
|
|
217
240
|
end
|
|
218
241
|
|
|
219
|
-
def owner_loop(
|
|
220
|
-
while
|
|
221
|
-
event =
|
|
242
|
+
def owner_loop(driver)
|
|
243
|
+
while driver.running
|
|
244
|
+
event = driver.events.dequeue
|
|
222
245
|
break if event.nil?
|
|
223
246
|
|
|
224
|
-
process_event(
|
|
247
|
+
process_event(driver, event)
|
|
225
248
|
end
|
|
226
249
|
rescue StandardError => e
|
|
227
|
-
fatal_close(
|
|
250
|
+
fatal_close(driver, ConnectionLostError.new("driver crashed: #{e.class}: #{e.message}"))
|
|
228
251
|
ensure
|
|
229
|
-
fatal_close(
|
|
252
|
+
fatal_close(driver, ShutdownError.new('driver owner stopped before shutdown completed')) if driver.running
|
|
230
253
|
end
|
|
231
254
|
|
|
232
|
-
def process_event(
|
|
233
|
-
input_changed =
|
|
255
|
+
def process_event(driver, event)
|
|
256
|
+
input_changed = handle_event(driver, event)
|
|
257
|
+
return if input_changed == :stopped
|
|
258
|
+
|
|
259
|
+
drain_results(driver) if input_changed
|
|
260
|
+
pump_requests(driver) if driver.running
|
|
261
|
+
finish_graceful_close(driver) if driver.running && driver.draining && drained?(driver)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def handle_event(driver, event)
|
|
234
265
|
case event
|
|
235
266
|
when :requests
|
|
236
|
-
|
|
267
|
+
driver.request_event_pending = false
|
|
237
268
|
when :submission_finished
|
|
238
269
|
nil
|
|
239
270
|
when :readable
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
read_available(d)
|
|
243
|
-
input_changed = true
|
|
244
|
-
ensure
|
|
245
|
-
d.reader_rearm.enqueue(:rearm) if d.running
|
|
246
|
-
end
|
|
271
|
+
handle_readable_event(driver)
|
|
272
|
+
return true
|
|
247
273
|
when :writable
|
|
248
|
-
|
|
249
|
-
flush_output(d)
|
|
274
|
+
handle_writable_event(driver)
|
|
250
275
|
when :close
|
|
251
|
-
|
|
276
|
+
driver.draining = true
|
|
252
277
|
when Array
|
|
253
278
|
tag, payload = event
|
|
254
|
-
|
|
255
|
-
fatal_close(d, payload)
|
|
256
|
-
return
|
|
257
|
-
end
|
|
279
|
+
return abort_event(driver, tag, payload)
|
|
258
280
|
end
|
|
259
281
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
false
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def handle_writable_event(driver)
|
|
286
|
+
driver.writer_armed = false
|
|
287
|
+
flush_output(driver)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def handle_readable_event(driver)
|
|
291
|
+
driver.readable_events += 1
|
|
292
|
+
read_available(driver)
|
|
293
|
+
true
|
|
294
|
+
ensure
|
|
295
|
+
driver.reader_rearm.enqueue(:rearm) if driver.running
|
|
263
296
|
end
|
|
264
297
|
|
|
265
|
-
def
|
|
266
|
-
return
|
|
267
|
-
return unless d.running
|
|
298
|
+
def abort_event(driver, tag, payload)
|
|
299
|
+
return false unless tag == :abort
|
|
268
300
|
|
|
269
|
-
|
|
270
|
-
|
|
301
|
+
fatal_close(driver, payload)
|
|
302
|
+
:stopped
|
|
271
303
|
end
|
|
272
304
|
|
|
273
|
-
def
|
|
305
|
+
def notify_requests(driver)
|
|
306
|
+
return if driver.request_event_pending
|
|
307
|
+
return unless driver.running
|
|
308
|
+
|
|
309
|
+
driver.request_event_pending = true
|
|
310
|
+
driver.events.enqueue(:requests)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def pump_requests(driver)
|
|
274
314
|
dispatched = false
|
|
275
|
-
while
|
|
276
|
-
request =
|
|
315
|
+
while driver.inflight.size < driver.max_in_flight && !driver.requests.empty?
|
|
316
|
+
request = driver.requests.dequeue
|
|
277
317
|
break unless request
|
|
278
318
|
next if request.cancelled?
|
|
279
319
|
|
|
280
|
-
|
|
281
|
-
unless send_unit(d, request)
|
|
282
|
-
d.dispatching = nil
|
|
283
|
-
next
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
request.dispatched!
|
|
287
|
-
d.inflight << request
|
|
288
|
-
d.dispatching = nil
|
|
289
|
-
d.dispatches += 1
|
|
290
|
-
dispatched = true
|
|
320
|
+
dispatched = dispatch_request(driver, request) || dispatched
|
|
291
321
|
end
|
|
292
322
|
|
|
293
|
-
flush_output(
|
|
323
|
+
flush_output(driver) if dispatched && !driver.needs_flush
|
|
294
324
|
end
|
|
295
325
|
|
|
296
|
-
def
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
rescue PG::UnableToSend => e
|
|
300
|
-
d.dispatching = nil
|
|
301
|
-
request.reject!(
|
|
302
|
-
NotDispatchedError.new("query was rejected before libpq accepted it: #{e.class}: #{e.message}")
|
|
303
|
-
)
|
|
304
|
-
return false if reusable_after_send_rejection?(d)
|
|
305
|
-
|
|
306
|
-
raise ConnectionLostError, "dispatch failed: #{e.class}: #{e.message}"
|
|
307
|
-
rescue PG::Error => e
|
|
308
|
-
d.dispatching = nil
|
|
309
|
-
request.reject!(
|
|
310
|
-
NotDispatchedError.new("query was rejected before libpq accepted it: #{e.class}: #{e.message}")
|
|
311
|
-
)
|
|
312
|
-
raise ConnectionLostError, "dispatch failed: #{e.class}: #{e.message}"
|
|
313
|
-
rescue ProtocolError
|
|
314
|
-
raise
|
|
315
|
-
rescue StandardError => e
|
|
316
|
-
request.reject!(e)
|
|
317
|
-
return false
|
|
318
|
-
end
|
|
326
|
+
def dispatch_request(driver, request)
|
|
327
|
+
driver.dispatching = request
|
|
328
|
+
return false unless send_unit(driver, request)
|
|
319
329
|
|
|
320
|
-
|
|
330
|
+
request.dispatched!
|
|
331
|
+
driver.inflight << request
|
|
332
|
+
driver.dispatching = nil
|
|
333
|
+
driver.dispatches += 1
|
|
321
334
|
true
|
|
335
|
+
ensure
|
|
336
|
+
driver.dispatching = nil if request&.settled?
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def send_unit(driver, request)
|
|
340
|
+
return false unless send_request(driver, request)
|
|
341
|
+
|
|
342
|
+
place_sync(driver)
|
|
343
|
+
true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def send_request(driver, request)
|
|
347
|
+
send_command(driver.conn, request)
|
|
348
|
+
true
|
|
349
|
+
rescue PG::UnableToSend => e
|
|
350
|
+
reject_send_error(driver, request, e, recoverable: reusable_after_send_rejection?(driver))
|
|
351
|
+
rescue PG::Error => e
|
|
352
|
+
reject_send_error(driver, request, e)
|
|
353
|
+
rescue ProtocolError
|
|
354
|
+
raise
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
request.reject!(e)
|
|
357
|
+
false
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def place_sync(driver)
|
|
361
|
+
driver.caps.place_sync(driver.conn)
|
|
322
362
|
rescue PG::Error => e
|
|
323
363
|
raise ConnectionLostError, "dispatch Sync failed: #{e.class}: #{e.message}"
|
|
324
364
|
end
|
|
325
365
|
|
|
366
|
+
def reject_send_error(driver, request, error, recoverable: false)
|
|
367
|
+
driver.dispatching = nil
|
|
368
|
+
request.reject!(NotDispatchedError.new("query was rejected before libpq accepted it: #{error.class}: #{error.message}"))
|
|
369
|
+
return false if recoverable
|
|
370
|
+
|
|
371
|
+
raise ConnectionLostError, "dispatch failed: #{error.class}: #{error.message}"
|
|
372
|
+
end
|
|
373
|
+
|
|
326
374
|
def send_command(conn, request)
|
|
327
|
-
|
|
375
|
+
operation = request.operation
|
|
376
|
+
params = request.params
|
|
377
|
+
case operation
|
|
328
378
|
when :query
|
|
329
|
-
conn.send_query_params(request.sql,
|
|
379
|
+
conn.send_query_params(request.sql, params)
|
|
330
380
|
when :prepare
|
|
331
|
-
|
|
332
|
-
conn.send_prepare(request.statement_name, request.sql)
|
|
333
|
-
else
|
|
334
|
-
conn.send_prepare(request.statement_name, request.sql, request.param_types)
|
|
335
|
-
end
|
|
381
|
+
send_prepare(conn, request)
|
|
336
382
|
when :prepared_query
|
|
337
|
-
conn.send_query_prepared(request.statement_name,
|
|
383
|
+
conn.send_query_prepared(request.statement_name, params)
|
|
384
|
+
else
|
|
385
|
+
raise ProtocolError, "unsupported request operation #{operation.inspect}"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def send_prepare(conn, request)
|
|
390
|
+
param_types = request.param_types
|
|
391
|
+
statement_name = request.statement_name
|
|
392
|
+
sql = request.sql
|
|
393
|
+
if param_types.nil?
|
|
394
|
+
conn.send_prepare(statement_name, sql)
|
|
338
395
|
else
|
|
339
|
-
|
|
396
|
+
conn.send_prepare(statement_name, sql, param_types)
|
|
340
397
|
end
|
|
341
398
|
end
|
|
342
399
|
|
|
343
|
-
def reusable_after_send_rejection?(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
d.conn.pipeline_status != PG::PQ_PIPELINE_OFF
|
|
400
|
+
def reusable_after_send_rejection?(driver)
|
|
401
|
+
conn = driver.conn
|
|
402
|
+
!conn.finished? && conn.status == PG::CONNECTION_OK && conn.pipeline_status != PG::PQ_PIPELINE_OFF
|
|
347
403
|
rescue PG::Error
|
|
348
404
|
false
|
|
349
405
|
end
|
|
350
406
|
|
|
351
|
-
def flush_output(
|
|
352
|
-
return unless
|
|
407
|
+
def flush_output(driver)
|
|
408
|
+
return unless driver.running
|
|
353
409
|
|
|
354
|
-
|
|
355
|
-
if
|
|
356
|
-
|
|
410
|
+
driver.flush_calls += 1
|
|
411
|
+
if driver.conn.sync_flush
|
|
412
|
+
driver.needs_flush = false
|
|
357
413
|
else
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
arm_writer(
|
|
414
|
+
driver.needs_flush = true
|
|
415
|
+
driver.flush_incomplete += 1
|
|
416
|
+
arm_writer(driver)
|
|
361
417
|
end
|
|
362
418
|
rescue PG::Error => e
|
|
363
419
|
raise ConnectionLostError, "flush failed: #{e.class}: #{e.message}"
|
|
364
420
|
end
|
|
365
421
|
|
|
366
|
-
def arm_writer(
|
|
367
|
-
return if
|
|
368
|
-
return unless
|
|
422
|
+
def arm_writer(driver)
|
|
423
|
+
return if driver.writer_armed
|
|
424
|
+
return unless driver.running
|
|
369
425
|
|
|
370
|
-
|
|
371
|
-
|
|
426
|
+
driver.writer_armed = true
|
|
427
|
+
driver.writer_commands.enqueue(:wait_writable)
|
|
372
428
|
end
|
|
373
429
|
|
|
374
|
-
def read_available(
|
|
375
|
-
|
|
430
|
+
def read_available(driver)
|
|
431
|
+
driver.conn.consume_input
|
|
376
432
|
rescue PG::Error => e
|
|
377
433
|
raise ConnectionLostError, "read failed: #{e.class}: #{e.message}"
|
|
378
434
|
end
|
|
379
435
|
|
|
380
|
-
def drain_results(
|
|
436
|
+
def drain_results(driver)
|
|
381
437
|
read = 0
|
|
438
|
+
conn = driver.conn
|
|
439
|
+
inflight = driver.inflight
|
|
382
440
|
|
|
383
441
|
begin
|
|
384
|
-
while !
|
|
385
|
-
result =
|
|
442
|
+
while !inflight.empty? && !conn.is_busy
|
|
443
|
+
result = conn.sync_get_result
|
|
386
444
|
read += 1
|
|
387
|
-
request =
|
|
388
|
-
raise ProtocolError,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
request.query_boundary!
|
|
392
|
-
next
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
status = result.result_status
|
|
396
|
-
case status
|
|
397
|
-
when PG::PGRES_TUPLES_OK
|
|
398
|
-
ensure_before_query_boundary!(request, status)
|
|
399
|
-
request.accept_result(result)
|
|
400
|
-
when PG::PGRES_PIPELINE_SYNC
|
|
401
|
-
clear_result(result)
|
|
402
|
-
complete_front(d, request)
|
|
403
|
-
when PG::PGRES_COMMAND_OK, PG::PGRES_EMPTY_QUERY
|
|
404
|
-
ensure_before_query_boundary!(request, status)
|
|
405
|
-
request.accept_result(result)
|
|
406
|
-
when PG::PGRES_FATAL_ERROR
|
|
407
|
-
ensure_before_query_boundary!(request, status)
|
|
408
|
-
request.record_error!(query_error(result), result: result)
|
|
409
|
-
when PG::PGRES_PIPELINE_ABORTED
|
|
410
|
-
ensure_before_query_boundary!(request, status)
|
|
411
|
-
clear_result(result)
|
|
412
|
-
request.record_error!(PipelineAbortedError.new("pipeline unit aborted"))
|
|
413
|
-
when PG::PGRES_BAD_RESPONSE
|
|
414
|
-
clear_result(result)
|
|
415
|
-
raise ProtocolError, "server response was not understood"
|
|
416
|
-
when PG::PGRES_COPY_IN, PG::PGRES_COPY_OUT, PG::PGRES_COPY_BOTH
|
|
417
|
-
clear_result(result)
|
|
418
|
-
raise ProtocolError, "COPY is not supported on the multiplexed pipeline"
|
|
419
|
-
else
|
|
420
|
-
clear_result(result)
|
|
421
|
-
raise ProtocolError, "unexpected pipeline result status #{status}"
|
|
422
|
-
end
|
|
445
|
+
request = inflight.first
|
|
446
|
+
raise ProtocolError, 'result without an in-flight request' unless request
|
|
447
|
+
|
|
448
|
+
handle_pipeline_result(driver, request, result)
|
|
423
449
|
end
|
|
424
450
|
ensure
|
|
425
|
-
|
|
451
|
+
driver.results_read += read if read.positive?
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def handle_pipeline_result(driver, request, result)
|
|
456
|
+
if result.nil?
|
|
457
|
+
request.query_boundary!
|
|
458
|
+
return
|
|
426
459
|
end
|
|
460
|
+
|
|
461
|
+
handle_pipeline_status(driver, request, result, result.result_status)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def handle_pipeline_status(driver, request, result, status)
|
|
465
|
+
case status
|
|
466
|
+
when PG::PGRES_TUPLES_OK
|
|
467
|
+
accept_tuples_ok(driver, request, result)
|
|
468
|
+
when PG::PGRES_PIPELINE_SYNC
|
|
469
|
+
complete_pipeline_sync(driver, request, result)
|
|
470
|
+
when PG::PGRES_COMMAND_OK, PG::PGRES_EMPTY_QUERY
|
|
471
|
+
accept_command_result(request, result, status)
|
|
472
|
+
when PG::PGRES_FATAL_ERROR
|
|
473
|
+
record_fatal_result(request, result, status)
|
|
474
|
+
when PG::PGRES_PIPELINE_ABORTED
|
|
475
|
+
record_aborted_result(request, result, status)
|
|
476
|
+
when PG::PGRES_BAD_RESPONSE
|
|
477
|
+
reject_pipeline_status!(result, 'server response was not understood')
|
|
478
|
+
when PG::PGRES_COPY_IN, PG::PGRES_COPY_OUT, PG::PGRES_COPY_BOTH
|
|
479
|
+
reject_pipeline_status!(result, 'COPY is not supported on the multiplexed pipeline')
|
|
480
|
+
else
|
|
481
|
+
reject_pipeline_status!(result, "unexpected pipeline result status #{status}")
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def complete_pipeline_sync(driver, request, result)
|
|
486
|
+
clear_result(result)
|
|
487
|
+
complete_front(driver, request)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def accept_command_result(request, result, status)
|
|
491
|
+
ensure_before_query_boundary!(request, status)
|
|
492
|
+
request.accept_result(result)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def record_fatal_result(request, result, status)
|
|
496
|
+
ensure_before_query_boundary!(request, status)
|
|
497
|
+
request.record_error!(query_error(result), result: result)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def record_aborted_result(request, result, status)
|
|
501
|
+
ensure_before_query_boundary!(request, status)
|
|
502
|
+
clear_result(result)
|
|
503
|
+
request.record_error!(PipelineAbortedError.new('pipeline unit aborted'))
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def accept_tuples_ok(driver, request, result)
|
|
507
|
+
ensure_before_query_boundary!(request, PG::PGRES_TUPLES_OK)
|
|
508
|
+
begin
|
|
509
|
+
apply_type_map(driver, request, result)
|
|
510
|
+
request.accept_result(result)
|
|
511
|
+
rescue StandardError => e
|
|
512
|
+
detail = e.message
|
|
513
|
+
message = e.is_a?(Error) ? detail : "typed result mapping failed: #{e.class}: #{detail}"
|
|
514
|
+
request.record_error!(QueryError.new(message, cause_result: result), result: result)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def reject_pipeline_status!(result, message)
|
|
519
|
+
clear_result(result)
|
|
520
|
+
raise ProtocolError, message
|
|
427
521
|
end
|
|
428
522
|
|
|
429
523
|
def ensure_before_query_boundary!(request, status)
|
|
@@ -432,61 +526,62 @@ module PgPipeline
|
|
|
432
526
|
raise ProtocolError, "result status #{status} arrived after query boundary"
|
|
433
527
|
end
|
|
434
528
|
|
|
435
|
-
def complete_front(
|
|
436
|
-
|
|
529
|
+
def complete_front(driver, request)
|
|
530
|
+
inflight = driver.inflight
|
|
531
|
+
raise ProtocolError, 'sync does not match FIFO front' unless request.equal?(inflight.first)
|
|
437
532
|
|
|
438
|
-
|
|
439
|
-
|
|
533
|
+
inflight.shift
|
|
534
|
+
driver.units_completed += 1
|
|
440
535
|
request.finish!
|
|
441
536
|
end
|
|
442
537
|
|
|
443
538
|
def query_error(result)
|
|
444
539
|
message = result.error_message.to_s.strip
|
|
445
|
-
message =
|
|
540
|
+
message = 'query failed' if message.empty?
|
|
446
541
|
QueryError.new(message, cause_result: result)
|
|
447
542
|
end
|
|
448
543
|
|
|
449
|
-
def drained?(
|
|
450
|
-
|
|
544
|
+
def drained?(driver)
|
|
545
|
+
driver.submitting.zero? && driver.requests.empty? && driver.inflight.empty? && !driver.needs_flush && driver.dispatching.nil?
|
|
451
546
|
end
|
|
452
547
|
|
|
453
|
-
def finish_graceful_close(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
548
|
+
def finish_graceful_close(driver)
|
|
549
|
+
driver.accepting = false
|
|
550
|
+
driver.running = false
|
|
551
|
+
driver.conn.exit_pipeline_mode
|
|
457
552
|
rescue PG::Error => e
|
|
458
|
-
fail_all(
|
|
553
|
+
fail_all(driver, ConnectionLostError.new("failed to exit pipeline mode: #{e.message}"))
|
|
459
554
|
ensure
|
|
460
|
-
teardown_watchers(
|
|
555
|
+
teardown_watchers(driver)
|
|
461
556
|
end
|
|
462
557
|
|
|
463
|
-
def fatal_close(
|
|
464
|
-
return unless
|
|
558
|
+
def fatal_close(driver, error)
|
|
559
|
+
return unless driver.running || driver.accepting
|
|
465
560
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
fail_all(
|
|
561
|
+
driver.accepting = false
|
|
562
|
+
driver.running = false
|
|
563
|
+
driver.requests.close(not_dispatched_error(error))
|
|
564
|
+
fail_all(driver, error)
|
|
470
565
|
|
|
471
|
-
teardown_watchers(
|
|
566
|
+
teardown_watchers(driver)
|
|
472
567
|
end
|
|
473
568
|
|
|
474
|
-
def fail_all(
|
|
569
|
+
def fail_all(driver, error)
|
|
570
|
+
dispatching = driver.dispatching
|
|
571
|
+
inflight = driver.inflight
|
|
475
572
|
uncertain = []
|
|
476
|
-
uncertain <<
|
|
477
|
-
uncertain.concat(
|
|
478
|
-
queued =
|
|
573
|
+
uncertain << dispatching if dispatching
|
|
574
|
+
uncertain.concat(inflight)
|
|
575
|
+
queued = driver.requests.drain
|
|
479
576
|
|
|
480
|
-
uncertain.compact.uniq
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
request.reject!(not_dispatched_error(error)) unless request.settled?
|
|
486
|
-
end
|
|
577
|
+
reject_requests(uncertain.compact.uniq) { indeterminate_error(error) }
|
|
578
|
+
reject_requests(queued) { not_dispatched_error(error) }
|
|
579
|
+
driver.dispatching = nil
|
|
580
|
+
inflight.clear
|
|
581
|
+
end
|
|
487
582
|
|
|
488
|
-
|
|
489
|
-
|
|
583
|
+
def reject_requests(requests)
|
|
584
|
+
requests.each { |request| request.reject!(yield) unless request.settled? }
|
|
490
585
|
end
|
|
491
586
|
|
|
492
587
|
def not_dispatched_error(error)
|
|
@@ -500,7 +595,7 @@ module PgPipeline
|
|
|
500
595
|
|
|
501
596
|
IndeterminateResultError.new(
|
|
502
597
|
"#{error.message}; request was dispatched but its Sync was not observed, " \
|
|
503
|
-
|
|
598
|
+
'so execution/commit outcome is indeterminate'
|
|
504
599
|
)
|
|
505
600
|
end
|
|
506
601
|
|
|
@@ -511,76 +606,76 @@ module PgPipeline
|
|
|
511
606
|
WATCHER_POLL_INTERVAL
|
|
512
607
|
end
|
|
513
608
|
|
|
514
|
-
def wait_socket_readable(
|
|
609
|
+
def wait_socket_readable(driver, timeout)
|
|
515
610
|
loop do
|
|
516
|
-
return false unless
|
|
517
|
-
return true if
|
|
611
|
+
return false unless driver.running
|
|
612
|
+
return true if driver.socket.wait_readable(timeout)
|
|
518
613
|
end
|
|
519
614
|
end
|
|
520
615
|
|
|
521
|
-
def wait_socket_writable(
|
|
616
|
+
def wait_socket_writable(driver, timeout)
|
|
522
617
|
loop do
|
|
523
|
-
return false unless
|
|
524
|
-
return true if
|
|
618
|
+
return false unless driver.running
|
|
619
|
+
return true if driver.socket.wait_writable(timeout)
|
|
525
620
|
end
|
|
526
621
|
end
|
|
527
622
|
|
|
528
|
-
def reader_watcher(
|
|
623
|
+
def reader_watcher(driver)
|
|
529
624
|
timeout = watcher_wait_timeout
|
|
530
625
|
|
|
531
|
-
while
|
|
532
|
-
break unless wait_socket_readable(
|
|
626
|
+
while driver.running
|
|
627
|
+
break unless wait_socket_readable(driver, timeout)
|
|
533
628
|
|
|
534
|
-
|
|
535
|
-
command =
|
|
536
|
-
break unless command == :rearm &&
|
|
629
|
+
driver.events.enqueue(:readable)
|
|
630
|
+
command = driver.reader_rearm.dequeue
|
|
631
|
+
break unless command == :rearm && driver.running
|
|
537
632
|
end
|
|
538
633
|
rescue StandardError => e
|
|
539
|
-
if
|
|
540
|
-
|
|
634
|
+
if driver.running
|
|
635
|
+
driver.events.enqueue([:abort, ConnectionLostError.new("reader watcher failed: #{e.class}: #{e.message}")])
|
|
541
636
|
end
|
|
542
637
|
end
|
|
543
638
|
|
|
544
|
-
def writer_watcher(
|
|
639
|
+
def writer_watcher(driver)
|
|
545
640
|
timeout = watcher_wait_timeout
|
|
546
641
|
|
|
547
|
-
while
|
|
548
|
-
command =
|
|
549
|
-
break unless command == :wait_writable &&
|
|
550
|
-
break unless wait_socket_writable(
|
|
642
|
+
while driver.running
|
|
643
|
+
command = driver.writer_commands.dequeue
|
|
644
|
+
break unless command == :wait_writable && driver.running
|
|
645
|
+
break unless wait_socket_writable(driver, timeout)
|
|
551
646
|
|
|
552
|
-
|
|
647
|
+
driver.events.enqueue(:writable) if driver.running
|
|
553
648
|
end
|
|
554
649
|
rescue StandardError => e
|
|
555
|
-
if
|
|
556
|
-
|
|
650
|
+
if driver.running
|
|
651
|
+
driver.events.enqueue([:abort, ConnectionLostError.new("writer watcher failed: #{e.class}: #{e.message}")])
|
|
557
652
|
end
|
|
558
653
|
end
|
|
559
654
|
|
|
560
|
-
def teardown_watchers(
|
|
561
|
-
tasks = release_watchers(
|
|
562
|
-
close_wait_points(
|
|
655
|
+
def teardown_watchers(driver)
|
|
656
|
+
tasks = release_watchers(driver)
|
|
657
|
+
close_wait_points(driver)
|
|
563
658
|
begin
|
|
564
|
-
|
|
659
|
+
driver.socket&.close
|
|
565
660
|
rescue StandardError
|
|
566
661
|
nil
|
|
567
662
|
end
|
|
568
663
|
stop_watchers(tasks)
|
|
569
|
-
join_watchers(
|
|
570
|
-
|
|
571
|
-
safe_close_conn(
|
|
664
|
+
join_watchers(driver, tasks)
|
|
665
|
+
driver.socket = nil
|
|
666
|
+
safe_close_conn(driver)
|
|
572
667
|
nil
|
|
573
668
|
end
|
|
574
669
|
|
|
575
|
-
def release_watchers(
|
|
576
|
-
tasks = [
|
|
577
|
-
|
|
578
|
-
|
|
670
|
+
def release_watchers(driver)
|
|
671
|
+
tasks = [driver.reader_task, driver.writer_task].compact
|
|
672
|
+
driver.reader_task = nil
|
|
673
|
+
driver.writer_task = nil
|
|
579
674
|
tasks
|
|
580
675
|
end
|
|
581
676
|
|
|
582
|
-
def close_wait_points(
|
|
583
|
-
[
|
|
677
|
+
def close_wait_points(driver)
|
|
678
|
+
[driver.reader_rearm, driver.writer_commands, driver.events].each do |queue|
|
|
584
679
|
queue&.close
|
|
585
680
|
rescue StandardError
|
|
586
681
|
nil
|
|
@@ -597,12 +692,12 @@ module PgPipeline
|
|
|
597
692
|
nil
|
|
598
693
|
end
|
|
599
694
|
|
|
600
|
-
def join_watchers(
|
|
695
|
+
def join_watchers(driver, tasks)
|
|
601
696
|
Array(tasks).each do |task|
|
|
602
697
|
task.wait(WATCHER_JOIN_TIMEOUT)
|
|
603
698
|
rescue Runtime::TimeoutError
|
|
604
|
-
|
|
605
|
-
warn_leaked_task(
|
|
699
|
+
driver.leaked_watchers += 1
|
|
700
|
+
warn_leaked_task(driver, task, WATCHER_JOIN_TIMEOUT)
|
|
606
701
|
rescue Runtime::Cancel, StandardError
|
|
607
702
|
nil
|
|
608
703
|
end
|
|
@@ -610,16 +705,16 @@ module PgPipeline
|
|
|
610
705
|
nil
|
|
611
706
|
end
|
|
612
707
|
|
|
613
|
-
def join_owner(
|
|
614
|
-
owner =
|
|
708
|
+
def join_owner(driver)
|
|
709
|
+
owner = driver.owner_task
|
|
615
710
|
return if owner.nil?
|
|
616
711
|
return if Fiber.current.equal?(owner.fiber)
|
|
617
712
|
|
|
618
713
|
begin
|
|
619
714
|
owner.wait(OWNER_JOIN_TIMEOUT)
|
|
620
715
|
rescue Runtime::TimeoutError
|
|
621
|
-
|
|
622
|
-
warn_leaked_task(
|
|
716
|
+
driver.leaked_watchers += 1
|
|
717
|
+
warn_leaked_task(driver, owner, OWNER_JOIN_TIMEOUT)
|
|
623
718
|
rescue Runtime::Cancel, StandardError
|
|
624
719
|
nil
|
|
625
720
|
end
|
|
@@ -627,23 +722,32 @@ module PgPipeline
|
|
|
627
722
|
nil
|
|
628
723
|
end
|
|
629
724
|
|
|
630
|
-
def warn_leaked_task(
|
|
631
|
-
return if ENV[
|
|
725
|
+
def warn_leaked_task(driver, task, timeout)
|
|
726
|
+
return if ENV['PG_PIPELINE_SILENCE_WARNINGS']
|
|
632
727
|
|
|
633
728
|
warn(
|
|
634
729
|
"pg_pipeline: task #{task.name.inspect} did not exit within " \
|
|
635
|
-
"#{timeout}s and has been leaked (total #{
|
|
636
|
-
|
|
730
|
+
"#{timeout}s and has been leaked (total #{driver.leaked_watchers}). " \
|
|
731
|
+
'Closing the socket and, on schedulers without #fiber_interrupt, the ' \
|
|
637
732
|
"#{WATCHER_POLL_INTERVAL}s watcher poll did not release the fiber in time."
|
|
638
733
|
)
|
|
639
734
|
end
|
|
640
735
|
|
|
641
|
-
def safe_close_conn(
|
|
642
|
-
|
|
736
|
+
def safe_close_conn(driver)
|
|
737
|
+
driver.conn.close unless driver.conn.finished?
|
|
643
738
|
rescue StandardError
|
|
644
739
|
nil
|
|
645
740
|
end
|
|
646
741
|
|
|
742
|
+
def apply_type_map(driver, request, result)
|
|
743
|
+
return unless request.type_map
|
|
744
|
+
|
|
745
|
+
maps = driver.type_maps
|
|
746
|
+
raise Error, 'typed statement has no type-map registry' unless maps
|
|
747
|
+
|
|
748
|
+
maps.apply!(request, result, driver.conn)
|
|
749
|
+
end
|
|
750
|
+
|
|
647
751
|
def clear_result(result)
|
|
648
752
|
result.clear if result.respond_to?(:clear)
|
|
649
753
|
end
|