google-cloud-pubsub 1.0.2 → 1.1.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,309 @@
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, :ordering_key
29
+
30
+ def initialize publisher, ordering_key
31
+ @publisher = publisher
32
+ @ordering_key = ordering_key
33
+ @items = []
34
+ @queue = []
35
+ @default_message_bytes = publisher.topic_name.bytesize + 2
36
+ @total_message_bytes = @default_message_bytes
37
+ @publishing = false
38
+ @stopping = false
39
+ @canceled = false
40
+
41
+ # init MonitorMixin
42
+ super()
43
+ end
44
+
45
+ ##
46
+ # Adds a message and callback to the batch.
47
+ #
48
+ # The method will indicate how the message is added. It will either be
49
+ # added to the active list of items, it will be queued to be picked up
50
+ # once the active publishing job has been completed, or it will
51
+ # indicate that the batch is full and a publishing job should be
52
+ # created.
53
+ #
54
+ # @param [Google::Cloud::PubSub::V1::PubsubMessage] msg The message.
55
+ # @param [Proc, nil] callback The callback.
56
+ #
57
+ # @return [Symbol] The state of the batch.
58
+ #
59
+ # * `:added` - Added to the active list of items to be published.
60
+ # * `:queued` - Batch is publishing, and the messsage is queued.
61
+ # * `:full` - Batch is full and ready to be published, and the
62
+ # message is queued.
63
+ #
64
+ def add msg, callback
65
+ synchronize do
66
+ raise AsyncPublisherStopped if @stopping
67
+ raise OrderingKeyError, @ordering_key if @canceled
68
+
69
+ if @publishing
70
+ queue_add msg, callback
71
+ :queued
72
+ elsif try_add msg, callback
73
+ :added
74
+ else
75
+ queue_add msg, callback
76
+ :full
77
+ end
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Marks the batch to be published.
83
+ #
84
+ # The method will indicate whether a new publishing job should be
85
+ # started to publish the batch. See {publishing?}.
86
+ #
87
+ # @param [Boolean] stop Indicates whether the batch should also be
88
+ # marked for stopping, and any existing publish job should publish
89
+ # all items until the batch is empty.
90
+ #
91
+ # @return [Boolean] Returns whether a new publishing job should be
92
+ # started to publish the batch. If the batch is already being
93
+ # published then this will return `false`.
94
+ #
95
+ def publish! stop: nil
96
+ synchronize do
97
+ @stopping = true if stop
98
+
99
+ return false if @canceled
100
+
101
+ # If we are already publishing, do not indicate a new job needs to
102
+ # be started.
103
+ return false if @publishing
104
+
105
+ @publishing = !(@items.empty? && @queue.empty?)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Indicates whether the batch has an active publishing job.
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ def publishing?
115
+ # This probably does not need to be synchronized
116
+ @publishing
117
+ end
118
+
119
+ ##
120
+ # Indicates whether the batch has been stopped and all items will be
121
+ # published until the batch is empty.
122
+ #
123
+ # @return [Boolean]
124
+ #
125
+ def stopping?
126
+ # This does not need to be synchronized because nothing un-stops
127
+ @stopping
128
+ end
129
+
130
+ ##
131
+ # Rebalances the batch by moving any queued items that will fit into
132
+ # the active item list.
133
+ #
134
+ # This method is only intended to be used by the active publishing
135
+ # job.
136
+ #
137
+ def rebalance!
138
+ synchronize do
139
+ return [] if @canceled
140
+
141
+ until @queue.empty?
142
+ item = @queue.first
143
+ if try_add item.msg, item.callback
144
+ @queue.shift
145
+ next
146
+ end
147
+ break
148
+ end
149
+
150
+ @items
151
+ end
152
+ end
153
+
154
+ # rubocop:disable Metrics/MethodLength
155
+
156
+ ##
157
+ # Resets the batch after a successful publish. This clears the active
158
+ # item list and moves the queued items that will fit into the active
159
+ # item list.
160
+ #
161
+ # If the batch has enough queued items to fill the batch again, the
162
+ # publishing job should continue to publish the reset batch until the
163
+ # batch indicated it should stop.
164
+ #
165
+ # This method is only intended to be used by the active publishing
166
+ # job.
167
+ #
168
+ # @return [Boolean] Whether the active publishing job should continue
169
+ # publishing after the reset.
170
+ #
171
+ def reset!
172
+ synchronize do
173
+ @items = []
174
+ @total_message_bytes = @default_message_bytes
175
+
176
+ if @canceled
177
+ @queue = []
178
+ @publishing = false
179
+ return false
180
+ end
181
+
182
+ until @queue.empty?
183
+ item = @queue.first
184
+ added = try_add item.msg, item.callback
185
+ break unless added
186
+ @queue.shift
187
+ end
188
+
189
+ return false unless @publishing
190
+ if @items.empty?
191
+ @publishing = false
192
+ return false
193
+ else
194
+ return true if stopping?
195
+ if @queue.empty?
196
+ @publishing = false
197
+ return false
198
+ end
199
+ end
200
+ end
201
+ true
202
+ end
203
+
204
+ # rubocop:enable Metrics/MethodLength
205
+
206
+ ##
207
+ # Cancel the batch and hault futher batches until resumed. See
208
+ # {#resume!} and {#canceled?}.
209
+ #
210
+ # @return [Array<Item}] All items, including queued items
211
+ #
212
+ def cancel!
213
+ synchronize do
214
+ @canceled = true
215
+ @items + @queue
216
+ end
217
+ end
218
+
219
+ ##
220
+ # Resume the batch and proceed to publish messages. See {#cancel!} and
221
+ # {#canceled?}.
222
+ #
223
+ # @return [Boolean] Whether the batch was resumed.
224
+ #
225
+ def resume!
226
+ synchronize do
227
+ # Return false if the batch is not canceled
228
+ return false unless @canceled
229
+
230
+ @items = []
231
+ @queue = []
232
+ @total_message_bytes = @default_message_bytes
233
+ @publishing = false
234
+ @canceled = false
235
+ end
236
+ true
237
+ end
238
+
239
+ ##
240
+ # Indicates whether the batch has been canceled due to an error while
241
+ # publishing. See {#cancel!} and {#resume!}.
242
+ #
243
+ # @return [Boolean]
244
+ #
245
+ def canceled?
246
+ # This does not need to be synchronized because nothing un-stops
247
+ synchronize { @canceled }
248
+ end
249
+
250
+ ##
251
+ # Determines whether the batch is empty and ready to be culled.
252
+ #
253
+ def empty?
254
+ synchronize do
255
+ return false if @publishing || @canceled || @stopping
256
+
257
+ @items.empty? && @queue.empty?
258
+ end
259
+ end
260
+
261
+ protected
262
+
263
+ def items_add msg, callback
264
+ item = Item.new msg, callback
265
+ @items << item
266
+ @total_message_bytes += item.bytesize + 2
267
+ end
268
+
269
+ def try_add msg, callback
270
+ if @items.empty?
271
+ # Always add when empty, even if bytesize is bigger than total
272
+ items_add msg, callback
273
+ return true
274
+ end
275
+ new_message_count = total_message_count + 1
276
+ new_message_bytes = total_message_bytes + msg.to_proto.bytesize + 2
277
+ if new_message_count > @publisher.max_messages ||
278
+ new_message_bytes >= @publisher.max_bytes
279
+ return false
280
+ end
281
+ items_add msg, callback
282
+ true
283
+ end
284
+
285
+ def queue_add msg, callback
286
+ item = Item.new msg, callback
287
+ @queue << item
288
+ end
289
+
290
+ def total_message_count
291
+ @items.count
292
+ end
293
+
294
+ def total_message_bytes
295
+ @total_message_bytes
296
+ end
297
+
298
+ Item = Struct.new :msg, :callback do
299
+ def bytesize
300
+ msg.to_proto.bytesize
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+
307
+ Pubsub = PubSub unless const_defined? :Pubsub
308
+ end
309
+ end
@@ -84,14 +84,12 @@ module Google
84
84
  data = data.read
85
85
  end
86
86
  # Convert data to encoded byte array to match the protobuf defn
87
- data_bytes = \
88
- String(data).dup.force_encoding(Encoding::ASCII_8BIT).freeze
87
+ data_bytes = String(data).dup.force_encoding(Encoding::ASCII_8BIT).freeze
89
88
 
90
89
  # Convert attributes to strings to match the protobuf definition
91
90
  attributes = Hash[attributes.map { |k, v| [String(k), String(v)] }]
92
91
 
93
- Google::Cloud::PubSub::V1::PubsubMessage.new data: data_bytes,
94
- attributes: attributes
92
+ Google::Cloud::PubSub::V1::PubsubMessage.new data: data_bytes, attributes: attributes
95
93
  end
96
94
  end
97
95
  end
@@ -28,9 +28,7 @@ module Google
28
28
  # Force the object to be a Time object.
29
29
  time = time.to_time
30
30
 
31
- Google::Protobuf::Timestamp.new \
32
- seconds: time.to_i,
33
- nanos: time.nsec
31
+ Google::Protobuf::Timestamp.new seconds: time.to_i, nanos: time.nsec
34
32
  end
35
33
 
36
34
  def timestamp_to_time timestamp
@@ -42,9 +40,7 @@ module Google
42
40
  def number_to_duration number
43
41
  return nil if number.nil?
44
42
 
45
- Google::Protobuf::Duration.new \
46
- seconds: number.to_i,
47
- nanos: (number.remainder(1) * 1000000000).round
43
+ Google::Protobuf::Duration.new seconds: number.to_i, nanos: (number.remainder(1) * 1_000_000_000).round
48
44
  end
49
45
 
50
46
  def duration_to_number duration
@@ -52,7 +48,37 @@ module Google
52
48
 
53
49
  return duration.seconds if duration.nanos.zero?
54
50
 
55
- duration.seconds + (duration.nanos / 1000000000.0)
51
+ duration.seconds + (duration.nanos / 1_000_000_000.0)
52
+ end
53
+
54
+ def pubsub_message data, attributes, ordering_key, extra_attrs
55
+ # TODO: allow data to be a Message object,
56
+ # then ensure attributes and ordering_key are nil
57
+ if data.is_a?(::Hash) && (attributes.nil? || attributes.empty?)
58
+ attributes = data.merge extra_attrs
59
+ data = nil
60
+ else
61
+ attributes = Hash(attributes).merge extra_attrs
62
+ end
63
+ # Convert IO-ish objects to strings
64
+ if data.respond_to?(:read) && data.respond_to?(:rewind)
65
+ data.rewind
66
+ data = data.read
67
+ end
68
+ # Convert data to encoded byte array to match the protobuf defn
69
+ data_bytes = String(data).dup.force_encoding(Encoding::ASCII_8BIT).freeze
70
+
71
+ # Convert attributes to strings to match the protobuf definition
72
+ attributes = Hash[attributes.map { |k, v| [String(k), String(v)] }]
73
+
74
+ # Ordering Key must always be a string
75
+ ordering_key = String(ordering_key).freeze
76
+
77
+ Google::Cloud::PubSub::V1::PubsubMessage.new(
78
+ data: data_bytes,
79
+ attributes: attributes,
80
+ ordering_key: ordering_key
81
+ )
56
82
  end
57
83
  end
58
84
 
@@ -0,0 +1,85 @@
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 "google/cloud/errors"
17
+
18
+ module Google
19
+ module Cloud
20
+ module PubSub
21
+ ##
22
+ # Indicates that the {AsyncPublisher} has been stopped and cannot accept
23
+ # messages to publish.
24
+ #
25
+ class AsyncPublisherStopped < Google::Cloud::Error
26
+ def initialize message = "Can't publish when stopped."
27
+ super message
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Indicates that the {AsyncPublisher} has not been enabled to publish
33
+ # messages with an ordering key. Use
34
+ # {AsyncPublisher#enable_message_ordering!} to enable publishing ordered
35
+ # messages.
36
+ #
37
+ class OrderedMessagesDisabled < Google::Cloud::Error
38
+ def initialize message = "Ordered messages are disabled."
39
+ super message
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Indicates that the {Subscriber} for a {Subscription} with message
45
+ # ordering enabled has observed that a message has been delivered out of
46
+ # order.
47
+ #
48
+ class OrderedMessageDeliveryError < Google::Cloud::Error
49
+ attr_reader :ordered_message
50
+
51
+ def initialize ordered_message
52
+ @ordered_message = ordered_message
53
+
54
+ super "Ordered message delivered out of order."
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Indicates that messages using the {#ordering_key} are not being
60
+ # published due to error. Future calls to {Topic#publish_async} with the
61
+ # {#ordering_key} will fail with this error.
62
+ #
63
+ # To allow future messages with the {#ordering_key} to be published, the
64
+ # {#ordering_key} must be passed to {Topic#resume_publish}.
65
+ #
66
+ # If this error is retrieved from {PublishResult#error}, inspect `cause`
67
+ # for the error raised while publishing.
68
+ #
69
+ # @!attribute [r] ordering_key
70
+ # @return [String] The ordering key that is in a failed state.
71
+ #
72
+ class OrderingKeyError < Google::Cloud::Error
73
+ attr_reader :ordering_key
74
+
75
+ def initialize ordering_key
76
+ @ordering_key = ordering_key
77
+
78
+ super "Can't publish message using #{ordering_key}."
79
+ end
80
+ end
81
+ end
82
+
83
+ Pubsub = PubSub unless const_defined? :Pubsub
84
+ end
85
+ end