google-cloud-pubsub 3.4.1 → 3.5.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/google/cloud/pubsub/message_listener/inventory.rb +26 -1
- data/lib/google/cloud/pubsub/message_listener/stream.rb +52 -12
- data/lib/google/cloud/pubsub/message_listener.rb +29 -7
- data/lib/google/cloud/pubsub/subscriber.rb +51 -2
- data/lib/google/cloud/pubsub/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5694017436fe99de18ba1d45e73ecdb0941b0183e115c3d02cad10085dcaae2c
|
|
4
|
+
data.tar.gz: b4a7a35193d6b9e75614474dc6510d1c978bbaa49fc2973123da33ea38b33116
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f49b732799a033333beef767ac6667d603d066ff5bc5f7207b7c05ab127a4996e95f472a9377826ac2fc5898f6b31f710d2408473f611b1114815968edd91f95
|
|
7
|
+
data.tar.gz: 3cb7429db18f8d79c4ff6d1b0a1e488ebf090268fcb9e7ad35f19f4dced3e77c1616c89a8b6d070a9e9e4fc9fd57a3db0cdff2fb44cfc4f8432e57fe6c8719d2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Release History
|
|
2
2
|
|
|
3
|
+
### 3.5.0 (2026-09-14)
|
|
4
|
+
|
|
5
|
+
#### Features
|
|
6
|
+
|
|
7
|
+
* Support subscriber shutdown options ([#36480](https://github.com/googleapis/google-cloud-ruby/issues/36480))
|
|
8
|
+
#### Documentation
|
|
9
|
+
|
|
10
|
+
* Add subscriber shutdown documentation and examples ([#36482](https://github.com/googleapis/google-cloud-ruby/issues/36482))
|
|
11
|
+
|
|
3
12
|
### 3.4.1 (2026-08-24)
|
|
4
13
|
|
|
5
14
|
#### Bug Fixes
|
|
@@ -50,7 +50,7 @@ module Google
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def ack_ids
|
|
53
|
-
@inventory.keys
|
|
53
|
+
synchronize { @inventory.keys }
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def add *rec_msgs
|
|
@@ -109,6 +109,31 @@ module Google
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
##
|
|
113
|
+
# @private
|
|
114
|
+
# Blocks until the inventory is empty or until timeout expires.
|
|
115
|
+
#
|
|
116
|
+
# @param [Numeric, nil] timeout Maximum time in seconds to wait, or nil to wait indefinitely.
|
|
117
|
+
# @return [Boolean] true if inventory became empty, false if timed out.
|
|
118
|
+
def wait_until_empty timeout = nil
|
|
119
|
+
synchronize do
|
|
120
|
+
return true if @inventory.empty?
|
|
121
|
+
|
|
122
|
+
if timeout
|
|
123
|
+
target_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
124
|
+
while !@inventory.empty? && !@stopped
|
|
125
|
+
remaining = target_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
126
|
+
break if remaining <= 0
|
|
127
|
+
|
|
128
|
+
@wait_cond.wait remaining
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
@wait_cond.wait_while { !@inventory.empty? && !@stopped }
|
|
132
|
+
end
|
|
133
|
+
@inventory.empty?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
112
137
|
def start
|
|
113
138
|
@background_thread ||= Thread.new { background_run }
|
|
114
139
|
|
|
@@ -111,6 +111,11 @@ module Google
|
|
|
111
111
|
self
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
##
|
|
115
|
+
# @private
|
|
116
|
+
# Stops pulling messages from the subscription.
|
|
117
|
+
#
|
|
118
|
+
# @return [Stream] self for chaining.
|
|
114
119
|
def stop
|
|
115
120
|
synchronize do
|
|
116
121
|
break if @stopped
|
|
@@ -129,22 +134,45 @@ module Google
|
|
|
129
134
|
|
|
130
135
|
@keepalive_monitor.stop
|
|
131
136
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# Once all the callbacks are stopped, we can stop the inventory.
|
|
138
|
-
@inventory.stop
|
|
137
|
+
# When :nack_immediately, release all queued messages immediately and shut down callback pool.
|
|
138
|
+
if nack_immediately?
|
|
139
|
+
nack_unprocessed_messages!
|
|
140
|
+
@callback_thread_pool.shutdown
|
|
141
|
+
end
|
|
139
142
|
end
|
|
140
143
|
|
|
141
144
|
self
|
|
142
145
|
end
|
|
143
146
|
|
|
147
|
+
##
|
|
148
|
+
# @private
|
|
149
|
+
# Nacks all messages currently held in inventory.
|
|
150
|
+
def nack_unprocessed_messages!
|
|
151
|
+
synchronize do
|
|
152
|
+
ack_ids = @inventory.ack_ids
|
|
153
|
+
return if ack_ids.empty?
|
|
154
|
+
|
|
155
|
+
@subscriber.buffer.modify_ack_deadline 0, ack_ids
|
|
156
|
+
@subscriber.buffer.flush!
|
|
157
|
+
@inventory.remove ack_ids
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
144
161
|
def stopped?
|
|
145
162
|
synchronize { @stopped }
|
|
146
163
|
end
|
|
147
164
|
|
|
165
|
+
##
|
|
166
|
+
# @private
|
|
167
|
+
# Returns whether the subscriber is stopped and configured to nack unprocessed messages immediately.
|
|
168
|
+
#
|
|
169
|
+
# @return [Boolean]
|
|
170
|
+
def nack_immediately?
|
|
171
|
+
return false unless @stopped
|
|
172
|
+
|
|
173
|
+
@subscriber.shutdown_behavior == :nack_immediately
|
|
174
|
+
end
|
|
175
|
+
|
|
148
176
|
def stream_open?
|
|
149
177
|
synchronize { @stream_open }
|
|
150
178
|
end
|
|
@@ -157,14 +185,26 @@ module Google
|
|
|
157
185
|
!stopped?
|
|
158
186
|
end
|
|
159
187
|
|
|
188
|
+
##
|
|
189
|
+
# @private
|
|
190
|
+
# Blocks until all received messages are processed, or until timeout expires.
|
|
191
|
+
#
|
|
192
|
+
# @param [Numeric, nil] timeout The maximum seconds to wait, or nil to wait indefinitely.
|
|
193
|
+
# @return [Stream] self for chaining.
|
|
160
194
|
def wait! timeout = nil
|
|
161
|
-
|
|
162
|
-
@
|
|
195
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
196
|
+
emptied = @inventory.wait_until_empty timeout
|
|
197
|
+
nack_unprocessed_messages! unless emptied
|
|
163
198
|
|
|
199
|
+
@callback_thread_pool.shutdown
|
|
200
|
+
pool_timeout = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max if deadline
|
|
201
|
+
@callback_thread_pool.wait_for_termination pool_timeout
|
|
202
|
+
|
|
203
|
+
# Once all callbacks are finished and inventory is clear, stop the inventory.
|
|
204
|
+
@inventory.stop
|
|
164
205
|
self
|
|
165
206
|
end
|
|
166
207
|
|
|
167
|
-
|
|
168
208
|
def request_queue_active?
|
|
169
209
|
!@request_queue.nil?
|
|
170
210
|
end
|
|
@@ -466,7 +506,7 @@ module Google
|
|
|
466
506
|
subscriber.service.internal_logger.log :info, "callback-delivery" do
|
|
467
507
|
"message (ID #{rec_msg.message_id}, ackID #{rec_msg.ack_id}) delivery to user callbacks"
|
|
468
508
|
end
|
|
469
|
-
@subscriber.callback.call rec_msg unless
|
|
509
|
+
@subscriber.callback.call rec_msg unless nack_immediately?
|
|
470
510
|
rescue StandardError => e
|
|
471
511
|
subscriber.service.internal_logger.log :info, "callback-exceptions" do
|
|
472
512
|
"message (ID #{rec_msg.message_id}, ackID #{rec_msg.ack_id}) caused a user callback exception: " \
|
|
@@ -475,7 +515,7 @@ module Google
|
|
|
475
515
|
@subscriber.error! e
|
|
476
516
|
ensure
|
|
477
517
|
release rec_msg
|
|
478
|
-
if @sequencer &&
|
|
518
|
+
if @sequencer && !nack_immediately?
|
|
479
519
|
begin
|
|
480
520
|
@sequencer.next rec_msg
|
|
481
521
|
rescue OrderedMessageDeliveryError => e
|
|
@@ -60,6 +60,12 @@ module Google
|
|
|
60
60
|
# acknowledgement ({ReceivedMessage#ack!}) and delay messages
|
|
61
61
|
# ({ReceivedMessage#nack!}, {ReceivedMessage#modify_ack_deadline!}).
|
|
62
62
|
# Default is 4.
|
|
63
|
+
# @attr_reader [Symbol] shutdown_behavior The strategy used to handle
|
|
64
|
+
# unprocessed messages when stopping the subscriber (`:wait_for_processing`
|
|
65
|
+
# or `:nack_immediately`). Default is `:wait_for_processing`.
|
|
66
|
+
# @attr_reader [Numeric, nil] shutdown_timeout The maximum number of
|
|
67
|
+
# seconds to wait during shutdown before forcing remaining messages
|
|
68
|
+
# to be nacked. Default is `nil`.
|
|
63
69
|
#
|
|
64
70
|
class MessageListener
|
|
65
71
|
include MonitorMixin
|
|
@@ -71,6 +77,8 @@ module Google
|
|
|
71
77
|
attr_reader :message_ordering
|
|
72
78
|
attr_reader :callback_threads
|
|
73
79
|
attr_reader :push_threads
|
|
80
|
+
attr_reader :shutdown_behavior
|
|
81
|
+
attr_reader :shutdown_timeout
|
|
74
82
|
|
|
75
83
|
##
|
|
76
84
|
# @private Implementation attributes.
|
|
@@ -83,7 +91,7 @@ module Google
|
|
|
83
91
|
##
|
|
84
92
|
# @private Create an empty {MessageListener} object.
|
|
85
93
|
def initialize subscription_name, callback, deadline: nil, message_ordering: nil, streams: nil, inventory: nil,
|
|
86
|
-
threads: {}, service: nil
|
|
94
|
+
threads: {}, shutdown_behavior: :wait_for_processing, shutdown_timeout: nil, service: nil
|
|
87
95
|
super() # to init MonitorMixin
|
|
88
96
|
|
|
89
97
|
@callback = callback
|
|
@@ -91,6 +99,16 @@ module Google
|
|
|
91
99
|
@subscription_name = subscription_name
|
|
92
100
|
@deadline = deadline || 60
|
|
93
101
|
@streams = streams || 1
|
|
102
|
+
@shutdown_behavior = shutdown_behavior || :wait_for_processing
|
|
103
|
+
@shutdown_timeout = shutdown_timeout
|
|
104
|
+
|
|
105
|
+
unless [:wait_for_processing, :nack_immediately].include? @shutdown_behavior
|
|
106
|
+
raise ArgumentError, "Invalid shutdown_behavior: #{@shutdown_behavior.inspect}"
|
|
107
|
+
end
|
|
108
|
+
if @shutdown_timeout && (!@shutdown_timeout.is_a?(Numeric) || @shutdown_timeout.negative?)
|
|
109
|
+
raise ArgumentError, "Invalid shutdown_timeout: #{@shutdown_timeout.inspect}"
|
|
110
|
+
end
|
|
111
|
+
|
|
94
112
|
coerce_inventory inventory
|
|
95
113
|
@message_ordering = message_ordering
|
|
96
114
|
@callback_threads = Integer(threads[:callback] || 8)
|
|
@@ -145,7 +163,7 @@ module Google
|
|
|
145
163
|
@started = false
|
|
146
164
|
@stopped = true
|
|
147
165
|
@stream_pool.map(&:stop)
|
|
148
|
-
wait_stop_buffer_thread!
|
|
166
|
+
wait_stop_buffer_thread! @shutdown_timeout
|
|
149
167
|
self
|
|
150
168
|
end
|
|
151
169
|
end
|
|
@@ -160,12 +178,14 @@ module Google
|
|
|
160
178
|
# stopped.
|
|
161
179
|
#
|
|
162
180
|
# @param [Number, nil] timeout The number of seconds to block until the
|
|
163
|
-
# subscriber is fully stopped. Default will block indefinitely
|
|
181
|
+
# subscriber is fully stopped. Default will block indefinitely or use
|
|
182
|
+
# configured `shutdown_timeout`.
|
|
164
183
|
#
|
|
165
184
|
# @return [MessageListener] returns self so calls can be chained.
|
|
166
185
|
#
|
|
167
186
|
def wait! timeout = nil
|
|
168
|
-
|
|
187
|
+
timeout ||= @shutdown_timeout
|
|
188
|
+
wait_stop_buffer_thread! timeout
|
|
169
189
|
@wait_stop_buffer_thread.join timeout
|
|
170
190
|
self
|
|
171
191
|
end
|
|
@@ -178,11 +198,13 @@ module Google
|
|
|
178
198
|
# The same as calling {#stop} and {#wait!}.
|
|
179
199
|
#
|
|
180
200
|
# @param [Number, nil] timeout The number of seconds to block until the
|
|
181
|
-
# listener is fully stopped. Default will block indefinitely
|
|
201
|
+
# listener is fully stopped. Default will block indefinitely or use
|
|
202
|
+
# configured `shutdown_timeout`.
|
|
182
203
|
#
|
|
183
204
|
# @return [MessageListener] returns self so calls can be chained.
|
|
184
205
|
#
|
|
185
206
|
def stop! timeout = nil
|
|
207
|
+
timeout ||= @shutdown_timeout
|
|
186
208
|
stop
|
|
187
209
|
wait! timeout
|
|
188
210
|
end
|
|
@@ -365,10 +387,10 @@ module Google
|
|
|
365
387
|
|
|
366
388
|
##
|
|
367
389
|
# Starts a new thread to call wait! (blocking) on each Stream and then stop the TimedUnaryBuffer.
|
|
368
|
-
def wait_stop_buffer_thread!
|
|
390
|
+
def wait_stop_buffer_thread! timeout = nil
|
|
369
391
|
synchronize do
|
|
370
392
|
@wait_stop_buffer_thread ||= Thread.new do
|
|
371
|
-
@stream_pool.
|
|
393
|
+
@stream_pool.each { |s| s.wait! timeout }
|
|
372
394
|
# Shutdown the buffer TimerTask (and flush the buffer) after the streams are all stopped.
|
|
373
395
|
@buffer.stop
|
|
374
396
|
end
|
|
@@ -324,6 +324,16 @@ module Google
|
|
|
324
324
|
# acknowledgement ({ReceivedMessage#ack!}) and modify ack deadline
|
|
325
325
|
# messages ({ReceivedMessage#nack!},
|
|
326
326
|
# {ReceivedMessage#modify_ack_deadline!}). Default is 4.
|
|
327
|
+
# @param [Symbol] shutdown_behavior The strategy used to handle
|
|
328
|
+
# unprocessed messages when stopping the subscriber.
|
|
329
|
+
# Supported values:
|
|
330
|
+
# * `:wait_for_processing` (default) - Waits for in-flight callbacks
|
|
331
|
+
# to complete and acknowledge or nack messages.
|
|
332
|
+
# * `:nack_immediately` - Immediately nacks all unprocessed messages
|
|
333
|
+
# back to Pub/Sub for rapid redelivery to other subscribers.
|
|
334
|
+
# @param [Numeric, nil] shutdown_timeout The maximum number of seconds
|
|
335
|
+
# to wait during shutdown before forcing remaining in-flight messages
|
|
336
|
+
# to be nacked. Default is `nil` (blocks indefinitely until callbacks finish).
|
|
327
337
|
#
|
|
328
338
|
# @yield [received_message] a block for processing new messages
|
|
329
339
|
# @yieldparam [ReceivedMessage] received_message the newly received
|
|
@@ -408,13 +418,52 @@ module Google
|
|
|
408
418
|
# # Shut down the subscriber when ready to stop receiving messages.
|
|
409
419
|
# listener.stop!
|
|
410
420
|
#
|
|
411
|
-
|
|
421
|
+
# @example Immediately release unprocessed messages on shutdown:
|
|
422
|
+
# require "google/cloud/pubsub"
|
|
423
|
+
#
|
|
424
|
+
# pubsub = Google::Cloud::PubSub.new
|
|
425
|
+
#
|
|
426
|
+
# subscriber = pubsub.subscriber "my-topic-sub"
|
|
427
|
+
#
|
|
428
|
+
# listener = subscriber.listen shutdown_behavior: :nack_immediately do |received_message|
|
|
429
|
+
# # process message
|
|
430
|
+
# puts "Data: #{received_message.message.data}"
|
|
431
|
+
# received_message.acknowledge!
|
|
432
|
+
# end
|
|
433
|
+
#
|
|
434
|
+
# listener.start
|
|
435
|
+
#
|
|
436
|
+
# # Shut down immediately, releasing (nacking) any unprocessed messages
|
|
437
|
+
# listener.stop!
|
|
438
|
+
#
|
|
439
|
+
# @example Gracefully shut down with a custom timeout:
|
|
440
|
+
# require "google/cloud/pubsub"
|
|
441
|
+
#
|
|
442
|
+
# pubsub = Google::Cloud::PubSub.new
|
|
443
|
+
#
|
|
444
|
+
# subscriber = pubsub.subscriber "my-topic-sub"
|
|
445
|
+
#
|
|
446
|
+
# listener = subscriber.listen shutdown_behavior: :wait_for_processing, shutdown_timeout: 30 do |received_message|
|
|
447
|
+
# # process message
|
|
448
|
+
# puts "Data: #{received_message.message.data}"
|
|
449
|
+
# received_message.acknowledge!
|
|
450
|
+
# end
|
|
451
|
+
#
|
|
452
|
+
# listener.start
|
|
453
|
+
#
|
|
454
|
+
# # Wait up to 30 seconds for in-flight callbacks before forcing remaining to nack
|
|
455
|
+
# listener.stop!
|
|
456
|
+
#
|
|
457
|
+
def listen deadline: nil, message_ordering: nil, streams: nil, inventory: nil, threads: {},
|
|
458
|
+
shutdown_behavior: :wait_for_processing, shutdown_timeout: nil, &block
|
|
412
459
|
ensure_service!
|
|
413
460
|
deadline ||= self.deadline
|
|
414
461
|
message_ordering = message_ordering? if message_ordering.nil?
|
|
415
462
|
|
|
416
463
|
MessageListener.new name, block, deadline: deadline, streams: streams, inventory: inventory,
|
|
417
|
-
message_ordering: message_ordering, threads: threads,
|
|
464
|
+
message_ordering: message_ordering, threads: threads,
|
|
465
|
+
shutdown_behavior: shutdown_behavior, shutdown_timeout: shutdown_timeout,
|
|
466
|
+
service: service
|
|
418
467
|
end
|
|
419
468
|
|
|
420
469
|
##
|