google-cloud-pubsub 0.26.0 → 2.6.1

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