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



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/workhorse/daemon.rb', line 47

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

#lockfileFile?

Returns Lockfile handle to close in forked children.

Returns:

  • (File, nil)

    Lockfile handle to close in forked children



39
40
41
# File 'lib/workhorse/daemon.rb', line 39

def lockfile
  @lockfile
end

#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



213
214
215
216
# File 'lib/workhorse/daemon.rb', line 213

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)



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/workhorse/daemon.rb', line 222

def restart_logging
  code = 0

  Workhorse.debug_log("restart_logging: sending HUP to #{@workers.count} worker(s)")

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

    Workhorse.debug_log("restart_logging: worker ##{worker.id} (#{worker.name}): pid=#{pid.inspect}, active=#{active.inspect}")

    next unless pid && active

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

  Workhorse.debug_log("restart_logging: done, exit code=#{code}")
  return code
end

#soft_restartInteger

Sends USR1 signal to all workers to initiate a soft restart. Workers will finish their current jobs before shutting down. The watch mechanism will then start fresh workers. This method returns immediately (fire-and-forget).

Returns:

  • (Integer)

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



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/workhorse/daemon.rb', line 255

def soft_restart
  code = 0

  Workhorse.debug_log("Daemon: sending USR1 to #{@workers.count} worker(s)")

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

    Workhorse.debug_log("Daemon soft_restart: worker ##{worker.id} (#{worker.name}): pid=#{pid.inspect}, active=#{active.inspect}")

    next unless pid && active

    begin
      Process.kill 'USR1', pid
      Workhorse.debug_log("Daemon: USR1 sent successfully to PID #{pid}")
      puts "Worker (#{worker.name}) ##{worker.id}: Sent soft-restart signal"
    rescue Errno::ESRCH
      Workhorse.debug_log("Daemon: USR1 failed for PID #{pid}: process not found")
      warn "Worker (#{worker.name}) ##{worker.id}: Process not found"
      code = 2
    end
  end

  Workhorse.debug_log("Daemon soft_restart: done, exit code=#{code}")
  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)



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
116
117
118
119
120
121
122
123
124
125
# File 'lib/workhorse/daemon.rb', line 82

def start(quiet: false)
  code = 0

  Workhorse.debug_log("Daemon: starting #{@workers.count} worker(s)")

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

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

    if pid_file && pid && active
      Workhorse.debug_log("Daemon start: worker ##{worker.id} (#{worker.name}) already running (PID #{pid})")
      messages << ["Worker ##{worker.id} (#{worker.name}): Already started (PID #{pid})", 2] unless quiet
      code = 2
    elsif pid_file
      Workhorse.debug_log("Daemon start: worker ##{worker.id} (#{worker.name}) has stale pid file (PID #{pid.inspect}), starting")
      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
      Workhorse.debug_log("Daemon start: worker ##{worker.id} (#{worker.name}) not running, starting")
      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

  Workhorse.debug_log("Daemon: start complete, exit code=#{code}")
  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)



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/workhorse/daemon.rb', line 163

def status(quiet: false)
  code = 0

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

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

  Workhorse.debug_log("Daemon: status complete, exit code=#{code}")
  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)



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/workhorse/daemon.rb', line 132

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

  Workhorse.debug_log("Daemon: stopping #{@workers.count} worker(s) (kill=#{kill})")

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

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

  Workhorse.debug_log("Daemon: stop complete, exit code=#{code}")
  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



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/workhorse/daemon.rb', line 191

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

  status_code = status(quiet: true)
  Workhorse.debug_log("Daemon watch: should_be_running=#{should_be_running}, status_code=#{status_code}")

  if should_be_running && status_code != 0
    Workhorse.debug_log('Daemon watch: starting workers')
    return start(quiet: Workhorse.silence_watcher)
  else
    Workhorse.debug_log('Daemon watch: no action needed')
    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



74
75
76
# File 'lib/workhorse/daemon.rb', line 74

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