Class: Workhorse::Daemon

Inherits:
Object
  • Object
show all
Defined in:
lib/workhorse/daemon.rb

Overview

Daemon class for managing multiple worker processes. Provides functionality to start, stop, restart, and monitor worker processes through a simple Ruby DSL.

Defined Under Namespace

Classes: ShellHandler, Worker

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pidfile: nil, quiet: false) {|ScopedEnv| ... } ⇒ Daemon

Creates a new daemon instance.

Parameters:

  • pidfile (String, nil) (defaults to: nil)

    Path template for PID files (use %i placeholder for worker ID)

  • quiet (Boolean) (defaults to: false)

    Whether to suppress output during operations

Yields:

  • (ScopedEnv)

    Configuration block for defining workers

Raises:

  • (RuntimeError)

    If no workers are defined or pidfile format is invalid



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/workhorse/daemon.rb', line 43

def initialize(pidfile: nil, quiet: false, &_block)
  @pidfile = pidfile
  @quiet = quiet
  @workers = []

  yield ScopedEnv.new(self, [:worker])

  @count = @workers.count

  fail 'No workers are defined.' if @count < 1

  FileUtils.mkdir_p('tmp/pids')

  if @pidfile.nil?
    @pidfile = @count > 1 ? 'tmp/pids/workhorse.%i.pid' : 'tmp/pids/workhorse.pid'
  elsif @count > 1 && !@pidfile.include?('%s')
    fail 'Pidfile must include placeholder "%s" for worker id when specifying a count > 1.'
  elsif @count == 0 && @pidfile.include?('%s')
    fail 'Pidfile must not include placeholder "%s" for worker id when specifying a count of 1.'
  end
end

Instance Attribute Details

#workersArray<Worker> (readonly)

Returns Array of defined workers.

Returns:

  • (Array<Worker>)

    Array of defined workers



35
36
37
# File 'lib/workhorse/daemon.rb', line 35

def workers
  @workers
end

Instance Method Details

#restartInteger

Restarts all workers by stopping and then starting them.

Returns:

  • (Integer)

    Exit code from start operation



188
189
190
191
# File 'lib/workhorse/daemon.rb', line 188

def restart
  stop
  return start
end

#restart_loggingInteger

Sends HUP signal to all workers to restart their logging. Useful for log rotation without full process restart.

Returns:

  • (Integer)

    Exit code (0 = success, 2 = some signals failed)



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/workhorse/daemon.rb', line 197

def restart_logging
  code = 0

  for_each_worker do |worker|
    _pid_file, pid, active = read_pid(worker)

    next unless pid && active

    begin
      Process.kill 'HUP', pid
      puts "Worker (#{worker.name}) ##{worker.id}: Sent signal for restart-logging"
    rescue Errno::ESRCH
      warn "Worker (#{worker.name}) ##{worker.id}: Could not send signal for restart-logging, process not found"
      code = 2
    end
  end

  return code
end

#start(quiet: false) ⇒ Integer

Starts all defined workers.

Parameters:

  • quiet (Boolean) (defaults to: false)

    Whether to suppress status output

Returns:

  • (Integer)

    Exit code (0 = success, 2 = some workers already running)



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/workhorse/daemon.rb', line 78

def start(quiet: false)
  code = 0

  # Holds messages in format [[<message>, <severity>]]
  messages = []

  for_each_worker do |worker|
    pid_file, pid, active = read_pid(worker)

    if pid_file && pid && active
      messages << ["Worker ##{worker.id} (#{worker.name}): Already started (PID #{pid})", 2] unless quiet
      code = 2
    elsif pid_file
      File.delete pid_file

      shutdown_file = pid ? Workhorse::Worker.shutdown_file_for(pid) : nil
      shutdown_file = nil if shutdown_file && !File.exist?(shutdown_file)

      messages << ["Worker ##{worker.id} (#{worker.name}): Starting (stale pid file)", 1] unless quiet || shutdown_file
      start_worker worker
      FileUtils.rm(shutdown_file) if shutdown_file
    else
      messages << ["Worker ##{worker.id} (#{worker.name}): Starting", 1] unless quiet
      start_worker worker
    end
  end

  if messages.any?
    min = messages.min_by(&:last)[1]

    # Only print messages if there is at least one message with severity 1
    if min == 1
      messages.each { |(message, _severity)| warn message }
    end
  end

  return code
end

#status(quiet: false) ⇒ Integer

Checks the status of all workers.

Parameters:

  • quiet (Boolean) (defaults to: false)

    Whether to suppress status output

Returns:

  • (Integer)

    Exit code (0 = all running, 2 = some not running)



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/workhorse/daemon.rb', line 147

def status(quiet: false)
  code = 0

  for_each_worker do |worker|
    pid_file, pid, active = read_pid(worker)

    if pid_file && pid && active
      puts "Worker ##{worker.id} (#{worker.name}): Running" unless quiet
    elsif pid_file
      warn "Worker ##{worker.id} (#{worker.name}): Not running (stale PID file)" unless quiet
      code = 2
    else
      warn "Worker ##{worker.id} (#{worker.name}): Not running" unless quiet
      code = 2
    end
  end

  return code
end

#stop(kill = false, quiet: false) ⇒ Integer

Stops all running workers.

Parameters:

  • kill (Boolean) (defaults to: false)

    Whether to use KILL signal instead of TERM/INT

  • quiet (Boolean) (defaults to: false)

    Whether to suppress status output

Returns:

  • (Integer)

    Exit code (0 = success, 2 = some workers already stopped)



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/workhorse/daemon.rb', line 122

def stop(kill = false, quiet: false)
  code = 0

  for_each_worker do |worker|
    pid_file, pid, active = read_pid(worker)

    if pid_file && pid && active
      puts "Worker (#{worker.name}) ##{worker.id}: Stopping" unless quiet
      stop_worker pid_file, pid, kill: kill
    elsif pid_file
      File.delete pid_file
      puts "Worker (#{worker.name}) ##{worker.id}: Already stopped (stale PID file)" unless quiet
    else
      warn "Worker (#{worker.name}) ##{worker.id}: Already stopped" unless quiet
      code = 2
    end
  end

  return code
end

#watchInteger

Watches workers and starts them if they’re not running. In Rails environments, respects the tmp/stop.txt file.

Returns:

  • (Integer)

    Exit code from start operation or 0 if no action needed



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/workhorse/daemon.rb', line 171

def watch
  if defined?(Rails)
    should_be_running = !File.exist?(Rails.root.join('tmp/stop.txt'))
  else
    should_be_running = true
  end

  if should_be_running && status(quiet: true) != 0
    return start(quiet: Workhorse.silence_watcher)
  else
    return 0
  end
end

#worker(name = 'Job Worker') { ... } ⇒ void

This method returns an undefined value.

Defines a worker process.

Parameters:

  • name (String) (defaults to: 'Job Worker')

    Display name for the worker

Yields:

  • Block containing the worker’s execution logic



70
71
72
# File 'lib/workhorse/daemon.rb', line 70

def worker(name = 'Job Worker', &block)
  @workers << Worker.new(@workers.size + 1, name, &block)
end