Class: PumaPlus::Hooks

Inherits:
Object
  • Object
show all
Defined in:
lib/puma_plus/hooks.rb

Overview

Lifecycle hooks from the config file.

These are the one part of the configuration that cannot be translated into flags for the Go server, because they are blocks. So the config file is read twice: once by the puma-plus launcher, which turns settings into flags and then execs Go, and again here by the Ruby process that actually runs the application, which is the only process where a hook can meaningfully run.

Reading it twice means any top-level side effect in the config file happens twice. That is worth knowing and is why config files should declare rather than do -- the same advice puma's own docs give, for the same reason.

Semantics follow puma exactly (verified against puma/lib/puma/cluster.rb:438 and cluster/worker.rb:58,151):

before_fork          once in the shepherd, before ANY worker is forked
on_worker_boot(idx)  in each worker after fork, before it serves
on_worker_shutdown   in each worker, before it exits

The timing of on_worker_boot is unusually clean here. A worker becomes visible to Go by dialing in, so running boot hooks before that dial means Go cannot dispatch a request to a worker whose hooks have not finished. In puma the equivalent guarantee needs the master to track a booted state; here it falls out of the connection being the readiness signal.

Constant Summary collapse

EMPTY =
{}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hooks, logger: $stderr, path: nil) ⇒ Hooks

Returns a new instance of Hooks.



48
49
50
51
52
# File 'lib/puma_plus/hooks.rb', line 48

def initialize(hooks, logger: $stderr, path: nil)
  @hooks = hooks || EMPTY
  @logger = logger
  @path = path
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



54
55
56
# File 'lib/puma_plus/hooks.rb', line 54

def path
  @path
end

Class Method Details

.load(path, logger: $stderr) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/puma_plus/hooks.rb', line 33

def self.load(path, logger: $stderr)
  return new(EMPTY, logger: logger) unless path && File.exist?(path)

  # A throwaway Options: every non-hook directive in the file still runs and
  # still sets values, and all of them are discarded. The launcher already
  # turned those into flags, and honouring them here would let a worker
  # disagree with the server about its own configuration.
  dsl = ConfigFile.load(path, Options.new({}))
  new(dsl.hooks, logger: logger, path: path)
rescue StandardError => e
  # A config file that raises when read for hooks would otherwise take down
  # a worker with a stack trace and no indication that hooks were the cause.
  raise ConfigError, "loading hooks from #{path}: #{e.class}: #{e.message}"
end

Instance Method Details

#any?(name) ⇒ Boolean

Returns:

  • (Boolean)


56
# File 'lib/puma_plus/hooks.rb', line 56

def any?(name) = !(@hooks[name].nil? || @hooks[name].empty?)

#namesObject



58
# File 'lib/puma_plus/hooks.rb', line 58

def names = @hooks.keys.select { |k| any?(k) }

#run(name, *args, fatal: true) ⇒ Object

Run every block registered for name, in declaration order.

fatal: is the difference between "this process cannot do its job" and "this process is leaving anyway". A failed on_worker_boot means the worker would serve requests without whatever the hook was supposed to establish -- a database connection, most often -- so it dies loudly instead of serving wrongly. A failed shutdown hook is logged and the shutdown continues, because refusing to exit helps nobody.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/puma_plus/hooks.rb', line 68

def run(name, *args, fatal: true)
  blocks = @hooks[name]
  return if blocks.nil? || blocks.empty?

  blocks.each_with_index do |blk, i|
    blk.call(*args)
  rescue StandardError, ScriptError => e
    where = "#{name} hook #{i + 1}/#{blocks.size}#{@path ? " from #{@path}" : ''}"
    if fatal
      @logger.puts "[puma-plus] #{where} failed: #{e.class}: #{e.message}"
      Array(e.backtrace).first(8).each { |l| @logger.puts "[puma-plus]   #{l}" }
      raise HookError, "#{where} failed: #{e.class}: #{e.message}"
    end

    @logger.puts "[puma-plus] #{where} failed (continuing): #{e.class}: #{e.message}"
  end
end