Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,27 @@ jobs:
STREAM_API_SECRET: ${{ secrets.STREAM_VIDEO_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_VIDEO_BASE_URL }}
run: make test-integration-video

integration-gcp-lb:
name: GCP load balancer keep-alive
runs-on: ubuntu-latest
environment: ci

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1.0'

- name: Install dependencies
run: bundle install --jobs 4 --retry 3

- name: Run GCP keep-alive integration test
env:
STREAM_API_KEY: ${{ vars.STREAM_GCP_API_KEY || vars.STREAM_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_GCP_API_SECRET || secrets.STREAM_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_GCP_BASE_URL }}
run: make test-integration-gcp-lb
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## [Unreleased]

### Fixed

- Default `idle_timeout` for `net_http_persistent` is now `25` seconds (was `55`).
GCP SSL-proxy load balancers close idle TLS around ~30s; keeping pooled
connections for 55s (AWS ALB idle minus 5s) caused
`SSL_read: unexpected eof while reading` (`GetStreamRuby::TransportError`)
on the next request. Override with `idle_timeout:` or `STREAM_IDLE_TIMEOUT`.

## [10.0.0] - 2026-07-24

### Added
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# GetStream Ruby SDK Makefile

.PHONY: help install test test-unit test-integration test-integration-chat test-integration-video lint format clean setup
.PHONY: help install test test-unit test-integration test-integration-chat test-integration-video test-integration-gcp-lb lint format clean setup

# Default target
help: ## Show this help message
Expand Down Expand Up @@ -40,6 +40,9 @@ test-integration-feed: ## Run feed integration tests only
test-integration-video: ## Run video integration tests only
bundle exec rspec spec/integration/video_integration_spec.rb spec/integration/video_client_integration_spec.rb

test-integration-gcp-lb: ## Run GCP load-balancer keep-alive integration test
bundle exec rspec spec/integration/gcp_lb_keepalive_spec.rb

test-all: ## Run all tests (unit + integration)
bundle exec rspec spec/

Expand Down
1 change: 1 addition & 0 deletions lib/getstream_ruby/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ def configure_adapter(connection)

# Default: net_http_persistent with the 5-knob config.
# Never set Connection: close; net_http_persistent keeps connections alive natively.
# idle_timeout must be below the GCP SSL-proxy idle (~30s); AWS ALB is 60s.
idle = @configuration.idle_timeout
connection.adapter :net_http_persistent, pool_size: @configuration.max_conns_per_host do |http|

Expand Down
6 changes: 4 additions & 2 deletions lib/getstream_ruby/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,14 @@ def assign_timeouts_and_pool(options, use_env:)
env_request_timeout = ENV.fetch('STREAM_REQUEST_TIMEOUT', nil) || ENV.fetch('STREAM_TIMEOUT', nil)
@request_timeout = (request_timeout || timeout || env_request_timeout || 30).to_i
@max_conns_per_host = (max_conns_per_host || ENV.fetch('STREAM_MAX_CONNS_PER_HOST', nil) || 5).to_i
@idle_timeout = (idle_timeout || ENV.fetch('STREAM_IDLE_TIMEOUT', nil) || 55).to_i
# 25s sits under GCP SSL-proxy idle (~30s) and AWS ALB idle (60s) so
# net_http_persistent drops the conn before the LB does (CHA-4943).
@idle_timeout = (idle_timeout || ENV.fetch('STREAM_IDLE_TIMEOUT', nil) || 25).to_i
@connect_timeout = (connect_timeout || ENV.fetch('STREAM_CONNECT_TIMEOUT', nil) || 10).to_i
else
@request_timeout = (request_timeout || timeout || 30).to_i
@max_conns_per_host = (max_conns_per_host || 5).to_i
@idle_timeout = (idle_timeout || 55).to_i
@idle_timeout = (idle_timeout || 25).to_i
@connect_timeout = (connect_timeout || 10).to_i
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/connection_pooling_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def capture_adapter_call
line = info_lines.first
expect(line).to include('client.initialized')
expect(line).to include('stream.client.max_conns_per_host=5')
expect(line).to include('stream.client.idle_timeout_seconds=55')
expect(line).to include('stream.client.idle_timeout_seconds=25')
expect(line).to include('stream.client.connect_timeout_seconds=10')
expect(line).to include('stream.client.request_timeout_seconds=30')
expect(line).to include('stream.client.user_http_client=false')
Expand Down
2 changes: 1 addition & 1 deletion spec/getstream_ruby_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
expect(client.configuration.faraday_adapter_options).to eq({})
expect(client.configuration.connection_keep_alive).to eq(true)
expect(client.configuration.max_conns_per_host).to eq(5)
expect(client.configuration.idle_timeout).to eq(55)
expect(client.configuration.idle_timeout).to eq(25)
expect(client.configuration.connect_timeout).to eq(10)
expect(client.configuration.request_timeout).to eq(30)
expect(client.configuration.http_client).to be_nil
Expand Down
48 changes: 48 additions & 0 deletions spec/integration/gcp_lb_keepalive_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require 'rspec'
require 'uri'
require_relative 'chat_test_helpers'

# CHA-4943: GCP SSL-proxy LBs close idle TLS around ~30s. The SDK pool must
# drop connections before that, otherwise the next request raises
# GetStreamRuby::TransportError (SSL_read: unexpected eof while reading).
#
# Skipped unless STREAM_BASE_URL is a GCP edge hostname so default CI (AWS /
# geo-routed chat.stream-io-api.com) does not wait 35s for a no-op.
RSpec.describe 'GCP load balancer keep-alive', type: :integration do

include ChatTestHelpers

# Match on the parsed host suffix rather than a substring so a URL like
# https://gcp.stream-io-api.com.example.com does not slip through.
def gcp_edge_base_url?
host = URI(ENV.fetch('STREAM_BASE_URL', '')).host.to_s
host.end_with?('.gcp.stream-io-api.com')
rescue URI::InvalidURIError
false
end

before(:all) do

skip 'STREAM_BASE_URL must be a *.gcp.stream-io-api.com edge' unless gcp_edge_base_url?
init_chat_client

end

after(:all) do

cleanup_chat_resources if gcp_edge_base_url?

end

it 'reuses the pooled client after 35s idle without TLS EOF' do

ids, = create_test_users(1)
sleep ENV.fetch('STREAM_GCP_LB_IDLE_SLEEP', '35').to_i
expect { create_test_users(1) }.not_to raise_error
expect(ids).not_to be_empty

end

end
Loading