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
@@ -63,25 +63,29 @@ module RocketJob
63
63
  logger.info "Started"
64
64
 
65
65
  until shutdown?
66
- sleep_seconds = Config.max_poll_seconds
67
66
  reset_filter_if_expired
68
67
  job = next_available_job
69
68
 
70
- # Returns true when work was completed, but no other work is available
71
- if job&.rocket_job_work(self, false)
72
- # Return the database connections for this fiber back to the connection pool
73
- if defined?(ActiveRecord::Base)
74
- if ActiveRecord::Base.respond_to?(:connection_handler)
75
- # Rails 7 .2
76
- ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
69
+ if job
70
+ # Returns true when this job has no more work immediately available,
71
+ # for example a batch job that has run out of slices, or was throttled.
72
+ no_immediate_work = job.rocket_job_work(self, false)
73
+
74
+ # Return the database connections for this thread back to the connection pool.
75
+ release_active_record_connections
76
+
77
+ sleep_seconds =
78
+ if no_immediate_work
79
+ # Stagger workers so that they don't all poll at the same time.
80
+ random_wait_interval
77
81
  else
78
- ActiveRecord::Base.clear_active_connections!
82
+ # Work was performed and more queued work is likely available,
83
+ # so poll again immediately rather than waiting a full interval.
84
+ 0
79
85
  end
80
- end
81
-
82
-
83
- # Stagger workers so that they don't all poll at the same time.
84
- sleep_seconds = random_wait_interval
86
+ else
87
+ # No work was found, so wait the full poll interval before checking again.
88
+ sleep_seconds = Config.max_poll_seconds
85
89
  end
86
90
 
87
91
  wait_for_shutdown?(sleep_seconds)
@@ -91,12 +95,18 @@ module RocketJob
91
95
  rescue Exception => e
92
96
  logger.fatal("Unhandled exception in job processing thread", e)
93
97
  ensure
94
- if defined?(ActiveRecord::Base)
95
- if ActiveRecord::Base.respond_to?(:connection_handler)
96
- ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
97
- else
98
- ActiveRecord::Base.clear_active_connections!
99
- end
98
+ release_active_record_connections
99
+ end
100
+
101
+ # Return the database connections for this thread back to the connection pool.
102
+ def release_active_record_connections
103
+ return unless defined?(ActiveRecord::Base)
104
+
105
+ if ActiveRecord::Base.respond_to?(:connection_handler)
106
+ # Rails 7.2+
107
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
108
+ else
109
+ ActiveRecord::Base.clear_active_connections!
100
110
  end
101
111
  end
102
112
 
@@ -149,12 +159,19 @@ module RocketJob
149
159
  # Whether the supplied job has been throttled and should be ignored.
150
160
  def throttled_job?(job)
151
161
  # Evaluate job throttles, if any.
152
- filter = job.rocket_job_throttles.matching_filter(job)
153
- return false unless filter
154
-
155
- add_to_current_filter(filter)
156
- # Restore retrieved job so that other workers can process it later
157
- job.set(worker_name: nil, state: :queued)
162
+ throttle = job.rocket_job_throttles.matching_throttle(job)
163
+ return false unless throttle
164
+
165
+ add_to_current_filter(throttle.extract_filter(job))
166
+ # Restore retrieved job so that other workers can process it later, recording
167
+ # why it was throttled so it is visible in Mission Control. This reuses the
168
+ # write that requeues the job, so it adds no extra database round trip.
169
+ job.set(
170
+ worker_name: nil,
171
+ state: :queued,
172
+ throttled_by: throttle.extract_description(job),
173
+ throttled_at: Time.now
174
+ )
158
175
  true
159
176
  end
160
177
 
@@ -164,14 +181,46 @@ module RocketJob
164
181
  # Applies the current filter to exclude filtered jobs.
165
182
  #
166
183
  # Returns nil if no jobs are available for processing.
184
+ #
185
+ # Notes:
186
+ # - An already running batch job is _joined_ with a read-only query, since
187
+ # concurrency for a batch job is coordinated per-slice (see
188
+ # `Sliced::Input#next_slice`), not on the job document. Writing
189
+ # `worker_name`/`state` to the shared job document on every poll turns it
190
+ # into a write-contention hotspot: with many workers MongoDB serializes the
191
+ # updates and retries the write conflicts (server log id 46404), which is
192
+ # what makes a large batch job slow down as workers are added.
193
+ # - Only a queued job is claimed with a write, transitioning it to running.
194
+ # That write is naturally distributed: each queued job is claimed once.
167
195
  def find_and_assign_job
168
196
  SemanticLogger.silence(:info) do
169
197
  scheduled = RocketJob::Job.where(run_at: nil).or(:run_at.lte => Time.now)
170
198
  working = RocketJob::Job.queued.or(state: "running", sub_state: "processing")
171
199
  query = RocketJob::Job.and(working, scheduled)
172
200
  query = query.and(current_filter) unless current_filter.blank?
173
- update = {"$set" => {"worker_name" => name, "state" => "running"}}
174
- query.sort(priority: 1, _id: 1).find_one_and_update(update, bypass_document_validation: true)
201
+ query = query.sort(priority: 1, _id: 1)
202
+
203
+ # Retry only when a queued job was claimed by another worker first.
204
+ loop do
205
+ job = query.first
206
+ return nil unless job
207
+
208
+ # Already running batch job: join it without writing to the job document.
209
+ return job if job.running?
210
+
211
+ # Queued job: claim it atomically, guarding on it still being queued.
212
+ # find_one_and_update returns the pre-image (state: queued) so that the
213
+ # caller still runs throttles and `start!`.
214
+ # Clear any stale throttle reason from a previous throttled poll so a job
215
+ # that is now allowed to run no longer reports as throttled in Mission Control.
216
+ update = {
217
+ "$set" => {"worker_name" => name, "state" => "running"},
218
+ "$unset" => {"throttled_by" => "", "throttled_at" => ""}
219
+ }
220
+ claimed = RocketJob::Job.queued.where(id: job.id).
221
+ find_one_and_update(update, bypass_document_validation: true)
222
+ return claimed if claimed
223
+ end
175
224
  end
176
225
  end
177
226
 
data/lib/rocketjob.rb CHANGED
@@ -23,6 +23,7 @@ module RocketJob
23
23
  autoload :ActiveWorker, "rocket_job/active_worker"
24
24
  autoload :Batch, "rocket_job/batch"
25
25
  autoload :CLI, "rocket_job/cli"
26
+ autoload :Deprecation, "rocket_job/deprecation"
26
27
  autoload :DirmonEntry, "rocket_job/dirmon_entry"
27
28
  autoload :Event, "rocket_job/event"
28
29
  autoload :Heartbeat, "rocket_job/heartbeat"
@@ -31,7 +32,6 @@ module RocketJob
31
32
  autoload :LookupCollection, "rocket_job/lookup_collection"
32
33
  autoload :Worker, "rocket_job/worker"
33
34
  autoload :Performance, "rocket_job/performance"
34
- autoload :RactorWorker, "rocket_job/ractor_worker"
35
35
  autoload :Server, "rocket_job/server"
36
36
  autoload :Sliced, "rocket_job/sliced"
37
37
  autoload :Subscriber, "rocket_job/subscriber"
@@ -83,7 +83,7 @@ module RocketJob
83
83
  autoload :UploadFileJob, "rocket_job/jobs/upload_file_job"
84
84
 
85
85
  module ReEncrypt
86
- autoload :RelationalJob, "rocket_job/jobs/re_encrypt/relational_job"
86
+ autoload :RelationalJob, "rocket_job/jobs/re_encrypt/relational_job"
87
87
  end
88
88
  end
89
89
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocketjob
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.2
4
+ version: 7.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-01-31 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: aasm
@@ -56,62 +55,65 @@ dependencies:
56
55
  name: iostreams
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
- - - ">="
58
+ - - "~>"
60
59
  - !ruby/object:Gem::Version
61
- version: '1.9'
60
+ version: '2.2'
62
61
  type: :runtime
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
- - - ">="
65
+ - - "~>"
67
66
  - !ruby/object:Gem::Version
68
- version: '1.9'
67
+ version: '2.2'
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: mongoid
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
72
  - - ">="
74
73
  - !ruby/object:Gem::Version
75
- version: '7.5'
74
+ version: '8.1'
76
75
  type: :runtime
77
76
  prerelease: false
78
77
  version_requirements: !ruby/object:Gem::Requirement
79
78
  requirements:
80
79
  - - ">="
81
80
  - !ruby/object:Gem::Version
82
- version: '7.5'
81
+ version: '8.1'
83
82
  - !ruby/object:Gem::Dependency
84
83
  name: semantic_logger
85
84
  requirement: !ruby/object:Gem::Requirement
86
85
  requirements:
87
- - - ">="
86
+ - - "~>"
88
87
  - !ruby/object:Gem::Version
89
- version: '4.7'
88
+ version: '5.0'
90
89
  type: :runtime
91
90
  prerelease: false
92
91
  version_requirements: !ruby/object:Gem::Requirement
93
92
  requirements:
94
- - - ">="
93
+ - - "~>"
95
94
  - !ruby/object:Gem::Version
96
- version: '4.7'
95
+ version: '5.0'
97
96
  - !ruby/object:Gem::Dependency
98
97
  name: symmetric-encryption
99
98
  requirement: !ruby/object:Gem::Requirement
100
99
  requirements:
101
- - - ">="
100
+ - - "~>"
102
101
  - !ruby/object:Gem::Version
103
- version: '4.3'
102
+ version: '4.6'
104
103
  type: :runtime
105
104
  prerelease: false
106
105
  version_requirements: !ruby/object:Gem::Requirement
107
106
  requirements:
108
- - - ">="
107
+ - - "~>"
109
108
  - !ruby/object:Gem::Version
110
- version: '4.3'
111
- description:
112
- email:
109
+ version: '4.6'
110
+ description: Rocket Job is a distributed, priority-based, MongoDB-backed batch processing
111
+ system for Ruby. Run conventional background jobs, or split a single job's input
112
+ into slices and process it concurrently across thousands of workers, spilling from
113
+ memory to disk so very large files never fall over.
113
114
  executables:
114
115
  - rocketjob
116
+ - rocketjob_batch_perf
115
117
  - rocketjob_perf
116
118
  extensions: []
117
119
  extra_rdoc_files: []
@@ -143,6 +145,7 @@ files:
143
145
  - lib/rocket_job/category/output.rb
144
146
  - lib/rocket_job/cli.rb
145
147
  - lib/rocket_job/config.rb
148
+ - lib/rocket_job/deprecation.rb
146
149
  - lib/rocket_job/dirmon_entry.rb
147
150
  - lib/rocket_job/event.rb
148
151
  - lib/rocket_job/extensions/iostreams/path.rb
@@ -186,7 +189,6 @@ files:
186
189
  - lib/rocket_job/plugins/state_machine.rb
187
190
  - lib/rocket_job/plugins/throttle_dependent_jobs.rb
188
191
  - lib/rocket_job/plugins/transaction.rb
189
- - lib/rocket_job/ractor_worker.rb
190
192
  - lib/rocket_job/railtie.rb
191
193
  - lib/rocket_job/rocket_job.rb
192
194
  - lib/rocket_job/server.rb
@@ -217,11 +219,16 @@ files:
217
219
  - lib/rocket_job/worker.rb
218
220
  - lib/rocket_job/worker_pool.rb
219
221
  - lib/rocketjob.rb
220
- homepage: https://rocketjob.io
222
+ homepage: https://rocketjob.reidmorrison.com
221
223
  licenses:
222
224
  - Apache-2.0
223
- metadata: {}
224
- post_install_message:
225
+ metadata:
226
+ bug_tracker_uri: https://github.com/reidmorrison/rocketjob/issues
227
+ changelog_uri: https://github.com/reidmorrison/rocketjob/blob/main/CHANGELOG.md
228
+ documentation_uri: https://rocketjob.reidmorrison.com
229
+ homepage_uri: https://rocketjob.reidmorrison.com
230
+ source_code_uri: https://github.com/reidmorrison/rocketjob/tree/v7.1.0
231
+ rubygems_mfa_required: 'true'
225
232
  rdoc_options: []
226
233
  require_paths:
227
234
  - lib
@@ -229,15 +236,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
229
236
  requirements:
230
237
  - - ">="
231
238
  - !ruby/object:Gem::Version
232
- version: '2.7'
239
+ version: 3.2.0
233
240
  required_rubygems_version: !ruby/object:Gem::Requirement
234
241
  requirements:
235
242
  - - ">="
236
243
  - !ruby/object:Gem::Version
237
244
  version: '0'
238
245
  requirements: []
239
- rubygems_version: 3.5.3
240
- signing_key:
246
+ rubygems_version: 3.6.9
241
247
  specification_version: 4
242
- summary: Ruby's missing batch processing system.
248
+ summary: Process millions of records across thousands of workers. A distributed, MongoDB-backed
249
+ batch processing system for Ruby.
243
250
  test_files: []
@@ -1,42 +0,0 @@
1
- module RocketJob
2
- # Run each worker in its own "Ractor".
3
- class RactorWorker < Worker
4
- attr_reader :thread
5
-
6
- def initialize(id:, server_name:)
7
- super(id: id, server_name: server_name)
8
- @shutdown = Concurrent::Event.new
9
- @thread = Ractor.new(name: "rocketjob-#{id}") { run }
10
- end
11
-
12
- def alive?
13
- @thread.alive?
14
- end
15
-
16
- def backtrace
17
- @thread.backtrace
18
- end
19
-
20
- def join(*args)
21
- @thread.join(*args)
22
- end
23
-
24
- # Send each active worker the RocketJob::ShutdownException so that stops processing immediately.
25
- def kill
26
- @thread.raise(Shutdown, "Shutdown due to kill request for worker: #{name}") if @thread.alive?
27
- end
28
-
29
- def shutdown?
30
- @shutdown.set?
31
- end
32
-
33
- def shutdown!
34
- @shutdown.set
35
- end
36
-
37
- # Returns [true|false] whether the shutdown indicator was set
38
- def wait_for_shutdown?(timeout = nil)
39
- @shutdown.wait(timeout)
40
- end
41
- end
42
- end