rocketjob 6.3.2 → 7.1.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -316
  3. data/bin/rocketjob_batch_perf +1 -1
  4. data/lib/rocket_job/batch/categories.rb +39 -26
  5. data/lib/rocket_job/batch/io.rb +53 -22
  6. data/lib/rocket_job/batch/logger.rb +2 -2
  7. data/lib/rocket_job/batch/model.rb +2 -2
  8. data/lib/rocket_job/batch/performance.rb +1 -1
  9. data/lib/rocket_job/batch/statistics.rb +9 -2
  10. data/lib/rocket_job/batch/throttle.rb +7 -2
  11. data/lib/rocket_job/batch/throttle_running_workers.rb +5 -1
  12. data/lib/rocket_job/batch/throttle_windows.rb +4 -2
  13. data/lib/rocket_job/batch/worker.rb +51 -11
  14. data/lib/rocket_job/category/base.rb +119 -1
  15. data/lib/rocket_job/category/input.rb +166 -40
  16. data/lib/rocket_job/category/output.rb +7 -0
  17. data/lib/rocket_job/config.rb +6 -1
  18. data/lib/rocket_job/deprecation.rb +51 -0
  19. data/lib/rocket_job/dirmon_entry.rb +6 -4
  20. data/lib/rocket_job/event.rb +84 -4
  21. data/lib/rocket_job/extensions/mongo/logging.rb +1 -0
  22. data/lib/rocket_job/extensions/mongoid/clients/options.rb +3 -3
  23. data/lib/rocket_job/extensions/mongoid/contextual/mongo.rb +3 -4
  24. data/lib/rocket_job/extensions/mongoid/factory.rb +2 -2
  25. data/lib/rocket_job/extensions/psych/yaml_tree.rb +8 -4
  26. data/lib/rocket_job/extensions/rocket_job_adapter.rb +1 -1
  27. data/lib/rocket_job/jobs/active_job.rb +1 -1
  28. data/lib/rocket_job/jobs/copy_file_job.rb +11 -1
  29. data/lib/rocket_job/jobs/dirmon_job.rb +3 -1
  30. data/lib/rocket_job/jobs/on_demand_batch_job.rb +1 -1
  31. data/lib/rocket_job/jobs/upload_file_job.rb +8 -5
  32. data/lib/rocket_job/lookup_collection.rb +2 -2
  33. data/lib/rocket_job/performance.rb +1 -1
  34. data/lib/rocket_job/plugins/cron.rb +6 -3
  35. data/lib/rocket_job/plugins/document.rb +0 -2
  36. data/lib/rocket_job/plugins/job/logger.rb +2 -2
  37. data/lib/rocket_job/plugins/job/model.rb +1 -1
  38. data/lib/rocket_job/plugins/job/persistence.rb +2 -2
  39. data/lib/rocket_job/plugins/job/throttle.rb +14 -2
  40. data/lib/rocket_job/plugins/job/throttle_running_jobs.rb +4 -1
  41. data/lib/rocket_job/plugins/job/worker.rb +5 -2
  42. data/lib/rocket_job/plugins/retry.rb +36 -35
  43. data/lib/rocket_job/plugins/throttle_dependent_jobs.rb +7 -2
  44. data/lib/rocket_job/plugins/transaction.rb +2 -2
  45. data/lib/rocket_job/sliced/input.rb +17 -12
  46. data/lib/rocket_job/sliced/slice.rb +3 -3
  47. data/lib/rocket_job/sliced/slices.rb +3 -6
  48. data/lib/rocket_job/sliced/writer/input.rb +4 -4
  49. data/lib/rocket_job/supervisor.rb +2 -2
  50. data/lib/rocket_job/thread_worker.rb +3 -3
  51. data/lib/rocket_job/throttle_definition.rb +19 -2
  52. data/lib/rocket_job/throttle_definitions.rb +14 -9
  53. data/lib/rocket_job/version.rb +1 -1
  54. data/lib/rocket_job/worker.rb +77 -28
  55. data/lib/rocketjob.rb +2 -2
  56. metadata +34 -27
  57. data/lib/rocket_job/ractor_worker.rb +0 -42
@@ -94,11 +94,12 @@ module RocketJob
94
94
  end
95
95
 
96
96
  VALID_INSTANCE_METHODS = %i[upload upload_file_name= full_file_name=].freeze
97
+ private_constant :VALID_INSTANCE_METHODS
97
98
 
98
99
  # Validates job_class is a Rocket Job
99
100
  def job_implements_upload
100
101
  klass = job_class
101
- return if klass.nil? || klass.instance_methods.any? { |m| VALID_INSTANCE_METHODS.include?(m) }
102
+ return if klass.nil? || klass.instance_methods.intersect?(VALID_INSTANCE_METHODS)
102
103
 
103
104
  errors.add(:job_class_name,
104
105
  "#{job_class} must implement any one of: :#{VALID_INSTANCE_METHODS.join(' :')} instance methods")
@@ -120,14 +121,16 @@ module RocketJob
120
121
  klass = job_class
121
122
  return unless klass
122
123
 
123
- properties.each_pair do |k, _v|
124
- next if klass.public_method_defined?("#{k}=".to_sym)
124
+ # Mongoid 9 returns Hash field keys as Strings, earlier versions as Symbols.
125
+ properties.each_pair do |raw_key, value|
126
+ k = raw_key.to_sym
127
+ next if klass.public_method_defined?(:"#{k}=")
125
128
 
126
129
  if %i[output_categories input_categories].include?(k)
127
130
  category_class = k == :input_categories ? RocketJob::Category::Input : RocketJob::Category::Output
128
- properties[k].each do |category|
131
+ value.each do |category|
129
132
  category.each_pair do |key, _value|
130
- next if category_class.public_method_defined?("#{key}=".to_sym)
133
+ next if category_class.public_method_defined?(:"#{key}=")
131
134
 
132
135
  errors.add(
133
136
  :properties,
@@ -12,8 +12,8 @@ module RocketJob
12
12
  # end
13
13
  #
14
14
  # input_category(:my_lookup).find(id: 123).first
15
- def upload(batch_size: 10_000, &block)
16
- BatchUploader.upload(batch_size: batch_size, &block)
15
+ def upload(batch_size: 10_000, &)
16
+ BatchUploader.upload(batch_size: batch_size, &)
17
17
  end
18
18
 
19
19
  # Looks up the value at the specified id.
@@ -18,7 +18,7 @@ module RocketJob
18
18
  # Loads the queue with jobs to be processed once the queue is loaded.
19
19
  # Retain the first and last job for timings, all others are destroyed on completion.
20
20
  def run_test_case(count = self.count)
21
- if RocketJob::Server.where(:state.in => %w[running paused]).count.zero?
21
+ if RocketJob::Server.where(:state.in => %w[running paused]).none?
22
22
  raise "Please start servers before starting the performance test"
23
23
  end
24
24
 
@@ -18,7 +18,8 @@ module RocketJob
18
18
 
19
19
  # Whether to prevent another instance of this job from running with the exact _same_ cron schedule.
20
20
  # Another job instance with a different `cron_schedule` string is permitted.
21
- field :cron_singleton, type: Mongoid::Boolean, default: true, class_attribute: true, user_editable: true, copy_on_restart: true
21
+ field :cron_singleton, type: Mongoid::Boolean, default: true, class_attribute: true, user_editable: true,
22
+ copy_on_restart: true
22
23
 
23
24
  # Whether to re-schedule the next job occurrence when this job starts, or when it is complete.
24
25
  #
@@ -38,7 +39,8 @@ module RocketJob
38
39
  # at any time until after the job has failed, or is aborted.
39
40
  # - To prevent this job creating any new duplicate instances during subsequent processing,
40
41
  # its `cron_schedule` is set to `nil` after it fails or is aborted.
41
- field :cron_after_start, type: Mongoid::Boolean, default: true, class_attribute: true, user_editable: true, copy_on_restart: true
42
+ field :cron_after_start, type: Mongoid::Boolean, default: true, class_attribute: true, user_editable: true,
43
+ copy_on_restart: true
42
44
 
43
45
  validates_each :cron_schedule do |record, attr, value|
44
46
  record.errors.add(attr, "Invalid cron_schedule: #{value.inspect}") if value && !Fugit::Cron.new(value)
@@ -88,7 +90,8 @@ module RocketJob
88
90
  def rocket_job_cron_singleton_check
89
91
  return if cron_schedule.nil? || completed? || aborted? || !rocket_job_cron_duplicate?
90
92
 
91
- errors.add(:state, "Another instance of #{self.class.name} is already queued, running, failed, or paused with the same cron schedule: #{cron_schedule}")
93
+ errors.add(:state,
94
+ "Another instance of #{self.class.name} is already queued, running, failed, or paused with the same cron schedule: #{cron_schedule}")
92
95
  end
93
96
  end
94
97
  end
@@ -11,7 +11,6 @@ module RocketJob
11
11
  store_in client: "rocketjob"
12
12
  end
13
13
 
14
- # rubocop:disable Style/RedundantSort
15
14
  module ClassMethods
16
15
  # Mongoid does not apply ordering, add sort
17
16
  def first
@@ -23,7 +22,6 @@ module RocketJob
23
22
  all.sort("_id" => -1).first
24
23
  end
25
24
  end
26
- # rubocop:enable Style/RedundantSort
27
25
 
28
26
  private
29
27
 
@@ -17,7 +17,7 @@ module RocketJob
17
17
  # - log_exception logs entire exception if raised
18
18
  # - on_exception_level changes log level from info to error on exception
19
19
  # - silence noisy jobs by raising log level
20
- def rocket_job_around_logger(&block)
20
+ def rocket_job_around_logger(&)
21
21
  logger.info("Start #perform")
22
22
  logger.measure_info(
23
23
  "Completed #perform",
@@ -25,7 +25,7 @@ module RocketJob
25
25
  log_exception: :full,
26
26
  on_exception_level: :error,
27
27
  silence: log_level,
28
- &block
28
+ &
29
29
  )
30
30
  end
31
31
  end
@@ -185,7 +185,7 @@ module RocketJob
185
185
  self.rocket_job_restart_attributes += [name.to_sym]
186
186
  end
187
187
 
188
- super(name, options)
188
+ super
189
189
  end
190
190
 
191
191
  # Builds this job instance from the supplied properties hash.
@@ -79,8 +79,8 @@ module RocketJob
79
79
  return
80
80
  end
81
81
 
82
- job_attrs = self.class.rocket_job_restart_attributes.each_with_object({}) do |attr, attrs|
83
- attrs[attr] = send(attr)
82
+ job_attrs = self.class.rocket_job_restart_attributes.to_h do |attr|
83
+ [attr, send(attr)]
84
84
  end
85
85
  job_attrs.merge!(overrides)
86
86
 
@@ -30,6 +30,13 @@ module RocketJob
30
30
 
31
31
  included do
32
32
  class_attribute :rocket_job_throttles
33
+
34
+ # Human readable reason why this job is currently being throttled.
35
+ # Set when a throttle prevents the job from running, cleared when it starts.
36
+ # Surfaced in Rocket Job Mission Control.
37
+ field :throttled_by, type: String
38
+ # Time at which this job was last throttled.
39
+ field :throttled_at, type: Time
33
40
  end
34
41
 
35
42
  module ClassMethods
@@ -45,11 +52,16 @@ module RocketJob
45
52
  # Or, a block that will return the filter.
46
53
  # Default: :throttle_filter_class (Throttle all jobs of this class)
47
54
  #
55
+ # description: [String|Proc]
56
+ # Human readable reason why the job is throttled, persisted to `throttled_by`
57
+ # and shown in Mission Control. A Proc is called with the job and must
58
+ # return a String. Default: a humanized version of the method name.
59
+ #
48
60
  # Note: Throttles are executed in the order they are defined.
49
- def define_throttle(method_name, filter: :throttle_filter_class)
61
+ def define_throttle(method_name, filter: :throttle_filter_class, description: nil)
50
62
  # Duplicate to prevent modifying parent class throttles
51
63
  definitions = rocket_job_throttles ? rocket_job_throttles.deep_dup : ThrottleDefinitions.new
52
- definitions.add(method_name, filter)
64
+ definitions.add(method_name, filter, description)
53
65
  self.rocket_job_throttles = definitions
54
66
  end
55
67
 
@@ -32,7 +32,10 @@ module RocketJob
32
32
  # Allow jobs to be throttled by group name instance of the job class name.
33
33
  field :throttle_group, type: String, class_attribute: true, user_editable: true, copy_on_restart: true
34
34
 
35
- define_throttle :throttle_running_jobs_exceeded?
35
+ define_throttle :throttle_running_jobs_exceeded?,
36
+ description: lambda { |job, *|
37
+ "Throttled: maximum of #{job.throttle_running_jobs} running jobs reached"
38
+ }
36
39
  end
37
40
 
38
41
  private
@@ -66,10 +66,13 @@ module RocketJob
66
66
  # re_raise_exceptions: [true|false]
67
67
  # Re-raise the exception after updating the job
68
68
  # Default: false
69
- def fail_on_exception!(re_raise_exceptions = false, &block)
70
- SemanticLogger.named_tagged(job: id.to_s, &block)
69
+ def fail_on_exception!(re_raise_exceptions = false, &)
70
+ SemanticLogger.named_tagged(job: id.to_s, &)
71
71
  rescue Exception => e
72
72
  SemanticLogger.named_tagged(job: id.to_s) do
73
+ # Not a guard clause: the `fail` event clears `worker_name`, so the
74
+ # branches are not equivalent and must not be collapsed.
75
+ # rubocop:disable-next Style/GuardClause
73
76
  if failed? || !may_fail?
74
77
  self.exception = JobException.from_exception(e)
75
78
  exception.worker_name = worker_name
@@ -27,41 +27,42 @@ module RocketJob
27
27
  # MyCronJob.create!(retry_limit: 0)
28
28
  #
29
29
  # Below is a table of the delay for each retry attempt, as well as the _total_ duration spent retrying a job for
30
- # the specified number of retries, excluding actual processing time.
30
+ # the specified number of retries, excluding actual processing time. The delay before retry `n` is
31
+ # `(n - 1)**4 + 5` seconds.
31
32
  #
32
33
  # |---------|------------|----------------|
33
34
  # | Attempt | Delay | Total Duration |
34
35
  # |---------|------------|----------------|
35
- # | 01 | 6.000s | 6.000s |
36
- # | 02 | 21.000s | 27.000s |
37
- # | 03 | 1m 26s | 1m 53s |
38
- # | 04 | 4m 21s | 6m 14s |
39
- # | 05 | 10m 30s | 16m 44s |
40
- # | 06 | 21m 41s | 38m 25s |
41
- # | 07 | 40m 6s | 20h 18m |
42
- # | 08 | 20h 8m | 21h 26m |
43
- # | 09 | 20h 49m | 23h 16m |
44
- # | 10 | 21h 46m | 2h 3m |
45
- # | 11 | 23h 4m | 6h 7m |
46
- # | 12 | 0h 45m | 11h 52m |
47
- # | 13 | 2h 56m | 1d 19h 48m |
48
- # | 14 | 5h 40m | 1d 6h 29m |
49
- # | 15 | 9h 3m | 2d 20h 33m |
50
- # | 16 | 13h 12m | 2d 14h 45m |
51
- # | 17 | 18h 12m | 3d 13h 57m |
52
- # | 18 | 1d 0h 9m | 5d 19h 7m |
53
- # | 19 | 1d 7h 12m | 6d 7h 19m |
54
- # | 20 | 1d 15h 26m | 8d 3h 46m |
55
- # | 21 | 2d 1h 1m | 10d 9h 47m |
56
- # | 22 | 2d 12h 4m | 13d 2h 51m |
57
- # | 23 | 3d 0h 44m | 16d 8h 35m |
58
- # | 24 | 3d 15h 9m | 20d 4h 45m |
59
- # | 25 | 4d 7h 30m | 24d 17h 16 |
60
- # | 26 | 5d 1h 56m | 30d 0h 12m |
61
- # | 27 | 6d 22h 37m | 36d 3h 49m |
62
- # | 28 | 7d 21h 44m | 43d 6h 34m |
63
- # | 29 | 8d 23h 28m | 51d 11h 2m |
64
- # | 30 | 9d 4h 0m | 61d 20h 2m |
36
+ # | 01 | 5.000s | 5.000s |
37
+ # | 02 | 6.000s | 11.000s |
38
+ # | 03 | 21.000s | 32.000s |
39
+ # | 04 | 1m 26s | 1m 58s |
40
+ # | 05 | 4m 21s | 6m 19s |
41
+ # | 06 | 10m 30s | 16m 49s |
42
+ # | 07 | 21m 41s | 38m 30s |
43
+ # | 08 | 40m 6s | 1h 18m |
44
+ # | 09 | 1h 8m | 2h 26m |
45
+ # | 10 | 1h 49m | 4h 16m |
46
+ # | 11 | 2h 46m | 7h 3m |
47
+ # | 12 | 4h 4m | 11h 7m |
48
+ # | 13 | 5h 45m | 16h 52m |
49
+ # | 14 | 7h 56m | 1d 0h 49m |
50
+ # | 15 | 10h 40m | 1d 11h 29m |
51
+ # | 16 | 14h 3m | 2d 1h 33m |
52
+ # | 17 | 18h 12m | 2d 19h 45m |
53
+ # | 18 | 23h 12m | 3d 18h 57m |
54
+ # | 19 | 1d 5h 9m | 5d 0h 7m |
55
+ # | 20 | 1d 12h 12m | 6d 12h 19m |
56
+ # | 21 | 1d 20h 26m | 8d 8h 46m |
57
+ # | 22 | 2d 6h 1m | 10d 14h 47m |
58
+ # | 23 | 2d 17h 4m | 13d 7h 51m |
59
+ # | 24 | 3d 5h 44m | 16d 13h 36m |
60
+ # | 25 | 3d 20h 9m | 20d 9h 45m |
61
+ # | 26 | 4d 12h 30m | 24d 22h 16m |
62
+ # | 27 | 5d 6h 56m | 30d 5h 12m |
63
+ # | 28 | 6d 3h 37m | 36d 8h 50m |
64
+ # | 29 | 7d 2h 44m | 43d 11h 34m |
65
+ # | 30 | 8d 4h 28m | 51d 16h 2m |
65
66
  # |---------|------------|----------------|
66
67
  module Retry
67
68
  extend ActiveSupport::Concern
@@ -70,7 +71,7 @@ module RocketJob
70
71
  after_fail :rocket_job_retry
71
72
 
72
73
  # Maximum number of times to retry this job.
73
- # The default of 25 is almost 25 days of retries.
74
+ # The default of 25 retries spans about 20 days, see the table above.
74
75
  field :retry_limit, type: Integer, default: 25, class_attribute: true, user_editable: true, copy_on_restart: true
75
76
 
76
77
  # List of times when this job failed
@@ -115,7 +116,7 @@ module RocketJob
115
116
  #
116
117
  # For example, to see the durations for the first 25 retries:
117
118
  # count = 25
118
- # intervals = (1..count).map { |attempt| attempt**4 + 5 }
119
+ # intervals = (0...count).map { |failures| (failures**4) + 5 }
119
120
  #
120
121
  # Display the above intervals as human readable durations:
121
122
  # intervals.map { |seconds| RocketJob.seconds_as_duration(seconds) }
@@ -124,9 +125,9 @@ module RocketJob
124
125
  # RocketJob.seconds_as_duration(intervals.sum)
125
126
  #
126
127
  # Or, to see the total durations based on the number of retries:
127
- # (0..count).map{|i| "#{i+1} ==> #{RocketJob.seconds_as_duration(intervals[0..i].sum)}"}
128
+ # (0...count).map { |i| "#{i + 1} ==> #{RocketJob.seconds_as_duration(intervals[0..i].sum)}" }
128
129
  def rocket_job_retry_seconds_to_delay
129
- rocket_job_failure_count**4 + 5
130
+ (rocket_job_failure_count**4) + 5
130
131
  end
131
132
  end
132
133
  end
@@ -13,8 +13,13 @@ module RocketJob
13
13
  included do
14
14
  field :dependent_jobs, type: Array, class_attribute: true, user_editable: true, copy_on_restart: true
15
15
 
16
- define_throttle :dependent_jobs_running?
17
- define_batch_throttle :dependent_jobs_running? if respond_to?(:define_batch_throttle)
16
+ dependent_jobs_description = lambda { |job, *|
17
+ "Throttled: dependent jobs are running (#{Array(job.dependent_jobs).join(', ')})"
18
+ }
19
+ define_throttle :dependent_jobs_running?, description: dependent_jobs_description
20
+ if respond_to?(:define_batch_throttle)
21
+ define_batch_throttle :dependent_jobs_running?, description: dependent_jobs_description
22
+ end
18
23
  end
19
24
 
20
25
  class_methods do
@@ -41,8 +41,8 @@ module RocketJob
41
41
 
42
42
  private
43
43
 
44
- def rocket_job_transaction(&block)
45
- ActiveRecord::Base.transaction(&block)
44
+ def rocket_job_transaction(&)
45
+ ActiveRecord::Base.transaction(&)
46
46
  end
47
47
  end
48
48
  end
@@ -1,10 +1,10 @@
1
1
  module RocketJob
2
2
  module Sliced
3
3
  class Input < Slices
4
- def upload(**args, &block)
4
+ def upload(**args, &)
5
5
  # Create indexes before uploading
6
6
  create_indexes
7
- Writer::Input.collect(self, **args, &block)
7
+ Writer::Input.collect(self, **args, &)
8
8
  rescue Exception => e
9
9
  drop
10
10
  raise(e)
@@ -117,19 +117,24 @@ module RocketJob
117
117
  )
118
118
  end
119
119
 
120
- # Returns the next slice to work on in id order
121
- # Returns nil if there are currently no queued slices
120
+ # Claims and returns the next queued slice for this worker.
121
+ # Returns nil if there are currently no queued slices.
122
122
  #
123
- # If a slice is in queued state it will be started and assigned to this worker
123
+ # No explicit sort is applied. Forcing the global minimum `_id` (a
124
+ # `sort("_id" => 1)`) makes every worker target the same document, so under
125
+ # concurrency they collide on the atomic claim: one wins and the rest hit a
126
+ # WriteConflict ("Document no longer matches the predicate") and retry,
127
+ # which throttles a large batch job as workers are added. Without the sort,
128
+ # the `{state: 1, _id: 1}` index still yields queued slices in roughly `_id`
129
+ # (upload) order, but concurrent workers land on different documents instead
130
+ # of contending for one. Output ordering is unaffected: each output slice
131
+ # inherits its input slice's `_id` and downloads read in `_id` order.
124
132
  def next_slice(worker_name)
125
- # TODO: Will it perform faster without the id sort?
126
- # I.e. Just process on a FIFO basis?
127
133
  document = all.queued.
128
- sort("_id" => 1).
129
- find_one_and_update(
130
- {"$set" => {worker_name: worker_name, state: "running", started_at: Time.now}},
131
- return_document: :after
132
- )
134
+ find_one_and_update(
135
+ {"$set" => {worker_name: worker_name, state: "running", started_at: Time.now}},
136
+ return_document: :after
137
+ )
133
138
  document.collection_name = collection_name if document
134
139
  document
135
140
  end
@@ -154,8 +154,8 @@ module RocketJob
154
154
  end
155
155
 
156
156
  # Fail this slice if an exception occurs during processing.
157
- def fail_on_exception!(re_raise_exceptions = false, &block)
158
- SemanticLogger.named_tagged(slice: id.to_s, &block)
157
+ def fail_on_exception!(re_raise_exceptions = false, &)
158
+ SemanticLogger.named_tagged(slice: id.to_s, &)
159
159
  rescue Exception => e
160
160
  SemanticLogger.named_tagged(slice: id.to_s) do
161
161
  if failed? || !may_fail?
@@ -175,7 +175,7 @@ module RocketJob
175
175
 
176
176
  # Always add records to any updates.
177
177
  def atomic_updates(*args)
178
- r = super(*args)
178
+ r = super
179
179
  (r["$set"] ||= {})["records"] = serialize_records if @records
180
180
  r
181
181
  end
@@ -44,8 +44,8 @@ module RocketJob
44
44
 
45
45
  # Returns output slices in the order of their id
46
46
  # which is usually the order in which they were written.
47
- def each(&block)
48
- all.sort(id: 1).each(&block)
47
+ def each(&)
48
+ all.sort(id: 1).each(&)
49
49
  end
50
50
 
51
51
  # Insert a new slice into the collection
@@ -120,7 +120,7 @@ module RocketJob
120
120
  end
121
121
 
122
122
  # Forward additional methods.
123
- def_instance_delegators :@all, :collection, :count, :delete_all, :first, :find, :last, :nor, :not, :or, :to_a, :where
123
+ def_instance_delegators :@all, :collection, :count, :delete_all, :find, :nor, :not, :or, :to_a, :where
124
124
 
125
125
  # Drop this collection when it is no longer needed
126
126
  def drop
@@ -145,7 +145,6 @@ module RocketJob
145
145
  end
146
146
 
147
147
  # Mongoid does not apply ordering, add sort
148
- # rubocop:disable Style/RedundantSort
149
148
  def first
150
149
  all.sort("_id" => 1).first
151
150
  end
@@ -154,8 +153,6 @@ module RocketJob
154
153
  all.sort("_id" => -1).first
155
154
  end
156
155
 
157
- # rubocop:enable Style/RedundantSort
158
-
159
156
  # Returns [Array<Struct>] grouped exceptions by class name,
160
157
  # and unique exception messages by exception class.
161
158
  #
@@ -9,8 +9,9 @@ module RocketJob
9
9
  #
10
10
  # Parameters
11
11
  # on_first: [Proc]
12
- # Block to call on the first line only, instead of storing in the slice.
13
- # Useful for extracting the header row
12
+ # Called with each line before the first that is stored, instead of storing it, until it returns true.
13
+ # Useful for extracting the header row, after any blank lines before it, see
14
+ # Category::Input#extract_header_callback.
14
15
  # Default: nil
15
16
  #
16
17
  # slice_size: [Integer]
@@ -42,8 +43,7 @@ module RocketJob
42
43
 
43
44
  def <<(line)
44
45
  if @on_first
45
- @on_first.call(line)
46
- @on_first = nil
46
+ @on_first = nil if @on_first.call(line)
47
47
  return self
48
48
  end
49
49
  @slice << line
@@ -92,8 +92,8 @@ module RocketJob
92
92
  end
93
93
  end
94
94
 
95
- def synchronize(&block)
96
- @mutex.synchronize(&block)
95
+ def synchronize(&)
96
+ @mutex.synchronize(&)
97
97
  end
98
98
  end
99
99
  end
@@ -8,7 +8,7 @@ module RocketJob
8
8
  attr_reader :thread
9
9
 
10
10
  def initialize(id:, server_name:)
11
- super(id: id, server_name: server_name)
11
+ super
12
12
  @shutdown = Concurrent::Event.new
13
13
  @thread = Thread.new { run }
14
14
  end
@@ -21,8 +21,8 @@ module RocketJob
21
21
  @thread.backtrace
22
22
  end
23
23
 
24
- def join(*args)
25
- @thread.join(*args)
24
+ def join(*)
25
+ @thread.join(*)
26
26
  end
27
27
 
28
28
  # Send each active worker the RocketJob::ShutdownException so that stops processing immediately.
@@ -1,10 +1,18 @@
1
1
  module RocketJob
2
2
  class ThrottleDefinition
3
- attr_reader :method_name, :filter
3
+ attr_reader :method_name, :filter, :description
4
4
 
5
- def initialize(method_name, filter)
5
+ # Parameters:
6
+ # description: [String|Proc|nil]
7
+ # Human readable reason why the job is throttled, persisted to the job as
8
+ # `throttled_by` and surfaced in Mission Control.
9
+ # When a Proc, it is called with the same arguments as the throttle and must
10
+ # return a String, allowing the reason to include runtime detail.
11
+ # When nil, a humanized version of the method name is used.
12
+ def initialize(method_name, filter, description = nil)
6
13
  @method_name = method_name.to_sym
7
14
  @filter = filter
15
+ @description = description
8
16
  end
9
17
 
10
18
  # Returns [true|false] whether the throttle was triggered.
@@ -33,5 +41,14 @@ module RocketJob
33
41
  job.send(filter)
34
42
  end
35
43
  end
44
+
45
+ # Returns [String] the human readable reason why the job is throttled.
46
+ def extract_description(job, *)
47
+ return description.call(job, *) if description.is_a?(Proc)
48
+ return description if description
49
+
50
+ # Default: humanize the method name, dropping a trailing `?` or `_exceeded`.
51
+ method_name.to_s.sub(/_exceeded\?\z/, "").sub(/\?\z/, "").tr("_", " ").capitalize
52
+ end
36
53
  end
37
54
  end
@@ -6,13 +6,13 @@ module RocketJob
6
6
  @throttles = []
7
7
  end
8
8
 
9
- def add(method_name, filter)
9
+ def add(method_name, filter, description = nil)
10
10
  unless filter.is_a?(Symbol) || filter.is_a?(Proc)
11
11
  raise(ArgumentError, "Filter for #{method_name} must be a Symbol or Proc")
12
12
  end
13
13
  raise(ArgumentError, "Cannot define #{method_name} twice, undefine previous throttle first") if exist?(method_name)
14
14
 
15
- @throttles += [ThrottleDefinition.new(method_name, filter)]
15
+ @throttles += [ThrottleDefinition.new(method_name, filter, description)]
16
16
  end
17
17
 
18
18
  # Undefine a previously defined throttle
@@ -25,15 +25,20 @@ module RocketJob
25
25
  throttles.any? { |throttle| throttle.method_name == method_name }
26
26
  end
27
27
 
28
- # Returns the matching filter,
28
+ # Returns [ThrottleDefinition] the first throttle that was triggered,
29
29
  # or nil if no throttles were triggered.
30
- def matching_filter(job, *args)
31
- throttles.each do |throttle|
32
- next unless throttle.throttled?(job, *args)
30
+ #
31
+ # The returned definition exposes both the filter (`extract_filter`) and the
32
+ # human readable reason (`extract_description`) so callers can apply the filter
33
+ # and persist why the job was throttled.
34
+ def matching_throttle(job, *args)
35
+ throttles.find { |throttle| throttle.throttled?(job, *args) }
36
+ end
33
37
 
34
- return throttle.extract_filter(job, *args)
35
- end
36
- nil
38
+ # Returns the matching filter,
39
+ # or nil if no throttles were triggered.
40
+ def matching_filter(job, *)
41
+ matching_throttle(job, *)&.extract_filter(job, *)
37
42
  end
38
43
 
39
44
  def deep_dup
@@ -1,3 +1,3 @@
1
1
  module RocketJob
2
- VERSION = "6.3.2".freeze
2
+ VERSION = "7.1.0".freeze
3
3
  end