Skip to content

Commit f5bccb2

Browse files
authored
Merge pull request #103 from zombocom/schneems/retry-visiting-url
Retry on ReadTimeout from website.visit
2 parents 39237f1 + ae6b7e0 commit f5bccb2

File tree

5 files changed

+74
-9
lines changed

5 files changed

+74
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## HEAD
22

3+
- Fix: Net::ReadTimeout errors on `website.visit` are now retried by default (https://github.com/zombocom/rundoc/pull/103)
34
- Added: When a background process (`background.start`) does not exit cleanly, print its logs for help with debugging. (https://github.com/zombocom/rundoc/pull/104)
45

56
## 4.1.3

lib/rundoc/code_command/website/driver.rb

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ class Rundoc::CodeCommand::Website
66
class Driver
77
attr_reader :session
88

9-
def initialize(name:, url:, width: 1024, height: 720, visible: false)
9+
def initialize(name:, url:, width: 1024, height: 720, visible: false, io: $stdout, read_timeout: 60)
10+
@io = io
1011
browser_options = ::Selenium::WebDriver::Chrome::Options.new
1112
browser_options.args << "--headless" unless visible
1213
browser_options.args << "--disable-gpu" if Gem.win_platform?
@@ -15,7 +16,10 @@ def initialize(name:, url:, width: 1024, height: 720, visible: false)
1516
@width = width
1617
@height = height
1718

18-
@driver = Capybara::Selenium::Driver.new(nil, browser: :chrome, options: browser_options)
19+
client = Selenium::WebDriver::Remote::Http::Default.new
20+
client.read_timeout = read_timeout
21+
22+
@driver = Capybara::Selenium::Driver.new(nil, browser: :chrome, options: browser_options, http_client: client)
1923
driver_name = :"rundoc_driver_#{name}"
2024
Capybara.register_driver(driver_name) do |app|
2125
@driver
@@ -24,8 +28,20 @@ def initialize(name:, url:, width: 1024, height: 720, visible: false)
2428
@session = Capybara::Session.new(driver_name)
2529
end
2630

27-
def visit(url)
28-
@session.visit(url)
31+
def visit(url, max_attempts: 3, delay: 1)
32+
attempts = 0
33+
begin
34+
@session.visit(url)
35+
rescue ::Net::ReadTimeout => e
36+
attempts += 1
37+
if attempts > max_attempts
38+
raise e
39+
else
40+
@io.puts "Error visiting url (#{attempts}/#{max_attempts}) `#{url}`:\n#{e}"
41+
sleep delay
42+
retry
43+
end
44+
end
2945
end
3046

3147
def timestamp
@@ -54,7 +70,7 @@ def safe_eval(code, env = {})
5470
msg = +""
5571
msg << "Error running code #{code.inspect} at #{current_url.inspect}\n"
5672
msg << "saving a screenshot to: `tmp/error.png`"
57-
puts msg
73+
@io.puts msg
5874
error_path = env[:context].screenshots_dir.join("error.png")
5975
session.save_screenshot(error_path)
6076
raise e
@@ -66,13 +82,13 @@ def screenshot(screenshots_dir:, upload: false)
6682
file_name = self.class.next_screenshot_name
6783
file_path = screenshots_dir.join(file_name)
6884
session.save_screenshot(file_path)
69-
puts "Screenshot saved to #{file_path}"
85+
@io.puts "Screenshot saved to #{file_path}"
7086

7187
return file_path unless upload
7288

7389
case upload
7490
when "s3", "aws"
75-
puts "Uploading screenshot to S3"
91+
@io.puts "Uploading screenshot to S3"
7692
require "aws-sdk-s3"
7793
ENV.fetch("AWS_ACCESS_KEY_ID")
7894
s3 = Aws::S3::Resource.new(region: ENV.fetch("AWS_REGION"))

lib/rundoc/code_command/website/visit.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
class Rundoc::CodeCommand::Website
44
class Visit < Rundoc::CodeCommand
5-
def initialize(name:, url: nil, scroll: nil, height: 720, width: 1024, visible: false)
5+
def initialize(name:, url: nil, scroll: nil, height: 720, width: 1024, visible: false, max_attempts: 3)
66
@name = name
77
@url = url
88
@scroll = scroll
99
@height = height
1010
@width = width
1111
@visible = visible
12+
@max_attempts = max_attempts
1213
end
1314

1415
def driver
@@ -33,7 +34,7 @@ def call(env = {})
3334

3435
puts message
3536

36-
driver.visit(@url) if @url
37+
driver.visit(@url, max_attempts: @max_attempts) if @url
3738
driver.scroll(@scroll) if @scroll
3839

3940
return "" if contents.nil? || contents.empty?

test/integration/website_test.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,23 @@ def test_screenshot_command
3434
end
3535
end
3636
end
37+
38+
def test_retry_client_closed_early
39+
tcp_unexpected_exit do |port|
40+
io = StringIO.new
41+
driver = Rundoc::CodeCommand::Website::Driver.new(name: SecureRandom.hex, url: nil, read_timeout: 0.1, io: io)
42+
assert_raises(Net::ReadTimeout) do
43+
driver.visit("http://localhost:#{port}", max_attempts: 3, delay: 0)
44+
end
45+
46+
logs = io.string
47+
assert_logs_include(logs: logs, include_str: "Error visiting url (1/3)")
48+
assert_logs_include(logs: logs, include_str: "Error visiting url (2/3)")
49+
assert_logs_include(logs: logs, include_str: "Error visiting url (3/3)")
50+
end
51+
end
52+
53+
def assert_logs_include(logs:, include_str:)
54+
assert logs.include?(include_str), "Expected logs to include #{include_str} but they didnt. Logs:\n#{logs}"
55+
end
3756
end

test/test_helper.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
require "minitest/autorun"
1010
require "mocha/minitest"
1111
require "tmpdir"
12+
require "socket"
13+
require "timeout"
1214

1315
class Minitest::Test
1416
SUCCESS_DIRNAME = Rundoc::CLI::DEFAULTS::ON_SUCCESS_DIR
@@ -81,4 +83,30 @@ def called?
8183

8284
attr_reader :value
8385
end
86+
87+
# Yields port, exits unexpectedly
88+
def tcp_unexpected_exit(timeout: 30)
89+
Timeout.timeout(timeout) do
90+
threads = []
91+
server = TCPServer.new("127.0.0.1", 0)
92+
port = server.addr[1]
93+
threads << Thread.new do
94+
# Accept one connection, then raise an error to simulate unexpected close
95+
server.accept
96+
raise "Unexpected server error!"
97+
rescue
98+
# Simulate crash, but let ensure run
99+
end.tap { |t| t.abort_on_exception = false }
100+
101+
threads << Thread.new do
102+
yield port
103+
end
104+
105+
while threads.all?(&:alive?)
106+
sleep 0.1
107+
end
108+
ensure
109+
server.close if server && !server.closed?
110+
end
111+
end
84112
end

0 commit comments

Comments
 (0)