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.
@@ -1,4 +1,17 @@
1
1
  module Workhorse
2
+ # ActiveRecord model representing a job in the database.
3
+ # This class manages the job lifecycle and state transitions within the Workhorse system.
4
+ #
5
+ # @example Creating a job
6
+ # job = DbJob.create!(
7
+ # queue: 'default',
8
+ # handler: Marshal.dump(job_instance),
9
+ # priority: 0
10
+ # )
11
+ #
12
+ # @example Querying jobs by state
13
+ # waiting_jobs = DbJob.waiting
14
+ # failed_jobs = DbJob.failed
2
15
  class DbJob < ActiveRecord::Base
3
16
  STATE_WAITING = :waiting
4
17
  STATE_LOCKED = :locked
@@ -14,26 +27,45 @@ module Workhorse
14
27
 
15
28
  self.table_name = 'jobs'
16
29
 
30
+ # Returns jobs in waiting state.
31
+ #
32
+ # @return [ActiveRecord::Relation] Jobs waiting to be processed
17
33
  def self.waiting
18
34
  where(state: STATE_WAITING)
19
35
  end
20
36
 
37
+ # Returns jobs in locked state.
38
+ #
39
+ # @return [ActiveRecord::Relation] Jobs currently locked by workers
21
40
  def self.locked
22
41
  where(state: STATE_LOCKED)
23
42
  end
24
43
 
44
+ # Returns jobs in started state.
45
+ #
46
+ # @return [ActiveRecord::Relation] Jobs currently being executed
25
47
  def self.started
26
48
  where(state: STATE_STARTED)
27
49
  end
28
50
 
51
+ # Returns jobs in succeeded state.
52
+ #
53
+ # @return [ActiveRecord::Relation] Jobs that completed successfully
29
54
  def self.succeeded
30
55
  where(state: STATE_SUCCEEDED)
31
56
  end
32
57
 
58
+ # Returns jobs in failed state.
59
+ #
60
+ # @return [ActiveRecord::Relation] Jobs that failed during execution
33
61
  def self.failed
34
62
  where(state: STATE_FAILED)
35
63
  end
36
64
 
65
+ # Returns a relation with split locked_by field for easier querying.
66
+ # Extracts host, PID, and random string components from locked_by.
67
+ #
68
+ # @return [ActiveRecord::Relation] Relation with additional computed columns
37
69
  # @private
38
70
  def self.with_split_locked_by
39
71
  select(<<~SQL)
@@ -74,6 +106,9 @@ module Workhorse
74
106
  # ("failed"), make sure the actions performed in the job are repeatable or
75
107
  # have been rolled back. E.g. if the job already wrote something to an
76
108
  # external API, it may cause inconsistencies if the job is performed again.
109
+ #
110
+ # @param force [Boolean] Whether to force reset without state validation
111
+ # @raise [RuntimeError] If job is not in a final state and force is false
77
112
  def reset!(force = false)
78
113
  unless force
79
114
  assert_state! STATE_SUCCEEDED, STATE_FAILED
@@ -90,6 +125,10 @@ module Workhorse
90
125
  save!
91
126
  end
92
127
 
128
+ # Marks the job as locked by a specific worker.
129
+ #
130
+ # @param worker_id [String] The ID of the worker locking this job
131
+ # @raise [RuntimeError] If the job is dirty or already locked
93
132
  # @private Only to be used by workhorse
94
133
  def mark_locked!(worker_id)
95
134
  if changed?
@@ -108,6 +147,9 @@ module Workhorse
108
147
  save!
109
148
  end
110
149
 
150
+ # Marks the job as started.
151
+ #
152
+ # @raise [RuntimeError] If the job is not in locked state
111
153
  # @private Only to be used by workhorse
112
154
  def mark_started!
113
155
  assert_state! STATE_LOCKED
@@ -117,6 +159,10 @@ module Workhorse
117
159
  save!
118
160
  end
119
161
 
162
+ # Marks the job as failed with the given exception.
163
+ #
164
+ # @param exception [Exception] The exception that caused the failure
165
+ # @raise [RuntimeError] If the job is not in locked or started state
120
166
  # @private Only to be used by workhorse
121
167
  def mark_failed!(exception)
122
168
  assert_state! STATE_LOCKED, STATE_STARTED
@@ -127,6 +173,9 @@ module Workhorse
127
173
  save!
128
174
  end
129
175
 
176
+ # Marks the job as succeeded.
177
+ #
178
+ # @raise [RuntimeError] If the job is not in started state
130
179
  # @private Only to be used by workhorse
131
180
  def mark_succeeded!
132
181
  assert_state! STATE_STARTED
@@ -136,6 +185,10 @@ module Workhorse
136
185
  save!
137
186
  end
138
187
 
188
+ # Asserts that the job is in one of the specified states.
189
+ #
190
+ # @param states [Array<Symbol>] Valid states for the job
191
+ # @raise [RuntimeError] If the job is not in any of the specified states
139
192
  def assert_state!(*states)
140
193
  unless states.include?(state.to_sym)
141
194
  fail "Job #{id} is not in state #{states.inspect} but in state #{state.inspect}."
@@ -1,6 +1,16 @@
1
1
  module Workhorse
2
+ # Module providing job enqueuing functionality.
3
+ # Extended by the main Workhorse module to provide enqueuing capabilities.
4
+ # Supports plain Ruby objects, ActiveJob instances, and Rails operations.
2
5
  module Enqueuer
3
- # Enqueue any object that is serializable and has a `perform` method
6
+ # Enqueues any object that is serializable and has a `perform` method.
7
+ #
8
+ # @param job [Object] The job object to enqueue (must respond to #perform)
9
+ # @param queue [String, Symbol, nil] The queue name
10
+ # @param priority [Integer] Job priority (lower numbers = higher priority)
11
+ # @param perform_at [Time] When to perform the job
12
+ # @param description [String, nil] Optional job description
13
+ # @return [Workhorse::DbJob] The created database job record
4
14
  def enqueue(job, queue: nil, priority: 0, perform_at: Time.now, description: nil)
5
15
  return DbJob.create!(
6
16
  queue: queue,
@@ -11,7 +21,13 @@ module Workhorse
11
21
  )
12
22
  end
13
23
 
14
- # Enqueue an ActiveJob job
24
+ # Enqueues an ActiveJob job instance.
25
+ #
26
+ # @param job [ActiveJob::Base] The ActiveJob instance to enqueue
27
+ # @param perform_at [Time] When to perform the job
28
+ # @param queue [String, Symbol, nil] Optional queue override
29
+ # @param description [String, nil] Optional job description
30
+ # @return [Workhorse::DbJob] The created database job record
15
31
  def enqueue_active_job(job, perform_at: Time.now, queue: nil, description: nil)
16
32
  wrapper_job = Jobs::RunActiveJob.new(job.serialize)
17
33
  queue ||= job.queue_name if job.queue_name.present?
@@ -26,7 +42,12 @@ module Workhorse
26
42
  return db_job
27
43
  end
28
44
 
29
- # Enqueue the execution of an operation by its class and params
45
+ # Enqueues the execution of a Rails operation by its class and parameters.
46
+ #
47
+ # @param cls [Class] The operation class to execute
48
+ # @param args [Array] Variable arguments (workhorse_args, op_args)
49
+ # @return [Workhorse::DbJob] The created database job record
50
+ # @raise [ArgumentError] If wrong number of arguments provided
30
51
  def enqueue_op(cls, *args)
31
52
  case args.size
32
53
  when 0
@@ -1,4 +1,14 @@
1
1
  module Workhorse::Jobs
2
+ # Job for cleaning up old succeeded jobs from the database.
3
+ # This maintenance job helps keep the jobs table from growing indefinitely
4
+ # by removing successfully completed jobs older than a specified age.
5
+ #
6
+ # @example Schedule cleanup job
7
+ # Workhorse.enqueue(CleanupSucceededJobs.new(max_age: 30))
8
+ #
9
+ # @example Daily cleanup with cron
10
+ # # Clean up jobs older than 14 days every day at 2 AM
11
+ # Workhorse.enqueue(CleanupSucceededJobs.new, perform_at: 1.day.from_now.beginning_of_day + 2.hours)
2
12
  class CleanupSucceededJobs
3
13
  # Instantiates a new job.
4
14
  #
@@ -8,6 +18,9 @@ module Workhorse::Jobs
8
18
  @max_age = max_age
9
19
  end
10
20
 
21
+ # Executes the cleanup by deleting old succeeded jobs.
22
+ #
23
+ # @return [void]
11
24
  def perform
12
25
  age_limit = seconds_ago(@max_age)
13
26
  Workhorse::DbJob.where(
@@ -17,6 +30,11 @@ module Workhorse::Jobs
17
30
 
18
31
  private
19
32
 
33
+ # Calculates a timestamp for the given number of days ago.
34
+ #
35
+ # @param days [Integer] Number of days in the past
36
+ # @return [Time] Timestamp for the specified days ago
37
+ # @private
20
38
  def seconds_ago(days)
21
39
  Time.now - (days * 24 * 60 * 60)
22
40
  end
@@ -1,22 +1,36 @@
1
1
  module Workhorse::Jobs
2
- # This job picks up jobs that remained `locked` or `started` (running) for
2
+ # Job that detects and reports stale jobs in the system.
3
+ # This monitoring job picks up jobs that remained `locked` or `started` (running) for
3
4
  # more than a certain amount of time. If any of these jobs are found, an
4
5
  # exception is thrown (which may cause a notification if you configured
5
- # `on_exception` accordingly).
6
+ # {Workhorse.on_exception} accordingly).
6
7
  #
7
8
  # The thresholds are obtained from the configuration options
8
- # {Workhorse.stale_detection_locked_to_started_threshold
9
- # config.stale_detection_locked_to_started_threshold} and
10
- # {Workhorse.stale_detection_run_time_threshold
11
- # config.stale_detection_run_time_threshold}.
9
+ # {Workhorse.stale_detection_locked_to_started_threshold} and
10
+ # {Workhorse.stale_detection_run_time_threshold}.
11
+ #
12
+ # @example Schedule stale job detection
13
+ # Workhorse.enqueue(DetectStaleJobsJob.new)
14
+ #
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
12
20
  class DetectStaleJobsJob
13
- # @private
21
+ # Creates a new stale job detection job.
22
+ # Reads configuration thresholds at initialization time.
14
23
  def initialize
15
24
  @locked_to_started_threshold = Workhorse.stale_detection_locked_to_started_threshold
16
25
  @run_time_threshold = Workhorse.stale_detection_run_time_threshold
17
26
  end
18
27
 
19
- # @private
28
+ # Executes the stale job detection.
29
+ # Checks for jobs that have been locked or running too long and raises
30
+ # an exception if any are found.
31
+ #
32
+ # @return [void]
33
+ # @raise [RuntimeError] If stale jobs are detected
20
34
  def perform
21
35
  messages = []
22
36
 
@@ -1,15 +1,32 @@
1
1
  module Workhorse::Jobs
2
+ # Wrapper job for executing ActiveJob instances within Workhorse.
3
+ # This job handles the deserialization and execution of ActiveJob jobs
4
+ # that have been enqueued through the Workhorse adapter.
5
+ #
6
+ # @example Internal usage
7
+ # wrapper = RunActiveJob.new(job.serialize)
8
+ # wrapper.perform
2
9
  class RunActiveJob
10
+ # @return [Hash] Serialized ActiveJob data
3
11
  attr_reader :job_data
4
12
 
13
+ # Creates a new ActiveJob wrapper.
14
+ #
15
+ # @param job_data [Hash] Serialized ActiveJob data from job.serialize
5
16
  def initialize(job_data)
6
17
  @job_data = job_data
7
18
  end
8
19
 
20
+ # Returns the ActiveJob class for this job.
21
+ #
22
+ # @return [Class, nil] The job class or nil if not found
9
23
  def job_class
10
24
  @job_data['job_class'].safe_constantize
11
25
  end
12
26
 
27
+ # Executes the wrapped ActiveJob.
28
+ #
29
+ # @return [void]
13
30
  def perform
14
31
  ActiveJob::Base.execute(@job_data)
15
32
  end
@@ -1,14 +1,33 @@
1
1
  module Workhorse::Jobs
2
+ # Job wrapper for executing Rails operations (trailblazer-operation or similar).
3
+ # This job allows enqueuing of operation classes with parameters for later execution.
4
+ #
5
+ # @example Enqueue an operation
6
+ # Workhorse.enqueue_op(MyOperation, { user_id: 123 })
7
+ #
8
+ # @example Manual instantiation
9
+ # job = RunRailsOp.new(MyOperation, { user_id: 123 })
10
+ # Workhorse.enqueue(job)
2
11
  class RunRailsOp
12
+ # Creates a new Rails operation job.
13
+ #
14
+ # @param cls [Class] The operation class to execute
15
+ # @param params [Hash] Parameters to pass to the operation
3
16
  def initialize(cls, params = {})
4
17
  @cls = cls
5
18
  @params = params
6
19
  end
7
20
 
21
+ # Returns the operation class for this job.
22
+ #
23
+ # @return [Class] The operation class
8
24
  def job_class
9
25
  @cls
10
26
  end
11
27
 
28
+ # Executes the Rails operation with the provided parameters.
29
+ #
30
+ # @return [void]
12
31
  def perform
13
32
  @cls.run!(@params)
14
33
  end
@@ -1,13 +1,30 @@
1
1
  module Workhorse
2
+ # Executes individual jobs within worker processes.
3
+ # The Performer handles job lifecycle management, error handling,
4
+ # and integration with Rails application executors.
5
+ #
6
+ # @example Basic usage (typically called internally)
7
+ # performer = Workhorse::Performer.new(job_id, worker)
8
+ # performer.perform
2
9
  class Performer
10
+ # @return [Workhorse::Worker] The worker that owns this performer
3
11
  attr_reader :worker
4
12
 
13
+ # Creates a new performer for a specific job.
14
+ #
15
+ # @param db_job_id [Integer] The ID of the {Workhorse::DbJob} to perform
16
+ # @param worker [Workhorse::Worker] The worker instance managing this performer
5
17
  def initialize(db_job_id, worker)
6
18
  @db_job = Workhorse::DbJob.find(db_job_id)
7
19
  @worker = worker
8
20
  @started = false
9
21
  end
10
22
 
23
+ # Executes the job with full error handling and state management.
24
+ # This method can only be called once per performer instance.
25
+ #
26
+ # @return [void]
27
+ # @raise [RuntimeError] If called more than once
11
28
  def perform
12
29
  begin # rubocop:disable Style/RedundantBegin
13
30
  fail 'Performer can only run once.' if @started
@@ -20,6 +37,12 @@ module Workhorse
20
37
 
21
38
  private
22
39
 
40
+ # Internal job execution with thread-local performer tracking.
41
+ # Wraps the job execution with Rails application executor if available.
42
+ #
43
+ # @return [void]
44
+ # @raise [Exception] Any exception raised during job execution
45
+ # @private
23
46
  def perform!
24
47
  begin # rubocop:disable Style/RedundantBegin
25
48
  Thread.current[:workhorse_current_performer] = self
@@ -50,6 +73,13 @@ module Workhorse
50
73
  end
51
74
  end
52
75
 
76
+ # Core job execution logic with state transitions.
77
+ # Handles marking job as started, deserializing and executing the job,
78
+ # and marking as succeeded.
79
+ #
80
+ # @return [void]
81
+ # @raise [Exception] Any exception raised during job execution
82
+ # @private
53
83
  def perform_wrapped
54
84
  # ---------------------------------------------------------------
55
85
  # Mark job as started
@@ -87,11 +117,23 @@ module Workhorse
87
117
  end
88
118
  end
89
119
 
120
+ # Logs a message with job ID prefix.
121
+ #
122
+ # @param text [String] The message to log
123
+ # @param level [Symbol] The log level
124
+ # @return [void]
125
+ # @private
90
126
  def log(text, level = :info)
91
127
  text = "[#{@db_job.id}] #{text}"
92
128
  worker.log text, level
93
129
  end
94
130
 
131
+ # Deserializes the job from the database handler field.
132
+ # Uses Marshal.load which is safe as long as jobs are enqueued through
133
+ # {Workhorse::Enqueuer}.
134
+ #
135
+ # @return [Object] The deserialized job instance
136
+ # @private
95
137
  def deserialized_job
96
138
  # The source is safe as long as jobs are always enqueued using
97
139
  # Workhorse::Enqueuer so it is ok to use Marshal.load.
@@ -1,4 +1,11 @@
1
1
  module Workhorse
2
+ # Database poller that discovers and locks jobs for execution.
3
+ # Handles job querying, global locking, and job distribution to workers.
4
+ # Supports both MySQL and Oracle databases with database-specific optimizations.
5
+ #
6
+ # @example Basic usage (typically used internally)
7
+ # poller = Workhorse::Poller.new(worker, proc { true })
8
+ # poller.start
2
9
  class Poller
3
10
  MIN_LOCK_TIMEOUT = 0.1 # In seconds
4
11
  MAX_LOCK_TIMEOUT = 1.0 # In seconds
@@ -6,9 +13,16 @@ module Workhorse
6
13
  ORACLE_LOCK_MODE = 6 # X_MODE (exclusive)
7
14
  ORACLE_LOCK_HANDLE = 478_564_848 # Randomly chosen number
8
15
 
16
+ # @return [Workhorse::Worker] The worker this poller serves
9
17
  attr_reader :worker
18
+
19
+ # @return [Arel::Table] The jobs table for query building
10
20
  attr_reader :table
11
21
 
22
+ # Creates a new poller for the given worker.
23
+ #
24
+ # @param worker [Workhorse::Worker] The worker to serve
25
+ # @param before_poll [Proc] Callback executed before each poll (should return boolean)
12
26
  def initialize(worker, before_poll = proc { true })
13
27
  @worker = worker
14
28
  @running = false
@@ -20,10 +34,17 @@ module Workhorse
20
34
  @before_poll = before_poll
21
35
  end
22
36
 
37
+ # Checks if the poller is currently running.
38
+ #
39
+ # @return [Boolean] True if poller is running
23
40
  def running?
24
41
  @running
25
42
  end
26
43
 
44
+ # Starts the poller in a background thread.
45
+ #
46
+ # @return [void]
47
+ # @raise [RuntimeError] If poller is already running
27
48
  def start
28
49
  fail 'Poller is already running.' if running?
29
50
  @running = true
@@ -55,18 +76,27 @@ module Workhorse
55
76
  end
56
77
  end
57
78
 
79
+ # Shuts down the poller and waits for completion.
80
+ #
81
+ # @return [void]
82
+ # @raise [RuntimeError] If poller is not running
58
83
  def shutdown
59
84
  fail 'Poller is not running.' unless running?
60
85
  @running = false
61
86
  wait
62
87
  end
63
88
 
89
+ # Waits for the poller thread to complete.
90
+ #
91
+ # @return [void]
64
92
  def wait
65
93
  @thread.join
66
94
  end
67
95
 
68
- # Call this to interrupt current sleep and perform the next poll as soon as
69
- # possible, then resume in the normal polling interval.
96
+ # Interrupts current sleep and performs the next poll immediately.
97
+ # After the poll, resumes normal polling interval.
98
+ #
99
+ # @return [void]
70
100
  def instant_repoll!
71
101
  worker.log 'Aborting next sleep to perform instant repoll', :debug
72
102
  @instant_repoll.make_true
@@ -74,6 +104,11 @@ module Workhorse
74
104
 
75
105
  private
76
106
 
107
+ # Cleans up jobs stuck in locked or started states from dead processes.
108
+ # Only cleans jobs from the current hostname.
109
+ #
110
+ # @return [void]
111
+ # @private
77
112
  def clean_stuck_jobs!
78
113
  with_global_lock timeout: MAX_LOCK_TIMEOUT do
79
114
  Workhorse.tx_callback.call do
@@ -131,6 +166,10 @@ module Workhorse
131
166
  end
132
167
  end
133
168
 
169
+ # Sleeps for the configured polling interval with instant repoll support.
170
+ #
171
+ # @return [void]
172
+ # @private
134
173
  def sleep
135
174
  remaining = worker.polling_interval
136
175
 
@@ -140,6 +179,14 @@ module Workhorse
140
179
  end
141
180
  end
142
181
 
182
+ # Executes a block with a global database lock.
183
+ # Supports both MySQL GET_LOCK and Oracle DBMS_LOCK.
184
+ #
185
+ # @param name [Symbol] Lock name identifier
186
+ # @param timeout [Integer] Lock timeout in seconds
187
+ # @yield Block to execute while holding the lock
188
+ # @return [void]
189
+ # @private
143
190
  def with_global_lock(name: :workhorse, timeout: 2, &_block)
144
191
  begin # rubocop:disable Style/RedundantBegin
145
192
  if @is_oracle
@@ -201,14 +248,18 @@ module Workhorse
201
248
  end
202
249
  end
203
250
 
251
+ # Performs a single poll cycle to discover and lock jobs.
252
+ #
253
+ # @return [void]
254
+ # @private
204
255
  def poll
205
256
  @instant_repoll.make_false
206
257
 
207
258
  timeout = [MIN_LOCK_TIMEOUT, [MAX_LOCK_TIMEOUT, worker.polling_interval].min].max
208
259
  with_global_lock timeout: timeout do
209
- Workhorse.tx_callback.call do
210
- job_ids = []
260
+ job_ids = []
211
261
 
262
+ Workhorse.tx_callback.call do
212
263
  # As we are the only thread posting into the worker pool, it is safe to
213
264
  # get the number of idle threads without mutex synchronization. The
214
265
  # actual number of idle workers at time of posting can only be larger
@@ -230,13 +281,23 @@ module Workhorse
230
281
  worker.log 'Rolling back transaction to unlock jobs, as worker has been shut down in the meantime'
231
282
  fail ActiveRecord::Rollback
232
283
  end
233
-
234
- job_ids.each { |job_id| worker.perform(job_id) }
235
284
  end
285
+
286
+ # This needs to be outside the above transaction because it runs the job
287
+ # in a new thread which opens a new connection. Even though it would be
288
+ # non-blocking and thus directly conclude the block and the transaction,
289
+ # there would still be a risk that the transaction is not committed yet
290
+ # when the job starts.
291
+ job_ids.each { |job_id| worker.perform(job_id) } if running?
236
292
  end
237
293
  end
238
294
 
239
- # Returns an Array of #{Workhorse::DbJob}s that can be started
295
+ # Returns an array of {Workhorse::DbJob}s that can be started.
296
+ # Uses complex SQL with UNIONs to respect queue ordering and limits.
297
+ #
298
+ # @param limit [Integer] Maximum number of jobs to return
299
+ # @return [Array<Workhorse::DbJob>] Jobs ready for execution
300
+ # @private
240
301
  def queued_db_jobs(limit)
241
302
  # ---------------------------------------------------------------
242
303
  # Select jobs to execute
@@ -1,9 +1,22 @@
1
1
  module Workhorse
2
- # Abstraction layer of a simple thread pool implementation used by the worker.
2
+ # Thread pool abstraction used by workers for concurrent job execution.
3
+ # Wraps Concurrent::ThreadPoolExecutor to provide a simpler interface
4
+ # and custom behavior for job processing.
5
+ #
6
+ # @example Basic usage
7
+ # pool = Workhorse::Pool.new(4)
8
+ # pool.post { puts "Working..." }
9
+ # pool.shutdown
3
10
  class Pool
11
+ # @return [Mutex] Synchronization mutex for thread safety
4
12
  attr_reader :mutex
13
+
14
+ # @return [Concurrent::AtomicFixnum] Thread-safe counter of active threads
5
15
  attr_reader :active_threads
6
16
 
17
+ # Creates a new thread pool with the specified size.
18
+ #
19
+ # @param size [Integer] Maximum number of threads in the pool
7
20
  def initialize(size)
8
21
  @size = size
9
22
  @executor = Concurrent::ThreadPoolExecutor.new(
@@ -18,11 +31,19 @@ module Workhorse
18
31
  @on_idle = nil
19
32
  end
20
33
 
34
+ # Sets a callback to be executed when the pool becomes idle.
35
+ #
36
+ # @yield Block to execute when all threads become idle
37
+ # @return [void]
21
38
  def on_idle(&block)
22
39
  @on_idle = block
23
40
  end
24
41
 
25
- # Posts a new work unit to the pool.
42
+ # Posts a new work unit to the pool for execution.
43
+ #
44
+ # @yield The work block to execute
45
+ # @return [void]
46
+ # @raise [RuntimeError] If all threads are busy
26
47
  def post
27
48
  mutex.synchronize do
28
49
  if idle.zero?
@@ -44,14 +65,18 @@ module Workhorse
44
65
  end
45
66
  end
46
67
 
47
- # Returns the number of idle threads.
68
+ # Returns the number of idle threads in the pool.
69
+ #
70
+ # @return [Integer] Number of idle threads
48
71
  def idle
49
72
  @size - @active_threads.value
50
73
  end
51
74
 
52
75
  # Waits until the pool is shut down. This will wait forever unless you
53
- # eventually call shutdown (either before calling `wait` or after it in
76
+ # eventually call {#shutdown} (either before calling `wait` or after it in
54
77
  # another thread).
78
+ #
79
+ # @return [void]
55
80
  def wait
56
81
  # Here we use a loop-sleep combination instead of using
57
82
  # ThreadPoolExecutor's `wait_for_termination`. See issue #21 for more
@@ -63,6 +88,9 @@ module Workhorse
63
88
  end
64
89
 
65
90
  # Shuts down the pool and waits for termination.
91
+ # All currently executing jobs will complete before shutdown.
92
+ #
93
+ # @return [void]
66
94
  def shutdown
67
95
  @executor.shutdown
68
96
  wait
@@ -1,11 +1,26 @@
1
1
  module Workhorse
2
+ # Scoped environment for method delegation.
3
+ # Used internally to provide scoped access to daemon configuration methods.
4
+ #
5
+ # @private
2
6
  class ScopedEnv
7
+ # Creates a new scoped environment.
8
+ #
9
+ # @param delegation_object [Object] Object to delegate method calls to
10
+ # @param methods [Array<Symbol>] Methods that should be delegated
11
+ # @param backup_binding [Object, nil] Fallback object for method resolution
3
12
  def initialize(delegation_object, methods, backup_binding = nil)
4
13
  @delegation_object = delegation_object
5
14
  @methods = methods
6
15
  @backup_binding = backup_binding
7
16
  end
8
17
 
18
+ # Handles method delegation to the configured objects.
19
+ #
20
+ # @param symbol [Symbol] Method name
21
+ # @param args [Array] Method arguments
22
+ # @param block [Proc, nil] Block to pass to the method
23
+ # @return [Object] Result of the delegated method call
9
24
  def method_missing(symbol, *args, &block)
10
25
  if @methods.include?(symbol)
11
26
  @delegation_object.send(symbol, *args, &block)
@@ -16,6 +31,11 @@ module Workhorse
16
31
  end
17
32
  end
18
33
 
34
+ # Checks if this object can respond to the given method.
35
+ #
36
+ # @param symbol [Symbol] Method name to check
37
+ # @param include_private [Boolean] Whether to include private methods
38
+ # @return [Boolean] True if method can be handled
19
39
  def respond_to_missing?(symbol, include_private = false)
20
40
  @methods.include?(symbol) || super
21
41
  end