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.
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pg"
3
+ require 'pg'
4
4
 
5
- require_relative "errors"
6
- require_relative "runtime"
7
- require_relative "bounded_queue"
8
- require_relative "server_caps"
9
- require_relative "request"
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
- @flush_incomplete = @dispatches = @leaked_watchers = @submitting = 0
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("SELECT 1", nil)
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("connection aborted"))
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["PG_PIPELINE_SILENCE_WARNINGS"]
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
- "(no PQsendPipelineSync). Queries still pipeline and still amortise RTT, but " \
129
- "one flush per unit caps local throughput; libpq >= 17 is recommended for " \
130
- "maximum throughput. Set PG_PIPELINE_SILENCE_WARNINGS=1 to silence this."
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(d)
144
- raise Error, "driver already started" if d.running
145
- raise Error, "driver start requires an active Fiber scheduler" unless Fiber.scheduler
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
- d.conn.setnonblocking(true)
148
- d.conn.enter_pipeline_mode
149
- d.socket = d.conn.socket_io
150
- d.accepting = true
151
- d.running = true
152
-
153
- d.reader_task = Runtime.spawn(name: :reader) { reader_watcher(d) }
154
- d.writer_task = Runtime.spawn(name: :writer) { writer_watcher(d) }
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
- d.accepting = false
159
- d.running = false
160
- teardown_watchers(d)
161
- join_owner(d)
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 submit(d, request)
166
- raise ShutdownError, "driver is not accepting work" unless d.accepting
167
- raise ProtocolError, "request must be new before submit" unless request.state == :new
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
- d.submitting += 1
184
+ driver.submitting += 1
170
185
  begin
171
- d.requests.enqueue(request)
186
+ driver.requests.enqueue(request)
172
187
  request.queued!
173
- notify_requests(d)
188
+ notify_requests(driver)
174
189
  ensure
175
- d.submitting -= 1
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 graceful_close(d)
183
- return unless d.running
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
- d.accepting = false
186
- d.requests.close(ShutdownError.new("driver is closing"))
187
- d.events.enqueue(:close)
188
- join_owner(d)
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!(d, error)
193
- return unless d.running
214
+ def abort!(driver, error)
215
+ return unless driver.running
194
216
 
195
- d.accepting = false
196
- d.requests.close(not_dispatched_error(error))
197
- d.events.enqueue([:abort, error])
198
- join_owner(d)
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(d, probe)
203
- return true if probe.settled? || !d.running
204
- return true unless exclusive_health_probe?(d, 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!(d, ConnectionLostError.new("idle health check timed out"))
228
+ abort!(driver, ConnectionLostError.new('idle health check timed out'))
207
229
  false
208
230
  end
209
231
 
210
- def exclusive_health_probe?(d, probe)
211
- d.accepting &&
212
- d.dispatching.nil? &&
213
- d.submitting.zero? &&
214
- d.requests.empty? &&
215
- d.inflight.length == 1 &&
216
- d.inflight.first.equal?(probe)
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(d)
220
- while d.running
221
- event = d.events.dequeue
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(d, event)
247
+ process_event(driver, event)
225
248
  end
226
249
  rescue StandardError => e
227
- fatal_close(d, ConnectionLostError.new("driver crashed: #{e.class}: #{e.message}"))
250
+ fatal_close(driver, ConnectionLostError.new("driver crashed: #{e.class}: #{e.message}"))
228
251
  ensure
229
- fatal_close(d, ShutdownError.new("driver owner stopped before shutdown completed")) if d.running
252
+ fatal_close(driver, ShutdownError.new('driver owner stopped before shutdown completed')) if driver.running
230
253
  end
231
254
 
232
- def process_event(d, event)
233
- input_changed = false
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
- d.request_event_pending = false
267
+ driver.request_event_pending = false
237
268
  when :submission_finished
238
269
  nil
239
270
  when :readable
240
- begin
241
- d.readable_events += 1
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
- d.writer_armed = false
249
- flush_output(d)
274
+ handle_writable_event(driver)
250
275
  when :close
251
- d.draining = true
276
+ driver.draining = true
252
277
  when Array
253
278
  tag, payload = event
254
- if tag == :abort
255
- fatal_close(d, payload)
256
- return
257
- end
279
+ return abort_event(driver, tag, payload)
258
280
  end
259
281
 
260
- drain_results(d) if input_changed
261
- pump_requests(d) if d.running
262
- finish_graceful_close(d) if d.running && d.draining && drained?(d)
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 notify_requests(d)
266
- return if d.request_event_pending
267
- return unless d.running
298
+ def abort_event(driver, tag, payload)
299
+ return false unless tag == :abort
268
300
 
269
- d.request_event_pending = true
270
- d.events.enqueue(:requests)
301
+ fatal_close(driver, payload)
302
+ :stopped
271
303
  end
272
304
 
273
- def pump_requests(d)
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 d.inflight.size < d.max_in_flight && !d.requests.empty?
276
- request = d.requests.dequeue
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
- d.dispatching = request
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(d) if dispatched && !d.needs_flush
323
+ flush_output(driver) if dispatched && !driver.needs_flush
294
324
  end
295
325
 
296
- def send_unit(d, request)
297
- begin
298
- send_command(d.conn, request)
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
- d.caps.place_sync(d.conn)
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
- case request.operation
375
+ operation = request.operation
376
+ params = request.params
377
+ case operation
328
378
  when :query
329
- conn.send_query_params(request.sql, request.params)
379
+ conn.send_query_params(request.sql, params)
330
380
  when :prepare
331
- if request.param_types.nil?
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, request.params)
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
- raise ProtocolError, "unsupported request operation #{request.operation.inspect}"
396
+ conn.send_prepare(statement_name, sql, param_types)
340
397
  end
341
398
  end
342
399
 
343
- def reusable_after_send_rejection?(d)
344
- !d.conn.finished? &&
345
- d.conn.status == PG::CONNECTION_OK &&
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(d)
352
- return unless d.running
407
+ def flush_output(driver)
408
+ return unless driver.running
353
409
 
354
- d.flush_calls += 1
355
- if d.conn.sync_flush
356
- d.needs_flush = false
410
+ driver.flush_calls += 1
411
+ if driver.conn.sync_flush
412
+ driver.needs_flush = false
357
413
  else
358
- d.needs_flush = true
359
- d.flush_incomplete += 1
360
- arm_writer(d)
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(d)
367
- return if d.writer_armed
368
- return unless d.running
422
+ def arm_writer(driver)
423
+ return if driver.writer_armed
424
+ return unless driver.running
369
425
 
370
- d.writer_armed = true
371
- d.writer_commands.enqueue(:wait_writable)
426
+ driver.writer_armed = true
427
+ driver.writer_commands.enqueue(:wait_writable)
372
428
  end
373
429
 
374
- def read_available(d)
375
- d.conn.consume_input
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(d)
436
+ def drain_results(driver)
381
437
  read = 0
438
+ conn = driver.conn
439
+ inflight = driver.inflight
382
440
 
383
441
  begin
384
- while !d.inflight.empty? && !d.conn.is_busy
385
- result = d.conn.sync_get_result
442
+ while !inflight.empty? && !conn.is_busy
443
+ result = conn.sync_get_result
386
444
  read += 1
387
- request = d.inflight.first
388
- raise ProtocolError, "result without an in-flight request" unless request
389
-
390
- if result.nil?
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
- d.results_read += read if read.positive?
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(d, request)
436
- raise ProtocolError, "sync does not match FIFO front" unless request.equal?(d.inflight.first)
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
- d.inflight.shift
439
- d.units_completed += 1
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 = "query failed" if message.empty?
540
+ message = 'query failed' if message.empty?
446
541
  QueryError.new(message, cause_result: result)
447
542
  end
448
543
 
449
- def drained?(d)
450
- d.submitting.zero? && d.requests.empty? && d.inflight.empty? && !d.needs_flush && d.dispatching.nil?
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(d)
454
- d.accepting = false
455
- d.running = false
456
- d.conn.exit_pipeline_mode
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(d, ConnectionLostError.new("failed to exit pipeline mode: #{e.message}"))
553
+ fail_all(driver, ConnectionLostError.new("failed to exit pipeline mode: #{e.message}"))
459
554
  ensure
460
- teardown_watchers(d)
555
+ teardown_watchers(driver)
461
556
  end
462
557
 
463
- def fatal_close(d, error)
464
- return unless d.running || d.accepting
558
+ def fatal_close(driver, error)
559
+ return unless driver.running || driver.accepting
465
560
 
466
- d.accepting = false
467
- d.running = false
468
- d.requests.close(not_dispatched_error(error))
469
- fail_all(d, error)
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(d)
566
+ teardown_watchers(driver)
472
567
  end
473
568
 
474
- def fail_all(d, error)
569
+ def fail_all(driver, error)
570
+ dispatching = driver.dispatching
571
+ inflight = driver.inflight
475
572
  uncertain = []
476
- uncertain << d.dispatching if d.dispatching
477
- uncertain.concat(d.inflight)
478
- queued = d.requests.drain
573
+ uncertain << dispatching if dispatching
574
+ uncertain.concat(inflight)
575
+ queued = driver.requests.drain
479
576
 
480
- uncertain.compact.uniq.each do |request|
481
- request.reject!(indeterminate_error(error)) unless request.settled?
482
- end
483
-
484
- queued.each do |request|
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
- d.dispatching = nil
489
- d.inflight.clear
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
- "so execution/commit outcome is indeterminate"
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(d, timeout)
609
+ def wait_socket_readable(driver, timeout)
515
610
  loop do
516
- return false unless d.running
517
- return true if d.socket.wait_readable(timeout)
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(d, timeout)
616
+ def wait_socket_writable(driver, timeout)
522
617
  loop do
523
- return false unless d.running
524
- return true if d.socket.wait_writable(timeout)
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(d)
623
+ def reader_watcher(driver)
529
624
  timeout = watcher_wait_timeout
530
625
 
531
- while d.running
532
- break unless wait_socket_readable(d, timeout)
626
+ while driver.running
627
+ break unless wait_socket_readable(driver, timeout)
533
628
 
534
- d.events.enqueue(:readable)
535
- command = d.reader_rearm.dequeue
536
- break unless command == :rearm && d.running
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 d.running
540
- d.events.enqueue([:abort, ConnectionLostError.new("reader watcher failed: #{e.class}: #{e.message}")])
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(d)
639
+ def writer_watcher(driver)
545
640
  timeout = watcher_wait_timeout
546
641
 
547
- while d.running
548
- command = d.writer_commands.dequeue
549
- break unless command == :wait_writable && d.running
550
- break unless wait_socket_writable(d, timeout)
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
- d.events.enqueue(:writable) if d.running
647
+ driver.events.enqueue(:writable) if driver.running
553
648
  end
554
649
  rescue StandardError => e
555
- if d.running
556
- d.events.enqueue([:abort, ConnectionLostError.new("writer watcher failed: #{e.class}: #{e.message}")])
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(d)
561
- tasks = release_watchers(d)
562
- close_wait_points(d)
655
+ def teardown_watchers(driver)
656
+ tasks = release_watchers(driver)
657
+ close_wait_points(driver)
563
658
  begin
564
- d.socket&.close
659
+ driver.socket&.close
565
660
  rescue StandardError
566
661
  nil
567
662
  end
568
663
  stop_watchers(tasks)
569
- join_watchers(d, tasks)
570
- d.socket = nil
571
- safe_close_conn(d)
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(d)
576
- tasks = [d.reader_task, d.writer_task].compact
577
- d.reader_task = nil
578
- d.writer_task = nil
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(d)
583
- [d.reader_rearm, d.writer_commands, d.events].each do |queue|
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(d, tasks)
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
- d.leaked_watchers += 1
605
- warn_leaked_task(d, task, WATCHER_JOIN_TIMEOUT)
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(d)
614
- owner = d.owner_task
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
- d.leaked_watchers += 1
622
- warn_leaked_task(d, owner, OWNER_JOIN_TIMEOUT)
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(d, task, timeout)
631
- return if ENV["PG_PIPELINE_SILENCE_WARNINGS"]
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 #{d.leaked_watchers}). " \
636
- "Closing the socket and, on schedulers without #fiber_interrupt, the " \
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(d)
642
- d.conn.close unless d.conn.finished?
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