google-cloud-pubsub 0.26.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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