workhorse 1.4.0.rc0 → 1.4.1

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: 66d11e1801fa64ed11dbb329dd843eef6436cdd2d999abf5786ffa013f768dbe
4
- data.tar.gz: d03c50b4d34f32492386ec8c20ba065ad862e7bf9e1c90b03e0641f1f0b8b4db
3
+ metadata.gz: df386a01e22eb4d5e05be449ef11a62341d28c3443fdef76b07ac7e8bca3e3d4
4
+ data.tar.gz: 0a4f325c0bf2cb08357a195a2297d0302df275abf3cd6d83329942d68419f7c8
5
5
  SHA512:
6
- metadata.gz: a3fa8b847ca0d0e68112a4f5c59c34fd24955010185bd5a4c712571e0e1dd8e3340156873bd7dc61f1e1186b04d4bda9cb9d3bfdad036209e760390a43e6a127
7
- data.tar.gz: ad309e55cfb166fcd95b907f75cf2a07009b8d2c49335256505a6e0f54172b44375fd7182b972806601bbd32df43b9431977367fcafe24400ae5769658a30a06
6
+ metadata.gz: 12b8ec75c276bf6d888e3f60b523a32cc387c6c4a935b20004fce4f63acbc4e7046979315eb7c6651953177d7289a3bf27fabc535da117a3d6a9c7be36f17ccd
7
+ data.tar.gz: 1d15f6cf25a9fe2878e9edf45a7b2ce69d0bf0904119efd2d6234e8c15dc7142ce81c427912f22f2459fa27967b58932497424646d5ea9af431d625a1e154a8a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.4.1 - 2026-02-18
4
+
5
+ * Close inherited lockfile fd in forked worker processes. Previously the
6
+ lockfile's file descriptor was inherited by children via `fork`, which could
7
+ prevent the POSIX `flock` from being released if the daemon process exited
8
+ abnormally.
9
+
10
+ * Fix `watch` and `kill` commands to actually abort when the lock is
11
+ unavailable. Previously the `flock` return value with `LOCK_NB` was not
12
+ checked, so the commands would silently proceed without the lock.
13
+
14
+ * Add error handling to the `HUP` signal handler for log reopening. Exceptions
15
+ from `logger.reopen` are now caught and reported via `on_exception`.
16
+
17
+ Sitrox reference: #120574.
18
+
19
+ ## 1.4.0 - 2026-02-12
20
+
21
+ * Stable release based on previous RC release.
22
+
3
23
  ## 1.4.0.rc0 - 2026-02-11
4
24
 
5
25
  * Add `soft-restart` daemon command for graceful worker restarts. Sends a
data/Rakefile CHANGED
@@ -6,6 +6,8 @@ task :gemspec do
6
6
  spec.summary = %(
7
7
  Multi-threaded job backend with database queuing for ruby.
8
8
  )
9
+ spec.license = 'MIT'
10
+ spec.homepage = 'https://github.com/sitrox/workhorse'
9
11
  spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
10
12
  spec.executables = []
11
13
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0.rc0
1
+ 1.4.1
@@ -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
@@ -15,27 +17,43 @@ module Workhorse
15
17
  case ARGV.first
16
18
  when 'start'
17
19
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
20
+ daemon.lockfile = lockfile
18
21
  status = daemon.start
19
22
  when 'stop'
20
23
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
24
+ daemon.lockfile = lockfile
21
25
  status = daemon.stop
22
26
  when 'kill'
23
- lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
24
- status = daemon.stop(true)
27
+ begin
28
+ lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
29
+ daemon.lockfile = lockfile
30
+ status = daemon.stop(true)
31
+ rescue LockNotAvailableError
32
+ status = 1
33
+ end
25
34
  when 'status'
26
35
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
36
+ daemon.lockfile = lockfile
27
37
  status = daemon.status
28
38
  when 'watch'
29
- lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
30
- status = daemon.watch
39
+ begin
40
+ lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
41
+ daemon.lockfile = lockfile
42
+ status = daemon.watch
43
+ rescue LockNotAvailableError
44
+ status = 1
45
+ end
31
46
  when 'restart'
32
47
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
48
+ daemon.lockfile = lockfile
33
49
  status = daemon.restart
34
50
  when 'restart-logging'
35
51
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
52
+ daemon.lockfile = lockfile
36
53
  status = daemon.restart_logging
37
54
  when 'soft-restart'
38
55
  lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
56
+ daemon.lockfile = lockfile
39
57
  status = daemon.soft_restart
40
58
  when 'usage'
41
59
  usage
@@ -105,7 +123,12 @@ module Workhorse
105
123
  def self.acquire_lock(lockfile_path, flags)
106
124
  if Workhorse.lock_shell_commands
107
125
  lockfile = File.open(lockfile_path, 'a')
108
- lockfile.flock(flags)
126
+ result = lockfile.flock(flags)
127
+
128
+ if result == false
129
+ lockfile.close
130
+ fail LockNotAvailableError, 'Could not acquire lock. Is another workhorse command already running?'
131
+ end
109
132
 
110
133
  return lockfile
111
134
  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)
@@ -261,6 +265,8 @@ module Workhorse
261
265
 
262
266
  pid = fork do
263
267
  $0 = process_name(worker)
268
+ # Close inherited lockfile fd to prevent holding the flock after parent exits
269
+ @lockfile&.close
264
270
  # Reopen pipes to prevent #107576
265
271
  $stdin.reopen File.open(File::NULL, 'r')
266
272
  null_out = File.open File::NULL, 'w'
@@ -296,11 +296,14 @@ module Workhorse
296
296
  def trap_log_reopen
297
297
  Signal.trap(LOG_REOPEN_SIGNAL) do
298
298
  Thread.new do
299
- logger.reopen
299
+ logger&.reopen
300
300
 
301
301
  if defined?(ActiveRecord::Base) && ActiveRecord::Base.logger && ActiveRecord::Base.logger != logger
302
302
  ActiveRecord::Base.logger.reopen
303
303
  end
304
+ rescue Exception => e
305
+ log %(Log reopen signal handler error: #{e.message}\n#{e.backtrace.join("\n")}), :error
306
+ Workhorse.on_exception.call(e)
304
307
  end.join
305
308
  end
306
309
  end
@@ -342,12 +345,10 @@ module Workhorse
342
345
 
343
346
  # Monitor in a separate thread to avoid blocking the signal handler
344
347
  @soft_restart_thread = Thread.new do
345
- begin
346
- wait_for_idle_then_shutdown
347
- rescue Exception => e
348
- log %(Soft restart error: #{e.message}\n#{e.backtrace.join("\n")}), :error
349
- Workhorse.on_exception.call(e)
350
- end
348
+ wait_for_idle_then_shutdown
349
+ rescue Exception => e
350
+ log %(Soft restart error: #{e.message}\n#{e.backtrace.join("\n")}), :error
351
+ Workhorse.on_exception.call(e)
351
352
  end
352
353
  end
353
354
 
@@ -360,15 +361,13 @@ module Workhorse
360
361
  # Start a new thread as certain functionality (such as logging) is not
361
362
  # available from within a trap context.
362
363
  Thread.new do
363
- begin
364
- log "\nCaught #{SOFT_RESTART_SIGNAL}, initiating soft restart..."
365
- soft_restart
366
- rescue Exception => e
367
- log %(Soft restart signal handler error: #{e.message}\n#{e.backtrace.join("\n")}), :error
368
- Workhorse.on_exception.call(e)
369
- end
364
+ log "\nCaught #{SOFT_RESTART_SIGNAL}, initiating soft restart..."
365
+ soft_restart
366
+ rescue Exception => e
367
+ log %(Soft restart signal handler error: #{e.message}\n#{e.backtrace.join("\n")}), :error
368
+ Workhorse.on_exception.call(e)
370
369
  end
371
- # Note: Unlike trap_termination, we don't join here because soft_restart
370
+ # NOTE: Unlike trap_termination, we don't join here because soft_restart
372
371
  # is designed to be fire-and-forget (it spawns its own monitoring thread).
373
372
  end
374
373
  end
data/workhorse.gemspec CHANGED
@@ -1,15 +1,17 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.4.0.rc0 ruby lib
2
+ # stub: workhorse 1.4.1 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "1.4.0.rc0"
6
+ s.version = "1.4.1"
7
7
 
8
- s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
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-11"
11
+ s.date = "2026-02-18"
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
+ s.homepage = "https://github.com/sitrox/workhorse".freeze
14
+ s.licenses = ["MIT".freeze]
13
15
  s.rubygems_version = "3.4.6".freeze
14
16
  s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
15
17
  s.test_files = ["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]
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.rc0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-11 00:00:00.000000000 Z
10
+ date: 2026-02-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -100,7 +100,9 @@ files:
100
100
  - test/workhorse/pool_test.rb
101
101
  - test/workhorse/worker_test.rb
102
102
  - workhorse.gemspec
103
- licenses: []
103
+ homepage: https://github.com/sitrox/workhorse
104
+ licenses:
105
+ - MIT
104
106
  metadata: {}
105
107
  rdoc_options: []
106
108
  require_paths:
@@ -112,9 +114,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
114
  version: '0'
113
115
  required_rubygems_version: !ruby/object:Gem::Requirement
114
116
  requirements:
115
- - - ">"
117
+ - - ">="
116
118
  - !ruby/object:Gem::Version
117
- version: 1.3.1
119
+ version: '0'
118
120
  requirements: []
119
121
  rubygems_version: 4.0.2
120
122
  specification_version: 4