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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c103954443d377024fc1a0c670bf809e48f2515
4
- data.tar.gz: a2b506b3a345fb5b73756eb15ef6a96c76de39a5
3
+ metadata.gz: abd5adf64822c8eefa9b20faf514b0af57414e64
4
+ data.tar.gz: a93679512d85b7c038980ce0144dad4c28dc87c6
5
5
  SHA512:
6
- metadata.gz: b5dda56bd07a86a42541f512fb1ea119333d257d9bc66dc65d73fb59180bfe584cbe2e55ebe51b337dfb6cafa0dd4c2ba013239657e269565b5126f6e0b09cf8
7
- data.tar.gz: 98e0759f6a404373b5e793a1eab6cb1c631573e49d4cce29e769ee40e18995bc7bf7f7afddb1cadf0c096255eaf6d938cbdfb8b69f19654358e18d97d967d122
6
+ metadata.gz: 0af752e4b5d18bc90dfab1fb702af73a092c128c800e53754bba04c16d6f2a6c12492b9f4e528120c97ba087e4b24fa38446534acd31334b503cc177232367d7
7
+ data.tar.gz: dae4bcaac46a4b3c2a85ef0f9e0292fec333ed8694eabf25d68cdbe9e4eda63744bf14b2b3dfc3c286d37bd25826815afa4c28d0e4c1b362299b306192b9c217
@@ -147,7 +147,30 @@ module Google
147
147
  # this: :that
148
148
  # ```
149
149
  #
150
- # Multiple messages can be published at the same time by passing a block:
150
+ # Messages can also be published in batches asynchronously using
151
+ # `publish_async`. (See {Google::Cloud::Pubsub::Topic#publish_async} and
152
+ # {Google::Cloud::Pubsub::AsyncPublisher})
153
+ #
154
+ # ```ruby
155
+ # require "google/cloud/pubsub"
156
+ #
157
+ # pubsub = Google::Cloud::Pubsub.new
158
+ #
159
+ # topic = pubsub.topic "my-topic"
160
+ # topic.publish_async "task completed" do |result|
161
+ # if result.succeeded?
162
+ # log_publish_success result.data
163
+ # else
164
+ # log_publish_failure result.data, result.error
165
+ # end
166
+ # end
167
+ #
168
+ # topic.async_publisher.stop.wait!
169
+ # ```
170
+ #
171
+ # Or multiple messages can be published in batches at the same time by
172
+ # passing a block to `publish`. (See
173
+ # {Google::Cloud::Pubsub::BatchPublisher})
151
174
  #
152
175
  # ```ruby
153
176
  # require "google/cloud/pubsub"
@@ -199,6 +222,29 @@ module Google
199
222
  # msgs = sub.wait_for_messages
200
223
  # ```
201
224
  #
225
+ # Messages can also be streamed from a subscription with a subscriber object
226
+ # that can be created using `listen`. (See
227
+ # {Google::Cloud::Pubsub::Subscription#listen} and
228
+ # {Google::Cloud::Pubsub::Subscriber})
229
+ #
230
+ # ```ruby
231
+ # require "google/cloud/pubsub"
232
+ #
233
+ # pubsub = Google::Cloud::Pubsub.new
234
+ #
235
+ # sub = pubsub.subscription "my-topic-sub"
236
+ #
237
+ # subscriber = sub.listen do |msg|
238
+ # # process msg
239
+ # msg.ack!
240
+ # end
241
+ #
242
+ # subscriber.start
243
+ #
244
+ # # Shut down the subscriber when ready to stop receiving messages.
245
+ # subscriber.stop.wait!
246
+ # ```
247
+ #
202
248
  # ## Acknowledging a Message
203
249
  #
204
250
  # Messages that are received can be acknowledged in Pub/Sub, marking the
@@ -311,9 +357,10 @@ module Google
311
357
  #
312
358
  # ## Listening for Messages
313
359
  #
314
- # Long running workers are easy to create with `listen`, which runs an
315
- # infinitely blocking loop to process messages as they are received. (See
316
- # {Google::Cloud::Pubsub::Subscription#listen})
360
+ # A subscriber object can be created using `listen`, which streams messages
361
+ # from the backend and processes them as they are received. (See
362
+ # {Google::Cloud::Pubsub::Subscription#listen} and
363
+ # {Google::Cloud::Pubsub::Subscriber})
317
364
  #
318
365
  # ```ruby
319
366
  # require "google/cloud/pubsub"
@@ -321,28 +368,22 @@ module Google
321
368
  # pubsub = Google::Cloud::Pubsub.new
322
369
  #
323
370
  # sub = pubsub.subscription "my-topic-sub"
324
- # sub.listen do |msg|
371
+ #
372
+ # subscriber = sub.listen do |msg|
325
373
  # # process msg
374
+ # msg.ack!
326
375
  # end
327
- # ```
328
376
  #
329
- # Messages are retrieved in batches for efficiency. The number of messages
330
- # pulled per batch can be limited with the `max` option:
377
+ # subscriber.start
331
378
  #
332
- # ```ruby
333
- # require "google/cloud/pubsub"
334
- #
335
- # pubsub = Google::Cloud::Pubsub.new
336
- #
337
- # sub = pubsub.subscription "my-topic-sub"
338
- # sub.listen max: 20 do |msg|
339
- # # process msg
340
- # end
379
+ # # Shut down the subscriber when ready to stop receiving messages.
380
+ # subscriber.stop.wait!
341
381
  # ```
342
382
  #
343
- # When processing time and the acknowledgement deadline are a concern,
344
- # messages can be automatically acknowledged as they are pulled with the
345
- # `autoack` option:
383
+ # The subscriber object can be configured to control the number of
384
+ # concurrent streams to open, the number of received messages to be
385
+ # collected, and the number of threads each stream opens for concurrent
386
+ # calls made to handle the received messages.
346
387
  #
347
388
  # ```ruby
348
389
  # require "google/cloud/pubsub"
@@ -350,19 +391,14 @@ module Google
350
391
  # pubsub = Google::Cloud::Pubsub.new
351
392
  #
352
393
  # sub = pubsub.subscription "my-topic-sub"
353
- # sub.listen autoack: true do |msg|
354
- # # process msg
355
- # end
356
- # ```
357
394
  #
358
- # ## Configuring timeout
359
- #
360
- # You can configure the request `timeout` value in seconds.
361
- #
362
- # ```ruby
363
- # require "google/cloud/pubsub"
395
+ # subscriber = sub.listen threads: { callback: 16 } do |msg|
396
+ # # store the message somewhere before acknowledging
397
+ # store_in_backend msg.data # takes a few seconds
398
+ # msg.ack!
399
+ # end
364
400
  #
365
- # pubsub = Google::Cloud::Pubsub.new timeout: 120
401
+ # subscriber.start
366
402
  # ```
367
403
  #
368
404
  # ## Working Across Projects
@@ -0,0 +1,328 @@
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
+ require "google/cloud/pubsub/publish_result"
19
+ require "google/cloud/pubsub/service"
20
+
21
+ module Google
22
+ module Cloud
23
+ module Pubsub
24
+ ##
25
+ # Used to publish multiple messages in batches to a topic. See
26
+ # {Google::Cloud::Pubsub::Topic#async_publisher}
27
+ #
28
+ # @example
29
+ # require "google/cloud/pubsub"
30
+ #
31
+ # pubsub = Google::Cloud::Pubsub.new
32
+ #
33
+ # topic = pubsub.topic "my-topic"
34
+ # topic.publish_async "task completed" do |result|
35
+ # if result.succeeded?
36
+ # log_publish_success result.data
37
+ # else
38
+ # log_publish_failure result.data, result.error
39
+ # end
40
+ # end
41
+ #
42
+ # topic.async_publisher.stop.wait!
43
+ #
44
+ # @attr_reader [String] topic_name The name of the topic the messages
45
+ # are published to. In the form of
46
+ # "/projects/project-identifier/topics/topic-name".
47
+ # @attr_reader [Integer] max_bytes The maximum size of messages to be
48
+ # collected before the batch is published. Default is 10,000,000
49
+ # (10MB).
50
+ # @attr_reader [Integer] max_messages The maximum number of messages to
51
+ # be collected before the batch is published. Default is 1,000.
52
+ # @attr_reader [Numeric] interval The number of seconds to collect
53
+ # messages before the batch is published. Default is 0.25.
54
+ # @attr_reader [Numeric] publish_threads The number of threads used to
55
+ # publish messages. Default is 4.
56
+ # @attr_reader [Numeric] callback_threads The number of threads to
57
+ # handle the published messages' callbacks. Default is 8.
58
+ #
59
+ class AsyncPublisher
60
+ include MonitorMixin
61
+
62
+ attr_reader :topic_name, :max_bytes, :max_messages, :interval,
63
+ :publish_threads, :callback_threads
64
+ ##
65
+ # @private Implementation accessors
66
+ attr_reader :service, :batch, :publish_thread_pool,
67
+ :callback_thread_pool
68
+
69
+ ##
70
+ # @private Create a new instance of the object.
71
+ def initialize topic_name, service, max_bytes: 10000000,
72
+ max_messages: 1000, interval: 0.25, threads: {}
73
+ @topic_name = service.topic_path topic_name
74
+ @service = service
75
+
76
+ @max_bytes = max_bytes
77
+ @max_messages = max_messages
78
+ @interval = interval
79
+ @publish_threads = (threads[:publish] || 4).to_i
80
+ @callback_threads = (threads[:callback] || 8).to_i
81
+
82
+ @cond = new_cond
83
+
84
+ # init MonitorMixin
85
+ super()
86
+ end
87
+
88
+ ##
89
+ # Add a message to the async publisher to be published to the topic.
90
+ # Messages will be collected in batches and published together.
91
+ # See {Google::Cloud::Pubsub::Topic#publish_async}
92
+ def publish data = nil, attributes = {}, &block
93
+ msg = create_pubsub_message data, attributes
94
+
95
+ synchronize do
96
+ fail "Can't publish when stopped." if @stopped
97
+
98
+ if @batch.nil?
99
+ @batch ||= Batch.new self
100
+ @batch.add msg, block
101
+ else
102
+ unless @batch.try_add msg, block
103
+ publish_batch!
104
+ @batch = Batch.new self
105
+ @batch.add msg, block
106
+ end
107
+ end
108
+
109
+ init_resources!
110
+
111
+ publish_batch! if @batch.ready?
112
+
113
+ @cond.signal
114
+ end
115
+ nil
116
+ end
117
+
118
+ ##
119
+ # Begins the process of stopping the publisher. Messages already in
120
+ # the queue will be published, but no new messages can be added. Use
121
+ # {#wait!} to block until the publisher is fully stopped and all
122
+ # pending messages have been published.
123
+ #
124
+ # @return [AsyncPublisher] returns self so calls can be chained.
125
+ def stop
126
+ synchronize do
127
+ break if @stopped
128
+
129
+ @stopped = true
130
+ publish_batch!
131
+ @cond.signal
132
+ @publish_thread_pool.shutdown if @publish_thread_pool
133
+ end
134
+
135
+ self
136
+ end
137
+
138
+ ##
139
+ # Blocks until the publisher is fully stopped, all pending messages
140
+ # have been published, and all callbacks have completed. Does not stop
141
+ # the publisher. To stop the publisher, first call {#stop} and then
142
+ # call {#wait!} to block until the publisher is stopped.
143
+ #
144
+ # @return [AsyncPublisher] returns self so calls can be chained.
145
+ def wait! timeout = nil
146
+ synchronize do
147
+ if @publish_thread_pool
148
+ @publish_thread_pool.wait_for_termination timeout
149
+ end
150
+
151
+ if @callback_thread_pool
152
+ @callback_thread_pool.shutdown
153
+ @callback_thread_pool.wait_for_termination timeout
154
+ end
155
+ end
156
+
157
+ self
158
+ end
159
+
160
+ ##
161
+ # Forces all messages in the current batch to be published
162
+ # immediately.
163
+ #
164
+ # @return [AsyncPublisher] returns self so calls can be chained.
165
+ def flush
166
+ synchronize do
167
+ publish_batch!
168
+ @cond.signal
169
+ end
170
+
171
+ self
172
+ end
173
+
174
+ ##
175
+ # Whether the publisher has been started.
176
+ #
177
+ # @return [boolean] `true` when started, `false` otherwise.
178
+ def started?
179
+ !stopped?
180
+ end
181
+
182
+ ##
183
+ # Whether the publisher has been stopped.
184
+ #
185
+ # @return [boolean] `true` when stopped, `false` otherwise.
186
+ def stopped?
187
+ synchronize { @stopped }
188
+ end
189
+
190
+ protected
191
+
192
+ def init_resources!
193
+ @first_published_at ||= Time.now
194
+ @publish_thread_pool ||= Concurrent::FixedThreadPool.new \
195
+ @publish_threads
196
+ @callback_thread_pool ||= Concurrent::FixedThreadPool.new \
197
+ @callback_threads
198
+ @thread ||= Thread.new { run_background }
199
+ end
200
+
201
+ def run_background
202
+ synchronize do
203
+ until @stopped
204
+ if @batch.nil?
205
+ @cond.wait
206
+ next
207
+ end
208
+
209
+ time_since_first_publish = Time.now - @first_published_at
210
+ if time_since_first_publish > @interval
211
+ # interval met, publish the batch...
212
+ publish_batch!
213
+ @cond.wait
214
+ else
215
+ # still waiting for the interval to publish the batch...
216
+ @cond.wait(@interval - time_since_first_publish)
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ def publish_batch!
223
+ return unless @batch
224
+
225
+ publish_batch_async @topic_name, @batch
226
+ @batch = nil
227
+ @first_published_at = nil
228
+ end
229
+
230
+ def publish_batch_async topic_name, batch
231
+ Concurrent::Future.new(executor: @publish_thread_pool) do
232
+ begin
233
+ grpc = @service.publish topic_name, batch.messages
234
+ batch.items.zip(Array(grpc.message_ids)) do |item, id|
235
+ next unless item.callback
236
+
237
+ item.msg.message_id = id
238
+ publish_result = PublishResult.from_grpc(item.msg)
239
+ execute_callback_async item.callback, publish_result
240
+ end
241
+ rescue => e
242
+ batch.items.each do |item|
243
+ next unless item.callback
244
+
245
+ publish_result = PublishResult.from_error(item.msg, e)
246
+ execute_callback_async item.callback, publish_result
247
+ end
248
+ end
249
+ end.execute
250
+ end
251
+
252
+ def execute_callback_async callback, publish_result
253
+ Concurrent::Future.new(executor: @callback_thread_pool) do
254
+ callback.call publish_result
255
+ end.execute
256
+ end
257
+
258
+ def create_pubsub_message data, attributes
259
+ attributes ||= {}
260
+ if data.is_a?(::Hash) && attributes.empty?
261
+ attributes = data
262
+ data = nil
263
+ end
264
+ # Convert IO-ish objects to strings
265
+ if data.respond_to?(:read) && data.respond_to?(:rewind)
266
+ data.rewind
267
+ data = data.read
268
+ end
269
+ # Convert data to encoded byte array to match the protobuf defn
270
+ data_bytes = String(data).dup.force_encoding("ASCII-8BIT").freeze
271
+
272
+ # Convert attributes to strings to match the protobuf definition
273
+ attributes = Hash[attributes.map { |k, v| [String(k), String(v)] }]
274
+
275
+ Google::Pubsub::V1::PubsubMessage.new data: data_bytes,
276
+ attributes: attributes
277
+ end
278
+
279
+ ##
280
+ # @private
281
+ class Batch
282
+ attr_reader :messages, :callbacks
283
+
284
+ def initialize publisher
285
+ @publisher = publisher
286
+ @messages = []
287
+ @callbacks = []
288
+ end
289
+
290
+ def add msg, callback
291
+ @messages << msg
292
+ @callbacks << callback
293
+ end
294
+
295
+ def try_add msg, callback
296
+ if total_message_count + 1 > @publisher.max_messages ||
297
+ total_message_bytes + msg.to_proto.size >= @publisher.max_bytes
298
+ return false
299
+ end
300
+ add msg, callback
301
+ true
302
+ end
303
+
304
+ def ready?
305
+ total_message_count >= @publisher.max_messages ||
306
+ total_message_bytes >= @publisher.max_bytes
307
+ end
308
+
309
+ def total_message_count
310
+ @messages.count
311
+ end
312
+
313
+ def total_message_bytes
314
+ @messages.map(&:to_proto).map(&:size).inject(0, :+)
315
+ end
316
+
317
+ def items
318
+ @messages.zip(@callbacks).map do |msg, callback|
319
+ Item.new msg, callback
320
+ end
321
+ end
322
+
323
+ Item = Struct.new :msg, :callback
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end