Skip to content

Commit 65402d1

Browse files
author
Daniel Schmidt
committed
Send :read_timeout when streaming events (#584)
- Corrects a bug in `Docker::Connection.compile_request_params` that incorrectly stripped out `read_timeout` when it was nil. - Sets `Docker::Event.stream` to skip timeouts by default. - Sets `Docker::Event.stream` to avoid automatically retrying timeout-related events by default. - Adds timeout-related event streaming tests. A particularly slow (60s+) test can be enabled by setting `RUN_SLOW_TESTS=1` in the testing environment.
1 parent f52be4d commit 65402d1

File tree

4 files changed

+72
-20
lines changed

4 files changed

+72
-20
lines changed

lib/docker/connection.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,24 +137,24 @@ def version
137137
end
138138

139139
private
140-
# Given an HTTP method, path, optional query, extra options, and block,
141-
# compiles a request.
140+
142141
def compile_request_params(http_method, path, query = nil, opts = nil, &block)
143-
query ||= {}
144142
opts ||= {}
145-
headers = opts.delete(:headers) || {}
146-
content_type = opts[:body].nil? ? 'text/plain' : 'application/json'
147-
user_agent = "Swipely/Docker-API #{Docker::VERSION}"
143+
query ||= opts.delete(:query) || {}
144+
145+
default_headers = {
146+
'Content-Type' => opts[:body].nil? ? 'text/plain' : 'application/json',
147+
'User-Agent' => "Swipely/Docker-API #{Docker::VERSION}",
148+
}
149+
headers = default_headers.merge(opts.delete(:headers) || {})
150+
148151
{
149-
:method => http_method,
150-
:path => path,
151-
:query => query,
152-
:headers => { 'Content-Type' => content_type,
153-
'User-Agent' => user_agent,
154-
}.merge(headers),
155-
:expects => (200..204).to_a << 301 << 304,
156-
:idempotent => http_method == :get,
157-
:request_block => block,
158-
}.merge(opts).reject { |_, v| v.nil? }
152+
method: http_method,
153+
path: path,
154+
headers:,
155+
query:,
156+
expects: (200..204).to_a << 301 << 304,
157+
idempotent: http_method == :get,
158+
}.merge(opts).tap { |params| params[:request_block] = block if block }
159159
end
160160
end

lib/docker/event.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,21 @@ class << self
3030
include Docker::Error
3131

3232
def stream(opts = {}, conn = Docker.connection, &block)
33-
conn.get('/events', opts, :response_block => lambda { |b, r, t|
34-
b.each_line do |line|
35-
block.call(new_event(line, r, t))
33+
# Disable timeouts by default
34+
opts[:read_timeout] = nil unless opts.key? :read_timeout
35+
36+
# By default, avoid retrying timeout errors. Set opts[:retry_errors] to override this.
37+
opts[:retry_errors] ||= Excon::DEFAULT_RETRY_ERRORS.reject do |cls|
38+
cls == Excon::Error::Timeout
39+
end
40+
41+
opts[:response_block] = lambda do |chunk, remaining, total|
42+
chunk.each_line do |event_json|
43+
block.call(new_event(event_json, remaining, total))
3644
end
37-
})
45+
end
46+
47+
conn.get('/events', opts.delete(:query), opts)
3848
end
3949

4050
def since(since, opts = {}, conn = Docker.connection, &block)

spec/docker/event_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'spec_helper'
4+
require 'securerandom'
45

56
SingleCov.covered! uncovered: 5
67

@@ -87,6 +88,43 @@
8788

8889
container.remove
8990
end
91+
92+
context 'with timeouts' do
93+
# @see https://github.com/upserve/docker-api/issues/584
94+
95+
it 'does not (seem to) time out by default' do
96+
# @note Excon passes timeout-related arguments directly to IO.select, which in turn makes a system call,
97+
# so it's not possible to mock this (e.g. w/Timecop).
98+
skip_slow_test
99+
expect { Timeout.timeout(65) { stream_events_with_timeout } }
100+
.to raise_error Timeout::Error
101+
end
102+
103+
it 'times out immediately if read_timeout < Timeout.timeout' do
104+
expect { Timeout.timeout(1) { stream_events_with_timeout(0) } }
105+
.to raise_error Docker::Error::TimeoutError
106+
end
107+
108+
it 'times out after timeout(1) if read_timeout=2' do
109+
expect { Timeout.timeout(1) { stream_events_with_timeout(2) } }
110+
.to raise_error Timeout::Error
111+
end
112+
113+
private
114+
115+
def stream_events_with_timeout(read_timeout = [])
116+
opts = {
117+
# Filter to avoid unexpected Docker events interfering with timeout behavior
118+
query: { filters: { container: [SecureRandom.uuid] }.to_json },
119+
# Use [] to differentiate between explicit nil and not providing an arg (falling back to the default)
120+
read_timeout:,
121+
}.reject { |_, v| v.empty? rescue false }
122+
123+
Docker::Event.stream(opts) do |event|
124+
raise "Unexpected event interfered with timeout test: #{event}"
125+
end
126+
end
127+
end
90128
end
91129

92130
describe ".since" do

spec/spec_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def project_dir
2424
end
2525

2626
module SpecHelpers
27+
def skip_slow_test
28+
skip "Disabled because ENV['RUN_SLOW_TESTS'] not set" unless ENV['RUN_SLOW_TESTS']
29+
end
30+
2731
def skip_without_auth
2832
skip "Disabled because of missing auth" if ENV['DOCKER_API_USER'] == 'debbie_docker'
2933
end

0 commit comments

Comments
 (0)