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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/notification"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module PgPipeline
|
|
8
|
+
class BoundedQueue
|
|
9
|
+
def initialize(limit)
|
|
10
|
+
@limit = Integer(limit)
|
|
11
|
+
raise ArgumentError, "limit must be >= 1" if @limit < 1
|
|
12
|
+
rescue ArgumentError, TypeError
|
|
13
|
+
raise ArgumentError, "limit must be an integer >= 1"
|
|
14
|
+
else
|
|
15
|
+
@items = []
|
|
16
|
+
@consumers = []
|
|
17
|
+
@producers = []
|
|
18
|
+
@closed = false
|
|
19
|
+
@close_error = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def enqueue(item)
|
|
23
|
+
loop do
|
|
24
|
+
raise_close_error if @closed
|
|
25
|
+
|
|
26
|
+
if @items.size < @limit
|
|
27
|
+
@items << item
|
|
28
|
+
wake_one(@consumers)
|
|
29
|
+
return item
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
wait_on(@producers)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dequeue
|
|
37
|
+
loop do
|
|
38
|
+
unless @items.empty?
|
|
39
|
+
item = @items.shift
|
|
40
|
+
wake_one(@producers)
|
|
41
|
+
return item
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return nil if @closed
|
|
45
|
+
|
|
46
|
+
wait_on(@consumers)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def close(error)
|
|
51
|
+
return if @closed
|
|
52
|
+
|
|
53
|
+
@closed = true
|
|
54
|
+
@close_error = error
|
|
55
|
+
wake_all(@consumers)
|
|
56
|
+
wake_all(@producers)
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def empty? = @items.empty?
|
|
61
|
+
def size = @items.size
|
|
62
|
+
def waiting_producers = @producers.size
|
|
63
|
+
def waiting_consumers = @consumers.size
|
|
64
|
+
|
|
65
|
+
def drain
|
|
66
|
+
items = @items
|
|
67
|
+
@items = []
|
|
68
|
+
wake_all(@producers)
|
|
69
|
+
items
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def wait_on(list)
|
|
75
|
+
notification = Async::Notification.new
|
|
76
|
+
list << notification
|
|
77
|
+
completed = false
|
|
78
|
+
begin
|
|
79
|
+
notification.wait
|
|
80
|
+
completed = true
|
|
81
|
+
ensure
|
|
82
|
+
still_queued = list.delete(notification)
|
|
83
|
+
wake_one(list) if !completed && still_queued.nil? && !@closed
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def wake_one(list)
|
|
88
|
+
list.shift&.signal
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def wake_all(list)
|
|
92
|
+
pending = list.dup
|
|
93
|
+
list.clear
|
|
94
|
+
pending.each(&:signal)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def raise_close_error
|
|
98
|
+
error = @close_error || ShutdownError.new("queue closed")
|
|
99
|
+
raise error.class, error.message
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "pool"
|
|
7
|
+
require_relative "request"
|
|
8
|
+
require_relative "session_guard"
|
|
9
|
+
require_relative "session"
|
|
10
|
+
require_relative "transaction"
|
|
11
|
+
|
|
12
|
+
module PgPipeline
|
|
13
|
+
class Client
|
|
14
|
+
attr_reader :guard
|
|
15
|
+
|
|
16
|
+
def initialize(connection_args = nil, guard: :default, **pool_opts)
|
|
17
|
+
@guard = SessionGuard.normalize_mode!(guard)
|
|
18
|
+
@pool = Pool.new(connection_args, **pool_opts)
|
|
19
|
+
@started = false
|
|
20
|
+
@owner_thread = nil
|
|
21
|
+
@scheduler = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.open(connection_args = nil, **opts, &block)
|
|
25
|
+
ClientOps.open(connection_args, opts, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start(parent: Async::Task.current) = ClientOps.start(self, parent)
|
|
29
|
+
def query(sql, params = []) = ClientOps.query(self, sql, params)
|
|
30
|
+
def stats = ClientOps.stats(self)
|
|
31
|
+
|
|
32
|
+
def session(&block)
|
|
33
|
+
ClientOps.session(self, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def transaction(&block)
|
|
37
|
+
ClientOps.transaction(self, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def close = ClientOps.close(self)
|
|
41
|
+
def abort! = ClientOps.abort!(self)
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :pool, :owner_thread, :scheduler
|
|
46
|
+
attr_accessor :started
|
|
47
|
+
attr_writer :owner_thread, :scheduler
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module ClientOps
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
def open(connection_args, opts)
|
|
54
|
+
client = Client.new(connection_args, **opts).start
|
|
55
|
+
begin
|
|
56
|
+
yield client
|
|
57
|
+
ensure
|
|
58
|
+
client.close
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def start(client, parent)
|
|
63
|
+
raise Error, "client already started" if started?(client)
|
|
64
|
+
|
|
65
|
+
pool(client).start(parent: parent)
|
|
66
|
+
client.__send__(:owner_thread=, Thread.current)
|
|
67
|
+
client.__send__(:scheduler=, Fiber.scheduler)
|
|
68
|
+
client.__send__(:started=, true)
|
|
69
|
+
client
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def query(client, sql, params)
|
|
73
|
+
ensure_started!(client)
|
|
74
|
+
sql = sql.to_s
|
|
75
|
+
SessionGuard.assert_multiplexable!(sql, mode: client.guard)
|
|
76
|
+
|
|
77
|
+
request = submit_with_failover(client, sql, params)
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
request.wait
|
|
81
|
+
ensure
|
|
82
|
+
request.cancel! unless request.settled?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# NotDispatchedError is safe to retry on a fresh Request by contract.
|
|
87
|
+
# ShutdownError is retried only while the current Request is still pre-dispatch.
|
|
88
|
+
def submit_with_failover(client, sql, params)
|
|
89
|
+
attempts = 0
|
|
90
|
+
limit = [pool(client).pipeline_size, 1].max
|
|
91
|
+
last_error = nil
|
|
92
|
+
|
|
93
|
+
while attempts < limit
|
|
94
|
+
request = Request.new(sql: sql, params: params)
|
|
95
|
+
begin
|
|
96
|
+
pool(client).__send__(:pipeline_driver).submit(request)
|
|
97
|
+
return request
|
|
98
|
+
rescue NotDispatchedError => e
|
|
99
|
+
last_error = e
|
|
100
|
+
attempts += 1
|
|
101
|
+
rescue ShutdownError => e
|
|
102
|
+
last_error = e
|
|
103
|
+
attempts += 1
|
|
104
|
+
break unless request.state == :new && !request.settled?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
error = last_error || NotDispatchedError.new("no live pipeline connections; request was not dispatched")
|
|
109
|
+
raise(error.is_a?(NotDispatchedError) ? error : NotDispatchedError.new(error.message))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def session(client)
|
|
113
|
+
ensure_started!(client)
|
|
114
|
+
|
|
115
|
+
pool(client).__send__(:with_pinned) do |conn|
|
|
116
|
+
session = Session.new(conn)
|
|
117
|
+
begin
|
|
118
|
+
yield session
|
|
119
|
+
ensure
|
|
120
|
+
SessionOps.close!(session)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def transaction(client)
|
|
126
|
+
ensure_started!(client)
|
|
127
|
+
|
|
128
|
+
pool(client).__send__(:with_pinned) do |conn|
|
|
129
|
+
tx = Transaction.new(conn)
|
|
130
|
+
begin
|
|
131
|
+
TransactionOps.run(tx) { |transaction| yield transaction }
|
|
132
|
+
ensure
|
|
133
|
+
SessionOps.close!(tx)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def stats(client)
|
|
139
|
+
ensure_started!(client)
|
|
140
|
+
pool(client).stats
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def close(client)
|
|
144
|
+
return unless started?(client)
|
|
145
|
+
|
|
146
|
+
ensure_context!(client)
|
|
147
|
+
pool(client).graceful_close
|
|
148
|
+
client.__send__(:started=, false)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def abort!(client)
|
|
152
|
+
return unless started?(client)
|
|
153
|
+
|
|
154
|
+
ensure_context!(client)
|
|
155
|
+
pool(client).abort!
|
|
156
|
+
client.__send__(:started=, false)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def pool(client)
|
|
160
|
+
client.__send__(:pool)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ensure_started!(client)
|
|
164
|
+
raise Error, "client not started; call #start or use Client.open" unless started?(client)
|
|
165
|
+
|
|
166
|
+
ensure_context!(client)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def ensure_context!(client)
|
|
170
|
+
return if Thread.current.equal?(client.__send__(:owner_thread)) &&
|
|
171
|
+
Fiber.scheduler.equal?(client.__send__(:scheduler))
|
|
172
|
+
|
|
173
|
+
raise Error, "client is reactor-local and cannot be used from another thread/scheduler"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def started?(client)
|
|
177
|
+
client.__send__(:started)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|