Skip to content

feat(pubsub): implement streaming keep-alive logic#34653

Open
torreypayne wants to merge 18 commits into
mainfrom
pubsub-streaming-keepalive
Open

feat(pubsub): implement streaming keep-alive logic#34653
torreypayne wants to merge 18 commits into
mainfrom
pubsub-streaming-keepalive

Conversation

@torreypayne

@torreypayne torreypayne commented Jun 22, 2026

Copy link
Copy Markdown
Member

Overview

Implements proactive streaming keep-alive logic and connection health monitoring in Google::Cloud::PubSub::MessageListener::Stream, mirroring the design implemented in the .NET Pub/Sub client (dotnet#15649).

Long-running bi-directional gRPC streaming pull connections (StreamingPull) can experience silent TCP drops, intermediary network timeouts, or read deadlocks during periods of low message volume. This change introduces background timer tasks to push regular keep-alive requests and actively monitor server Pong timestamps.

Key Changes

  • Protocol Version Initialization: Explicitly initializes protocol_version = 1 on the initial StreamingPullRequest protobuf to enable bi-directional stream keep-alive support.
  • Unconditional Keep-Alive Pings: Configures a background timer task (@stream_keepalive_task) to dispatch empty StreamingPullRequest pings at regular intervals (default 30 seconds), regardless of current lease inventory volume.
  • Pong Monitoring & Automatic Reconnection: Introduces @pong_monitor_task to inspect timestamps (@last_ping_at, @last_pong_at). If a keep-alive response is overdue by more than pong_deadline seconds (default 15 seconds), the monitor raises RestartStream to safely recycle the connection and back off.
  • Concurrency Timestamp Guard: Guards ping timestamp updates (@last_ping_at = now if @last_pong_at >= @last_ping_at) to ensure consecutive un-ponged pings cannot overwrite the timestamp of an overdue request.

Testing & Validation

  • Unit Test Suite (keepalive_test.rb): Added targeted unit test coverage asserting protocol version flags, timer intervals, deadline timeouts, and non-disruptive Pong handling.
  • Resiliency & Robustness Suite: Validated against live GCP test instances (helical-zone-771) across simulated TCP socket hangs, sub-millisecond deadline starvation, and post-recovery downstream message delivery.

Fixes b/427319802

@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch 2 times, most recently from cf5df9b to b1acc8a Compare June 23, 2026 04:52
@torreypayne torreypayne marked this pull request as ready for review June 23, 2026 15:54
@torreypayne torreypayne requested review from a team and yoshi-approver as code owners June 23, 2026 15:54
@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch 2 times, most recently from 94e9f14 to ad59cd2 Compare June 23, 2026 17:29

@robertvoinescu-work robertvoinescu-work left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM from a functional perspective. Just a few minor comments.

Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch 2 times, most recently from ca925d3 to e53a9bd Compare June 26, 2026 20:41
@quartzmo

This comment was marked as resolved.

@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch from 26bcbb1 to 24055fd Compare June 30, 2026 15:53
@torreypayne torreypayne added kokoro:run Add this label to force Kokoro to re-run the tests. kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Jun 30, 2026
@yoshi-kokoro yoshi-kokoro removed kokoro:run Add this label to force Kokoro to re-run the tests. kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Jun 30, 2026
@torreypayne

This comment was marked as resolved.

Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
yoshi-automation and others added 6 commits July 6, 2026 16:16
- Wrap unpause_streaming! in synchronize block and reset @last_pong_at to prevent false-positive stream restarts upon unblocking flow control.
- Extract send_keepalive_ping! and check_liveness! helper methods on Stream and re-create TimerTask instances on start/stop to support clean restarts and deterministic unit testing.
- Overhaul keepalive_test.rb with synchronous unit test coverage for timer helpers, unpause race condition, and TimerTask re-creation, while increasing CI polling timeouts to prevent flakiness.
…g real-time threads

- Remove listener.start from synchronous unit tests (test_0003, test_0004, test_0005) so they execute on standalone Stream instances without spawning background threads or real-time TimerTask loops.
- Isolate ENV['PUBSUB_TEST_PONG_DEADLINE'] = '0.05' from global before block so short 50ms timeouts only apply during asynchronous integration tests, eliminating flakiness on CI in slow build environments (Ubuntu Ruby 3.2 and Windows Ruby 4.0).
… concurrent keep-alive logic

- Add concise reasoning comment for @pong_monitor_task execution interval (1/5th keepalive, floored at 10ms).
- Set @stream_opened = false under synchronization inside backoff_and_wait! before condition wait to prevent @pong_monitor_task from interrupting retry delays and causing double-backoffs.
- Restore multi-line comment above conditional in check_liveness! and sprinkle structured play-by-play comments across concurrent helper methods (send_keepalive_ping!, check_liveness!, unpause_streaming!).
- Update keepalive_test.rb integration test to configure attr_accessors instead of removed test ENV variables.
@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch from 009d0ec to 9461a56 Compare July 6, 2026 23:17
…ing sentinel on stream restart

- Enrich check_liveness! error logging with elapsed seconds since last pong, current inventory count, and flow control pause state.
- Push closing sentinel to @request_queue before raising RestartStream to guarantee both write-side and read-side gRPC C-core streams unblock cleanly during TCP blackholes.
@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch from 2fa07a4 to cca4878 Compare July 7, 2026 15:36
…guard in background_run

- Explain why @stream_opened is set to false prior to calling streaming_pull (to temporarily disable liveness monitoring during gRPC handshake).
- Document why ping/pong timestamps are re-initialized to monotonic now alongside setting @stream_opened = true after streaming_pull returns (to give the new stream a fresh deadline window and prevent false-positive disconnect detections from stale timestamps).
…ream-closing sentinel

- Add descriptive comment above @request_queue&.push self on line 505 explaining how pushing self terminates EnumeratorQueue#each to cleanly send HTTP/2 END_STREAM.
- Extract numeric keep-alive intervals, monitor polling ratios, reconnect backoff delays, and unpause thresholds into well-named constants on Google::Cloud::PubSub::MessageListener::Stream.
…_wait!

- Add unit test suggested by @quartzmo asserting @stream_opened is set to false inside backoff_and_wait! prior to @pause_cond.wait, guaranteeing the liveness checker cannot interrupt exponential backoff sleep.
Comment thread google-cloud-pubsub/test/google/cloud/pubsub/message_listener/keepalive_test.rb Outdated
…ream

- Extract stream health monitoring, keep-alive timers, monotonic timestamps (@last_ping_at, @last_pong_at), and liveness checking into a cohesive collaborator class Google::Cloud::PubSub::MessageListener::KeepaliveMonitor.
- Keep Stream focused entirely on primary message delivery, inventory lease renewal, and flow control.
- Delegate keepalive_interval and pong_deadline accessors from Stream to KeepaliveMonitor for 100% backwards compatibility.
…eepaliveMonitor and Stream

- Restore line-by-line attr_accessor and attr_reader declarations with descriptive comments on KeepaliveMonitor.
- Restore original comments above keepalive_interval and pong_deadline forwarding accessors on Stream.
quartzmo

This comment was marked as resolved.

…onitor

- Restore comment blocks documenting DEFAULT_INTERVAL, DEFAULT_DEADLINE, MONITOR_DIVISOR, and MIN_MONITOR_INTERVAL.
- Restore concurrency and flow control rationale comments inside send_ping! and check_liveness!.
- Validate happy path effectiveness: continuous multi-interval ping/pong cycling and unpause timestamp atomicity.
- Validate resiliency & failure recovery: silent network black hole recovery via stream-closing sentinel + RestartStream, and backoff sleep isolation during transient GRPC::Unavailable errors.
…flag

- Allow standard 'bundle exec rake test' commands to automatically enable SimpleCov code coverage when COVERAGE=true is passed.
…r and Stream

- Add acceptance/pubsub/keepalive_acceptance_test.rb running against real Google Cloud Pub/Sub live production instances.
- Validate real gRPC wire keep-alive ping/pong heartbeat, live message delivery/ACK, and flow-control unpause resiliency.
…est/helper.rb

- Remove SimpleCov environment trigger from test/helper.rb so coverage configuration can be handled in a separate PR.

@aandreassa aandreassa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks great, just a couple of Ruby things & docs to think about!

listener.wait!

elapsed = attempts[1] - attempts[0]
puts "\n[b/528401453 Test] Elapsed delay between attempts: #{elapsed.round(3)}s"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the outputs in this file (or capture it if needed)? It is better to prevent this test from polluting relevant output logs.

end

# keepalive_interval used to send pings and maintain a healthy stream
def keepalive_interval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these? Maybe we should keep it minimal and consistent.
You are only using it on tests, and you already call stream.keepalive_monitor.last_ping_at, for example.

MIN_MONITOR_INTERVAL = 0.01

# keepalive_interval used to send pings and maintain a healthy stream
attr_accessor :interval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pls have YARD docstrings for these attr_accessors?

rg -U '^\s*#[^\n]*\n\s*attr_accessor' for examples.

Alternatively, do we need these if they are for testing only? After modernizing in 3.0, we try to keep this library pretty lightweight.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look and LMK if I should do another pass

self
end

def stream_opened?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this confusing. stream_opened as in opened in the past and its done, or currently open? You should try using present tense for predicates.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to present tense (stream_open); should I add is_... to make it more verb-y?

class MessageListener
##
# @private
# Monitors gRPC streaming connection health via periodic bi-directional keep-alive pings and pongs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please document what this class does at a high level (here or initialize)? I'd love to see more YARD docstrings overall.

Comment thread google-cloud-pubsub/lib/google/cloud/pubsub/message_listener/stream.rb Outdated
# background timer task for monitoring stream liveness
attr_reader :monitor_task

def initialize stream, interval: DEFAULT_INTERVAL, deadline: DEFAULT_DEADLINE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do something like (pls double check convention)

# Initializes the KeepaliveMonitor.
# 
# @param [Stream] stream The parent streaming pull connection to monitor.
# @param [Float] interval The interval in seconds between keep-alive pings.
# @param [Float] deadline The deadline in seconds for receiving a pong.

and document others in-line below.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented your suggestion as-is; LMK what you think

# Monitors gRPC streaming connection health via periodic bi-directional keep-alive pings and pongs.
#
# Tracks ping and pong timestamps over the active gRPC stream to detect silent connection drops or freezes,
# and triggers a stream restart when server responses exceed the configured pong deadline.

@torreypayne torreypayne Jul 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aandreassa LMK if this additional context is useful/sufficient to address your comment on L23

@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch from 9ee8d60 to 43562dd Compare July 16, 2026 18:00
…tream

- Rename stream_opened? / @stream_opened to present tense stream_open? / @stream_open
- Remove redundant forwarding accessors (keepalive_interval, pong_deadline, send_keepalive_ping!, and check_liveness!) from Stream
- Add comprehensive YARD docstrings (with @Private for internal accessors) for KeepaliveMonitor and Stream
- Extract PROTOCOL_VERSION = 1 into a formal Stream constant
- Remove diagnostic puts outputs from bug_regression_test.rb
@torreypayne torreypayne force-pushed the pubsub-streaming-keepalive branch from 43562dd to f4927d5 Compare July 16, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants