workhorse 1.4.4 → 1.5.0

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: 02c3e3163a1b21b8a983c756e9be5e916b2cc7017401ab3484905c04fe56293f
4
- data.tar.gz: 52ffb9742bf24ea226e3aa3f7edf2f0a5deecdefa57310c3176a40bec00f227f
3
+ metadata.gz: d1159cb21bdb8fcb4a31ae5857ad5dcc16191879ac6f8025830b97cb6627df8c
4
+ data.tar.gz: c963ee57e992cf754cde1e91df6a93d7d207aff663b3bbdae443050605918e63
5
5
  SHA512:
6
- metadata.gz: ce5cd0d30660a0bb9829c84957e76589d5e65e9c47795c7ff414101df2b57f8661fbf01cd4b73bb4a69b7901f4fc43796a3a43e6f400c31b5c1a5b6b1eff8e56
7
- data.tar.gz: 3594464e0fd00d036c9fe163daeea5a17d78d2fcd79095be05dac95753fbce43530812881529052af28917240920f70f706b708f7902d38636caa4e5c6995d3a
6
+ metadata.gz: 05b57e6750c6bdf4d693d503f0d0659f7dcb23c49c6a3f11d47ed31ba665cf5b5bae1f1aa6c6ea925fa35125260da37e9b31aeccd644e9cbefce26c50284104b
7
+ data.tar.gz: f9832222d5b3bd0edb56089c06a6eb1ba36ef4d496580aab83d75a75760c583dd2b74647a0d196be58b3e4f5b91096228bee0cbb7df7cfe9b6efbb7bc92bb32e
data/.releaser_config CHANGED
@@ -1,3 +1,3 @@
1
1
  version_file: VERSION
2
- always_from_master: false
2
+ always_from_master: true
3
3
  gem_style: github
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.5.0 - 2026-07-01
4
+
5
+ * Change `DetectStaleJobsJob` to accept `locked_to_started_threshold` and
6
+ `run_time_threshold` as keyword arguments instead of reading from global
7
+ configuration. The config options `Workhorse.stale_detection_locked_to_started_threshold`
8
+ and `Workhorse.stale_detection_run_time_threshold` have been removed.
9
+
10
+ If you were previously configuring thresholds globally:
11
+
12
+ ```ruby
13
+ # Before
14
+ Workhorse.setup do |config|
15
+ config.stale_detection_locked_to_started_threshold = 300
16
+ config.stale_detection_run_time_threshold = 3600
17
+ end
18
+ Workhorse.enqueue(DetectStaleJobsJob.new)
19
+
20
+ # After
21
+ Workhorse.enqueue(DetectStaleJobsJob.new(locked_to_started_threshold: 300, run_time_threshold: 3600))
22
+ ```
23
+
24
+ * Add `queues` keyword argument to `DetectStaleJobsJob` to restrict stale job
25
+ detection to specific queues. When omitted, all queues are checked (existing
26
+ behavior).
27
+
28
+ Sitrox reference: #132256.
29
+
30
+ ## 1.4.5 - 2026-05-09
31
+
32
+ * Close the lockfile after releasing the lock in the ShellHandler.
33
+
34
+ Sitrox reference: #120574.
35
+
36
+ * Prevent YJIT from being enabled when `RUBY_YJIT_ENABLE=0` is set. This allows
37
+ running workhorse commands in systemd services that set
38
+ `MemoryDenyWriteExecute=yes`.
39
+
40
+ Sitrox reference: #120574.
41
+
3
42
  ## 1.4.4 - 2026-04-28
4
43
 
5
44
  * Make debug logging (enabled if `config.debug_log_path` is set) more verbose.
data/README.md CHANGED
@@ -529,6 +529,17 @@ ActiveSupport.on_load :workhorse_db_job do
529
529
  end
530
530
  ```
531
531
 
532
+ ## Running in systemd services
533
+
534
+ When running workhorse commands inside a systemd service that sets
535
+ `MemoryDenyWriteExecute=yes`, Ruby's YJIT cannot allocate executable memory and
536
+ will crash. To prevent this, set `RUBY_YJIT_ENABLE=0` before invoking the
537
+ workhorse command:
538
+
539
+ ```bash
540
+ RUBY_YJIT_ENABLE=0 ./bin/workhorse restart-logging
541
+ ```
542
+
532
543
  ## Debug logging
533
544
 
534
545
  Workhorse includes an optional debug log for diagnosing issues with signal
data/RUBY_VERSION CHANGED
@@ -1 +1 @@
1
- ruby-3.2.1-p31
1
+ ruby-3.3.5
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.4
1
+ 1.5.0
@@ -88,6 +88,7 @@ module Workhorse
88
88
  if lockfile
89
89
  Workhorse.debug_log("ShellHandler: releasing lock for #{ARGV.first}")
90
90
  lockfile.flock(File::LOCK_UN)
91
+ lockfile.close
91
92
  end
92
93
  Workhorse.debug_log("ShellHandler: exiting with status #{status}")
93
94
  exit! status
@@ -145,7 +146,7 @@ module Workhorse
145
146
 
146
147
  def self.acquire_lock(lockfile_path, flags)
147
148
  if Workhorse.lock_shell_commands
148
- lockfile = File.open(lockfile_path, 'a')
149
+ lockfile = File.open(lockfile_path, 'a') # rubocop:disable Style/FileOpen
149
150
  result = lockfile.flock(flags)
150
151
 
151
152
  if result == false
@@ -310,7 +310,7 @@ module Workhorse
310
310
  @lockfile&.close
311
311
  # Reopen pipes to prevent #107576
312
312
  $stdin.reopen File.open(File::NULL, 'r')
313
- null_out = File.open File::NULL, 'w'
313
+ null_out = File.open(File::NULL, 'w') # rubocop:disable Style/FileOpen
314
314
  $stdout.reopen null_out
315
315
  $stderr.reopen null_out
316
316
 
@@ -5,24 +5,27 @@ module Workhorse::Jobs
5
5
  # exception is thrown (which may cause a notification if you configured
6
6
  # {Workhorse.on_exception} accordingly).
7
7
  #
8
- # The thresholds are obtained from the configuration options
9
- # {Workhorse.stale_detection_locked_to_started_threshold} and
10
- # {Workhorse.stale_detection_run_time_threshold}.
11
- #
12
- # @example Schedule stale job detection
8
+ # @example Schedule stale job detection with default thresholds
13
9
  # Workhorse.enqueue(DetectStaleJobsJob.new)
14
10
  #
15
- # @example Configure thresholds
16
- # Workhorse.setup do |config|
17
- # config.stale_detection_locked_to_started_threshold = 300 # 5 minutes
18
- # config.stale_detection_run_time_threshold = 3600 # 1 hour
19
- # end
11
+ # @example Schedule with custom thresholds
12
+ # Workhorse.enqueue(DetectStaleJobsJob.new(locked_to_started_threshold: 300, run_time_threshold: 3600))
13
+ #
14
+ # @example Only check specific queues
15
+ # Workhorse.enqueue(DetectStaleJobsJob.new(queues: ['mailer', 'reports']))
20
16
  class DetectStaleJobsJob
21
17
  # Creates a new stale job detection job.
22
- # Reads configuration thresholds at initialization time.
23
- def initialize
24
- @locked_to_started_threshold = Workhorse.stale_detection_locked_to_started_threshold
25
- @run_time_threshold = Workhorse.stale_detection_run_time_threshold
18
+ #
19
+ # @param locked_to_started_threshold [Integer] Maximum number of seconds a job is
20
+ # allowed to stay 'locked' before an exception is raised. Set to 0 to skip this check.
21
+ # @param run_time_threshold [Integer] Maximum number of seconds a job is allowed to
22
+ # run before an exception is raised. Set to 0 to skip this check.
23
+ # @param queues [Array<String>, nil] If given, only check jobs in these queues.
24
+ # If `nil` (default), all queues are checked.
25
+ def initialize(locked_to_started_threshold: 3 * 60, run_time_threshold: 12 * 60, queues: nil)
26
+ @locked_to_started_threshold = locked_to_started_threshold
27
+ @run_time_threshold = run_time_threshold
28
+ @queues = queues
26
29
  end
27
30
 
28
31
  # Executes the stale job detection.
@@ -38,6 +41,7 @@ module Workhorse::Jobs
38
41
  if @locked_to_started_threshold != 0
39
42
  rel = Workhorse::DbJob.locked
40
43
  rel = rel.where('locked_at < ?', @locked_to_started_threshold.seconds.ago)
44
+ rel = rel.where(queue: @queues) if @queues
41
45
  ids = rel.pluck(:id)
42
46
 
43
47
  unless ids.empty?
@@ -50,6 +54,7 @@ module Workhorse::Jobs
50
54
  if @run_time_threshold != 0
51
55
  rel = Workhorse::DbJob.started
52
56
  rel = rel.where('started_at < ?', @run_time_threshold.seconds.ago)
57
+ rel = rel.where(queue: @queues) if @queues
53
58
  ids = rel.pluck(:id)
54
59
 
55
60
  unless ids.empty?
data/lib/workhorse.rb CHANGED
@@ -8,6 +8,16 @@ require 'workhorse/enqueuer'
8
8
  require 'workhorse/scoped_env'
9
9
  require 'workhorse/active_job_extension'
10
10
 
11
+ # Prevent YJIT from being enabled when RUBY_YJIT_ENABLE is explicitly set to 0.
12
+ # systemd's logrotate.service typically sets MemoryDenyWriteExecute=yes, which
13
+ # prevents mprotect(PROT_EXEC). Rails unconditionally calls
14
+ # RubyVM::YJIT.enable on boot, which triggers a fatal Ruby [BUG] in that
15
+ # environment. Set RUBY_YJIT_ENABLE=0 in the logrotate postrotate script
16
+ # to prevent this.
17
+ if ENV['RUBY_YJIT_ENABLE'] == '0' && defined?(RubyVM::YJIT) && !RubyVM::YJIT.enabled?
18
+ RubyVM::YJIT.define_singleton_method(:enable) { |**| nil }
19
+ end
20
+
11
21
  # Main Gem module.
12
22
  module Workhorse
13
23
  # Check if the available Arel version is greater or equal than 7.0.0
@@ -88,18 +98,6 @@ module Workhorse
88
98
  mattr_accessor :clean_stuck_jobs
89
99
  self.clean_stuck_jobs = false
90
100
 
91
- # This setting is for {Workhorse::Jobs::DetectStaleJobsJob} and specifies the
92
- # maximum number of seconds a job is allowed to stay 'locked' before this job
93
- # throws an exception. Set this to 0 to skip this check.
94
- mattr_accessor :stale_detection_locked_to_started_threshold
95
- self.stale_detection_locked_to_started_threshold = 3 * 60
96
-
97
- # This setting is for {Workhorse::Jobs::DetectStaleJobsJob} and specifies the
98
- # maximum number of seconds a job is allowed to run before this job throws an
99
- # exception. Set this to 0 to skip this check.
100
- mattr_accessor :stale_detection_run_time_threshold
101
- self.stale_detection_run_time_threshold = 12 * 60
102
-
103
101
  # Maximum memory usage per {Workhorse::Worker} process in MB.
104
102
  # When exceeded, the watch command will restart the worker. Set to 0 to disable.
105
103
  #
@@ -0,0 +1,37 @@
1
+ require 'English'
2
+ require 'test_helper'
3
+ require 'bundler'
4
+
5
+ class Workhorse::YjitTest < ActiveSupport::TestCase
6
+ YJIT_CHECK_SCRIPT = <<~RUBY.freeze
7
+ require 'bundler/setup'
8
+ require 'workhorse'
9
+ RubyVM::YJIT.enable
10
+ print RubyVM::YJIT.enabled?
11
+ RUBY
12
+
13
+ def test_yjit_not_enabled_when_ruby_yjit_enable_is_zero
14
+ skip 'RubyVM::YJIT.enable not available' unless defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
15
+
16
+ # Sanity check: YJIT can actually be enabled in this environment
17
+ without_env = run_ruby_script(YJIT_CHECK_SCRIPT, 'RUBY_YJIT_ENABLE' => nil)
18
+ skip 'YJIT cannot be enabled in this environment' unless without_env == 'true'
19
+
20
+ # With RUBY_YJIT_ENABLE=0, RubyVM::YJIT.enable should be a no-op
21
+ with_env = run_ruby_script(YJIT_CHECK_SCRIPT, 'RUBY_YJIT_ENABLE' => '0')
22
+ assert_equal 'false', with_env, 'YJIT should not be enabled when RUBY_YJIT_ENABLE=0'
23
+ end
24
+
25
+ private
26
+
27
+ def run_ruby_script(script, env = {})
28
+ output = nil
29
+ Bundler.with_unbundled_env do
30
+ IO.popen(env, %w[bundle exec ruby -e] + [script], chdir: Rails.root.to_s, err: %i[child out]) do |io|
31
+ output = io.read.strip
32
+ end
33
+ end
34
+ assert $CHILD_STATUS.success?, "Ruby subprocess failed (exit #{$CHILD_STATUS.exitstatus}): #{output}"
35
+ output
36
+ end
37
+ end
data/workhorse.gemspec CHANGED
@@ -1,24 +1,24 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.4.4 ruby lib
2
+ # stub: workhorse 1.5.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "1.4.4"
6
+ s.version = "1.5.0".freeze
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-04-28"
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]
11
+ s.date = "2026-07-01"
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, "test/workhorse/yjit_test.rb".freeze, "workhorse.gemspec".freeze]
13
13
  s.homepage = "https://github.com/sitrox/workhorse".freeze
14
14
  s.licenses = ["MIT".freeze]
15
- s.rubygems_version = "3.4.6".freeze
15
+ s.rubygems_version = "3.5.18".freeze
16
16
  s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
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]
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, "test/workhorse/yjit_test.rb".freeze]
18
18
 
19
19
  s.specification_version = 4
20
20
 
21
- s.add_runtime_dependency(%q<activesupport>.freeze, [">= 7.0.0"])
22
- s.add_runtime_dependency(%q<activerecord>.freeze, [">= 7.0.0"])
23
- s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 0"])
21
+ s.add_runtime_dependency(%q<activesupport>.freeze, [">= 7.0.0".freeze])
22
+ s.add_runtime_dependency(%q<activerecord>.freeze, [">= 7.0.0".freeze])
23
+ s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 0".freeze])
24
24
  end
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
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-28 00:00:00.000000000 Z
10
+ date: 2026-07-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -99,6 +99,7 @@ files:
99
99
  - test/workhorse/poller_test.rb
100
100
  - test/workhorse/pool_test.rb
101
101
  - test/workhorse/worker_test.rb
102
+ - test/workhorse/yjit_test.rb
102
103
  - workhorse.gemspec
103
104
  homepage: https://github.com/sitrox/workhorse
104
105
  licenses:
@@ -118,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
119
  - !ruby/object:Gem::Version
119
120
  version: '0'
120
121
  requirements: []
121
- rubygems_version: 4.0.2
122
+ rubygems_version: 4.0.11
122
123
  specification_version: 4
123
124
  summary: Multi-threaded job backend with database queuing for ruby.
124
125
  test_files:
@@ -133,3 +134,4 @@ test_files:
133
134
  - test/workhorse/poller_test.rb
134
135
  - test/workhorse/pool_test.rb
135
136
  - test/workhorse/worker_test.rb
137
+ - test/workhorse/yjit_test.rb