google-cloud-pubsub 1.0.2 → 1.1.0

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