google-cloud-pubsub 0.26.0 → 0.27.0

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,220 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "monitor"
17
+ require "concurrent"
18
+
19
+ module Google
20
+ module Cloud
21
+ module Pubsub
22
+ class Subscriber
23
+ ##
24
+ # @private
25
+ # # AsyncPusher
26
+ #
27
+ class AsyncPusher
28
+ include MonitorMixin
29
+
30
+ attr_reader :batch
31
+ attr_reader :max_bytes, :interval
32
+
33
+ def initialize stream, max_bytes: 10000000, interval: 0.25
34
+ @stream = stream
35
+
36
+ @max_bytes = max_bytes
37
+ @interval = interval
38
+
39
+ @cond = new_cond
40
+
41
+ # init MonitorMixin
42
+ super()
43
+ end
44
+
45
+ def acknowledge ack_ids
46
+ return true if ack_ids.empty?
47
+
48
+ synchronize do
49
+ ack_ids.each do |ack_id|
50
+ if @batch.nil?
51
+ @batch = Batch.new max_bytes: @max_bytes
52
+ @batch.ack ack_id
53
+ else
54
+ unless @batch.try_ack ack_id
55
+ push_batch_request!
56
+
57
+ @batch = Batch.new max_bytes: @max_bytes
58
+ @batch.ack ack_id
59
+ end
60
+ end
61
+
62
+ @batch_created_at ||= Time.now
63
+ @background_thread ||= Thread.new { run_background }
64
+
65
+ push_batch_request! if @batch.ready?
66
+ end
67
+
68
+ @cond.signal
69
+ end
70
+
71
+ nil
72
+ end
73
+
74
+ def delay deadline, ack_ids
75
+ return true if ack_ids.empty?
76
+
77
+ synchronize do
78
+ ack_ids.each do |ack_id|
79
+ if @batch.nil?
80
+ @batch = Batch.new max_bytes: @max_bytes
81
+ @batch.delay deadline, ack_id
82
+ else
83
+ unless @batch.try_delay deadline, ack_id
84
+ push_batch_request!
85
+
86
+ @batch = Batch.new max_bytes: @max_bytes
87
+ @batch.delay deadline, ack_id
88
+ end
89
+ end
90
+
91
+ @batch_created_at ||= Time.now
92
+ @background_thread ||= Thread.new { run_background }
93
+
94
+ push_batch_request! if @batch.ready?
95
+ end
96
+
97
+ @cond.signal
98
+ end
99
+
100
+ nil
101
+ end
102
+
103
+ def stop
104
+ synchronize do
105
+ break if @stopped
106
+
107
+ @stopped = true
108
+ push_batch_request!
109
+ @cond.signal
110
+ end
111
+
112
+ self
113
+ end
114
+
115
+ def wait!
116
+ synchronize do
117
+ @background_thread.join if @background_thread
118
+ end
119
+
120
+ self
121
+ end
122
+
123
+ def flush
124
+ synchronize do
125
+ push_batch_request!
126
+ @cond.signal
127
+ end
128
+
129
+ self
130
+ end
131
+
132
+ def started?
133
+ !stopped?
134
+ end
135
+
136
+ def stopped?
137
+ synchronize { @stopped }
138
+ end
139
+
140
+ protected
141
+
142
+ def run_background
143
+ synchronize do
144
+ until @stopped
145
+ if @batch.nil?
146
+ @cond.wait
147
+ next
148
+ end
149
+
150
+ time_since_first_publish = Time.now - @batch_created_at
151
+ if time_since_first_publish > @interval
152
+ # interval met, publish the batch...
153
+ push_batch_request!
154
+ @cond.wait
155
+ else
156
+ # still waiting for the interval to publish the batch...
157
+ @cond.wait(@interval - time_since_first_publish)
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ def push_batch_request!
164
+ return unless @batch
165
+
166
+ request = @batch.request
167
+ Concurrent::Future.new(executor: @stream.push_thread_pool) do
168
+ @stream.push request
169
+ end.execute
170
+
171
+ @batch = nil
172
+ @batch_created_at = nil
173
+ end
174
+
175
+ class Batch
176
+ attr_reader :max_bytes, :request
177
+
178
+ def initialize max_bytes: 10000000
179
+ @max_bytes = max_bytes
180
+ @request = Google::Pubsub::V1::StreamingPullRequest.new
181
+ end
182
+
183
+ def ack ack_id
184
+ @request.ack_ids << ack_id
185
+ end
186
+
187
+ def try_ack ack_id
188
+ addl_bytes = ack_id.size
189
+ return false if total_message_bytes + addl_bytes >= @max_bytes
190
+
191
+ ack ack_id
192
+ true
193
+ end
194
+
195
+ def delay deadline, ack_id
196
+ @request.modify_deadline_seconds << deadline
197
+ @request.modify_deadline_ack_ids << ack_id
198
+ end
199
+
200
+ def try_delay deadline, ack_id
201
+ addl_bytes = deadline.to_s.size + ack_id.size
202
+ return false if total_message_bytes + addl_bytes >= @max_bytes
203
+
204
+ delay deadline, ack_id
205
+ true
206
+ end
207
+
208
+ def ready?
209
+ total_message_bytes >= @max_bytes
210
+ end
211
+
212
+ def total_message_bytes
213
+ request.to_proto.size
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "thread"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Pubsub
21
+ class Subscriber
22
+ # @private
23
+ class EnumeratorQueue
24
+ def initialize sentinel = nil
25
+ @queue = Queue.new
26
+ @sentinel = sentinel
27
+ end
28
+
29
+ def push obj
30
+ @queue.push obj
31
+ end
32
+
33
+ def dump_queue
34
+ objs = []
35
+ objs << @queue.pop until @queue.empty?
36
+ objs
37
+ end
38
+
39
+ def each
40
+ return enum_for(:each) unless block_given?
41
+
42
+ loop do
43
+ obj = @queue.pop
44
+ break if obj.equal? @sentinel
45
+ yield obj
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,376 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/pubsub/subscriber/async_pusher"
17
+ require "google/cloud/pubsub/subscriber/enumerator_queue"
18
+ require "google/cloud/pubsub/service"
19
+ require "google/cloud/errors"
20
+ require "monitor"
21
+ require "concurrent"
22
+
23
+ module Google
24
+ module Cloud
25
+ module Pubsub
26
+ class Subscriber
27
+ ##
28
+ # @private
29
+ class Stream
30
+ include MonitorMixin
31
+
32
+ ##
33
+ # @private Implementation attributes.
34
+ attr_reader :callback_thread_pool, :push_thread_pool
35
+
36
+ ##
37
+ # Subscriber attributes.
38
+ attr_reader :subscriber
39
+
40
+ ##
41
+ # @private Create an empty Subscriber::Stream object.
42
+ def initialize subscriber
43
+ @subscriber = subscriber
44
+
45
+ @request_queue = nil
46
+ @stopped = nil
47
+ @paused = nil
48
+ @pause_cond = new_cond
49
+
50
+ @inventory = Inventory.new self, subscriber.stream_inventory
51
+ @callback_thread_pool = Concurrent::FixedThreadPool.new \
52
+ subscriber.callback_threads
53
+ @push_thread_pool = Concurrent::FixedThreadPool.new \
54
+ subscriber.push_threads
55
+
56
+ super() # to init MonitorMixin
57
+ end
58
+
59
+ def start
60
+ synchronize do
61
+ break if @request_queue
62
+
63
+ start_streaming!
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ def stop
70
+ synchronize do
71
+ break if @stopped
72
+ break if @request_queue.nil?
73
+
74
+ @request_queue.push self
75
+ @inventory.stop
76
+ @stopped = true
77
+ end
78
+
79
+ self
80
+ end
81
+
82
+ def stopped?
83
+ synchronize { @stopped }
84
+ end
85
+
86
+ def paused?
87
+ synchronize { @paused }
88
+ end
89
+
90
+ def wait!
91
+ synchronize do
92
+ @background_thread.join if @background_thread
93
+
94
+ @callback_thread_pool.shutdown
95
+ @callback_thread_pool.wait_for_termination
96
+
97
+ @async_pusher.stop.wait! if @async_pusher
98
+
99
+ @push_thread_pool.shutdown
100
+ @push_thread_pool.wait_for_termination
101
+ end
102
+
103
+ self
104
+ end
105
+
106
+ ##
107
+ # @private
108
+ def acknowledge *messages
109
+ ack_ids = coerce_ack_ids messages
110
+ return true if ack_ids.empty?
111
+
112
+ synchronize do
113
+ @async_pusher ||= AsyncPusher.new self
114
+ @async_pusher.acknowledge ack_ids
115
+ @inventory.remove ack_ids
116
+ unpause_streaming!
117
+ end
118
+
119
+ true
120
+ end
121
+
122
+ ##
123
+ # @private
124
+ def delay deadline, *messages
125
+ mod_ack_ids = coerce_ack_ids messages
126
+ return true if mod_ack_ids.empty?
127
+
128
+ synchronize do
129
+ @async_pusher ||= AsyncPusher.new self
130
+ @async_pusher.delay deadline, mod_ack_ids
131
+ @inventory.remove mod_ack_ids
132
+ unpause_streaming!
133
+ end
134
+
135
+ true
136
+ end
137
+
138
+ def async_pusher
139
+ synchronize { @async_pusher }
140
+ end
141
+
142
+ def push request
143
+ synchronize { @request_queue.push request }
144
+ end
145
+
146
+ def inventory
147
+ synchronize { @inventory }
148
+ end
149
+
150
+ ##
151
+ # @private
152
+ def delay_inventory!
153
+ synchronize do
154
+ return true if @inventory.empty?
155
+
156
+ @async_pusher ||= AsyncPusher.new self
157
+ @async_pusher.delay subscriber.deadline, @inventory.ack_ids
158
+ end
159
+
160
+ true
161
+ end
162
+
163
+ # @private
164
+ def to_s
165
+ format "(inventory: %i, status: %s)", inventory.count, status
166
+ end
167
+
168
+ # @private
169
+ def inspect
170
+ "#<#{self.class.name} #{self}>"
171
+ end
172
+
173
+ protected
174
+
175
+ # rubocop:disable all
176
+
177
+ def background_run enum
178
+ until synchronize { @stopped }
179
+ synchronize do
180
+ if @paused
181
+ @pause_cond.wait
182
+ next
183
+ end
184
+ end
185
+
186
+ begin
187
+ # Cannot syncronize the enumerator, causes deadlock
188
+ response = enum.next
189
+ response.received_messages.each do |rec_msg_grpc|
190
+ rec_msg = ReceivedMessage.from_grpc(rec_msg_grpc, self)
191
+ synchronize do
192
+ @inventory.add rec_msg.ack_id
193
+
194
+ perform_callback_async rec_msg
195
+ end
196
+ end
197
+ synchronize { pause_streaming! }
198
+ rescue StopIteration
199
+ break
200
+ end
201
+ end
202
+ rescue GRPC::DeadlineExceeded, GRPC::Unavailable, GRPC::Cancelled
203
+ # The GAPIC layer will raise DeadlineExceeded when stream is opened
204
+ # longer than the timeout value it is configured for. When this
205
+ # happends, restart the stream stealthly.
206
+ # Also stealthly restart the stream on Unavailable and Cancelled.
207
+ synchronize { start_streaming! }
208
+ rescue => e
209
+ fail Google::Cloud::Error.from_error(e)
210
+ end
211
+
212
+ # rubocop:enable all
213
+
214
+ def perform_callback_async rec_msg
215
+ Concurrent::Future.new(executor: callback_thread_pool) do
216
+ subscriber.callback.call rec_msg
217
+ end.execute
218
+ end
219
+
220
+ def start_streaming!
221
+ # signal to the previous queue to shut down
222
+ old_queue = []
223
+ old_queue = @request_queue.dump_queue if @request_queue
224
+
225
+ @request_queue = EnumeratorQueue.new self
226
+ @request_queue.push initial_input_request
227
+ old_queue.each { |obj| @request_queue.push obj }
228
+ output_enum = subscriber.service.streaming_pull @request_queue.each
229
+
230
+ @stopped = nil
231
+ @paused = nil
232
+
233
+ # create new background thread to handle new enumerator
234
+ @background_thread = Thread.new(output_enum) do |enum|
235
+ background_run enum
236
+ end
237
+ end
238
+
239
+ def pause_streaming!
240
+ return unless pause_streaming?
241
+
242
+ @paused = true
243
+ end
244
+
245
+ def pause_streaming?
246
+ return if @paused
247
+
248
+ @inventory.full?
249
+ end
250
+
251
+ def unpause_streaming!
252
+ return unless unpause_streaming?
253
+
254
+ @paused = nil
255
+ # signal to the background thread that we are unpaused
256
+ @pause_cond.broadcast
257
+ end
258
+
259
+ def unpause_streaming?
260
+ return if @paused.nil?
261
+
262
+ @inventory.count < @inventory.limit*0.8
263
+ end
264
+
265
+ def initial_input_request
266
+ Google::Pubsub::V1::StreamingPullRequest.new.tap do |req|
267
+ req.subscription = subscriber.subscription_name
268
+ req.stream_ack_deadline_seconds = subscriber.deadline
269
+ req.modify_deadline_ack_ids += @inventory.ack_ids
270
+ req.modify_deadline_seconds += \
271
+ @inventory.ack_ids.map { subscriber.deadline }
272
+ end
273
+ end
274
+
275
+ ##
276
+ # Makes sure the values are the `ack_id`. If given several
277
+ # {ReceivedMessage} objects extract the `ack_id` values.
278
+ def coerce_ack_ids messages
279
+ Array(messages).flatten.map do |msg|
280
+ msg.respond_to?(:ack_id) ? msg.ack_id : msg.to_s
281
+ end
282
+ end
283
+
284
+ def status
285
+ return "not started" if @background_thread.nil?
286
+
287
+ status = @background_thread.status
288
+ return "error" if status.nil?
289
+ return "stopped" if status == false
290
+ status
291
+ end
292
+
293
+ ##
294
+ # @private
295
+ class Inventory
296
+ include MonitorMixin
297
+
298
+ attr_reader :stream, :limit
299
+
300
+ def initialize stream, limit
301
+ @stream = stream
302
+ @limit = limit
303
+ @_ack_ids = []
304
+ @wait_cond = new_cond
305
+
306
+ super()
307
+ end
308
+
309
+ def ack_ids
310
+ @_ack_ids
311
+ end
312
+
313
+ def add *ack_ids
314
+ ack_ids = Array(ack_ids).flatten
315
+ synchronize do
316
+ @_ack_ids += ack_ids
317
+ @background_thread ||= Thread.new { background_run }
318
+ end
319
+ end
320
+
321
+ def remove *ack_ids
322
+ ack_ids = Array(ack_ids).flatten
323
+ synchronize do
324
+ @_ack_ids -= ack_ids
325
+ if @_ack_ids.empty?
326
+ if @background_thread
327
+ @background_thread.kill
328
+ @background_thread = nil
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ def count
335
+ synchronize do
336
+ @_ack_ids.count
337
+ end
338
+ end
339
+
340
+ def empty?
341
+ synchronize do
342
+ @_ack_ids.empty?
343
+ end
344
+ end
345
+
346
+ def stop
347
+ synchronize do
348
+ @stopped = true
349
+ @background_thread.kill if @background_thread
350
+ end
351
+ end
352
+
353
+ def full?
354
+ count >= limit
355
+ end
356
+
357
+ protected
358
+
359
+ def background_run
360
+ until synchronize { @stopped }
361
+ delay = calc_delay
362
+ synchronize { @wait_cond.wait delay }
363
+
364
+ stream.delay_inventory!
365
+ end
366
+ end
367
+
368
+ def calc_delay
369
+ (stream.subscriber.deadline - 3) * rand(0.8..0.9)
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
375
+ end
376
+ end