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.
@@ -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