Skip to content

Commit abb505c

Browse files
authored
Improve server process cleanup and readiness checks (#199)
* Improve server process cleanup and readiness checks - Add process group management for better child process cleanup - Add warning logging when process group kill fails with EPERM - Improve server readiness check to accept only 200-399 HTTP status codes - Reject 404 and 5xx responses as "not ready" for more reliable health checks These improvements provide better debugging visibility and reduce flaky tests by ensuring the server is truly ready before running tests. Fixes #198
1 parent a460821 commit abb505c

File tree

3 files changed

+87
-12
lines changed

3 files changed

+87
-12
lines changed

lib/cypress_on_rails/configuration.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class Configuration
2222
attr_accessor :server_host
2323
attr_accessor :server_port
2424
attr_accessor :transactional_server
25+
# HTTP path to check for server readiness (default: '/')
26+
# Can be set via CYPRESS_RAILS_READINESS_PATH environment variable
27+
attr_accessor :server_readiness_path
28+
# Timeout in seconds for individual HTTP readiness checks (default: 5)
29+
# Can be set via CYPRESS_RAILS_READINESS_TIMEOUT environment variable
30+
attr_accessor :server_readiness_timeout
2531

2632
# Attributes for backwards compatibility
2733
def cypress_folder
@@ -62,6 +68,8 @@ def reset
6268
self.server_host = ENV.fetch('CYPRESS_RAILS_HOST', 'localhost')
6369
self.server_port = ENV.fetch('CYPRESS_RAILS_PORT', nil)
6470
self.transactional_server = true
71+
self.server_readiness_path = ENV.fetch('CYPRESS_RAILS_READINESS_PATH', '/')
72+
self.server_readiness_timeout = ENV.fetch('CYPRESS_RAILS_READINESS_TIMEOUT', '5').to_i
6573
end
6674

6775
def tagged_logged

lib/cypress_on_rails/server.rb

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'socket'
22
require 'timeout'
33
require 'fileutils'
4+
require 'net/http'
45
require 'cypress_on_rails/configuration'
56

67
module CypressOnRails
@@ -9,13 +10,16 @@ class Server
910

1011
def initialize(options = {})
1112
config = CypressOnRails.configuration
12-
13+
1314
@framework = options[:framework] || :cypress
1415
@host = options[:host] || config.server_host
1516
@port = options[:port] || config.server_port || find_available_port
1617
@port = @port.to_i if @port
1718
@install_folder = options[:install_folder] || config.install_folder || detect_install_folder
1819
@transactional = options.fetch(:transactional, config.transactional_server)
20+
# Process management: track PID and process group for proper cleanup
21+
@server_pid = nil
22+
@server_pgid = nil
1923
end
2024

2125
def open
@@ -105,34 +109,91 @@ def spawn_server
105109

106110
puts "Starting Rails server: #{server_args.join(' ')}"
107111

108-
spawn(*server_args, out: $stdout, err: $stderr)
112+
@server_pid = spawn(*server_args, out: $stdout, err: $stderr, pgroup: true)
113+
begin
114+
@server_pgid = Process.getpgid(@server_pid)
115+
rescue Errno::ESRCH => e
116+
# Edge case: process terminated before we could get pgid
117+
# This is OK - send_term_signal will fall back to single-process kill
118+
CypressOnRails.configuration.logger.warn("Process #{@server_pid} terminated immediately after spawn: #{e.message}")
119+
@server_pgid = nil
120+
end
121+
@server_pid
109122
end
110123

111124
def wait_for_server(timeout = 30)
112125
Timeout.timeout(timeout) do
113126
loop do
114-
begin
115-
TCPSocket.new(host, port).close
116-
break
117-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
118-
sleep 0.1
119-
end
127+
break if server_responding?
128+
sleep 0.1
120129
end
121130
end
122131
rescue Timeout::Error
123132
raise "Rails server failed to start on #{host}:#{port} after #{timeout} seconds"
124133
end
125134

135+
def server_responding?
136+
config = CypressOnRails.configuration
137+
readiness_path = config.server_readiness_path || '/'
138+
timeout = config.server_readiness_timeout || 5
139+
uri = URI("http://#{host}:#{port}#{readiness_path}")
140+
141+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: timeout, read_timeout: timeout) do |http|
142+
http.get(uri.path)
143+
end
144+
145+
# Accept 200-399 (success and redirects), reject 404 and 5xx
146+
# 3xx redirects are considered "ready" because the server is responding correctly
147+
(200..399).cover?(response.code.to_i)
148+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, Errno::ETIMEDOUT, SocketError,
149+
Net::OpenTimeout, Net::ReadTimeout, Net::HTTPBadResponse
150+
false
151+
end
152+
126153
def stop_server(pid)
127-
if pid
128-
puts "Stopping Rails server (PID: #{pid})"
129-
Process.kill('TERM', pid)
130-
Process.wait(pid)
154+
return unless pid
155+
156+
puts "Stopping Rails server (PID: #{pid})"
157+
send_term_signal(pid)
158+
159+
begin
160+
Timeout.timeout(10) do
161+
Process.wait(pid)
162+
end
163+
rescue Timeout::Error
164+
CypressOnRails.configuration.logger.warn("Server did not terminate after TERM signal, sending KILL")
165+
safe_kill_process('KILL', pid)
166+
Process.wait(pid) rescue Errno::ESRCH
131167
end
132168
rescue Errno::ESRCH
133169
# Process already terminated
134170
end
135171

172+
def send_term_signal(pid)
173+
if @server_pgid && process_exists?(pid)
174+
Process.kill('TERM', -@server_pgid)
175+
else
176+
safe_kill_process('TERM', pid)
177+
end
178+
rescue Errno::ESRCH, Errno::EPERM => e
179+
CypressOnRails.configuration.logger.warn("Failed to kill process group #{@server_pgid}: #{e.message}, trying single process")
180+
safe_kill_process('TERM', pid)
181+
end
182+
183+
def process_exists?(pid)
184+
return false unless pid
185+
Process.kill(0, pid)
186+
true
187+
rescue Errno::ESRCH, Errno::EPERM
188+
false
189+
end
190+
191+
def safe_kill_process(signal, pid)
192+
Process.kill(signal, pid) if pid
193+
rescue Errno::ESRCH, Errno::EPERM
194+
# Process already terminated or permission denied
195+
end
196+
136197
def base_url
137198
"http://#{host}:#{port}"
138199
end

spec/cypress_on_rails/configuration_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
expect(CypressOnRails.configuration.logger).to_not be_nil
1111
expect(CypressOnRails.configuration.before_request).to_not be_nil
1212
expect(CypressOnRails.configuration.vcr_options).to eq({})
13+
expect(CypressOnRails.configuration.server_readiness_path).to eq('/')
14+
expect(CypressOnRails.configuration.server_readiness_timeout).to eq(5)
1315
end
1416

1517
it 'can be configured' do
@@ -22,12 +24,16 @@
2224
config.logger = my_logger
2325
config.before_request = before_request_lambda
2426
config.vcr_options = { hook_into: :webmock }
27+
config.server_readiness_path = '/health'
28+
config.server_readiness_timeout = 10
2529
end
2630
expect(CypressOnRails.configuration.api_prefix).to eq('/api')
2731
expect(CypressOnRails.configuration.install_folder).to eq('my/path')
2832
expect(CypressOnRails.configuration.use_middleware?).to eq(false)
2933
expect(CypressOnRails.configuration.logger).to eq(my_logger)
3034
expect(CypressOnRails.configuration.before_request).to eq(before_request_lambda)
3135
expect(CypressOnRails.configuration.vcr_options).to eq(hook_into: :webmock)
36+
expect(CypressOnRails.configuration.server_readiness_path).to eq('/health')
37+
expect(CypressOnRails.configuration.server_readiness_timeout).to eq(10)
3238
end
3339
end

0 commit comments

Comments
 (0)