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 +4 -4
- data/CHANGELOG.md +20 -0
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/lib/workhorse/daemon/shell_handler.rb +28 -5
- data/lib/workhorse/daemon.rb +6 -0
- data/lib/workhorse/worker.rb +14 -15
- data/workhorse.gemspec +6 -4
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df386a01e22eb4d5e05be449ef11a62341d28c3443fdef76b07ac7e8bca3e3d4
|
|
4
|
+
data.tar.gz: 0a4f325c0bf2cb08357a195a2297d0302df275abf3cd6d83329942d68419f7c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
data/lib/workhorse/daemon.rb
CHANGED
|
@@ -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'
|
data/lib/workhorse/worker.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
6
|
+
s.version = "1.4.1"
|
|
7
7
|
|
|
8
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
|
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
|
+
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.
|
|
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-
|
|
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
|
-
|
|
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:
|
|
119
|
+
version: '0'
|
|
118
120
|
requirements: []
|
|
119
121
|
rubygems_version: 4.0.2
|
|
120
122
|
specification_version: 4
|