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,26 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'request'
|
|
5
5
|
|
|
6
6
|
module PgPipeline
|
|
7
7
|
class PreparedStatement
|
|
8
|
-
attr_reader :name, :sql, :param_types, :physical_name
|
|
8
|
+
attr_reader :name, :sql, :param_types, :physical_name, :typed
|
|
9
9
|
|
|
10
|
-
def initialize(client:, name:, physical_name:, sql:, param_types: nil)
|
|
10
|
+
def initialize(client:, name:, physical_name:, sql:, param_types: nil, typed: false)
|
|
11
11
|
@client = client
|
|
12
12
|
@name = PreparedStatementOps.snapshot_name(name)
|
|
13
13
|
@physical_name = PreparedStatementOps.snapshot_name(physical_name)
|
|
14
14
|
@sql = RequestOps.snapshot_sql(sql)
|
|
15
15
|
@param_types = RequestOps.snapshot_param_types(param_types)
|
|
16
|
+
@typed = !!typed
|
|
16
17
|
freeze
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
def typed? = @typed
|
|
21
|
+
|
|
19
22
|
def query(params = RequestOps::EMPTY_PARAMS) = PreparedStatementOps.query(self, params)
|
|
20
23
|
alias call query
|
|
21
24
|
|
|
22
25
|
def inspect
|
|
23
|
-
|
|
26
|
+
flag = @typed ? ' typed' : ''
|
|
27
|
+
"#<#{self.class} name=#{@name.inspect} sql=#{@sql.inspect}#{flag}>"
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
private
|
|
@@ -37,11 +41,10 @@ module PgPipeline
|
|
|
37
41
|
|
|
38
42
|
def snapshot_name(name)
|
|
39
43
|
value = name.to_s
|
|
40
|
-
raise ArgumentError,
|
|
41
|
-
raise ArgumentError,
|
|
44
|
+
raise ArgumentError, 'prepared statement name must not be empty' if value.empty?
|
|
45
|
+
raise ArgumentError, 'prepared statement name must not contain NUL' if value.include?("\0")
|
|
42
46
|
|
|
43
47
|
value.frozen? ? value : value.dup.freeze
|
|
44
48
|
end
|
|
45
|
-
|
|
46
49
|
end
|
|
47
50
|
end
|
data/lib/pg_pipeline/request.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
4
|
|
|
5
5
|
module PgPipeline
|
|
6
6
|
class Request
|
|
7
7
|
attr_reader :sql, :params
|
|
8
8
|
attr_accessor :state, :cancelled, :settled, :result_seen, :query_boundary_seen,
|
|
9
|
-
:result, :error, :waiter, :waiter_scheduler
|
|
9
|
+
:result, :error, :waiter, :waiter_scheduler, :type_map
|
|
10
10
|
|
|
11
11
|
def initialize(sql:, params: nil)
|
|
12
12
|
@sql = RequestOps.snapshot_sql(sql)
|
|
@@ -20,6 +20,7 @@ module PgPipeline
|
|
|
20
20
|
@error = nil
|
|
21
21
|
@waiter = nil
|
|
22
22
|
@waiter_scheduler = nil
|
|
23
|
+
@type_map = nil
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def self.build(sql, params)
|
|
@@ -64,6 +65,7 @@ module PgPipeline
|
|
|
64
65
|
@error = nil
|
|
65
66
|
@waiter = nil
|
|
66
67
|
@waiter_scheduler = nil
|
|
68
|
+
@type_map = nil
|
|
67
69
|
self
|
|
68
70
|
end
|
|
69
71
|
end
|
|
@@ -117,7 +119,7 @@ module PgPipeline
|
|
|
117
119
|
|
|
118
120
|
def snapshot_params(params)
|
|
119
121
|
return EMPTY_PARAMS if params.nil?
|
|
120
|
-
raise ArgumentError,
|
|
122
|
+
raise ArgumentError, 'params must be an Array' unless params.is_a?(Array)
|
|
121
123
|
return EMPTY_PARAMS if params.empty?
|
|
122
124
|
|
|
123
125
|
return params if params.frozen? && immutable_values?(params)
|
|
@@ -158,24 +160,23 @@ module PgPipeline
|
|
|
158
160
|
|
|
159
161
|
def snapshot_name(name)
|
|
160
162
|
value = name.to_s
|
|
161
|
-
raise ArgumentError,
|
|
163
|
+
raise ArgumentError, 'statement_name must not be empty' if value.empty?
|
|
162
164
|
|
|
163
165
|
value.frozen? ? value : value.dup.freeze
|
|
164
166
|
end
|
|
165
167
|
|
|
166
168
|
def snapshot_param_types(param_types)
|
|
167
169
|
return nil if param_types.nil?
|
|
168
|
-
raise ArgumentError,
|
|
170
|
+
raise ArgumentError, 'param_types must be an Array or nil' unless param_types.is_a?(Array)
|
|
169
171
|
|
|
170
172
|
param_types.map { |oid| oid.nil? ? nil : Integer(oid) }.freeze
|
|
171
173
|
rescue ArgumentError, TypeError
|
|
172
|
-
raise ArgumentError,
|
|
174
|
+
raise ArgumentError, 'param_types must contain only integer OIDs or nil'
|
|
173
175
|
end
|
|
174
176
|
|
|
175
177
|
def transition!(req, from, to)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
end
|
|
178
|
+
state = req.state
|
|
179
|
+
raise ProtocolError, "invalid request transition #{state.inspect} -> #{to.inspect}" unless state == from
|
|
179
180
|
|
|
180
181
|
req.state = to
|
|
181
182
|
end
|
|
@@ -203,14 +204,14 @@ module PgPipeline
|
|
|
203
204
|
end
|
|
204
205
|
|
|
205
206
|
def query_boundary!(req)
|
|
206
|
-
raise ProtocolError,
|
|
207
|
-
raise ProtocolError,
|
|
207
|
+
raise ProtocolError, 'query boundary before query result' unless req.result_seen
|
|
208
|
+
raise ProtocolError, 'duplicate query boundary' if req.query_boundary_seen
|
|
208
209
|
|
|
209
210
|
req.query_boundary_seen = true
|
|
210
211
|
end
|
|
211
212
|
|
|
212
213
|
def finish!(req)
|
|
213
|
-
raise ProtocolError,
|
|
214
|
+
raise ProtocolError, 'sync arrived before query boundary' unless req.query_boundary_seen
|
|
214
215
|
return if req.settled
|
|
215
216
|
|
|
216
217
|
req.settled = true
|
|
@@ -235,21 +236,23 @@ module PgPipeline
|
|
|
235
236
|
req.cancelled = true
|
|
236
237
|
clear_result(req.result)
|
|
237
238
|
req.result = nil
|
|
238
|
-
|
|
239
|
+
error = req.error
|
|
240
|
+
error.clear_result! if error.respond_to?(:clear_result!)
|
|
239
241
|
end
|
|
240
242
|
|
|
241
243
|
def wait(req)
|
|
242
244
|
park_waiter(req) unless req.settled
|
|
243
|
-
|
|
245
|
+
error = req.error
|
|
246
|
+
raise error if error
|
|
244
247
|
|
|
245
248
|
req.result
|
|
246
249
|
end
|
|
247
250
|
|
|
248
251
|
def park_waiter(req)
|
|
249
|
-
raise ProtocolError,
|
|
252
|
+
raise ProtocolError, 'request already has a waiter' if req.waiter
|
|
250
253
|
|
|
251
254
|
scheduler = Fiber.scheduler
|
|
252
|
-
raise Error,
|
|
255
|
+
raise Error, 'request wait requires an active Fiber scheduler' unless scheduler
|
|
253
256
|
|
|
254
257
|
waiter = Fiber.current
|
|
255
258
|
req.waiter = waiter
|
|
@@ -274,8 +277,8 @@ module PgPipeline
|
|
|
274
277
|
end
|
|
275
278
|
|
|
276
279
|
def assert_result_slot!(req)
|
|
277
|
-
raise ProtocolError,
|
|
278
|
-
raise ProtocolError,
|
|
280
|
+
raise ProtocolError, 'result arrived after query boundary' if req.query_boundary_seen
|
|
281
|
+
raise ProtocolError, 'multiple results for one pipeline unit' if req.result_seen
|
|
279
282
|
end
|
|
280
283
|
|
|
281
284
|
def clear_result(result)
|
|
@@ -5,7 +5,7 @@ module PgPipeline
|
|
|
5
5
|
class Semaphore
|
|
6
6
|
def initialize(limit)
|
|
7
7
|
@limit = Integer(limit)
|
|
8
|
-
raise ArgumentError,
|
|
8
|
+
raise ArgumentError, 'limit must be >= 0' if @limit.negative?
|
|
9
9
|
|
|
10
10
|
@available = @limit
|
|
11
11
|
@waiting = WaitList.new
|
|
@@ -71,7 +71,7 @@ module PgPipeline
|
|
|
71
71
|
def empty? = @head.nil?
|
|
72
72
|
|
|
73
73
|
def push(node)
|
|
74
|
-
raise ArgumentError,
|
|
74
|
+
raise ArgumentError, 'node already queued' if node.list
|
|
75
75
|
|
|
76
76
|
node.list = self
|
|
77
77
|
node.prev = @tail
|
|
@@ -108,7 +108,7 @@ module PgPipeline
|
|
|
108
108
|
@list = @prev = @next = nil
|
|
109
109
|
@granted = false
|
|
110
110
|
@scheduler = Fiber.scheduler
|
|
111
|
-
@waiter = {fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true}
|
|
111
|
+
@waiter = { fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true }
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def granted? = @granted
|
|
@@ -6,7 +6,7 @@ module PgPipeline
|
|
|
6
6
|
attr_reader :fiber, :name, :error
|
|
7
7
|
|
|
8
8
|
def self.spawn(name: nil, &block)
|
|
9
|
-
raise ArgumentError,
|
|
9
|
+
raise ArgumentError, 'block required' unless block
|
|
10
10
|
|
|
11
11
|
task = new(name: name)
|
|
12
12
|
task.__send__(:start, &block)
|
|
@@ -14,8 +14,13 @@ module PgPipeline
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def initialize(name: nil)
|
|
17
|
-
@fiber
|
|
18
|
-
@
|
|
17
|
+
@fiber = nil
|
|
18
|
+
@scheduler = nil
|
|
19
|
+
@blocker = nil
|
|
20
|
+
@result = nil
|
|
21
|
+
@error = nil
|
|
22
|
+
@done = false
|
|
23
|
+
@cancelled = false
|
|
19
24
|
|
|
20
25
|
@name = name
|
|
21
26
|
@waiters = []
|
|
@@ -24,7 +29,7 @@ module PgPipeline
|
|
|
24
29
|
def wait(timeout = nil)
|
|
25
30
|
unless @done
|
|
26
31
|
deadline = Runtime.deadline_for(timeout)
|
|
27
|
-
raise TimeoutError,
|
|
32
|
+
raise TimeoutError, 'task did not finish in time' unless join(deadline)
|
|
28
33
|
end
|
|
29
34
|
raise @error if @error
|
|
30
35
|
|
|
@@ -45,7 +50,7 @@ module PgPipeline
|
|
|
45
50
|
def cancelled? = @cancelled
|
|
46
51
|
|
|
47
52
|
def raise_if_cancelled!
|
|
48
|
-
raise Cancel,
|
|
53
|
+
raise Cancel, 'task stopped' if @cancelled
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def enter_block(waiter)
|
|
@@ -68,7 +73,7 @@ module PgPipeline
|
|
|
68
73
|
this.__send__(:adopt_fiber, Fiber.current)
|
|
69
74
|
Thread.current[Runtime::CURRENT_TASK_KEY] = this
|
|
70
75
|
begin
|
|
71
|
-
this.__send__(:complete,
|
|
76
|
+
this.__send__(:complete, yield, nil)
|
|
72
77
|
rescue Cancel
|
|
73
78
|
this.__send__(:complete, nil, nil)
|
|
74
79
|
rescue Exception => e
|
|
@@ -89,7 +94,7 @@ module PgPipeline
|
|
|
89
94
|
return false unless fiber&.alive?
|
|
90
95
|
return false unless scheduler&.respond_to?(:fiber_interrupt)
|
|
91
96
|
|
|
92
|
-
result = scheduler.fiber_interrupt(fiber, Cancel.new(
|
|
97
|
+
result = scheduler.fiber_interrupt(fiber, Cancel.new('task stopped'))
|
|
93
98
|
result != false
|
|
94
99
|
rescue FiberError
|
|
95
100
|
false
|
|
@@ -119,7 +124,8 @@ module PgPipeline
|
|
|
119
124
|
end
|
|
120
125
|
|
|
121
126
|
def wake_waiters
|
|
122
|
-
pending
|
|
127
|
+
pending = @waiters
|
|
128
|
+
@waiters = []
|
|
123
129
|
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
124
130
|
end
|
|
125
131
|
end
|
data/lib/pg_pipeline/runtime.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'timeout'
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
5
|
+
require_relative 'errors'
|
|
6
6
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
module Runtime
|
|
@@ -14,7 +14,7 @@ module PgPipeline
|
|
|
14
14
|
|
|
15
15
|
CURRENT_TASK_KEY = :pg_pipeline_current_task
|
|
16
16
|
WAITER_KEY = :pg_pipeline_waiter
|
|
17
|
-
DEADLINE_MESSAGE =
|
|
17
|
+
DEADLINE_MESSAGE = 'execution expired'
|
|
18
18
|
|
|
19
19
|
MISSING_TIMEOUT_HOOK_WARNING = <<~MESSAGE
|
|
20
20
|
pg_pipeline: %s does not implement #timeout_after.
|
|
@@ -32,7 +32,7 @@ module PgPipeline
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def scheduler!
|
|
35
|
-
Fiber.scheduler or raise Error,
|
|
35
|
+
Fiber.scheduler or raise Error, 'operation requires an active Fiber scheduler'
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def native_timeouts?(target = Fiber.scheduler)
|
|
@@ -48,9 +48,10 @@ module PgPipeline
|
|
|
48
48
|
fiber: nil, scheduler: nil, ready: false, blocker: nil
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
current_blocker = waiter[:blocker]
|
|
52
|
+
if current_blocker
|
|
53
|
+
raise Error, "waiter already parked on #{current_blocker.class}; " \
|
|
54
|
+
'a fiber may only park in one place at a time'
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
waiter[:fiber] = Fiber.current
|
|
@@ -116,19 +117,19 @@ module PgPipeline
|
|
|
116
117
|
return nil if timeout.nil?
|
|
117
118
|
|
|
118
119
|
seconds = Float(timeout)
|
|
119
|
-
raise ArgumentError,
|
|
120
|
+
raise ArgumentError, 'timeout must be non-negative and finite' unless seconds.finite? && seconds >= 0
|
|
120
121
|
|
|
121
122
|
monotonic_now + seconds
|
|
122
123
|
end
|
|
123
124
|
|
|
124
|
-
def with_timeout(duration)
|
|
125
|
+
def with_timeout(duration, &block)
|
|
125
126
|
return yield if duration.nil?
|
|
126
127
|
|
|
127
128
|
seconds = Float(duration)
|
|
128
|
-
raise ArgumentError,
|
|
129
|
+
raise ArgumentError, 'timeout must be non-negative and finite' unless seconds.finite? && seconds >= 0
|
|
129
130
|
|
|
130
131
|
begin
|
|
131
|
-
arm_deadline(seconds)
|
|
132
|
+
arm_deadline(seconds, &block)
|
|
132
133
|
rescue Deadline => e
|
|
133
134
|
raise TimeoutError, e.message
|
|
134
135
|
end
|
|
@@ -136,9 +137,7 @@ module PgPipeline
|
|
|
136
137
|
|
|
137
138
|
def arm_deadline(seconds, &block)
|
|
138
139
|
target = Fiber.scheduler
|
|
139
|
-
if native_timeouts?(target)
|
|
140
|
-
return target.timeout_after(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
141
|
-
end
|
|
140
|
+
return target.timeout_after(seconds, Deadline, DEADLINE_MESSAGE, &block) if native_timeouts?(target)
|
|
142
141
|
|
|
143
142
|
warn_missing_timeout_hook(target) if target
|
|
144
143
|
::Timeout.timeout(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
@@ -154,7 +153,7 @@ module PgPipeline
|
|
|
154
153
|
end
|
|
155
154
|
end
|
|
156
155
|
|
|
157
|
-
require_relative
|
|
158
|
-
require_relative
|
|
159
|
-
require_relative
|
|
160
|
-
require_relative
|
|
156
|
+
require_relative 'runtime/notification'
|
|
157
|
+
require_relative 'runtime/queue'
|
|
158
|
+
require_relative 'runtime/semaphore'
|
|
159
|
+
require_relative 'runtime/task'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'pg'
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
5
|
+
require_relative 'errors'
|
|
6
6
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
class ServerCaps
|
|
@@ -59,7 +59,7 @@ module PgPipeline
|
|
|
59
59
|
return true if supported?(caps)
|
|
60
60
|
|
|
61
61
|
raise UnsupportedServerError,
|
|
62
|
-
|
|
62
|
+
'pg_pipeline requires libpq >= 14, PostgreSQL protocol v3, and ' \
|
|
63
63
|
"ruby-pg pipeline bindings; libpq=#{caps.libpq_version}, " \
|
|
64
64
|
"protocol=#{caps.protocol_version}, pipeline_api=#{caps.pipeline_api}"
|
|
65
65
|
end
|
data/lib/pg_pipeline/session.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
4
|
|
|
5
5
|
module PgPipeline
|
|
6
6
|
class Session
|
|
@@ -30,10 +30,10 @@ module PgPipeline
|
|
|
30
30
|
|
|
31
31
|
def ensure_active!(session)
|
|
32
32
|
conn = connection(session)
|
|
33
|
-
raise Error,
|
|
33
|
+
raise Error, 'session handle is no longer active' unless session.active? && conn
|
|
34
34
|
return if Fiber.current.equal?(session.owner_fiber)
|
|
35
35
|
|
|
36
|
-
raise Error,
|
|
36
|
+
raise Error, 'session handle is fiber-local and cannot be used from another fiber'
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def query(session, sql, params)
|
|
@@ -43,19 +43,21 @@ module PgPipeline
|
|
|
43
43
|
|
|
44
44
|
def exec(session, sql, params = nil)
|
|
45
45
|
ensure_active!(session)
|
|
46
|
+
conn = connection(session)
|
|
46
47
|
if params.nil?
|
|
47
|
-
|
|
48
|
+
conn.exec(sql)
|
|
48
49
|
else
|
|
49
|
-
|
|
50
|
+
conn.exec_params(sql, params)
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def prepare(session, name, sql, param_types)
|
|
54
55
|
ensure_active!(session)
|
|
56
|
+
conn = connection(session)
|
|
55
57
|
if param_types
|
|
56
|
-
|
|
58
|
+
conn.prepare(name, sql, param_types)
|
|
57
59
|
else
|
|
58
|
-
|
|
60
|
+
conn.prepare(name, sql)
|
|
59
61
|
end
|
|
60
62
|
end
|
|
61
63
|
|