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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae1f8eddfa934835b8fc01e2a85606f9c93f7b353ca5b8d3ba328af3fc5e3cdc
4
- data.tar.gz: 70d186ba6e4b8e8114ca6f2184d726acff47311f05906a51662227457c3ef6a1
3
+ metadata.gz: 5694017436fe99de18ba1d45e73ecdb0941b0183e115c3d02cad10085dcaae2c
4
+ data.tar.gz: b4a7a35193d6b9e75614474dc6510d1c978bbaa49fc2973123da33ea38b33116
5
5
  SHA512:
6
- metadata.gz: 0d199c7bda57c229e349ceeb21d38311c6388047ba728caaf8967e7cec0076a17af6dd55b1e0cbaad88a8a81ffdeed4814c367c617a25a643986b4a929a12cfe
7
- data.tar.gz: cbfc81b692b367245d4d9b94282833805ee706588d61d2277bbfcfb4fff74e8b17c4aeae246f50e7d0f26f3f07b1862be088c42e92baf8b25ec7a47eb9302b73
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
- # Now that the reception thread is stopped, immediately stop the
133
- # callback thread pool. All queued callbacks will see the stream
134
- # is stopped and perform a noop.
135
- @callback_thread_pool.shutdown
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
- # Wait for all queued callbacks to be processed.
162
- @callback_thread_pool.wait_for_termination timeout
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 stopped?
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 && running?
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
- wait_stop_buffer_thread!
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.map(&:wait!)
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
- def listen deadline: nil, message_ordering: nil, streams: nil, inventory: nil, threads: {}, &block
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, service: service
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
  ##
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module PubSub
19
- VERSION = "3.4.1".freeze
19
+ VERSION = "3.5.0".freeze
20
20
  end
21
21
 
22
22
  Pubsub = PubSub unless const_defined? :Pubsub
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-cloud-pubsub
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.1
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore