From 36fdfdaab1ea18f8da90d7e34e4b431b06453245 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 27 Oct 2026 08:37:10 -0400 Subject: [PATCH] Reload the development server when application code changes Use a development-only Rerun watcher to rebuild mounted Grape routes in a fresh server process. Document the command and verify changed routes and responses over real HTTP on modern and legacy Goliath. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .rerun | 1 + Gemfile | 1 + Gemfile.lock | 13 ++++++ README.md | 12 ++++++ spec/development_reloading_spec.rb | 65 ++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 .rerun create mode 100644 spec/development_reloading_spec.rb diff --git a/.rerun b/.rerun new file mode 100644 index 0000000..6d9b1c3 --- /dev/null +++ b/.rerun @@ -0,0 +1 @@ +--pattern {app.rb,api/**/*.rb,app/**/*.rb,config/**/*.rb} diff --git a/Gemfile b/Gemfile index 1662e48..5c00e21 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'rack-contrib', '~> 1.4.0' group :development do gem 'rake' + gem 'rerun', '~> 0.14', require: false gem 'rubocop' end diff --git a/Gemfile.lock b/Gemfile.lock index fadafea..7c6da51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,6 +62,8 @@ GEM addressable (>= 2.1.1) eventmachine (>= 0.12.9) eventmachine (1.2.7) + ffi (1.17.4) + ffi (1.17.4-arm64-darwin) git-version-bump (0.19.1) goliath (1.0.7) async-rack @@ -89,6 +91,10 @@ GEM json (3.1.0) language_server-protocol (3.17.0.6) lint_roller (1.1.0) + listen (3.10.1) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) log4r (1.1.10) logger (1.7.0) minitest (6.0.6) @@ -118,11 +124,16 @@ GEM rack (>= 1.3) rainbow (3.1.1) rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) readline (0.0.4) reline regexp_parser (2.13.1) reline (0.7.0) io-console (~> 0.5) + rerun (0.14.0) + listen (~> 3.0) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -163,6 +174,7 @@ GEM zeitwerk (2.8.3) PLATFORMS + arm64-darwin-23 ruby DEPENDENCIES @@ -174,6 +186,7 @@ DEPENDENCIES rack-contrib (~> 1.4.0) rack-test rake + rerun (~> 0.14) rspec rubocop simplecov diff --git a/README.md b/README.md index 3b1242d..b88e189 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,18 @@ $ bundle exec ruby app.rb -sv `app.rb` enables Goliath's application runner. Loading the environment for Rake or tests loads only the API, avoiding an extra server starting when those processes exit. +### Development Reloading + +To pick up code changes automatically, run: + +``` +bundle exec rerun -- ruby app.rb -sv -e development +``` + +[Rerun](https://github.com/alexch/rerun) watches `app.rb` and Ruby files under `api/`, `app/`, and `config/`, restarting the server when files are added, changed, or removed. For example, edit the response in `api/ping.rb`, save, and request `/api/ping` again after the server restarts. On shared filesystems that do not deliver file notifications, add `--force-polling` before `--`. + +The whole process restarts deliberately: Grape compiles routes and retains references to mounted API classes, so reloading individual files with `Rack::Reloader` does not reliably update mounted routes. Requests can be interrupted during a restart; use this command only for development. The plain `bundle exec ruby app.rb -sv` command above does not watch or reload files. + ### Hello World Navigate to http://localhost:9000/api/ping with a browser or use `curl`. diff --git a/spec/development_reloading_spec.rb b/spec/development_reloading_spec.rb new file mode 100644 index 0000000..8c4f86c --- /dev/null +++ b/spec/development_reloading_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' +require 'fileutils' +require 'net/http' +require 'rbconfig' +require 'socket' +require 'tmpdir' +require 'timeout' + +describe 'Development reloading' do + def wait_for_response(port, path, body) + Timeout.timeout(30) do + loop do + begin + response = Net::HTTP.start('127.0.0.1', port, open_timeout: 1, read_timeout: 1) { |http| http.get(path) } + return response if response.code == '200' && response.body == body + rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, Net::ReadTimeout + # A restart briefly closes the listening socket and existing connections. + end + sleep 0.1 + end + end + end + + it 'replaces mounted routes and responses after an API file changes' do + Dir.mktmpdir('grape-on-goliath-reloading') do |directory| + root = File.expand_path('..', __dir__) + FileUtils.cp_r(%w[api app config app.rb .rerun].map { |path| File.join(root, path) }, directory) + socket = TCPServer.new('127.0.0.1', 0) + port = socket.addr[1] + socket.close + + File.open(File.join(directory, 'server.log'), 'w+') do |log| + pid = Process.spawn( + RbConfig.ruby, '-e', '$stdout.sync = true; load Gem.bin_path("rerun", "rerun")', '--', + '--background', '--no-notify', '--force-polling', '--', + RbConfig.ruby, 'app.rb', '-sv', '-e', 'development', '-p', port.to_s, + chdir: directory, pgroup: true, out: log, err: %i[child out] + ) + begin + expect(wait_for_response(port, '/api/ping', '{"ping":"pong"}').code).to eq('200') + Timeout.timeout(30) do + sleep 0.1 until File.read(log.path).include?('[rerun] Watching ') + end + + path = File.join(directory, 'api/ping.rb') + File.write(path, File.read(path).sub("get '/ping'", "get '/updated'").sub("'pong'", "'updated'")) + + expect(wait_for_response(port, '/api/updated', '{"ping":"updated"}').code).to eq('200') + expect(Net::HTTP.get_response(URI("http://127.0.0.1:#{port}/api/ping")).code).to eq('404') + rescue Timeout::Error + log.rewind + raise "Development server did not return the expected response:\n#{log.read}" + ensure + Process.kill('TERM', -pid) + begin + Timeout.timeout(10) { Process.wait(pid) } + rescue Timeout::Error + Process.kill('KILL', -pid) + Process.wait(pid) + end + end + end + end + end +end