Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Catch exceptions in hooks defined by users #2155

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions History.md
Expand Up @@ -17,6 +17,7 @@
* Preserve `BUNDLE_GEMFILE` env var when using `prune_bundler` (#1893)
* Send 408 request timeout even when queue requests is disabled (#2119)
* Rescue IO::WaitReadable instead of EAGAIN for blocking read (#2121)
* Rescue and log exceptions in hooks defined by users (on_worker_boot, after_worker_fork etc) (#1551)

* Refactor
* Remove unused loader argument from Plugin initializer (#2095)
Expand Down
10 changes: 5 additions & 5 deletions lib/puma/cluster.rb
Expand Up @@ -137,7 +137,7 @@ def spawn_workers

diff.times do
idx = next_worker_index
@launcher.config.run_hooks :before_worker_fork, idx
@launcher.config.run_hooks :before_worker_fork, idx, @launcher.events

pid = fork { worker(idx, master) }
if !pid
Expand All @@ -149,7 +149,7 @@ def spawn_workers
debug "Spawned worker: #{pid}"
@workers << Worker.new(idx, pid, @phase, @options)

@launcher.config.run_hooks :after_worker_fork, idx
@launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
end

if diff > 0
Expand Down Expand Up @@ -268,7 +268,7 @@ def worker(index, master)

# Invoke any worker boot hooks so they can get
# things in shape before booting the app.
@launcher.config.run_hooks :before_worker_boot, index
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events

server = start_server

Expand Down Expand Up @@ -310,7 +310,7 @@ def worker(index, master)

# Invoke any worker shutdown hooks so they can prevent the worker
# exiting until any background operations are completed
@launcher.config.run_hooks :before_worker_shutdown, index
@launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
ensure
@worker_write << "t#{Process.pid}\n" rescue nil
@worker_write.close
Expand Down Expand Up @@ -486,7 +486,7 @@ def run

@master_read, @worker_write = read, @wakeup

@launcher.config.run_hooks :before_fork, nil
@launcher.config.run_hooks :before_fork, nil, @launcher.events
GC.compact if GC.respond_to?(:compact)

spawn_workers
Expand Down
11 changes: 9 additions & 2 deletions lib/puma/configuration.rb
Expand Up @@ -275,8 +275,15 @@ def load_plugin(name)
@plugins.create name
end

def run_hooks(key, arg)
@options.all_of(key).each { |b| b.call arg }
def run_hooks(key, arg, events)
@options.all_of(key).each do |b|
begin
b.call arg
rescue => e
events.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
events.debug e.backtrace.join("\n")
end
end
end

def self.temp_path
Expand Down
2 changes: 1 addition & 1 deletion lib/puma/launcher.rb
Expand Up @@ -237,7 +237,7 @@ def reload_worker_directory
end

def restart!
@config.run_hooks :on_restart, self
@config.run_hooks :on_restart, self, @events

if Puma.jruby?
close_binder_listeners
Expand Down
56 changes: 56 additions & 0 deletions test/test_config.rb
Expand Up @@ -4,6 +4,7 @@
require_relative "helpers/config_file"

require "puma/configuration"
require 'puma/events'

class TestConfigFile < TestConfigFileBase
parallelize_me!
Expand Down Expand Up @@ -197,6 +198,61 @@ def test_config_raise_exception_on_sigterm
conf.options[:raise_exception_on_sigterm] = true
assert_equal conf.options[:raise_exception_on_sigterm], true
end

def test_run_hooks_on_restart_hook
assert_run_hooks :on_restart
end

def test_run_hooks_before_worker_fork
assert_run_hooks :before_worker_fork, configured_with: :on_worker_fork
end

def test_run_hooks_after_worker_fork
assert_run_hooks :after_worker_fork
end

def test_run_hooks_before_worker_boot
assert_run_hooks :before_worker_boot, configured_with: :on_worker_boot
end

def test_run_hooks_before_worker_shutdown
assert_run_hooks :before_worker_shutdown, configured_with: :on_worker_shutdown
end

def test_run_hooks_before_fork
assert_run_hooks :before_fork
end

def test_run_hooks_and_exception
conf = Puma::Configuration.new do |c|
c.on_restart do |a|
raise RuntimeError, 'Error from hook'
end
end
conf.load
events = Puma::Events.strings

conf.run_hooks :on_restart, 'ARG', events
expected = /WARNING hook on_restart failed with exception \(RuntimeError\) Error from hook/
assert_match expected, events.stdout.string
end

private

def assert_run_hooks(hook_name, options = {})
configured_with = options[:configured_with] || hook_name

messages = []
conf = Puma::Configuration.new do |c|
c.send(configured_with) do |a|
messages << "#{hook_name} is called with #{a}"
end
end
conf.load

conf.run_hooks hook_name, 'ARG', Puma::Events.strings
assert_equal messages, ["#{hook_name} is called with ARG"]
end
end

# Thread unsafe modification of ENV
Expand Down