pg_pipeline 0.2.1
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 +7 -0
- data/CHANGELOG.md +156 -0
- data/DESIGN.md +298 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/lib/pg_pipeline/bounded_queue.rb +102 -0
- data/lib/pg_pipeline/client.rb +180 -0
- data/lib/pg_pipeline/connection_driver.rb +502 -0
- data/lib/pg_pipeline/errors.rb +27 -0
- data/lib/pg_pipeline/pool.rb +420 -0
- data/lib/pg_pipeline/request.rb +146 -0
- data/lib/pg_pipeline/server_caps.rb +83 -0
- data/lib/pg_pipeline/session.rb +71 -0
- data/lib/pg_pipeline/session_guard.rb +213 -0
- data/lib/pg_pipeline/transaction.rb +92 -0
- data/lib/pg_pipeline/version.rb +5 -0
- data/lib/pg_pipeline.rb +15 -0
- metadata +155 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pg"
|
|
4
|
+
require "async"
|
|
5
|
+
require "async/queue"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "bounded_queue"
|
|
9
|
+
require_relative "server_caps"
|
|
10
|
+
require_relative "request"
|
|
11
|
+
|
|
12
|
+
module PgPipeline
|
|
13
|
+
class ConnectionDriver
|
|
14
|
+
DEFAULT_MAX_PENDING = 256
|
|
15
|
+
DEFAULT_MAX_IN_FLIGHT = 64
|
|
16
|
+
|
|
17
|
+
attr_reader :conn, :caps, :max_pending, :max_in_flight
|
|
18
|
+
attr_accessor :socket, :requests, :events, :reader_rearm, :writer_commands,
|
|
19
|
+
:inflight, :dispatching, :submitting, :accepting, :running,
|
|
20
|
+
:draining, :needs_flush, :writer_armed, :request_event_pending,
|
|
21
|
+
:owner_task, :reader_task, :writer_task
|
|
22
|
+
|
|
23
|
+
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT)
|
|
24
|
+
@max_pending = DriverOps.positive_integer!(max_pending, :max_pending)
|
|
25
|
+
@max_in_flight = DriverOps.positive_integer!(max_in_flight, :max_in_flight)
|
|
26
|
+
|
|
27
|
+
@conn = conn
|
|
28
|
+
@caps = ServerCaps.from_connection(conn)
|
|
29
|
+
@caps.assert_supported!
|
|
30
|
+
|
|
31
|
+
@requests = BoundedQueue.new(@max_pending)
|
|
32
|
+
@events = Async::Queue.new
|
|
33
|
+
@reader_rearm = Async::Queue.new
|
|
34
|
+
@writer_commands = Async::Queue.new
|
|
35
|
+
|
|
36
|
+
@inflight = []
|
|
37
|
+
@dispatching = nil
|
|
38
|
+
@submitting = 0
|
|
39
|
+
|
|
40
|
+
@accepting = false
|
|
41
|
+
@running = false
|
|
42
|
+
@draining = false
|
|
43
|
+
@needs_flush = false
|
|
44
|
+
@writer_armed = false
|
|
45
|
+
@request_event_pending = false
|
|
46
|
+
|
|
47
|
+
@socket = nil
|
|
48
|
+
@owner_task = nil
|
|
49
|
+
@reader_task = nil
|
|
50
|
+
@writer_task = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def start(parent: Async::Task.current) = DriverOps.start(self, parent)
|
|
54
|
+
def submit(request) = DriverOps.submit(self, request)
|
|
55
|
+
def load = @requests.size + @inflight.size + @submitting + (@dispatching ? 1 : 0)
|
|
56
|
+
def available? = @accepting && @running
|
|
57
|
+
def dead? = !@running && !@accepting
|
|
58
|
+
|
|
59
|
+
def stats
|
|
60
|
+
{
|
|
61
|
+
available: available?,
|
|
62
|
+
load: load,
|
|
63
|
+
pending: @requests.size,
|
|
64
|
+
in_flight: @inflight.size,
|
|
65
|
+
submitting: @submitting,
|
|
66
|
+
needs_flush: @needs_flush
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def health_check(timeout)
|
|
71
|
+
return true unless available?
|
|
72
|
+
|
|
73
|
+
probe = Request.new(sql: "SELECT 1", params: [])
|
|
74
|
+
begin
|
|
75
|
+
submit(probe)
|
|
76
|
+
Async::Task.current.with_timeout(timeout) { probe.wait }
|
|
77
|
+
true
|
|
78
|
+
rescue Async::TimeoutError
|
|
79
|
+
DriverOps.abort_timed_out_health_probe(self, probe)
|
|
80
|
+
rescue QueryError, PipelineAbortedError
|
|
81
|
+
true
|
|
82
|
+
rescue ShutdownError, NotDispatchedError
|
|
83
|
+
true
|
|
84
|
+
rescue ConnectionLostError, PG::Error
|
|
85
|
+
false
|
|
86
|
+
ensure
|
|
87
|
+
probe.cancel! unless probe.settled?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def graceful_close = DriverOps.graceful_close(self)
|
|
92
|
+
|
|
93
|
+
def abort!(error = ConnectionLostError.new("connection aborted"))
|
|
94
|
+
DriverOps.abort!(self, error)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
module DriverOps
|
|
99
|
+
module_function
|
|
100
|
+
|
|
101
|
+
SUCCESS_STATUSES = [
|
|
102
|
+
PG::PGRES_EMPTY_QUERY,
|
|
103
|
+
PG::PGRES_COMMAND_OK,
|
|
104
|
+
PG::PGRES_TUPLES_OK
|
|
105
|
+
].freeze
|
|
106
|
+
|
|
107
|
+
COPY_STATUSES = [
|
|
108
|
+
PG::PGRES_COPY_IN,
|
|
109
|
+
PG::PGRES_COPY_OUT,
|
|
110
|
+
PG::PGRES_COPY_BOTH
|
|
111
|
+
].freeze
|
|
112
|
+
|
|
113
|
+
def positive_integer!(value, name)
|
|
114
|
+
integer = Integer(value)
|
|
115
|
+
raise ArgumentError, "#{name} must be >= 1" if integer < 1
|
|
116
|
+
|
|
117
|
+
integer
|
|
118
|
+
rescue ArgumentError, TypeError
|
|
119
|
+
raise ArgumentError, "#{name} must be an integer >= 1"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def start(d, parent)
|
|
123
|
+
raise Error, "driver already started" if d.running
|
|
124
|
+
|
|
125
|
+
d.conn.setnonblocking(true)
|
|
126
|
+
d.conn.enter_pipeline_mode
|
|
127
|
+
|
|
128
|
+
d.socket = d.conn.socket_io
|
|
129
|
+
d.accepting = true
|
|
130
|
+
d.running = true
|
|
131
|
+
|
|
132
|
+
d.reader_task = parent.async { reader_watcher(d) }
|
|
133
|
+
d.writer_task = parent.async { writer_watcher(d) }
|
|
134
|
+
d.owner_task = parent.async { owner_loop(d) }
|
|
135
|
+
d
|
|
136
|
+
rescue Exception
|
|
137
|
+
d.accepting = false
|
|
138
|
+
d.running = false
|
|
139
|
+
stop_watchers(d)
|
|
140
|
+
safe_close_conn(d)
|
|
141
|
+
raise
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def submit(d, request)
|
|
145
|
+
raise ShutdownError, "driver is not accepting work" unless d.accepting
|
|
146
|
+
raise ProtocolError, "request must be new before submit" unless request.state == :new
|
|
147
|
+
|
|
148
|
+
d.submitting += 1
|
|
149
|
+
begin
|
|
150
|
+
d.requests.enqueue(request)
|
|
151
|
+
request.queued!
|
|
152
|
+
notify_requests(d)
|
|
153
|
+
ensure
|
|
154
|
+
d.submitting -= 1
|
|
155
|
+
d.events.enqueue(:submission_finished) if d.draining && d.running && d.submitting.zero?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
request
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def graceful_close(d)
|
|
162
|
+
return unless d.running
|
|
163
|
+
|
|
164
|
+
d.accepting = false
|
|
165
|
+
d.requests.close(ShutdownError.new("driver is closing"))
|
|
166
|
+
d.events.enqueue(:close)
|
|
167
|
+
d.owner_task.wait
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def abort!(d, error)
|
|
172
|
+
return unless d.running
|
|
173
|
+
|
|
174
|
+
d.accepting = false
|
|
175
|
+
d.requests.close(not_dispatched_error(error))
|
|
176
|
+
d.events.enqueue([:abort, error])
|
|
177
|
+
d.owner_task.wait unless Async::Task.current.equal?(d.owner_task)
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def abort_timed_out_health_probe(d, probe)
|
|
182
|
+
return true if probe.settled? || !d.running
|
|
183
|
+
return true unless exclusive_health_probe?(d, probe)
|
|
184
|
+
|
|
185
|
+
abort!(d, ConnectionLostError.new("idle health check timed out"))
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def exclusive_health_probe?(d, probe)
|
|
190
|
+
d.accepting &&
|
|
191
|
+
d.dispatching.nil? &&
|
|
192
|
+
d.submitting.zero? &&
|
|
193
|
+
d.requests.empty? &&
|
|
194
|
+
d.inflight.length == 1 &&
|
|
195
|
+
d.inflight.first.equal?(probe)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def owner_loop(d)
|
|
199
|
+
while d.running
|
|
200
|
+
event = d.events.dequeue
|
|
201
|
+
|
|
202
|
+
case event
|
|
203
|
+
when :requests
|
|
204
|
+
d.request_event_pending = false
|
|
205
|
+
when :submission_finished
|
|
206
|
+
nil
|
|
207
|
+
when :readable
|
|
208
|
+
begin
|
|
209
|
+
read_available(d)
|
|
210
|
+
ensure
|
|
211
|
+
d.reader_rearm.enqueue(:rearm) if d.running
|
|
212
|
+
end
|
|
213
|
+
when :writable
|
|
214
|
+
d.writer_armed = false
|
|
215
|
+
flush_output(d)
|
|
216
|
+
when :close
|
|
217
|
+
d.draining = true
|
|
218
|
+
when Array
|
|
219
|
+
tag, payload = event
|
|
220
|
+
if tag == :abort
|
|
221
|
+
fatal_close(d, payload)
|
|
222
|
+
break
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
drain_results(d)
|
|
227
|
+
pump_requests(d)
|
|
228
|
+
drain_results(d)
|
|
229
|
+
finish_graceful_close(d) if d.draining && drained?(d)
|
|
230
|
+
end
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
fatal_close(d, ConnectionLostError.new("driver crashed: #{e.class}: #{e.message}"))
|
|
233
|
+
ensure
|
|
234
|
+
fatal_close(d, ShutdownError.new("driver owner stopped before shutdown completed")) if d.running
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def notify_requests(d)
|
|
238
|
+
return if d.request_event_pending
|
|
239
|
+
|
|
240
|
+
d.request_event_pending = true
|
|
241
|
+
d.events.enqueue(:requests)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def pump_requests(d)
|
|
245
|
+
dispatched = false
|
|
246
|
+
|
|
247
|
+
while d.inflight.size < d.max_in_flight && !d.requests.empty?
|
|
248
|
+
request = d.requests.dequeue
|
|
249
|
+
break unless request
|
|
250
|
+
next if request.cancelled?
|
|
251
|
+
|
|
252
|
+
d.dispatching = request
|
|
253
|
+
unless send_unit(d, request)
|
|
254
|
+
d.dispatching = nil
|
|
255
|
+
next
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
request.dispatched!
|
|
259
|
+
d.inflight << request
|
|
260
|
+
d.dispatching = nil
|
|
261
|
+
dispatched = true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
flush_output(d) if dispatched || d.needs_flush
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def send_unit(d, request)
|
|
268
|
+
begin
|
|
269
|
+
d.conn.send_query_params(request.sql, request.params)
|
|
270
|
+
rescue PG::UnableToSend => e
|
|
271
|
+
d.dispatching = nil
|
|
272
|
+
request.reject!(
|
|
273
|
+
NotDispatchedError.new("query was rejected before libpq accepted it: #{e.class}: #{e.message}")
|
|
274
|
+
)
|
|
275
|
+
return false if reusable_after_send_rejection?(d)
|
|
276
|
+
|
|
277
|
+
raise ConnectionLostError, "dispatch failed: #{e.class}: #{e.message}"
|
|
278
|
+
rescue PG::Error => e
|
|
279
|
+
d.dispatching = nil
|
|
280
|
+
request.reject!(
|
|
281
|
+
NotDispatchedError.new("query was rejected before libpq accepted it: #{e.class}: #{e.message}")
|
|
282
|
+
)
|
|
283
|
+
raise ConnectionLostError, "dispatch failed: #{e.class}: #{e.message}"
|
|
284
|
+
rescue StandardError => e
|
|
285
|
+
# ruby-pg prepares/encodes query parameters before calling PQsendQueryParams.
|
|
286
|
+
# A Ruby-side encoder/coercion exception therefore belongs only to this
|
|
287
|
+
# request and must not poison unrelated work already in the pipeline.
|
|
288
|
+
request.reject!(e)
|
|
289
|
+
return false
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
d.caps.place_sync(d.conn)
|
|
293
|
+
true
|
|
294
|
+
rescue PG::Error => e
|
|
295
|
+
raise ConnectionLostError, "dispatch Sync failed: #{e.class}: #{e.message}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def reusable_after_send_rejection?(d)
|
|
299
|
+
!d.conn.finished? &&
|
|
300
|
+
d.conn.status == PG::CONNECTION_OK &&
|
|
301
|
+
d.conn.pipeline_status != PG::PQ_PIPELINE_OFF
|
|
302
|
+
rescue PG::Error
|
|
303
|
+
false
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def flush_output(d)
|
|
307
|
+
return unless d.running
|
|
308
|
+
|
|
309
|
+
if d.conn.sync_flush
|
|
310
|
+
d.needs_flush = false
|
|
311
|
+
else
|
|
312
|
+
d.needs_flush = true
|
|
313
|
+
arm_writer(d)
|
|
314
|
+
end
|
|
315
|
+
rescue PG::Error => e
|
|
316
|
+
raise ConnectionLostError, "flush failed: #{e.class}: #{e.message}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def arm_writer(d)
|
|
320
|
+
return if d.writer_armed
|
|
321
|
+
|
|
322
|
+
d.writer_armed = true
|
|
323
|
+
d.writer_commands.enqueue(:wait_writable)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def read_available(d)
|
|
327
|
+
d.conn.consume_input
|
|
328
|
+
drain_results(d)
|
|
329
|
+
rescue PG::Error => e
|
|
330
|
+
raise ConnectionLostError, "read failed: #{e.class}: #{e.message}"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def drain_results(d)
|
|
334
|
+
while !d.inflight.empty? && !d.conn.is_busy
|
|
335
|
+
result = d.conn.sync_get_result
|
|
336
|
+
request = d.inflight.first
|
|
337
|
+
raise ProtocolError, "result without an in-flight request" unless request
|
|
338
|
+
|
|
339
|
+
if result.nil?
|
|
340
|
+
request.query_boundary!
|
|
341
|
+
next
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
status = result.result_status
|
|
345
|
+
|
|
346
|
+
case status
|
|
347
|
+
when PG::PGRES_PIPELINE_SYNC
|
|
348
|
+
clear_result(result)
|
|
349
|
+
complete_front(d, request)
|
|
350
|
+
when PG::PGRES_PIPELINE_ABORTED
|
|
351
|
+
ensure_before_query_boundary!(request, status)
|
|
352
|
+
clear_result(result)
|
|
353
|
+
request.record_error!(PipelineAbortedError.new("pipeline unit aborted"))
|
|
354
|
+
when PG::PGRES_BAD_RESPONSE
|
|
355
|
+
clear_result(result)
|
|
356
|
+
raise ProtocolError, "server response was not understood"
|
|
357
|
+
when PG::PGRES_FATAL_ERROR
|
|
358
|
+
ensure_before_query_boundary!(request, status)
|
|
359
|
+
request.record_error!(query_error(result), result: result)
|
|
360
|
+
when *COPY_STATUSES
|
|
361
|
+
clear_result(result)
|
|
362
|
+
raise ProtocolError, "COPY is not supported on the multiplexed pipeline"
|
|
363
|
+
when *SUCCESS_STATUSES
|
|
364
|
+
ensure_before_query_boundary!(request, status)
|
|
365
|
+
request.accept_result(result)
|
|
366
|
+
else
|
|
367
|
+
clear_result(result)
|
|
368
|
+
raise ProtocolError, "unexpected pipeline result status #{status}"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def ensure_before_query_boundary!(request, status)
|
|
374
|
+
return unless request.query_boundary_seen?
|
|
375
|
+
|
|
376
|
+
raise ProtocolError, "result status #{status} arrived after query boundary"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def complete_front(d, request)
|
|
380
|
+
raise ProtocolError, "sync does not match FIFO front" unless request.equal?(d.inflight.first)
|
|
381
|
+
|
|
382
|
+
d.inflight.shift
|
|
383
|
+
request.finish!
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def query_error(result)
|
|
387
|
+
message = result.error_message.to_s.strip
|
|
388
|
+
message = "query failed" if message.empty?
|
|
389
|
+
QueryError.new(message, cause_result: result)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def drained?(d)
|
|
393
|
+
d.submitting.zero? && d.requests.empty? && d.inflight.empty? && !d.needs_flush && d.dispatching.nil?
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def finish_graceful_close(d)
|
|
397
|
+
d.accepting = false
|
|
398
|
+
d.running = false
|
|
399
|
+
|
|
400
|
+
begin
|
|
401
|
+
d.conn.exit_pipeline_mode
|
|
402
|
+
rescue PG::Error => e
|
|
403
|
+
fail_all(d, ConnectionLostError.new("failed to exit pipeline mode: #{e.message}"))
|
|
404
|
+
ensure
|
|
405
|
+
stop_watchers(d)
|
|
406
|
+
safe_close_conn(d)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def fatal_close(d, error)
|
|
411
|
+
return unless d.running || d.accepting
|
|
412
|
+
|
|
413
|
+
d.accepting = false
|
|
414
|
+
d.running = false
|
|
415
|
+
d.requests.close(not_dispatched_error(error))
|
|
416
|
+
fail_all(d, error)
|
|
417
|
+
stop_watchers(d)
|
|
418
|
+
safe_close_conn(d)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def fail_all(d, error)
|
|
422
|
+
uncertain = []
|
|
423
|
+
uncertain << d.dispatching if d.dispatching
|
|
424
|
+
uncertain.concat(d.inflight)
|
|
425
|
+
queued = d.requests.drain
|
|
426
|
+
|
|
427
|
+
uncertain.compact.uniq.each do |request|
|
|
428
|
+
request.reject!(indeterminate_error(error)) unless request.settled?
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
queued.each do |request|
|
|
432
|
+
request.reject!(not_dispatched_error(error)) unless request.settled?
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
d.dispatching = nil
|
|
436
|
+
d.inflight.clear
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def not_dispatched_error(error)
|
|
440
|
+
return error if error.is_a?(NotDispatchedError)
|
|
441
|
+
|
|
442
|
+
NotDispatchedError.new("#{error.message}; request was not dispatched")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def indeterminate_error(error)
|
|
446
|
+
return error if error.is_a?(IndeterminateResultError)
|
|
447
|
+
|
|
448
|
+
IndeterminateResultError.new(
|
|
449
|
+
"#{error.message}; request was dispatched but its Sync was not observed, " \
|
|
450
|
+
"so execution/commit outcome is indeterminate"
|
|
451
|
+
)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def reader_watcher(d)
|
|
455
|
+
while d.running
|
|
456
|
+
d.socket.wait_readable
|
|
457
|
+
break unless d.running
|
|
458
|
+
|
|
459
|
+
d.events.enqueue(:readable)
|
|
460
|
+
command = d.reader_rearm.dequeue
|
|
461
|
+
break unless command == :rearm && d.running
|
|
462
|
+
end
|
|
463
|
+
rescue StandardError => e
|
|
464
|
+
if d.running
|
|
465
|
+
d.events.enqueue([:abort, ConnectionLostError.new("reader watcher failed: #{e.class}: #{e.message}")])
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def writer_watcher(d)
|
|
470
|
+
while d.running
|
|
471
|
+
command = d.writer_commands.dequeue
|
|
472
|
+
break unless command == :wait_writable && d.running
|
|
473
|
+
|
|
474
|
+
d.socket.wait_writable
|
|
475
|
+
d.events.enqueue(:writable) if d.running
|
|
476
|
+
end
|
|
477
|
+
rescue StandardError => e
|
|
478
|
+
if d.running
|
|
479
|
+
d.events.enqueue([:abort, ConnectionLostError.new("writer watcher failed: #{e.class}: #{e.message}")])
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def stop_watchers(d)
|
|
484
|
+
d.reader_task&.stop
|
|
485
|
+
d.writer_task&.stop
|
|
486
|
+
d.reader_task = nil
|
|
487
|
+
d.writer_task = nil
|
|
488
|
+
rescue StandardError
|
|
489
|
+
nil
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def safe_close_conn(d)
|
|
493
|
+
d.conn.close unless d.conn.finished?
|
|
494
|
+
rescue StandardError
|
|
495
|
+
nil
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def clear_result(result)
|
|
499
|
+
result.clear if result.respond_to?(:clear)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgPipeline
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class UnsupportedServerError < Error; end
|
|
6
|
+
class UnsafeMultiplexError < Error; end
|
|
7
|
+
class PipelineAbortedError < Error; end
|
|
8
|
+
class ConnectionLostError < Error; end
|
|
9
|
+
class NotDispatchedError < ConnectionLostError; end
|
|
10
|
+
class IndeterminateResultError < ConnectionLostError; end
|
|
11
|
+
class ShutdownError < Error; end
|
|
12
|
+
class ProtocolError < Error; end
|
|
13
|
+
|
|
14
|
+
class QueryError < Error
|
|
15
|
+
attr_reader :cause_result
|
|
16
|
+
|
|
17
|
+
def initialize(message, cause_result: nil)
|
|
18
|
+
super(message)
|
|
19
|
+
@cause_result = cause_result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clear_result!
|
|
23
|
+
@cause_result&.clear if @cause_result.respond_to?(:clear)
|
|
24
|
+
@cause_result = nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|