google-cloud-pubsub 3.3.0 → 3.4.1
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 +12 -0
- data/lib/google/cloud/pubsub/async_publisher.rb +1 -1
- data/lib/google/cloud/pubsub/batch_publisher.rb +1 -1
- data/lib/google/cloud/pubsub/internal_logger.rb +1 -1
- data/lib/google/cloud/pubsub/message_listener/inventory.rb +1 -1
- data/lib/google/cloud/pubsub/message_listener/keepalive_monitor.rb +159 -0
- data/lib/google/cloud/pubsub/message_listener/stream.rb +131 -30
- data/lib/google/cloud/pubsub/message_listener/timed_unary_buffer.rb +6 -2
- data/lib/google/cloud/pubsub/service.rb +8 -4
- data/lib/google/cloud/pubsub/version.rb +1 -1
- data/lib/google/cloud/pubsub.rb +0 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae1f8eddfa934835b8fc01e2a85606f9c93f7b353ca5b8d3ba328af3fc5e3cdc
|
|
4
|
+
data.tar.gz: 70d186ba6e4b8e8114ca6f2184d726acff47311f05906a51662227457c3ef6a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d199c7bda57c229e349ceeb21d38311c6388047ba728caaf8967e7cec0076a17af6dd55b1e0cbaad88a8a81ffdeed4814c367c617a25a643986b4a929a12cfe
|
|
7
|
+
data.tar.gz: cbfc81b692b367245d4d9b94282833805ee706588d61d2277bbfcfb4fff74e8b17c4aeae246f50e7d0f26f3f07b1862be088c42e92baf8b25ec7a47eb9302b73
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Release History
|
|
2
2
|
|
|
3
|
+
### 3.4.1 (2026-08-24)
|
|
4
|
+
|
|
5
|
+
#### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* Propagate logger to v1 admin and schema clients ([#32440](https://github.com/googleapis/google-cloud-ruby/issues/32440)) ([#36298](https://github.com/googleapis/google-cloud-ruby/issues/36298))
|
|
8
|
+
|
|
9
|
+
### 3.4.0 (2026-07-29)
|
|
10
|
+
|
|
11
|
+
#### Features
|
|
12
|
+
|
|
13
|
+
* implement streaming keep-alive logic ([#34653](https://github.com/googleapis/google-cloud-ruby/issues/34653))
|
|
14
|
+
|
|
3
15
|
### 3.3.0 (2026-06-11)
|
|
4
16
|
|
|
5
17
|
#### Features
|
|
@@ -379,7 +379,7 @@ module Google
|
|
|
379
379
|
grpc = @service.publish topic_name,
|
|
380
380
|
items.map(&:msg),
|
|
381
381
|
compress: compress && batch.total_message_bytes >= compression_bytes_threshold
|
|
382
|
-
service.
|
|
382
|
+
service.internal_logger.log_batch "publish-batch", reason, "publish", items.count, items.sum(&:bytesize)
|
|
383
383
|
items.zip Array(grpc.message_ids) do |item, id|
|
|
384
384
|
@flow_controller.release item.bytesize
|
|
385
385
|
next unless item.callback
|
|
@@ -122,7 +122,7 @@ module Google
|
|
|
122
122
|
grpc = service.publish topic_name,
|
|
123
123
|
messages,
|
|
124
124
|
compress: compress && total_message_bytes >= compression_bytes_threshold
|
|
125
|
-
service.
|
|
125
|
+
service.internal_logger.log_batch "publish-batch", reason, "publish", messages.count, @total_message_bytes
|
|
126
126
|
to_gcloud_messages Array(grpc.message_ids)
|
|
127
127
|
end
|
|
128
128
|
end
|
|
@@ -31,7 +31,7 @@ module Google
|
|
|
31
31
|
def log level, subtag, &message_block
|
|
32
32
|
return unless VALID_LOG_LEVELS.include?(level) && block_given?
|
|
33
33
|
# Only log if the logger is explicitly tagged for 'pubsub'.
|
|
34
|
-
return unless @logger && @logger.progname == LOG_NAME
|
|
34
|
+
return unless @logger.respond_to?(:progname) && @logger.progname == LOG_NAME
|
|
35
35
|
|
|
36
36
|
@logger.public_send(level, "#{LOG_NAME}:#{subtag}", &message_block)
|
|
37
37
|
end
|
|
@@ -86,7 +86,7 @@ module Google
|
|
|
86
86
|
extension_time = Time.new - extension
|
|
87
87
|
expired, keep = @inventory.partition { |_ack_id, item| item.pulled_at < extension_time }
|
|
88
88
|
@inventory = keep.to_h
|
|
89
|
-
stream.subscriber.service.
|
|
89
|
+
stream.subscriber.service.internal_logger.log_expiry expired
|
|
90
90
|
@wait_cond.broadcast
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright 2026 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
|
+
require "concurrent"
|
|
16
|
+
|
|
17
|
+
module Google
|
|
18
|
+
module Cloud
|
|
19
|
+
module PubSub
|
|
20
|
+
class MessageListener
|
|
21
|
+
##
|
|
22
|
+
# @private
|
|
23
|
+
# Monitors gRPC streaming connection health via periodic bi-directional keep-alive pings and pongs.
|
|
24
|
+
#
|
|
25
|
+
# Tracks ping and pong timestamps over the active gRPC stream to detect silent connection drops or freezes,
|
|
26
|
+
# and triggers a stream restart when server responses exceed the configured pong deadline.
|
|
27
|
+
# This prevents firewalls and load balancers from dropping idle connections during periods of low
|
|
28
|
+
# message publisher volume, which minimizes message delivery latency by maintaining a healthy lease.
|
|
29
|
+
#
|
|
30
|
+
# For users, this ensures highly reliable, resilient, and responsive message delivery operations.
|
|
31
|
+
# It directly prevents issues where Google Cloud Pub/Sub subscribers may unexpectedly stall
|
|
32
|
+
# or stop receiving messages for extended periods due to unnotified intermediate network disruptions
|
|
33
|
+
# (e.g., TCP halfway drops). By proactively maintaining the connection's liveness, subscribers
|
|
34
|
+
# avoid sudden latency spikes and lost leases.
|
|
35
|
+
class KeepaliveMonitor
|
|
36
|
+
# Default interval in seconds between keep-alive ping requests.
|
|
37
|
+
DEFAULT_INTERVAL = 30.0
|
|
38
|
+
# Default deadline in seconds to receive a keep-alive pong response.
|
|
39
|
+
DEFAULT_DEADLINE = 15.0
|
|
40
|
+
# Divisor applied to keep-alive interval to calculate monitor polling interval (1/5th of interval).
|
|
41
|
+
MONITOR_DIVISOR = 5.0
|
|
42
|
+
# Minimum floor in seconds (10ms) for the monitor polling interval to prevent CPU spinning.
|
|
43
|
+
MIN_MONITOR_INTERVAL = 0.01
|
|
44
|
+
|
|
45
|
+
# Initializes the KeepaliveMonitor.
|
|
46
|
+
#
|
|
47
|
+
# @param [Stream] stream The parent streaming pull connection to monitor.
|
|
48
|
+
# @param [Float] interval The interval in seconds between keep-alive pings.
|
|
49
|
+
# @param [Float] deadline The deadline in seconds for receiving a pong.
|
|
50
|
+
def initialize stream, interval: DEFAULT_INTERVAL, deadline: DEFAULT_DEADLINE
|
|
51
|
+
@stream = stream
|
|
52
|
+
@interval = interval
|
|
53
|
+
@deadline = deadline
|
|
54
|
+
@last_ping_at = nil
|
|
55
|
+
@last_pong_at = nil
|
|
56
|
+
@ping_task = nil
|
|
57
|
+
@monitor_task = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Starts the background keep-alive ping and liveness monitor timer tasks.
|
|
61
|
+
#
|
|
62
|
+
# @return [KeepaliveMonitor] self for chaining.
|
|
63
|
+
def start
|
|
64
|
+
@stream.synchronize do
|
|
65
|
+
return self if @ping_task
|
|
66
|
+
|
|
67
|
+
@ping_task = Concurrent::TimerTask.new(execution_interval: [@interval, MIN_MONITOR_INTERVAL].max) do
|
|
68
|
+
send_ping!
|
|
69
|
+
end
|
|
70
|
+
@ping_task.execute
|
|
71
|
+
|
|
72
|
+
@monitor_task = Concurrent::TimerTask.new(
|
|
73
|
+
execution_interval: [@interval / MONITOR_DIVISOR, MIN_MONITOR_INTERVAL].max
|
|
74
|
+
) do
|
|
75
|
+
check_liveness!
|
|
76
|
+
end
|
|
77
|
+
@monitor_task.execute
|
|
78
|
+
end
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Shuts down and cleans up the background keep-alive and monitor timer tasks.
|
|
83
|
+
#
|
|
84
|
+
# @return [KeepaliveMonitor] self for chaining.
|
|
85
|
+
def stop
|
|
86
|
+
@stream.synchronize do
|
|
87
|
+
@ping_task&.shutdown
|
|
88
|
+
@ping_task = nil
|
|
89
|
+
@monitor_task&.shutdown
|
|
90
|
+
@monitor_task = nil
|
|
91
|
+
end
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Records the initial stream handshake by setting ping and pong timestamps to the current monotonic time.
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
def record_handshake!
|
|
99
|
+
@stream.synchronize do
|
|
100
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
101
|
+
@last_ping_at = now
|
|
102
|
+
@last_pong_at = now
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Records a received pong by updating the last pong timestamp to the current monotonic time.
|
|
107
|
+
#
|
|
108
|
+
# @return [void]
|
|
109
|
+
def record_pong!
|
|
110
|
+
@stream.synchronize do
|
|
111
|
+
@stream.log_info "received keepAlive pong from stream for subscription #{@stream.subscriber.subscription_name}"
|
|
112
|
+
@last_pong_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Evaluates stream state and sends a bi-directional keep-alive ping if appropriate.
|
|
117
|
+
#
|
|
118
|
+
# @return [void]
|
|
119
|
+
def send_ping!
|
|
120
|
+
@stream.synchronize do
|
|
121
|
+
# Push unconditional keep-alive pings only when the stream is actively open and request queue is active.
|
|
122
|
+
# Note: ACKs are sent via unary RPCs (TimedUnaryBuffer), not over this stream.
|
|
123
|
+
return unless @stream.stream_open? && !@stream.stopped? && @stream.request_queue_active?
|
|
124
|
+
|
|
125
|
+
@stream.log_info "sending keepAlive to stream for subscription #{@stream.subscriber.subscription_name}"
|
|
126
|
+
# Only advance @last_ping_at if the previous ping was successfully ponged.
|
|
127
|
+
# If a pong is outstanding (@last_pong_at < @last_ping_at), freezing @last_ping_at preserves the start time
|
|
128
|
+
# of the un-ponged cycle so check_liveness! can accurately evaluate the elapsed deadline.
|
|
129
|
+
@last_ping_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) if @last_pong_at.nil? || @last_ping_at.nil? || @last_pong_at >= @last_ping_at
|
|
130
|
+
@stream.send_ping_request!
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Checks whether a pong response has been received within the configured deadline,
|
|
135
|
+
# and triggers a stream restart if the deadline has been exceeded.
|
|
136
|
+
#
|
|
137
|
+
# @return [void]
|
|
138
|
+
def check_liveness!
|
|
139
|
+
@stream.synchronize do
|
|
140
|
+
# Do not check pong deadline if paused (client flow control inventory full).
|
|
141
|
+
# When paused, background_run waits on condition variable and stops calling enum.next,
|
|
142
|
+
# so incoming server pongs sit buffered in gRPC and last_pong_at stays un-updated.
|
|
143
|
+
return unless @stream.stream_open? && @last_ping_at && @last_pong_at && !@stream.stopped? && !@stream.paused?
|
|
144
|
+
|
|
145
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
if now - @last_ping_at >= @deadline && @last_pong_at < @last_ping_at
|
|
147
|
+
elapsed_pong = (now - @last_pong_at).round(2)
|
|
148
|
+
@stream.log_error "Keep-alive pong not received within #{@deadline}s (last pong #{elapsed_pong}s ago); restarting stream."
|
|
149
|
+
@stream.restart_stream_for_timeout!
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Pubsub = PubSub unless const_defined? :Pubsub
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
require "google/cloud/pubsub/message_listener/sequencer"
|
|
16
16
|
require "google/cloud/pubsub/message_listener/enumerator_queue"
|
|
17
17
|
require "google/cloud/pubsub/message_listener/inventory"
|
|
18
|
+
require "google/cloud/pubsub/message_listener/keepalive_monitor"
|
|
18
19
|
require "google/cloud/pubsub/service"
|
|
19
20
|
require "google/cloud/errors"
|
|
20
21
|
require "monitor"
|
|
@@ -49,6 +50,25 @@ module Google
|
|
|
49
50
|
# @private exactly_once_delivery_enabled.
|
|
50
51
|
attr_reader :exactly_once_delivery_enabled
|
|
51
52
|
|
|
53
|
+
##
|
|
54
|
+
# @private KeepaliveMonitor.
|
|
55
|
+
attr_reader :keepalive_monitor
|
|
56
|
+
|
|
57
|
+
# Initial backoff delay in seconds when reconnecting after a transient stream disconnection.
|
|
58
|
+
INITIAL_RECONNECT_DELAY = 1.0
|
|
59
|
+
|
|
60
|
+
# Maximum backoff delay in seconds for stream reconnection attempts.
|
|
61
|
+
MAX_RECONNECT_DELAY = 60.0
|
|
62
|
+
|
|
63
|
+
# Exponential backoff multiplier applied to reconnect delay on successive retry attempts.
|
|
64
|
+
RECONNECT_BACKOFF_MULTIPLIER = 1.5
|
|
65
|
+
|
|
66
|
+
# Inventory capacity ratio (80%) below which a flow-control paused stream will unpause.
|
|
67
|
+
UNPAUSE_INVENTORY_RATIO = 0.8
|
|
68
|
+
|
|
69
|
+
# The keep-alive streaming pull protocol version sent in the initial request during handshake.
|
|
70
|
+
PROTOCOL_VERSION = 1
|
|
71
|
+
|
|
52
72
|
##
|
|
53
73
|
# @private Create an empty Subscriber::Stream object.
|
|
54
74
|
def initialize subscriber
|
|
@@ -59,7 +79,12 @@ module Google
|
|
|
59
79
|
@request_queue = nil
|
|
60
80
|
@stopped = nil
|
|
61
81
|
@paused = nil
|
|
82
|
+
# Condition variable used to cooperatively pause the background thread during flow control
|
|
83
|
+
# Governed by the Stream's MonitorMixin `synchronize` lock.
|
|
62
84
|
@pause_cond = new_cond
|
|
85
|
+
# Condition variable used to cooperatively sleep the background thread during connection retries
|
|
86
|
+
# Governed by the Stream's MonitorMixin `synchronize` lock.
|
|
87
|
+
@backoff_cond = new_cond
|
|
63
88
|
@exactly_once_delivery_enabled = false
|
|
64
89
|
|
|
65
90
|
@inventory = Inventory.new self, **@subscriber.stream_inventory
|
|
@@ -68,17 +93,9 @@ module Google
|
|
|
68
93
|
|
|
69
94
|
@callback_thread_pool = Concurrent::ThreadPoolExecutor.new max_threads: @subscriber.callback_threads
|
|
70
95
|
|
|
71
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# push empty request every 30 seconds to keep stream alive
|
|
75
|
-
unless inventory.empty?
|
|
76
|
-
subscriber.service.logger.log :info, "subscriber-streams" do
|
|
77
|
-
"sending keepAlive to stream for subscription #{@subscriber.subscription_name}"
|
|
78
|
-
end
|
|
79
|
-
push Google::Cloud::PubSub::V1::StreamingPullRequest.new
|
|
80
|
-
end
|
|
81
|
-
end.execute
|
|
96
|
+
@keepalive_monitor = KeepaliveMonitor.new self
|
|
97
|
+
@stream_open = false
|
|
98
|
+
@reconnect_delay = nil
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
def start
|
|
@@ -86,6 +103,7 @@ module Google
|
|
|
86
103
|
break if @background_thread
|
|
87
104
|
|
|
88
105
|
@inventory.start
|
|
106
|
+
@keepalive_monitor.start
|
|
89
107
|
|
|
90
108
|
start_streaming!
|
|
91
109
|
end
|
|
@@ -97,7 +115,7 @@ module Google
|
|
|
97
115
|
synchronize do
|
|
98
116
|
break if @stopped
|
|
99
117
|
|
|
100
|
-
subscriber.service.
|
|
118
|
+
subscriber.service.internal_logger.log :info, "subscriber-streams" do
|
|
101
119
|
"stopping stream for subscription #{@subscriber.subscription_name}"
|
|
102
120
|
end
|
|
103
121
|
# Close the stream by pushing the sentinel value.
|
|
@@ -107,6 +125,9 @@ module Google
|
|
|
107
125
|
# Signal to the background thread that we are stopped.
|
|
108
126
|
@stopped = true
|
|
109
127
|
@pause_cond.broadcast
|
|
128
|
+
@backoff_cond.broadcast
|
|
129
|
+
|
|
130
|
+
@keepalive_monitor.stop
|
|
110
131
|
|
|
111
132
|
# Now that the reception thread is stopped, immediately stop the
|
|
112
133
|
# callback thread pool. All queued callbacks will see the stream
|
|
@@ -124,6 +145,10 @@ module Google
|
|
|
124
145
|
synchronize { @stopped }
|
|
125
146
|
end
|
|
126
147
|
|
|
148
|
+
def stream_open?
|
|
149
|
+
synchronize { @stream_open }
|
|
150
|
+
end
|
|
151
|
+
|
|
127
152
|
def paused?
|
|
128
153
|
synchronize { @paused }
|
|
129
154
|
end
|
|
@@ -139,6 +164,38 @@ module Google
|
|
|
139
164
|
self
|
|
140
165
|
end
|
|
141
166
|
|
|
167
|
+
|
|
168
|
+
def request_queue_active?
|
|
169
|
+
!@request_queue.nil?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def send_ping_request!
|
|
173
|
+
push Google::Cloud::PubSub::V1::StreamingPullRequest.new
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def restart_stream_for_timeout!
|
|
177
|
+
synchronize do
|
|
178
|
+
@stream_open = false
|
|
179
|
+
# Push self as a stream-closing sentinel to @request_queue.
|
|
180
|
+
# When EnumeratorQueue#each pops the sentinel object, it terminates the request enumerator,
|
|
181
|
+
# cleanly sending an HTTP/2 END_STREAM flag and unblocking the write-side gRPC C-core pipeline.
|
|
182
|
+
@request_queue&.push self
|
|
183
|
+
@background_thread&.raise RestartStream
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def log_info msg
|
|
188
|
+
subscriber.service.internal_logger.log :info, "subscriber-streams" do
|
|
189
|
+
msg
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def log_error msg
|
|
194
|
+
subscriber.service.internal_logger.log :error, "subscriber-streams" do
|
|
195
|
+
msg
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
142
199
|
##
|
|
143
200
|
# @private
|
|
144
201
|
def acknowledge *messages, &callback
|
|
@@ -219,11 +276,20 @@ module Google
|
|
|
219
276
|
|
|
220
277
|
# rubocop:disable all
|
|
221
278
|
|
|
279
|
+
def backoff_and_wait!
|
|
280
|
+
@reconnect_delay = @reconnect_delay ? [@reconnect_delay * RECONNECT_BACKOFF_MULTIPLIER, MAX_RECONNECT_DELAY].min : INITIAL_RECONNECT_DELAY
|
|
281
|
+
synchronize do
|
|
282
|
+
# Disable liveness checker during backoff sleep to prevent monitor task from interrupting and double-backing off
|
|
283
|
+
@stream_open = false
|
|
284
|
+
@backoff_cond.wait(@reconnect_delay + rand(0.0..0.5)) unless @stopped
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
222
288
|
def background_run
|
|
223
289
|
synchronize do
|
|
224
290
|
# Don't allow a stream to restart if already stopped
|
|
225
291
|
if @stopped
|
|
226
|
-
subscriber.service.
|
|
292
|
+
subscriber.service.internal_logger.log :debug, "subscriber-streams" do
|
|
227
293
|
"not filling stream for subscription #{@subscriber.subscription_name} because stream is already" \
|
|
228
294
|
" stopped"
|
|
229
295
|
end
|
|
@@ -240,16 +306,32 @@ module Google
|
|
|
240
306
|
# Always create a new request queue
|
|
241
307
|
@request_queue = EnumeratorQueue.new self
|
|
242
308
|
@request_queue.push initial_input_request
|
|
243
|
-
old_queue.each
|
|
309
|
+
old_queue.each do |obj|
|
|
310
|
+
@request_queue.push obj unless obj == self
|
|
311
|
+
end
|
|
244
312
|
end
|
|
245
313
|
|
|
246
314
|
# Call the StreamingPull API to get the response enumerator
|
|
247
315
|
options = { :"metadata" => { :"x-goog-request-params" => @subscriber.subscription_name } }
|
|
316
|
+
# Temporarily disable the liveness monitor while establishing the gRPC connection
|
|
317
|
+
# to prevent check_liveness! from evaluating stale timestamps during the handshake.
|
|
318
|
+
synchronize do
|
|
319
|
+
@stream_open = false
|
|
320
|
+
end
|
|
248
321
|
enum = @subscriber.service.streaming_pull @request_queue.each, options
|
|
249
|
-
subscriber.service.
|
|
322
|
+
subscriber.service.internal_logger.log :info, "subscriber-streams" do
|
|
250
323
|
"rpc: streamingPull, subscription: #{@subscriber.subscription_name}, stream opened"
|
|
251
324
|
end
|
|
252
325
|
|
|
326
|
+
# Once the stream handshake completes, initialize ping/pong timestamps to monotonic now
|
|
327
|
+
# and mark @stream_open = true under synchronization.
|
|
328
|
+
# This ensures @keepalive_monitor evaluates liveness against a fresh window and prevents
|
|
329
|
+
# false-positive disconnect detections from stale timestamps prior to reconnecting.
|
|
330
|
+
synchronize do
|
|
331
|
+
@keepalive_monitor.record_handshake!
|
|
332
|
+
@stream_open = true
|
|
333
|
+
end
|
|
334
|
+
|
|
253
335
|
loop do
|
|
254
336
|
synchronize do
|
|
255
337
|
if @paused && !@stopped
|
|
@@ -264,8 +346,17 @@ module Google
|
|
|
264
346
|
begin
|
|
265
347
|
# Cannot synchronize the enumerator, causes deadlock
|
|
266
348
|
response = enum.next
|
|
267
|
-
|
|
349
|
+
synchronize do
|
|
350
|
+
@keepalive_monitor.record_pong!
|
|
351
|
+
# Reset backoff delay only after successfully reading a frame from enum.next.
|
|
352
|
+
# If the connection drops immediately upon reading, @reconnect_delay is preserved.
|
|
353
|
+
@reconnect_delay = nil
|
|
354
|
+
end
|
|
268
355
|
received_messages = response.received_messages
|
|
356
|
+
# Skip processing properties and inventory on Pong frames (empty received_messages).
|
|
357
|
+
# Subscription properties on keep-alive Pongs are not valid.
|
|
358
|
+
next if received_messages.empty?
|
|
359
|
+
new_exactly_once_delivery_enabled = response&.subscription_properties&.exactly_once_delivery_enabled
|
|
269
360
|
|
|
270
361
|
# Use synchronize so changes happen atomically
|
|
271
362
|
synchronize do
|
|
@@ -305,23 +396,26 @@ module Google
|
|
|
305
396
|
GRPC::ResourceExhausted, GRPC::Unauthenticated,
|
|
306
397
|
GRPC::Unavailable => e
|
|
307
398
|
status_code = e.respond_to?(:code) ? e.code : e.class.name
|
|
308
|
-
subscriber.service.
|
|
399
|
+
subscriber.service.internal_logger.log :error, "subscriber-streams" do
|
|
309
400
|
"Subscriber stream for subscription #{@subscriber.subscription_name} has ended with status " \
|
|
310
401
|
"#{status_code}; will be retried."
|
|
311
402
|
end
|
|
312
403
|
# Restart the stream with an incremental back for a retriable error.
|
|
404
|
+
backoff_and_wait!
|
|
313
405
|
retry
|
|
314
406
|
rescue RestartStream
|
|
315
|
-
subscriber.service.
|
|
407
|
+
subscriber.service.internal_logger.log :info, "subscriber-streams" do
|
|
316
408
|
"Subscriber stream for subscription #{@subscriber.subscription_name} has ended; will be retried."
|
|
317
409
|
end
|
|
410
|
+
backoff_and_wait!
|
|
318
411
|
retry
|
|
319
412
|
rescue StandardError => e
|
|
320
|
-
subscriber.service.
|
|
413
|
+
subscriber.service.internal_logger.log :error, "subscriber-streams" do
|
|
321
414
|
"error on stream for subscription #{@subscriber.subscription_name}: #{e.inspect}"
|
|
322
415
|
end
|
|
323
416
|
@subscriber.error! e
|
|
324
417
|
|
|
418
|
+
backoff_and_wait!
|
|
325
419
|
retry
|
|
326
420
|
end
|
|
327
421
|
|
|
@@ -369,12 +463,12 @@ module Google
|
|
|
369
463
|
end
|
|
370
464
|
|
|
371
465
|
def perform_callback_sync rec_msg
|
|
372
|
-
subscriber.service.
|
|
466
|
+
subscriber.service.internal_logger.log :info, "callback-delivery" do
|
|
373
467
|
"message (ID #{rec_msg.message_id}, ackID #{rec_msg.ack_id}) delivery to user callbacks"
|
|
374
468
|
end
|
|
375
469
|
@subscriber.callback.call rec_msg unless stopped?
|
|
376
470
|
rescue StandardError => e
|
|
377
|
-
subscriber.service.
|
|
471
|
+
subscriber.service.internal_logger.log :info, "callback-exceptions" do
|
|
378
472
|
"message (ID #{rec_msg.message_id}, ackID #{rec_msg.ack_id}) caused a user callback exception: " \
|
|
379
473
|
"#{e.inspect}"
|
|
380
474
|
end
|
|
@@ -404,7 +498,7 @@ module Google
|
|
|
404
498
|
return unless pause_streaming?
|
|
405
499
|
|
|
406
500
|
@paused = true
|
|
407
|
-
subscriber.service.
|
|
501
|
+
subscriber.service.internal_logger.log :info, "subscriber-flow-control" do
|
|
408
502
|
"subscriber for #{@subscriber.subscription_name} is client-side flow control blocked"
|
|
409
503
|
end
|
|
410
504
|
end
|
|
@@ -417,21 +511,27 @@ module Google
|
|
|
417
511
|
end
|
|
418
512
|
|
|
419
513
|
def unpause_streaming!
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
514
|
+
synchronize do
|
|
515
|
+
return unless unpause_streaming?
|
|
516
|
+
|
|
517
|
+
@paused = nil
|
|
518
|
+
# Record pong when unpausing flow control. While paused, incoming server pongs sit buffered in gRPC,
|
|
519
|
+
# leaving last_pong_at stale. Updating timestamp guarantees the monitor thread will not trigger an immediate
|
|
520
|
+
# false-positive restart while the reader thread wakes up to drain buffered frames.
|
|
521
|
+
@keepalive_monitor.record_pong!
|
|
522
|
+
subscriber.service.internal_logger.log :info, "subscriber-flow-control" do
|
|
523
|
+
"subscriber for #{@subscriber.subscription_name} is unblocking client-side flow control"
|
|
524
|
+
end
|
|
525
|
+
# Signal to the background thread that we are unpaused
|
|
526
|
+
@pause_cond.broadcast
|
|
425
527
|
end
|
|
426
|
-
# signal to the background thread that we are unpaused
|
|
427
|
-
@pause_cond.broadcast
|
|
428
528
|
end
|
|
429
529
|
|
|
430
530
|
def unpause_streaming?
|
|
431
531
|
return false if @stopped
|
|
432
532
|
return false if @paused.nil?
|
|
433
533
|
|
|
434
|
-
@inventory.count < @inventory.limit *
|
|
534
|
+
@inventory.count < @inventory.limit * UNPAUSE_INVENTORY_RATIO
|
|
435
535
|
end
|
|
436
536
|
|
|
437
537
|
def initial_input_request
|
|
@@ -443,6 +543,7 @@ module Google
|
|
|
443
543
|
req.client_id = @subscriber.service.client_id
|
|
444
544
|
req.max_outstanding_messages = @inventory.limit
|
|
445
545
|
req.max_outstanding_bytes = @inventory.bytesize
|
|
546
|
+
req.protocol_version = PROTOCOL_VERSION
|
|
446
547
|
end
|
|
447
548
|
end
|
|
448
549
|
|
|
@@ -320,7 +320,9 @@ module Google
|
|
|
320
320
|
reason
|
|
321
321
|
end
|
|
322
322
|
requests[:acknowledge].each do |req|
|
|
323
|
-
@subscriber.service.
|
|
323
|
+
@subscriber.service.internal_logger.log_batch(
|
|
324
|
+
"ack-batch", new_reason, "ack", req.ack_ids.length, req.to_proto.bytesize
|
|
325
|
+
)
|
|
324
326
|
end
|
|
325
327
|
end
|
|
326
328
|
requests[:modify_ack_deadline] =
|
|
@@ -329,7 +331,9 @@ module Google
|
|
|
329
331
|
type = mod_deadline.zero? ? "nack" : "modack"
|
|
330
332
|
new_reason = mod_ack_reqs.length > 1 ? "#{reason} and partitioned for exceeding max bytes" : reason
|
|
331
333
|
mod_ack_reqs.each do |req|
|
|
332
|
-
@subscriber.service.
|
|
334
|
+
@subscriber.service.internal_logger.log_batch(
|
|
335
|
+
"ack-batch", new_reason, type, req.ack_ids.length, req.to_proto.bytesize
|
|
336
|
+
)
|
|
333
337
|
end
|
|
334
338
|
mod_ack_reqs
|
|
335
339
|
end.flatten
|
|
@@ -43,7 +43,7 @@ module Google
|
|
|
43
43
|
|
|
44
44
|
##
|
|
45
45
|
# @private The InternalLogger object.
|
|
46
|
-
attr_reader :
|
|
46
|
+
attr_reader :internal_logger
|
|
47
47
|
|
|
48
48
|
##
|
|
49
49
|
# Creates a new Service instance.
|
|
@@ -55,6 +55,7 @@ module Google
|
|
|
55
55
|
@client_id = SecureRandom.uuid.freeze
|
|
56
56
|
@universe_domain = universe_domain || ENV["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] || "googleapis.com"
|
|
57
57
|
@logger = logger
|
|
58
|
+
@internal_logger = InternalLogger.new logger
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def subscription_admin
|
|
@@ -67,6 +68,7 @@ module Google
|
|
|
67
68
|
config.lib_name = "gccl"
|
|
68
69
|
config.lib_version = Google::Cloud::PubSub::VERSION
|
|
69
70
|
config.metadata = { "google-cloud-resource-prefix": "projects/#{@project}" }
|
|
71
|
+
config.logger = @logger if @logger
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
74
|
attr_accessor :mocked_subscription_admin
|
|
@@ -81,6 +83,7 @@ module Google
|
|
|
81
83
|
config.lib_name = "gccl"
|
|
82
84
|
config.lib_version = Google::Cloud::PubSub::VERSION
|
|
83
85
|
config.metadata = { "google-cloud-resource-prefix": "projects/#{@project}" }
|
|
86
|
+
config.logger = @logger if @logger
|
|
84
87
|
end
|
|
85
88
|
end
|
|
86
89
|
attr_accessor :mocked_topic_admin
|
|
@@ -94,6 +97,7 @@ module Google
|
|
|
94
97
|
config.lib_name = "gccl"
|
|
95
98
|
config.lib_version = Google::Cloud::PubSub::VERSION
|
|
96
99
|
config.metadata = { "google-cloud-resource-prefix": "projects/#{@project}" }
|
|
100
|
+
config.logger = @logger if @logger
|
|
97
101
|
end
|
|
98
102
|
iam
|
|
99
103
|
end
|
|
@@ -110,6 +114,7 @@ module Google
|
|
|
110
114
|
config.lib_name = "gccl"
|
|
111
115
|
config.lib_version = Google::Cloud::PubSub::VERSION
|
|
112
116
|
config.metadata = { "google-cloud-resource-prefix": "projects/#{@project}" }
|
|
117
|
+
config.logger = @logger if @logger
|
|
113
118
|
end
|
|
114
119
|
end
|
|
115
120
|
attr_accessor :mocked_schemas
|
|
@@ -147,7 +152,7 @@ module Google
|
|
|
147
152
|
##
|
|
148
153
|
# Acknowledges receipt of a message.
|
|
149
154
|
def acknowledge subscription, *ack_ids
|
|
150
|
-
|
|
155
|
+
internal_logger.log_ack_nack ack_ids, "ack"
|
|
151
156
|
subscription_admin.acknowledge_internal subscription: subscription_path(subscription),
|
|
152
157
|
ack_ids: ack_ids
|
|
153
158
|
end
|
|
@@ -156,7 +161,7 @@ module Google
|
|
|
156
161
|
# Modifies the ack deadline for a specific message.
|
|
157
162
|
def modify_ack_deadline subscription, ids, deadline
|
|
158
163
|
if deadline.zero?
|
|
159
|
-
|
|
164
|
+
internal_logger.log_ack_nack Array(ids), "nack"
|
|
160
165
|
end
|
|
161
166
|
subscription_admin.modify_ack_deadline_internal subscription: subscription_path(subscription),
|
|
162
167
|
ack_ids: Array(ids),
|
|
@@ -203,7 +208,6 @@ module Google
|
|
|
203
208
|
rpc.timeout = timeout if rpc.respond_to? :timeout=
|
|
204
209
|
end
|
|
205
210
|
end
|
|
206
|
-
|
|
207
211
|
end
|
|
208
212
|
end
|
|
209
213
|
Pubsub = PubSub unless const_defined? :Pubsub
|
data/lib/google/cloud/pubsub.rb
CHANGED
|
@@ -122,7 +122,6 @@ module Google
|
|
|
122
122
|
project_id = project_id.to_s # Always cast to a string
|
|
123
123
|
raise ArgumentError, "project_id is missing" if project_id.empty?
|
|
124
124
|
|
|
125
|
-
logger = Google::Cloud::PubSub::InternalLogger.new logger
|
|
126
125
|
service = PubSub::Service.new project_id, credentials,
|
|
127
126
|
host: endpoint,
|
|
128
127
|
timeout: timeout,
|
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
|
+
version: 3.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Moore
|
|
@@ -100,6 +100,7 @@ files:
|
|
|
100
100
|
- lib/google/cloud/pubsub/message_listener.rb
|
|
101
101
|
- lib/google/cloud/pubsub/message_listener/enumerator_queue.rb
|
|
102
102
|
- lib/google/cloud/pubsub/message_listener/inventory.rb
|
|
103
|
+
- lib/google/cloud/pubsub/message_listener/keepalive_monitor.rb
|
|
103
104
|
- lib/google/cloud/pubsub/message_listener/sequencer.rb
|
|
104
105
|
- lib/google/cloud/pubsub/message_listener/stream.rb
|
|
105
106
|
- lib/google/cloud/pubsub/message_listener/timed_unary_buffer.rb
|