workhorse 1.3.0.rc3 → 1.3.0.rc4
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 +9 -2
- data/FAQ.md +10 -10
- data/LICENSE +1 -1
- data/README.md +22 -22
- data/VERSION +1 -1
- data/lib/active_job/queue_adapters/workhorse_adapter.rb +22 -6
- data/lib/workhorse/active_job_extension.rb +9 -0
- data/lib/workhorse/daemon.rb +99 -0
- data/lib/workhorse/db_job.rb +53 -0
- data/lib/workhorse/enqueuer.rb +24 -3
- data/lib/workhorse/jobs/cleanup_succeeded_jobs.rb +18 -0
- data/lib/workhorse/jobs/detect_stale_jobs_job.rb +22 -8
- data/lib/workhorse/jobs/run_active_job.rb +17 -0
- data/lib/workhorse/jobs/run_rails_op.rb +19 -0
- data/lib/workhorse/performer.rb +42 -0
- data/lib/workhorse/poller.rb +68 -7
- data/lib/workhorse/pool.rb +32 -4
- data/lib/workhorse/scoped_env.rb +20 -0
- data/lib/workhorse/worker.rb +93 -4
- data/lib/workhorse.rb +51 -18
- data/workhorse.gemspec +3 -3
- metadata +2 -2
data/lib/workhorse/worker.rb
CHANGED
@@ -1,25 +1,63 @@
|
|
1
1
|
module Workhorse
|
2
|
+
# Main worker class that manages job polling and execution.
|
3
|
+
# Workers poll the database for jobs, manage thread pools for parallel execution,
|
4
|
+
# and handle graceful shutdown and memory monitoring.
|
5
|
+
#
|
6
|
+
# @example Basic worker setup
|
7
|
+
# worker = Workhorse::Worker.new(
|
8
|
+
# queues: [:default, :urgent],
|
9
|
+
# pool_size: 4,
|
10
|
+
# polling_interval: 30
|
11
|
+
# )
|
12
|
+
# worker.start
|
13
|
+
# worker.wait
|
14
|
+
#
|
15
|
+
# @example Auto-terminating worker
|
16
|
+
# Workhorse::Worker.start_and_wait(
|
17
|
+
# queues: [:email, :reports],
|
18
|
+
# auto_terminate: true
|
19
|
+
# )
|
2
20
|
class Worker
|
3
21
|
LOG_LEVELS = %i[fatal error warn info debug].freeze
|
4
22
|
SHUTDOWN_SIGNALS = %w[TERM INT].freeze
|
5
23
|
LOG_REOPEN_SIGNAL = 'HUP'.freeze
|
6
24
|
|
25
|
+
# @return [Array<Symbol>] The queues this worker processes
|
7
26
|
attr_reader :queues
|
27
|
+
|
28
|
+
# @return [Symbol] Current worker state (:initialized, :running, :shutdown)
|
8
29
|
attr_reader :state
|
30
|
+
|
31
|
+
# @return [Integer] Number of threads in the worker pool
|
9
32
|
attr_reader :pool_size
|
33
|
+
|
34
|
+
# @return [Integer] Polling interval in seconds
|
10
35
|
attr_reader :polling_interval
|
36
|
+
|
37
|
+
# @return [Mutex] Synchronization mutex for thread safety
|
11
38
|
attr_reader :mutex
|
39
|
+
|
40
|
+
# @return [Logger, nil] Optional logger instance
|
12
41
|
attr_reader :logger
|
42
|
+
|
43
|
+
# @return [Workhorse::Poller] The poller instance
|
13
44
|
attr_reader :poller
|
14
45
|
|
15
46
|
# Instantiates and starts a new worker with the given arguments and then
|
16
47
|
# waits for its completion (i.e. an interrupt).
|
48
|
+
#
|
49
|
+
# @param args [Hash] Arguments passed to {#initialize}
|
50
|
+
# @return [void]
|
17
51
|
def self.start_and_wait(**args)
|
18
52
|
worker = new(**args)
|
19
53
|
worker.start
|
20
54
|
worker.wait
|
21
55
|
end
|
22
56
|
|
57
|
+
# Returns the path to the shutdown file for a given process ID.
|
58
|
+
#
|
59
|
+
# @param pid [Integer] Process ID
|
60
|
+
# @return [String, nil] Path to shutdown file or nil if not in Rails
|
23
61
|
# @private
|
24
62
|
def self.shutdown_file_for(pid)
|
25
63
|
return nil unless defined?(Rails)
|
@@ -69,6 +107,12 @@ module Workhorse
|
|
69
107
|
end
|
70
108
|
end
|
71
109
|
|
110
|
+
# Logs a message with worker ID prefix.
|
111
|
+
#
|
112
|
+
# @param text [String] The message to log
|
113
|
+
# @param level [Symbol] The log level (must be in LOG_LEVELS)
|
114
|
+
# @return [void]
|
115
|
+
# @raise [RuntimeError] If log level is invalid
|
72
116
|
def log(text, level = :info)
|
73
117
|
text = "[Job worker #{id}] #{text}"
|
74
118
|
puts text unless @quiet
|
@@ -77,20 +121,33 @@ module Workhorse
|
|
77
121
|
logger.send(level, text.strip)
|
78
122
|
end
|
79
123
|
|
124
|
+
# Returns the unique identifier for this worker.
|
125
|
+
# Format: hostname.pid.random_hex
|
126
|
+
#
|
127
|
+
# @return [String] Unique worker identifier
|
80
128
|
def id
|
81
129
|
@id ||= "#{hostname}.#{pid}.#{SecureRandom.hex(3)}"
|
82
130
|
end
|
83
131
|
|
132
|
+
# Returns the process ID of this worker.
|
133
|
+
#
|
134
|
+
# @return [Integer] Process ID
|
84
135
|
def pid
|
85
136
|
@pid ||= Process.pid
|
86
137
|
end
|
87
138
|
|
139
|
+
# Returns the hostname of the machine running this worker.
|
140
|
+
#
|
141
|
+
# @return [String] Hostname
|
88
142
|
def hostname
|
89
143
|
@hostname ||= Socket.gethostname
|
90
144
|
end
|
91
145
|
|
92
|
-
# Starts the worker. This call is not blocking - call {wait} for this
|
146
|
+
# Starts the worker. This call is not blocking - call {#wait} for this
|
93
147
|
# purpose.
|
148
|
+
#
|
149
|
+
# @return [void]
|
150
|
+
# @raise [RuntimeError] If worker is not in initialized state
|
94
151
|
def start
|
95
152
|
mutex.synchronize do
|
96
153
|
assert_state! :initialized
|
@@ -104,13 +161,20 @@ module Workhorse
|
|
104
161
|
end
|
105
162
|
end
|
106
163
|
|
164
|
+
# Asserts that the worker is in the expected state.
|
165
|
+
#
|
166
|
+
# @param state [Symbol] Expected state
|
167
|
+
# @return [void]
|
168
|
+
# @raise [RuntimeError] If worker is not in expected state
|
107
169
|
def assert_state!(state)
|
108
170
|
fail "Expected worker to be in state #{state} but current state is #{self.state}." unless self.state == state
|
109
171
|
end
|
110
172
|
|
111
|
-
# Shuts down worker and DB poller. Jobs currently
|
173
|
+
# Shuts down worker and DB poller. Jobs currently being processed are
|
112
174
|
# properly finished before this method returns. Subsequent calls to this
|
113
175
|
# method are ignored.
|
176
|
+
#
|
177
|
+
# @return [void]
|
114
178
|
def shutdown
|
115
179
|
# This is safe to be checked outside of the mutex as 'shutdown' is the
|
116
180
|
# final state this worker can be in.
|
@@ -131,19 +195,28 @@ module Workhorse
|
|
131
195
|
end
|
132
196
|
end
|
133
197
|
|
134
|
-
# Waits until the worker is shut down. This only happens if shutdown gets
|
198
|
+
# Waits until the worker is shut down. This only happens if {#shutdown} gets
|
135
199
|
# called - either by another thread or by enabling `auto_terminate` and
|
136
200
|
# receiving a respective signal. Use this method to let worker run
|
137
|
-
#
|
201
|
+
# indefinitely.
|
202
|
+
#
|
203
|
+
# @return [void]
|
138
204
|
def wait
|
139
205
|
@poller.wait
|
140
206
|
@pool.wait
|
141
207
|
end
|
142
208
|
|
209
|
+
# Returns the number of idle threads in the pool.
|
210
|
+
#
|
211
|
+
# @return [Integer] Number of idle threads
|
143
212
|
def idle
|
144
213
|
@pool.idle
|
145
214
|
end
|
146
215
|
|
216
|
+
# Schedules a job for execution in the thread pool.
|
217
|
+
#
|
218
|
+
# @param db_job_id [Integer] The ID of the {Workhorse::DbJob} to perform
|
219
|
+
# @return [void]
|
147
220
|
def perform(db_job_id)
|
148
221
|
begin # rubocop:disable Style/RedundantBegin
|
149
222
|
mutex.synchronize do
|
@@ -166,6 +239,10 @@ module Workhorse
|
|
166
239
|
|
167
240
|
private
|
168
241
|
|
242
|
+
# Checks current memory usage and initiates shutdown if limit exceeded.
|
243
|
+
#
|
244
|
+
# @return [Boolean] True if memory is within limits, false if exceeded
|
245
|
+
# @private
|
169
246
|
def check_memory
|
170
247
|
mem = current_memory_consumption
|
171
248
|
|
@@ -191,12 +268,20 @@ module Workhorse
|
|
191
268
|
return false
|
192
269
|
end
|
193
270
|
|
271
|
+
# Returns current memory consumption in MB.
|
272
|
+
#
|
273
|
+
# @return [Integer, nil] Memory usage in MB or nil if unable to determine
|
274
|
+
# @private
|
194
275
|
def current_memory_consumption
|
195
276
|
mem = `ps -p #{pid} -o rss=`&.strip
|
196
277
|
return nil if mem.blank?
|
197
278
|
return mem.to_i / 1024
|
198
279
|
end
|
199
280
|
|
281
|
+
# Sets up signal handler for log file reopening (HUP signal).
|
282
|
+
#
|
283
|
+
# @return [void]
|
284
|
+
# @private
|
200
285
|
def trap_log_reopen
|
201
286
|
Signal.trap(LOG_REOPEN_SIGNAL) do
|
202
287
|
Thread.new do
|
@@ -209,6 +294,10 @@ module Workhorse
|
|
209
294
|
end
|
210
295
|
end
|
211
296
|
|
297
|
+
# Sets up signal handlers for graceful termination (TERM/INT signals).
|
298
|
+
#
|
299
|
+
# @return [void]
|
300
|
+
# @private
|
212
301
|
def trap_termination
|
213
302
|
SHUTDOWN_SIGNALS.each do |signal|
|
214
303
|
Signal.trap(signal) do
|
data/lib/workhorse.rb
CHANGED
@@ -8,58 +8,83 @@ require 'workhorse/enqueuer'
|
|
8
8
|
require 'workhorse/scoped_env'
|
9
9
|
require 'workhorse/active_job_extension'
|
10
10
|
|
11
|
+
# Main Gem module.
|
11
12
|
module Workhorse
|
12
13
|
# Check if the available Arel version is greater or equal than 7.0.0
|
13
14
|
AREL_GTE_7 = Gem::Version.new(Arel::VERSION) >= Gem::Version.new('7.0.0')
|
14
15
|
|
15
16
|
extend Workhorse::Enqueuer
|
16
17
|
|
17
|
-
# Returns the performer currently performing the active job.
|
18
|
-
# called from within a job and the same thread.
|
18
|
+
# Returns the performer currently performing the active job.
|
19
|
+
# This can only be called from within a job and the same thread.
|
20
|
+
#
|
21
|
+
# @return [Workhorse::Performer] The current performer instance
|
22
|
+
# @raise [RuntimeError] If called outside of a job context
|
19
23
|
def self.performer
|
20
24
|
Thread.current[:workhorse_current_performer] \
|
21
25
|
|| fail('No performer is associated with the current thread. This method must always be called inside of a job.')
|
22
26
|
end
|
23
27
|
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# row.
|
28
|
+
# Maximum number of consecutive global lock failures before triggering error handling.
|
29
|
+
# A {Workhorse::Worker} will log an error and call the {.on_exception} callback if it can't
|
30
|
+
# obtain the global lock for this many times in a row.
|
31
|
+
#
|
32
|
+
# @return [Integer] The maximum number of allowed consecutive lock failures
|
27
33
|
mattr_accessor :max_global_lock_fails
|
28
34
|
self.max_global_lock_fails = 10
|
29
35
|
|
36
|
+
# Transaction callback used for database operations.
|
37
|
+
# Defaults to ActiveRecord::Base.transaction.
|
38
|
+
#
|
39
|
+
# @return [Proc] The transaction callback
|
30
40
|
mattr_accessor :tx_callback
|
31
41
|
self.tx_callback = proc do |*args, &block|
|
32
42
|
ActiveRecord::Base.transaction(*args, &block)
|
33
43
|
end
|
34
44
|
|
45
|
+
# Exception callback called when an exception occurs during job processing.
|
46
|
+
# Override this to integrate with your error reporting system.
|
47
|
+
#
|
48
|
+
# @return [Proc] The exception callback
|
35
49
|
mattr_accessor :on_exception
|
36
50
|
self.on_exception = proc do |exception|
|
37
51
|
# Do something with this exception, i.e.
|
38
52
|
# ExceptionNotifier.notify_exception(exception)
|
39
53
|
end
|
40
54
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
55
|
+
# Controls whether {Workhorse::Daemon::ShellHandler} commands use lockfiles.
|
56
|
+
# Set to false if you're handling locking yourself (e.g. in a wrapper script).
|
57
|
+
#
|
58
|
+
# @return [Boolean] Whether to lock shell commands
|
44
59
|
mattr_accessor :lock_shell_commands
|
45
60
|
self.lock_shell_commands = true
|
46
61
|
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
62
|
+
# Controls whether to silence exception callbacks for {Workhorse::Poller} exceptions.
|
63
|
+
# When true, {.on_exception} won't be called for poller failures, but exceptions
|
64
|
+
# will still be logged.
|
65
|
+
#
|
66
|
+
# @return [Boolean] Whether to silence poller exception callbacks
|
50
67
|
mattr_accessor :silence_poller_exceptions
|
51
68
|
self.silence_poller_exceptions = false
|
52
69
|
|
53
|
-
#
|
54
|
-
#
|
70
|
+
# Controls output verbosity for the watch command.
|
71
|
+
# When true, the watch command won't produce output (warnings still shown).
|
72
|
+
#
|
73
|
+
# @return [Boolean] Whether to silence watcher output
|
55
74
|
mattr_accessor :silence_watcher
|
56
75
|
self.silence_watcher = false
|
57
76
|
|
77
|
+
# Controls whether jobs are performed within database transactions.
|
78
|
+
# Individual job classes can override this with skip_tx?.
|
79
|
+
#
|
80
|
+
# @return [Boolean] Whether to perform jobs in transactions
|
58
81
|
mattr_accessor :perform_jobs_in_tx
|
59
82
|
self.perform_jobs_in_tx = true
|
60
83
|
|
61
|
-
#
|
62
|
-
# 'locked' or 'running'
|
84
|
+
# Controls automatic cleanup of stuck jobs on {Workhorse::Poller} startup.
|
85
|
+
# When enabled, pollers will clean jobs stuck in 'locked' or 'running' states.
|
86
|
+
#
|
87
|
+
# @return [Boolean] Whether to clean stuck jobs on startup
|
63
88
|
mattr_accessor :clean_stuck_jobs
|
64
89
|
self.clean_stuck_jobs = false
|
65
90
|
|
@@ -75,12 +100,20 @@ module Workhorse
|
|
75
100
|
mattr_accessor :stale_detection_run_time_threshold
|
76
101
|
self.stale_detection_run_time_threshold = 12 * 60
|
77
102
|
|
78
|
-
# Maximum memory
|
79
|
-
#
|
80
|
-
#
|
103
|
+
# Maximum memory usage per {Workhorse::Worker} process in MB.
|
104
|
+
# When exceeded, the watch command will restart the worker. Set to 0 to disable.
|
105
|
+
#
|
106
|
+
# @return [Integer] Memory limit in megabytes
|
81
107
|
mattr_accessor :max_worker_memory_mb
|
82
108
|
self.max_worker_memory_mb = 0
|
83
109
|
|
110
|
+
# Configuration method for setting up Workhorse options.
|
111
|
+
#
|
112
|
+
# @yield [self] Configuration block
|
113
|
+
# @example
|
114
|
+
# Workhorse.setup do |config|
|
115
|
+
# config.max_global_lock_fails = 5
|
116
|
+
# end
|
84
117
|
def self.setup
|
85
118
|
yield self
|
86
119
|
end
|
data/workhorse.gemspec
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: workhorse 1.3.0.
|
2
|
+
# stub: workhorse 1.3.0.rc4 ruby lib
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "workhorse".freeze
|
6
|
-
s.version = "1.3.0.
|
6
|
+
s.version = "1.3.0.rc4"
|
7
7
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
|
9
9
|
s.require_paths = ["lib".freeze]
|
10
10
|
s.authors = ["Sitrox".freeze]
|
11
|
-
s.date = "2025-
|
11
|
+
s.date = "2025-08-27"
|
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.rubygems_version = "3.4.6".freeze
|
14
14
|
s.summary = "Multi-threaded job backend with database queuing for ruby.".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.3.0.
|
4
|
+
version: 1.3.0.rc4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sitrox
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-27 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|