google-cloud-pubsub 0.26.0 → 2.6.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.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +12 -2
  3. data/AUTHENTICATION.md +178 -0
  4. data/CHANGELOG.md +659 -0
  5. data/CODE_OF_CONDUCT.md +40 -0
  6. data/CONTRIBUTING.md +187 -0
  7. data/EMULATOR.md +37 -0
  8. data/LICENSE +2 -2
  9. data/LOGGING.md +32 -0
  10. data/OVERVIEW.md +528 -0
  11. data/TROUBLESHOOTING.md +31 -0
  12. data/lib/google/cloud/pubsub/async_publisher/batch.rb +310 -0
  13. data/lib/google/cloud/pubsub/async_publisher.rb +402 -0
  14. data/lib/google/cloud/pubsub/batch_publisher.rb +100 -0
  15. data/lib/google/cloud/pubsub/convert.rb +91 -0
  16. data/lib/google/cloud/pubsub/credentials.rb +26 -10
  17. data/lib/google/cloud/pubsub/errors.rb +85 -0
  18. data/lib/google/cloud/pubsub/message.rb +80 -17
  19. data/lib/google/cloud/pubsub/policy.rb +17 -14
  20. data/lib/google/cloud/pubsub/project.rb +364 -250
  21. data/lib/google/cloud/pubsub/publish_result.rb +103 -0
  22. data/lib/google/cloud/pubsub/received_message.rb +162 -24
  23. data/lib/google/cloud/pubsub/retry_policy.rb +88 -0
  24. data/lib/google/cloud/pubsub/schema/list.rb +180 -0
  25. data/lib/google/cloud/pubsub/schema.rb +310 -0
  26. data/lib/google/cloud/pubsub/service.rb +281 -265
  27. data/lib/google/cloud/pubsub/snapshot/list.rb +21 -21
  28. data/lib/google/cloud/pubsub/snapshot.rb +55 -15
  29. data/lib/google/cloud/pubsub/subscriber/enumerator_queue.rb +54 -0
  30. data/lib/google/cloud/pubsub/subscriber/inventory.rb +173 -0
  31. data/lib/google/cloud/pubsub/subscriber/sequencer.rb +115 -0
  32. data/lib/google/cloud/pubsub/subscriber/stream.rb +400 -0
  33. data/lib/google/cloud/pubsub/subscriber/timed_unary_buffer.rb +230 -0
  34. data/lib/google/cloud/pubsub/subscriber.rb +417 -0
  35. data/lib/google/cloud/pubsub/subscription/list.rb +28 -28
  36. data/lib/google/cloud/pubsub/subscription/push_config.rb +268 -0
  37. data/lib/google/cloud/pubsub/subscription.rb +900 -172
  38. data/lib/google/cloud/pubsub/topic/list.rb +21 -21
  39. data/lib/google/cloud/pubsub/topic.rb +674 -95
  40. data/lib/google/cloud/pubsub/version.rb +6 -4
  41. data/lib/google/cloud/pubsub.rb +104 -439
  42. data/lib/google-cloud-pubsub.rb +60 -29
  43. metadata +88 -50
  44. data/README.md +0 -69
  45. data/lib/google/cloud/pubsub/topic/publisher.rb +0 -86
  46. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/duration.rb +0 -77
  47. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/field_mask.rb +0 -223
  48. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/timestamp.rb +0 -81
  49. data/lib/google/cloud/pubsub/v1/doc/google/pubsub/v1/pubsub.rb +0 -503
  50. data/lib/google/cloud/pubsub/v1/publisher_client.rb +0 -605
  51. data/lib/google/cloud/pubsub/v1/publisher_client_config.json +0 -96
  52. data/lib/google/cloud/pubsub/v1/subscriber_client.rb +0 -1104
  53. data/lib/google/cloud/pubsub/v1/subscriber_client_config.json +0 -127
  54. data/lib/google/cloud/pubsub/v1.rb +0 -17
  55. data/lib/google/pubsub/v1/pubsub_pb.rb +0 -187
  56. data/lib/google/pubsub/v1/pubsub_services_pb.rb +0 -159
@@ -0,0 +1,31 @@
1
+ # Troubleshooting
2
+
3
+ ## Where can I get more help?
4
+
5
+ ### Ask the Community
6
+
7
+ If you have a question about how to use a Google Cloud client library in your
8
+ project or are stuck in the Developer's console and don't know where to turn,
9
+ it's possible your questions have already been addressed by the community.
10
+
11
+ First, check out the appropriate tags on StackOverflow:
12
+ - [`google-cloud-platform+ruby+pubsub`][so-ruby]
13
+
14
+ Next, try searching through the issues on GitHub:
15
+
16
+ - [`api:pubsub` issues][gh-search-ruby]
17
+
18
+ Still nothing?
19
+
20
+ ### Ask the Developers
21
+
22
+ If you're experiencing a bug with the code, or have an idea for how it can be
23
+ improved, *please* create a new issue on GitHub so we can talk about it.
24
+
25
+ - [New issue][gh-ruby]
26
+
27
+ [so-ruby]: http://stackoverflow.com/questions/tagged/google-cloud-platform+ruby+pubsub
28
+
29
+ [gh-search-ruby]: https://github.com/googleapis/google-cloud-ruby/issues?q=label%3A%22api%3A+pubsub%22
30
+
31
+ [gh-ruby]: https://github.com/googleapis/google-cloud-ruby/issues/new
@@ -0,0 +1,310 @@
1
+ # Copyright 2019 Google LLC
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
+ # https://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 "google/cloud/pubsub/errors"
18
+
19
+ module Google
20
+ module Cloud
21
+ module PubSub
22
+ class AsyncPublisher
23
+ ##
24
+ # @private
25
+ class Batch
26
+ include MonitorMixin
27
+
28
+ attr_reader :items
29
+ attr_reader :ordering_key
30
+
31
+ def initialize publisher, ordering_key
32
+ # init MonitorMixin
33
+ super()
34
+
35
+ @publisher = publisher
36
+ @ordering_key = ordering_key
37
+ @items = []
38
+ @queue = []
39
+ @default_message_bytes = publisher.topic_name.bytesize + 2
40
+ @total_message_bytes = @default_message_bytes
41
+ @publishing = false
42
+ @stopping = false
43
+ @canceled = false
44
+ end
45
+
46
+ ##
47
+ # Adds a message and callback to the batch.
48
+ #
49
+ # The method will indicate how the message is added. It will either be
50
+ # added to the active list of items, it will be queued to be picked up
51
+ # once the active publishing job has been completed, or it will
52
+ # indicate that the batch is full and a publishing job should be
53
+ # created.
54
+ #
55
+ # @param [Google::Cloud::PubSub::V1::PubsubMessage] msg The message.
56
+ # @param [Proc, nil] callback The callback.
57
+ #
58
+ # @return [Symbol] The state of the batch.
59
+ #
60
+ # * `:added` - Added to the active list of items to be published.
61
+ # * `:queued` - Batch is publishing, and the messsage is queued.
62
+ # * `:full` - Batch is full and ready to be published, and the
63
+ # message is queued.
64
+ #
65
+ def add msg, callback
66
+ synchronize do
67
+ raise AsyncPublisherStopped if @stopping
68
+ raise OrderingKeyError, @ordering_key if @canceled
69
+
70
+ if @publishing
71
+ queue_add msg, callback
72
+ :queued
73
+ elsif try_add msg, callback
74
+ :added
75
+ else
76
+ queue_add msg, callback
77
+ :full
78
+ end
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Marks the batch to be published.
84
+ #
85
+ # The method will indicate whether a new publishing job should be
86
+ # started to publish the batch. See {publishing?}.
87
+ #
88
+ # @param [Boolean] stop Indicates whether the batch should also be
89
+ # marked for stopping, and any existing publish job should publish
90
+ # all items until the batch is empty.
91
+ #
92
+ # @return [Boolean] Returns whether a new publishing job should be
93
+ # started to publish the batch. If the batch is already being
94
+ # published then this will return `false`.
95
+ #
96
+ def publish! stop: nil
97
+ synchronize do
98
+ @stopping = true if stop
99
+
100
+ return false if @canceled
101
+
102
+ # If we are already publishing, do not indicate a new job needs to
103
+ # be started.
104
+ return false if @publishing
105
+
106
+ @publishing = !(@items.empty? && @queue.empty?)
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Indicates whether the batch has an active publishing job.
112
+ #
113
+ # @return [Boolean]
114
+ #
115
+ def publishing?
116
+ # This probably does not need to be synchronized
117
+ @publishing
118
+ end
119
+
120
+ ##
121
+ # Indicates whether the batch has been stopped and all items will be
122
+ # published until the batch is empty.
123
+ #
124
+ # @return [Boolean]
125
+ #
126
+ def stopping?
127
+ # This does not need to be synchronized because nothing un-stops
128
+ @stopping
129
+ end
130
+
131
+ ##
132
+ # Rebalances the batch by moving any queued items that will fit into
133
+ # the active item list.
134
+ #
135
+ # This method is only intended to be used by the active publishing
136
+ # job.
137
+ #
138
+ def rebalance!
139
+ synchronize do
140
+ return [] if @canceled
141
+
142
+ until @queue.empty?
143
+ item = @queue.first
144
+ if try_add item.msg, item.callback
145
+ @queue.shift
146
+ next
147
+ end
148
+ break
149
+ end
150
+
151
+ @items
152
+ end
153
+ end
154
+
155
+ # rubocop:disable Metrics/MethodLength
156
+
157
+ ##
158
+ # Resets the batch after a successful publish. This clears the active
159
+ # item list and moves the queued items that will fit into the active
160
+ # item list.
161
+ #
162
+ # If the batch has enough queued items to fill the batch again, the
163
+ # publishing job should continue to publish the reset batch until the
164
+ # batch indicated it should stop.
165
+ #
166
+ # This method is only intended to be used by the active publishing
167
+ # job.
168
+ #
169
+ # @return [Boolean] Whether the active publishing job should continue
170
+ # publishing after the reset.
171
+ #
172
+ def reset!
173
+ synchronize do
174
+ @items = []
175
+ @total_message_bytes = @default_message_bytes
176
+
177
+ if @canceled
178
+ @queue = []
179
+ @publishing = false
180
+ return false
181
+ end
182
+
183
+ until @queue.empty?
184
+ item = @queue.first
185
+ added = try_add item.msg, item.callback
186
+ break unless added
187
+ @queue.shift
188
+ end
189
+
190
+ return false unless @publishing
191
+ if @items.empty?
192
+ @publishing = false
193
+ return false
194
+ else
195
+ return true if stopping?
196
+ if @queue.empty?
197
+ @publishing = false
198
+ return false
199
+ end
200
+ end
201
+ end
202
+ true
203
+ end
204
+
205
+ # rubocop:enable Metrics/MethodLength
206
+
207
+ ##
208
+ # Cancel the batch and hault futher batches until resumed. See
209
+ # {#resume!} and {#canceled?}.
210
+ #
211
+ # @return [Array<Item}] All items, including queued items
212
+ #
213
+ def cancel!
214
+ synchronize do
215
+ @canceled = true
216
+ @items + @queue
217
+ end
218
+ end
219
+
220
+ ##
221
+ # Resume the batch and proceed to publish messages. See {#cancel!} and
222
+ # {#canceled?}.
223
+ #
224
+ # @return [Boolean] Whether the batch was resumed.
225
+ #
226
+ def resume!
227
+ synchronize do
228
+ # Return false if the batch is not canceled
229
+ return false unless @canceled
230
+
231
+ @items = []
232
+ @queue = []
233
+ @total_message_bytes = @default_message_bytes
234
+ @publishing = false
235
+ @canceled = false
236
+ end
237
+ true
238
+ end
239
+
240
+ ##
241
+ # Indicates whether the batch has been canceled due to an error while
242
+ # publishing. See {#cancel!} and {#resume!}.
243
+ #
244
+ # @return [Boolean]
245
+ #
246
+ def canceled?
247
+ # This does not need to be synchronized because nothing un-stops
248
+ synchronize { @canceled }
249
+ end
250
+
251
+ ##
252
+ # Determines whether the batch is empty and ready to be culled.
253
+ #
254
+ def empty?
255
+ synchronize do
256
+ return false if @publishing || @canceled || @stopping
257
+
258
+ @items.empty? && @queue.empty?
259
+ end
260
+ end
261
+
262
+ protected
263
+
264
+ def items_add msg, callback
265
+ item = Item.new msg, callback
266
+ @items << item
267
+ @total_message_bytes += item.bytesize + 2
268
+ end
269
+
270
+ def try_add msg, callback
271
+ if @items.empty?
272
+ # Always add when empty, even if bytesize is bigger than total
273
+ items_add msg, callback
274
+ return true
275
+ end
276
+ new_message_count = total_message_count + 1
277
+ new_message_bytes = total_message_bytes + msg.to_proto.bytesize + 2
278
+ if new_message_count > @publisher.max_messages ||
279
+ new_message_bytes >= @publisher.max_bytes
280
+ return false
281
+ end
282
+ items_add msg, callback
283
+ true
284
+ end
285
+
286
+ def queue_add msg, callback
287
+ item = Item.new msg, callback
288
+ @queue << item
289
+ end
290
+
291
+ def total_message_count
292
+ @items.count
293
+ end
294
+
295
+ def total_message_bytes
296
+ @total_message_bytes
297
+ end
298
+
299
+ Item = Struct.new :msg, :callback do
300
+ def bytesize
301
+ msg.to_proto.bytesize
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ Pubsub = PubSub unless const_defined? :Pubsub
309
+ end
310
+ end
@@ -0,0 +1,402 @@
1
+ # Copyright 2017 Google LLC
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
+ # https://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/errors"
19
+ require "google/cloud/pubsub/async_publisher/batch"
20
+ require "google/cloud/pubsub/publish_result"
21
+ require "google/cloud/pubsub/service"
22
+ require "google/cloud/pubsub/convert"
23
+
24
+ module Google
25
+ module Cloud
26
+ module PubSub
27
+ ##
28
+ # Used to publish multiple messages in batches to a topic. See
29
+ # {Google::Cloud::PubSub::Topic#async_publisher}
30
+ #
31
+ # @example
32
+ # require "google/cloud/pubsub"
33
+ #
34
+ # pubsub = Google::Cloud::PubSub.new
35
+ #
36
+ # topic = pubsub.topic "my-topic"
37
+ # topic.publish_async "task completed" do |result|
38
+ # if result.succeeded?
39
+ # log_publish_success result.data
40
+ # else
41
+ # log_publish_failure result.data, result.error
42
+ # end
43
+ # end
44
+ #
45
+ # topic.async_publisher.stop!
46
+ #
47
+ # @attr_reader [String] topic_name The name of the topic the messages are published to. The value is a
48
+ # fully-qualified topic name in the form `projects/{project_id}/topics/{topic_id}`.
49
+ # @attr_reader [Integer] max_bytes The maximum size of messages to be collected before the batch is published.
50
+ # Default is 1,000,000 (1MB).
51
+ # @attr_reader [Integer] max_messages The maximum number of messages to be collected before the batch is
52
+ # published. Default is 100.
53
+ # @attr_reader [Numeric] interval The number of seconds to collect messages before the batch is published. Default
54
+ # is 0.01.
55
+ # @attr_reader [Numeric] publish_threads The number of threads used to publish messages. Default is 2.
56
+ # @attr_reader [Numeric] callback_threads The number of threads to handle the published messages' callbacks.
57
+ # Default is 4.
58
+ #
59
+ class AsyncPublisher
60
+ include MonitorMixin
61
+
62
+ attr_reader :topic_name
63
+ attr_reader :max_bytes
64
+ attr_reader :max_messages
65
+ attr_reader :interval
66
+ attr_reader :publish_threads
67
+ attr_reader :callback_threads
68
+ ##
69
+ # @private Implementation accessors
70
+ attr_reader :service, :batch, :publish_thread_pool,
71
+ :callback_thread_pool
72
+
73
+ ##
74
+ # @private Create a new instance of the object.
75
+ def initialize topic_name, service, max_bytes: 1_000_000, max_messages: 100, interval: 0.01, threads: {}
76
+ # init MonitorMixin
77
+ super()
78
+ @topic_name = service.topic_path topic_name
79
+ @service = service
80
+
81
+ @max_bytes = max_bytes
82
+ @max_messages = max_messages
83
+ @interval = interval
84
+ @publish_threads = (threads[:publish] || 2).to_i
85
+ @callback_threads = (threads[:callback] || 4).to_i
86
+
87
+ @published_at = nil
88
+ @publish_thread_pool = Concurrent::ThreadPoolExecutor.new max_threads: @publish_threads
89
+ @callback_thread_pool = Concurrent::ThreadPoolExecutor.new max_threads: @callback_threads
90
+
91
+ @ordered = false
92
+ @batches = {}
93
+ @cond = new_cond
94
+
95
+ @thread = Thread.new { run_background }
96
+ end
97
+
98
+ ##
99
+ # Add a message to the async publisher to be published to the topic.
100
+ # Messages will be collected in batches and published together.
101
+ # See {Google::Cloud::PubSub::Topic#publish_async}
102
+ #
103
+ # @param [String, File] data The message payload. This will be converted
104
+ # to bytes encoded as ASCII-8BIT.
105
+ # @param [Hash] attributes Optional attributes for the message.
106
+ # @param [String] ordering_key Identifies related messages for which
107
+ # publish order should be respected.
108
+ # @yield [result] the callback for when the message has been published
109
+ # @yieldparam [PublishResult] result the result of the asynchronous
110
+ # publish
111
+ # @raise [Google::Cloud::PubSub::AsyncPublisherStopped] when the
112
+ # publisher is stopped. (See {#stop} and {#stopped?}.)
113
+ # @raise [Google::Cloud::PubSub::OrderedMessagesDisabled] when
114
+ # publishing a message with an `ordering_key` but ordered messages are
115
+ # not enabled. (See {#message_ordering?} and
116
+ # {#enable_message_ordering!}.)
117
+ # @raise [Google::Cloud::PubSub::OrderingKeyError] when publishing a
118
+ # message with an `ordering_key` that has already failed when
119
+ # publishing. Use {#resume_publish} to allow this `ordering_key` to be
120
+ # published again.
121
+ #
122
+ def publish data = nil, attributes = nil, ordering_key: nil, **extra_attrs, &callback
123
+ msg = Convert.pubsub_message data, attributes, ordering_key, extra_attrs
124
+
125
+ synchronize do
126
+ raise AsyncPublisherStopped if @stopped
127
+ raise OrderedMessagesDisabled if !@ordered && !msg.ordering_key.empty? # default is empty string
128
+
129
+ batch = resolve_batch_for_message msg
130
+ raise OrderingKeyError, batch.ordering_key if batch.canceled?
131
+ batch_action = batch.add msg, callback
132
+ if batch_action == :full
133
+ publish_batches!
134
+ elsif @published_at.nil?
135
+ # Set initial time to now to start the background counter
136
+ @published_at = Time.now
137
+ end
138
+ @cond.signal
139
+ end
140
+
141
+ nil
142
+ end
143
+
144
+ ##
145
+ # Begins the process of stopping the publisher. Messages already in
146
+ # the queue will be published, but no new messages can be added. Use
147
+ # {#wait!} to block until the publisher is fully stopped and all
148
+ # pending messages have been published.
149
+ #
150
+ # @return [AsyncPublisher] returns self so calls can be chained.
151
+ def stop
152
+ synchronize do
153
+ break if @stopped
154
+
155
+ @stopped = true
156
+ publish_batches! stop: true
157
+ @cond.signal
158
+ @publish_thread_pool.shutdown
159
+ end
160
+
161
+ self
162
+ end
163
+
164
+ ##
165
+ # Blocks until the publisher is fully stopped, all pending messages have
166
+ # been published, and all callbacks have completed, or until `timeout`
167
+ # seconds have passed.
168
+ #
169
+ # Does not stop the publisher. To stop the publisher, first call {#stop}
170
+ # and then call {#wait!} to block until the publisher is stopped
171
+ #
172
+ # @param [Number, nil] timeout The number of seconds to block until the
173
+ # publisher is fully stopped. Default will block indefinitely.
174
+ #
175
+ # @return [AsyncPublisher] returns self so calls can be chained.
176
+ def wait! timeout = nil
177
+ synchronize do
178
+ @publish_thread_pool.wait_for_termination timeout
179
+
180
+ @callback_thread_pool.shutdown
181
+ @callback_thread_pool.wait_for_termination timeout
182
+ end
183
+
184
+ self
185
+ end
186
+
187
+ ##
188
+ # Stop this publisher and block until the publisher is fully stopped,
189
+ # all pending messages have been published, and all callbacks have
190
+ # completed, or until `timeout` seconds have passed.
191
+ #
192
+ # The same as calling {#stop} and {#wait!}.
193
+ #
194
+ # @param [Number, nil] timeout The number of seconds to block until the
195
+ # publisher is fully stopped. Default will block indefinitely.
196
+ #
197
+ # @return [AsyncPublisher] returns self so calls can be chained.
198
+ def stop! timeout = nil
199
+ stop
200
+ wait! timeout
201
+ end
202
+
203
+ ##
204
+ # Forces all messages in the current batch to be published
205
+ # immediately.
206
+ #
207
+ # @return [AsyncPublisher] returns self so calls can be chained.
208
+ def flush
209
+ synchronize do
210
+ publish_batches!
211
+ @cond.signal
212
+ end
213
+
214
+ self
215
+ end
216
+
217
+ ##
218
+ # Whether the publisher has been started.
219
+ #
220
+ # @return [boolean] `true` when started, `false` otherwise.
221
+ def started?
222
+ !stopped?
223
+ end
224
+
225
+ ##
226
+ # Whether the publisher has been stopped.
227
+ #
228
+ # @return [boolean] `true` when stopped, `false` otherwise.
229
+ def stopped?
230
+ synchronize { @stopped }
231
+ end
232
+
233
+ ##
234
+ # Enables message ordering for messages with ordering keys. When
235
+ # enabled, messages published with the same `ordering_key` will be
236
+ # delivered in the order they were published.
237
+ #
238
+ # See {#message_ordering?}. See {Topic#publish_async},
239
+ # {Subscription#listen}, and {Message#ordering_key}.
240
+ #
241
+ def enable_message_ordering!
242
+ synchronize { @ordered = true }
243
+ end
244
+
245
+ ##
246
+ # Whether message ordering for messages with ordering keys has been
247
+ # enabled. When enabled, messages published with the same `ordering_key`
248
+ # will be delivered in the order they were published. When disabled,
249
+ # messages may be delivered in any order.
250
+ #
251
+ # See {#enable_message_ordering!}. See {Topic#publish_async},
252
+ # {Subscription#listen}, and {Message#ordering_key}.
253
+ #
254
+ # @return [Boolean]
255
+ #
256
+ def message_ordering?
257
+ synchronize { @ordered }
258
+ end
259
+
260
+ ##
261
+ # Resume publishing ordered messages for the provided ordering key.
262
+ #
263
+ # @param [String] ordering_key Identifies related messages for which
264
+ # publish order should be respected.
265
+ #
266
+ # @return [boolean] `true` when resumed, `false` otherwise.
267
+ #
268
+ def resume_publish ordering_key
269
+ synchronize do
270
+ batch = resolve_batch_for_ordering_key ordering_key
271
+ return if batch.nil?
272
+ batch.resume!
273
+ end
274
+ end
275
+
276
+ protected
277
+
278
+ def run_background
279
+ synchronize do
280
+ until @stopped
281
+ if @published_at.nil?
282
+ @cond.wait
283
+ next
284
+ end
285
+
286
+ time_since_first_publish = Time.now - @published_at
287
+ if time_since_first_publish > @interval
288
+ # interval met, flush the batches...
289
+ publish_batches!
290
+ @cond.wait
291
+ else
292
+ # still waiting for the interval to publish the batch...
293
+ timeout = @interval - time_since_first_publish
294
+ @cond.wait timeout
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ def resolve_batch_for_message msg
301
+ @batches[msg.ordering_key] ||= Batch.new self, msg.ordering_key
302
+ end
303
+
304
+ def resolve_batch_for_ordering_key ordering_key
305
+ @batches[ordering_key]
306
+ end
307
+
308
+ def publish_batches! stop: nil
309
+ @batches.reject! { |_ordering_key, batch| batch.empty? }
310
+ @batches.each_value do |batch|
311
+ ready = batch.publish! stop: stop
312
+ publish_batch_async @topic_name, batch if ready
313
+ end
314
+ # Set published_at to nil to wait indefinitely
315
+ @published_at = nil
316
+ end
317
+
318
+ def publish_batch_async topic_name, batch
319
+ # TODO: raise unless @publish_thread_pool.running?
320
+ return unless @publish_thread_pool.running?
321
+
322
+ Concurrent::Promises.future_on(
323
+ @publish_thread_pool, topic_name, batch
324
+ ) { |t, b| publish_batch_sync t, b }
325
+ end
326
+
327
+ # rubocop:disable Metrics/AbcSize
328
+ # rubocop:disable Metrics/MethodLength
329
+
330
+ def publish_batch_sync topic_name, batch
331
+ # The only batch methods that are safe to call from the loop are
332
+ # rebalance! and reset! because they are the only methods that are
333
+ # synchronized.
334
+ loop do
335
+ items = batch.rebalance!
336
+
337
+ unless items.empty?
338
+ grpc = @service.publish topic_name, items.map(&:msg)
339
+ items.zip Array(grpc.message_ids) do |item, id|
340
+ next unless item.callback
341
+
342
+ item.msg.message_id = id
343
+ publish_result = PublishResult.from_grpc item.msg
344
+ execute_callback_async item.callback, publish_result
345
+ end
346
+ end
347
+
348
+ break unless batch.reset!
349
+ end
350
+ rescue StandardError => e
351
+ items = batch.items
352
+
353
+ unless batch.ordering_key.empty?
354
+ retry if publish_batch_error_retryable? e
355
+ # Cancel the batch if the error is not to be retried.
356
+ begin
357
+ raise OrderingKeyError, batch.ordering_key
358
+ rescue OrderingKeyError => e
359
+ # The existing e variable is not set to OrderingKeyError
360
+ # Get all unsent messages for the callback
361
+ items = batch.cancel!
362
+ end
363
+ end
364
+
365
+ items.each do |item|
366
+ next unless item.callback
367
+
368
+ publish_result = PublishResult.from_error item.msg, e
369
+ execute_callback_async item.callback, publish_result
370
+ end
371
+
372
+ # publish will retry indefinitely, as long as there are unsent items.
373
+ retry if batch.reset!
374
+ end
375
+
376
+ # rubocop:enable Metrics/AbcSize
377
+ # rubocop:enable Metrics/MethodLength
378
+
379
+ PUBLISH_RETRY_ERRORS = [
380
+ GRPC::Cancelled, GRPC::DeadlineExceeded, GRPC::Internal,
381
+ GRPC::ResourceExhausted, GRPC::Unauthenticated, GRPC::Unavailable
382
+ ].freeze
383
+
384
+ def publish_batch_error_retryable? error
385
+ PUBLISH_RETRY_ERRORS.any? { |klass| error.is_a? klass }
386
+ end
387
+
388
+ def execute_callback_async callback, publish_result
389
+ return unless @callback_thread_pool.running?
390
+
391
+ Concurrent::Promises.future_on(
392
+ @callback_thread_pool, callback, publish_result
393
+ ) do |cback, p_result|
394
+ cback.call p_result
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ Pubsub = PubSub unless const_defined? :Pubsub
401
+ end
402
+ end