workhorse 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd78ce0b033690529240d1111c8545ab78dfd753fbe7fee551313b2a7d33fc26
4
- data.tar.gz: 9e7e22992ed727f60882f448f0003318cd40142402590cf199812d461db58378
3
+ metadata.gz: 0facaea70a5e826980ea41034914a0609a22fb3ed93c9fbe4a7607eb5a3854c9
4
+ data.tar.gz: 35cc28b0a31206fa5f38df60858c5c2054c28fd7c47a4bf0b4983c9cbbcc02d0
5
5
  SHA512:
6
- metadata.gz: e5d6560a8a6ef01c555222568eb0af6ad44c09484cfeee05093c3ad966e19ebe80f0d6754b85dccab869bb98ef279aede2dbc39da5f7a38b2b1d3eef054136f4
7
- data.tar.gz: 2422ef7003b9dc3e3e3409a794afc57a4ae33226366292dadc89e4afe823289e2aa27c45f5c7b0586262344115d8ca1839b7e84d15980c5a69099c26f3c3c599
6
+ metadata.gz: 551453702ecb4b89060a7e5fdba4639eccbb60c09c85ebd2fd561eec91787c1b8166be81e81b2bd2da46f250ba70c8426198bca404d1cf1e5fbf1c6c9b5096ee
7
+ data.tar.gz: e1ee8b9ae1267271f8be2befd5455d90614e02c8708b2413bc90f957b8d5fb852b2f805ea4d42b6362844c2e7ed774e2a0a727e83687d570d0c430174e264227
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.4.2 - 2026-02-20
4
+
5
+ * Detach forked worker processes into their own session using `Process.setsid`.
6
+ Previously, when the ShellHandler was the session leader (e.g. started via
7
+ cron), its exit would cause the kernel to send `SIGHUP` to all forked workers,
8
+ potentially crashing them during startup before signal handlers were installed.
9
+
10
+ * Add optional debug logging (`config.debug_log_path`) for diagnosing issues
11
+ with signal handling, process lifecycle, log rotation, and daemon commands.
12
+
13
+ Sitrox reference: #120574.
14
+
15
+ ## 1.4.1 - 2026-02-18
16
+
17
+ * Close inherited lockfile fd in forked worker processes. Previously the
18
+ lockfile's file descriptor was inherited by children via `fork`, which could
19
+ prevent the POSIX `flock` from being released if the daemon process exited
20
+ abnormally.
21
+
22
+ * Fix `watch` and `kill` commands to actually abort when the lock is
23
+ unavailable. Previously the `flock` return value with `LOCK_NB` was not
24
+ checked, so the commands would silently proceed without the lock.
25
+
26
+ * Add error handling to the `HUP` signal handler for log reopening. Exceptions
27
+ from `logger.reopen` are now caught and reported via `on_exception`.
28
+
29
+ Sitrox reference: #120574.
30
+
3
31
  ## 1.4.0 - 2026-02-12
4
32
 
5
33
  * Stable release based on previous RC release.
data/README.md CHANGED
@@ -522,7 +522,6 @@ Gem-internal model class `Workhorse::DbJob`, for example:
522
522
 
523
523
  ```ruby
524
524
  # config/initializers/workhorse.rb
525
-
526
525
  ActiveSupport.on_load :workhorse_db_job do
527
526
  # Code within this block will be run inside of the model class
528
527
  # Workhorse::DbJob.
@@ -530,6 +529,23 @@ ActiveSupport.on_load :workhorse_db_job do
530
529
  end
531
530
  ```
532
531
 
532
+ ## Debug logging
533
+
534
+ Workhorse includes an optional debug log for diagnosing issues with signal
535
+ handling, process lifecycle, log rotation, and daemon commands. To enable,
536
+ set `debug_log_path` to a writable file path:
537
+
538
+ ```ruby
539
+ # config/initializers/workhorse.rb
540
+ Workhorse.setup do |config|
541
+ config.debug_log_path = Rails.root.join('log', 'workhorse.debug.log')
542
+ end
543
+ ```
544
+
545
+ The debug log is designed to be safe for production use: all writes are
546
+ best-effort and silently ignore errors to avoid interfering with normal
547
+ operation. Set `debug_log_path` to `nil` (the default) to disable.
548
+
533
549
  ## Caveats
534
550
 
535
551
  ### Errors during polling / crashed workers
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0
1
+ 1.4.2
@@ -1,5 +1,7 @@
1
1
  module Workhorse
2
2
  class Daemon::ShellHandler
3
+ class LockNotAvailableError < StandardError; end
4
+
3
5
  def self.run(**options, &block)
4
6
  unless ARGV.one?
5
7
  usage
@@ -14,28 +16,62 @@ module Workhorse
14
16
  begin
15
17
  case ARGV.first
16
18
  when 'start'
19
+ Workhorse.debug_log('ShellHandler: start command invoked')
17
20
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
21
+ Workhorse.debug_log('ShellHandler: lock acquired for start')
22
+ daemon.lockfile = lockfile
18
23
  status = daemon.start
19
24
  when 'stop'
25
+ Workhorse.debug_log('ShellHandler: stop command invoked')
20
26
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
27
+ Workhorse.debug_log('ShellHandler: lock acquired for stop')
28
+ daemon.lockfile = lockfile
21
29
  status = daemon.stop
22
30
  when 'kill'
23
- lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
24
- status = daemon.stop(true)
31
+ Workhorse.debug_log('ShellHandler: kill command invoked')
32
+ begin
33
+ lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
34
+ Workhorse.debug_log('ShellHandler: lock acquired for kill')
35
+ daemon.lockfile = lockfile
36
+ status = daemon.stop(true)
37
+ rescue LockNotAvailableError
38
+ Workhorse.debug_log('ShellHandler: lock not available for kill')
39
+ status = 1
40
+ end
25
41
  when 'status'
42
+ Workhorse.debug_log('ShellHandler: status command invoked')
26
43
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
44
+ Workhorse.debug_log('ShellHandler: lock acquired for status')
45
+ daemon.lockfile = lockfile
27
46
  status = daemon.status
28
47
  when 'watch'
29
- lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
30
- status = daemon.watch
48
+ Workhorse.debug_log('ShellHandler: watch command invoked')
49
+ begin
50
+ lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
51
+ Workhorse.debug_log('ShellHandler: lock acquired for watch')
52
+ daemon.lockfile = lockfile
53
+ status = daemon.watch
54
+ rescue LockNotAvailableError
55
+ Workhorse.debug_log('ShellHandler: lock not available for watch')
56
+ status = 1
57
+ end
31
58
  when 'restart'
59
+ Workhorse.debug_log('ShellHandler: restart command invoked')
32
60
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
61
+ Workhorse.debug_log('ShellHandler: lock acquired for restart')
62
+ daemon.lockfile = lockfile
33
63
  status = daemon.restart
34
64
  when 'restart-logging'
65
+ Workhorse.debug_log('ShellHandler: restart-logging command invoked')
35
66
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
67
+ Workhorse.debug_log('ShellHandler: lock acquired for restart-logging')
68
+ daemon.lockfile = lockfile
36
69
  status = daemon.restart_logging
37
70
  when 'soft-restart'
71
+ Workhorse.debug_log('ShellHandler: soft-restart command invoked')
38
72
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
73
+ Workhorse.debug_log('ShellHandler: lock acquired for soft-restart')
74
+ daemon.lockfile = lockfile
39
75
  status = daemon.soft_restart
40
76
  when 'usage'
41
77
  usage
@@ -48,7 +84,10 @@ module Workhorse
48
84
  warn "#{e.message}\n#{e.backtrace.join("\n")}"
49
85
  status = 99
50
86
  ensure
51
- lockfile&.flock(File::LOCK_UN)
87
+ if lockfile
88
+ Workhorse.debug_log("ShellHandler: releasing lock for #{ARGV.first}")
89
+ lockfile.flock(File::LOCK_UN)
90
+ end
52
91
  exit! status
53
92
  end
54
93
  end
@@ -105,7 +144,12 @@ module Workhorse
105
144
  def self.acquire_lock(lockfile_path, flags)
106
145
  if Workhorse.lock_shell_commands
107
146
  lockfile = File.open(lockfile_path, 'a')
108
- lockfile.flock(flags)
147
+ result = lockfile.flock(flags)
148
+
149
+ if result == false
150
+ lockfile.close
151
+ fail LockNotAvailableError, 'Could not acquire lock. Is another workhorse command already running?'
152
+ end
109
153
 
110
154
  return lockfile
111
155
  end
@@ -34,6 +34,10 @@ module Workhorse
34
34
  # @private
35
35
  attr_reader :workers
36
36
 
37
+ # @return [File, nil] Lockfile handle to close in forked children
38
+ # @private
39
+ attr_accessor :lockfile
40
+
37
41
  # Creates a new daemon instance.
38
42
  #
39
43
  # @param pidfile [String, nil] Path template for PID files (use %i placeholder for worker ID)
@@ -197,20 +201,27 @@ module Workhorse
197
201
  def restart_logging
198
202
  code = 0
199
203
 
204
+ Workhorse.debug_log("restart_logging: sending HUP to #{@workers.count} worker(s)")
205
+
200
206
  for_each_worker do |worker|
201
207
  _pid_file, pid, active = read_pid(worker)
202
208
 
209
+ Workhorse.debug_log("restart_logging: worker ##{worker.id} (#{worker.name}): pid=#{pid.inspect}, active=#{active.inspect}")
210
+
203
211
  next unless pid && active
204
212
 
205
213
  begin
206
214
  Process.kill 'HUP', pid
215
+ Workhorse.debug_log("restart_logging: HUP sent successfully to PID #{pid}")
207
216
  puts "Worker (#{worker.name}) ##{worker.id}: Sent signal for restart-logging"
208
217
  rescue Errno::ESRCH
218
+ Workhorse.debug_log("restart_logging: HUP failed for PID #{pid}: process not found")
209
219
  warn "Worker (#{worker.name}) ##{worker.id}: Could not send signal for restart-logging, process not found"
210
220
  code = 2
211
221
  end
212
222
  end
213
223
 
224
+ Workhorse.debug_log("restart_logging: done, exit code=#{code}")
214
225
  return code
215
226
  end
216
227
 
@@ -223,20 +234,27 @@ module Workhorse
223
234
  def soft_restart
224
235
  code = 0
225
236
 
237
+ Workhorse.debug_log("Daemon: sending USR1 to #{@workers.count} worker(s)")
238
+
226
239
  for_each_worker do |worker|
227
240
  _pid_file, pid, active = read_pid(worker)
228
241
 
242
+ Workhorse.debug_log("Daemon soft_restart: worker ##{worker.id} (#{worker.name}): pid=#{pid.inspect}, active=#{active.inspect}")
243
+
229
244
  next unless pid && active
230
245
 
231
246
  begin
232
247
  Process.kill 'USR1', pid
248
+ Workhorse.debug_log("Daemon: USR1 sent successfully to PID #{pid}")
233
249
  puts "Worker (#{worker.name}) ##{worker.id}: Sent soft-restart signal"
234
250
  rescue Errno::ESRCH
251
+ Workhorse.debug_log("Daemon: USR1 failed for PID #{pid}: process not found")
235
252
  warn "Worker (#{worker.name}) ##{worker.id}: Process not found"
236
253
  code = 2
237
254
  end
238
255
  end
239
256
 
257
+ Workhorse.debug_log("Daemon soft_restart: done, exit code=#{code}")
240
258
  return code
241
259
  end
242
260
 
@@ -259,8 +277,16 @@ module Workhorse
259
277
  def start_worker(worker)
260
278
  check_rails_env if defined?(Rails)
261
279
 
280
+ Workhorse.debug_log("Daemon: forking worker ##{worker.id} (#{worker.name})")
262
281
  pid = fork do
282
+ # Detach from the parent's session so that the worker is not killed by
283
+ # SIGHUP when the parent (ShellHandler) exits. Without this, the kernel
284
+ # sends SIGHUP to the foreground process group when the session leader
285
+ # (e.g. a cron- or systemd-started ShellHandler) terminates.
286
+ Process.setsid
263
287
  $0 = process_name(worker)
288
+ # Close inherited lockfile fd to prevent holding the flock after parent exits
289
+ @lockfile&.close
264
290
  # Reopen pipes to prevent #107576
265
291
  $stdin.reopen File.open(File::NULL, 'r')
266
292
  null_out = File.open File::NULL, 'w'
@@ -272,6 +298,7 @@ module Workhorse
272
298
  worker.pid = pid
273
299
  File.write(pid_file_for(worker), pid)
274
300
  Process.detach(pid)
301
+ Workhorse.debug_log("Daemon: worker ##{worker.id} (#{worker.name}) forked with PID #{pid}")
275
302
  end
276
303
 
277
304
  # Stops a single worker process.
@@ -284,6 +311,7 @@ module Workhorse
284
311
  def stop_worker(pid_file, pid, kill: false)
285
312
  signals = kill ? %w[KILL] : %w[TERM INT]
286
313
 
314
+ Workhorse.debug_log("Daemon: stopping PID #{pid} with signals #{signals.join(', ')}")
287
315
  loop do
288
316
  begin
289
317
  signals.each { |signal| Process.kill(signal, pid) }
@@ -294,6 +322,7 @@ module Workhorse
294
322
  sleep 1
295
323
  end
296
324
 
325
+ Workhorse.debug_log("Daemon: PID #{pid} stopped")
297
326
  File.delete(pid_file)
298
327
  end
299
328
 
@@ -158,6 +158,8 @@ module Workhorse
158
158
  @poller.start
159
159
  log 'Started up'
160
160
 
161
+ Workhorse.debug_log("[Job worker #{id}] Started: PID=#{pid}, logger=#{describe_logger(logger)}")
162
+
161
163
  trap_termination if @auto_terminate
162
164
  trap_log_reopen
163
165
  trap_soft_restart
@@ -189,12 +191,14 @@ module Workhorse
189
191
  mutex.synchronize do
190
192
  assert_state! :running
191
193
 
194
+ Workhorse.debug_log("[Job worker #{id}] Shutdown starting")
192
195
  log 'Shutting down'
193
196
  @state = :shutdown
194
197
 
195
198
  @poller.shutdown
196
199
  @pool.shutdown
197
200
  log 'Shut down'
201
+ Workhorse.debug_log("[Job worker #{id}] Shutdown complete")
198
202
  end
199
203
  end
200
204
 
@@ -267,6 +271,8 @@ module Workhorse
267
271
 
268
272
  return true unless exceeded
269
273
 
274
+ Workhorse.debug_log("[Job worker #{id}] Memory limit exceeded: #{mem}MB > #{max}MB, initiating shutdown")
275
+
270
276
  if defined?(Rails)
271
277
  FileUtils.touch self.class.shutdown_file_for(pid)
272
278
  end
@@ -296,11 +302,16 @@ module Workhorse
296
302
  def trap_log_reopen
297
303
  Signal.trap(LOG_REOPEN_SIGNAL) do
298
304
  Thread.new do
299
- logger.reopen
305
+ Workhorse.debug_log("[Job worker #{id}] HUP received, logger state before reopen: #{describe_logger(logger)}")
300
306
 
301
- if defined?(ActiveRecord::Base) && ActiveRecord::Base.logger && ActiveRecord::Base.logger != logger
302
- ActiveRecord::Base.logger.reopen
303
- end
307
+ logger&.reopen
308
+ Workhorse.debug_log("[Job worker #{id}] Logger state after reopen: #{describe_logger(logger)}")
309
+
310
+ Workhorse.debug_log("[Job worker #{id}] HUP handling complete")
311
+ rescue Exception => e
312
+ Workhorse.debug_log("[Job worker #{id}] Logger reopen failed: #{e.class}: #{e.message}")
313
+ log %(Log reopen signal handler error: #{e.message}\n#{e.backtrace.join("\n")}), :error
314
+ Workhorse.on_exception.call(e)
304
315
  end.join
305
316
  end
306
317
  end
@@ -317,6 +328,7 @@ module Workhorse
317
328
  # quickly when called multiple times, this does not pose a risk of
318
329
  # keeping open a big number of "shutdown threads".
319
330
  Thread.new do
331
+ Workhorse.debug_log("[Job worker #{id}] #{signal} received, shutting down")
320
332
  log "\nCaught #{signal}, shutting worker down..."
321
333
  shutdown
322
334
  end.join
@@ -336,9 +348,14 @@ module Workhorse
336
348
 
337
349
  return unless @soft_restart_requested.make_true
338
350
 
351
+ Workhorse.debug_log("[Job worker #{id}] Soft restart initiated")
352
+
339
353
  # Create shutdown file for watch to detect
340
354
  shutdown_file = self.class.shutdown_file_for(pid)
341
- FileUtils.touch(shutdown_file) if shutdown_file
355
+ if shutdown_file
356
+ FileUtils.touch(shutdown_file)
357
+ Workhorse.debug_log("[Job worker #{id}] Shutdown file created: #{shutdown_file}")
358
+ end
342
359
 
343
360
  # Monitor in a separate thread to avoid blocking the signal handler
344
361
  @soft_restart_thread = Thread.new do
@@ -358,6 +375,7 @@ module Workhorse
358
375
  # Start a new thread as certain functionality (such as logging) is not
359
376
  # available from within a trap context.
360
377
  Thread.new do
378
+ Workhorse.debug_log("[Job worker #{id}] #{SOFT_RESTART_SIGNAL} received, initiating soft restart")
361
379
  log "\nCaught #{SOFT_RESTART_SIGNAL}, initiating soft restart..."
362
380
  soft_restart
363
381
  rescue Exception => e
@@ -369,16 +387,52 @@ module Workhorse
369
387
  end
370
388
  end
371
389
 
390
+ # Returns a human-readable description of a logger's internal state.
391
+ # Used for debug logging to diagnose log rotation issues.
392
+ #
393
+ # @param lgr [Logger, nil] The logger to describe
394
+ # @return [String] Description of the logger's state
395
+ # @private
396
+ def describe_logger(lgr)
397
+ return 'nil' unless lgr
398
+
399
+ parts = ["class=#{lgr.class}"]
400
+
401
+ logdev = lgr.instance_variable_get(:@logdev)
402
+ if logdev
403
+ parts << "filename=#{logdev.filename.inspect}" if logdev.respond_to?(:filename)
404
+
405
+ dev = logdev.respond_to?(:dev) ? logdev.dev : nil
406
+ if dev
407
+ parts << "closed=#{dev.closed?}"
408
+ unless dev.closed?
409
+ fileno = dev.fileno
410
+ parts << "fd=#{fileno}"
411
+ fd_path = "/proc/self/fd/#{fileno}"
412
+ parts << "fd_target=#{File.readlink(fd_path).inspect}" if File.exist?(fd_path)
413
+ end
414
+ end
415
+ else
416
+ parts << 'logdev=nil'
417
+ end
418
+
419
+ parts.join(', ')
420
+ rescue Exception => e
421
+ "error describing logger: #{e.class}: #{e.message}"
422
+ end
423
+
372
424
  # Waits for all jobs to complete, then shuts down the worker.
373
425
  # Called asynchronously from soft_restart.
374
426
  #
375
427
  # @return [void]
376
428
  # @private
377
429
  def wait_for_idle_then_shutdown
430
+ Workhorse.debug_log("[Job worker #{id}] Waiting for idle before soft restart shutdown (pool_size=#{@pool_size})")
378
431
  loop do
379
432
  break if @state == :shutdown
380
433
 
381
434
  if idle == @pool_size
435
+ Workhorse.debug_log("[Job worker #{id}] All threads idle, proceeding with soft restart shutdown")
382
436
  log 'All jobs completed, shutting down for soft restart'
383
437
  shutdown
384
438
  break
data/lib/workhorse.rb CHANGED
@@ -107,6 +107,31 @@ module Workhorse
107
107
  mattr_accessor :max_worker_memory_mb
108
108
  self.max_worker_memory_mb = 0
109
109
 
110
+ # Path to a debug log file for diagnosing log rotation and signal handling issues.
111
+ # When set, Workhorse writes timestamped debug entries to this file at key points
112
+ # (worker startup, HUP signal handling, restart-logging command flow).
113
+ # Set to nil to disable (default).
114
+ #
115
+ # @return [String, nil] Path to debug log file
116
+ mattr_accessor :debug_log_path
117
+ self.debug_log_path = nil
118
+
119
+ # Writes a debug message to the debug log file.
120
+ # Does nothing if {.debug_log_path} is nil.
121
+ # Silently ignores all exceptions to avoid interfering with normal operation.
122
+ #
123
+ # @param message [String] The message to log
124
+ # @return [void]
125
+ def self.debug_log(message)
126
+ return unless debug_log_path
127
+
128
+ File.open(debug_log_path, 'a') do |f|
129
+ f.write("[#{Time.now.iso8601(3)}] [PID #{Process.pid}] #{message}\n")
130
+ f.flush
131
+ end
132
+ rescue Exception # rubocop:disable Lint/SuppressedException
133
+ end
134
+
110
135
  # Configuration method for setting up Workhorse options.
111
136
  #
112
137
  # @yield [self] Configuration block
data/workhorse.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.4.0 ruby lib
2
+ # stub: workhorse 1.4.2 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "1.4.0"
6
+ s.version = "1.4.2"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Sitrox".freeze]
11
- s.date = "2026-02-12"
11
+ s.date = "2026-02-20"
12
12
  s.files = [".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "CHANGELOG.md".freeze, "FAQ.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "RUBY_VERSION".freeze, "Rakefile".freeze, "VERSION".freeze, "bin/rubocop".freeze, "lib/active_job/queue_adapters/workhorse_adapter.rb".freeze, "lib/generators/workhorse/install_generator.rb".freeze, "lib/generators/workhorse/templates/bin/workhorse.rb".freeze, "lib/generators/workhorse/templates/config/initializers/workhorse.rb".freeze, "lib/generators/workhorse/templates/create_table_jobs.rb".freeze, "lib/workhorse.rb".freeze, "lib/workhorse/active_job_extension.rb".freeze, "lib/workhorse/daemon.rb".freeze, "lib/workhorse/daemon/shell_handler.rb".freeze, "lib/workhorse/db_job.rb".freeze, "lib/workhorse/enqueuer.rb".freeze, "lib/workhorse/jobs/cleanup_succeeded_jobs.rb".freeze, "lib/workhorse/jobs/detect_stale_jobs_job.rb".freeze, "lib/workhorse/jobs/run_active_job.rb".freeze, "lib/workhorse/jobs/run_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/scoped_env.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/daemon_test.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "workhorse.gemspec".freeze]
13
13
  s.homepage = "https://github.com/sitrox/workhorse".freeze
14
14
  s.licenses = ["MIT".freeze]
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workhorse
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-12 00:00:00.000000000 Z
10
+ date: 2026-02-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport