solid_queue 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50d118a41d39dff4da741a1e7fb105b47571dd719b883fb53843e7b57f7aff66
4
- data.tar.gz: a1dc7ff9b71a07f41fab851ea0dd2f16170240e2a465098f98e75a96c33edb4b
3
+ metadata.gz: b4a85fb424e127543352846ee1b0b4aa958d4c77a7a9e1c26a8082464ae7bebc
4
+ data.tar.gz: f15b4db5c77c14af4b989fc8abe2670fd49006853ca9bfce0080e8b15df733c3
5
5
  SHA512:
6
- metadata.gz: f344e6ba1a395f014fc08437c1322eac9b3f5218874f4b824cacab042d20053ee9a5504601e37864e04ccc956fc1f08fe35979c9d4a7b19dc42cc53ae5502c56
7
- data.tar.gz: 27e1d8c00e2c68330d52e938bb00ecadaf246678e16b12b0e1269f8a4e8e3328a972b5a007a04d2839baf2edb480e9f0cae3b6099cd6009a75b11e85858ee6e2
6
+ metadata.gz: ba913d760e6f1ac8bac01f68b3ffeb61811952d13a7862731c517dea0ec3e8938b8fb5194113e356fe8a35a02f4d95b306f327772e21f8f6098349bd1235dc53
7
+ data.tar.gz: 7418c2db5be34b59e123395ee3edf519d703b6e0488a564a85c24dc22344d67e894ad8bb378f88a3cd6e0b9085602be1850dab60c168816e92b081364b720594
data/README.md CHANGED
@@ -145,6 +145,56 @@ When receiving a `QUIT` signal, if workers still have jobs in-flight, these will
145
145
 
146
146
  If processes have no chance of cleaning up before exiting (e.g. if someone pulls a cable somewhere), in-flight jobs might remain claimed by the processes executing them. Processes send heartbeats, and the supervisor checks and prunes processes with expired heartbeats, which will release any claimed jobs back to their queues. You can configure both the frequency of heartbeats and the threshold to consider a process dead. See the section below for this.
147
147
 
148
+
149
+ ### Dedicated database configuration
150
+
151
+ Solid Queue can be configured to run on a different database than the main application.
152
+
153
+ Configure the `connects_to` option in `config/application.rb` or your environment config, with the custom database configuration that will be used in the abstract `SolidQueue::Record` Active Record model.
154
+
155
+ ```ruby
156
+ # Use a separate DB for Solid Queue
157
+ config.solid_queue.connects_to = { database: { writing: :solid_queue_primary, reading: :solid_queue_replica } }
158
+ ```
159
+
160
+ Add the dedicated database configuration to `config/database.yml`, differentiating between the main app's database and the dedicated `solid_queue` database. Make sure to include the `migrations_paths` for the solid queue database. This is where migration files for Solid Queue tables will reside.
161
+
162
+ ```yml
163
+ default: &default
164
+ adapter: sqlite3
165
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
166
+ timeout: 5000
167
+
168
+ solid_queue: &solid_queue
169
+ <<: *default
170
+ migrations_paths: db/solid_queue_migrate
171
+
172
+ development:
173
+ primary:
174
+ <<: *default
175
+ # ...
176
+ solid_queue_primary:
177
+ <<: *solid_queue
178
+ # ...
179
+ solid_queue_replica:
180
+ <<: *solid_queue
181
+ # ...
182
+ ```
183
+
184
+ Install migrations and specify the dedicated database name with the `DATABASE` option. This will create the Solid Queue migration files in a separate directory, matching the value provided in `migrations_paths` in `config/database.yml`.
185
+
186
+ ```bash
187
+ $ bin/rails solid_queue:install:migrations DATABASE=solid_queue
188
+ ```
189
+
190
+ Note: If you've already run the solid queue install command (`bin/rails generate solid_queue:install`), the migration files will have already been generated under the primary database's `db/migrate/` directory. You can remove these files and keep the ones generated by the database-specific migration installation above.
191
+
192
+ Finally, run the migrations:
193
+
194
+ ```bash
195
+ $ bin/rails db:migrate
196
+ ```
197
+
148
198
  ### Other configuration settings
149
199
  _Note_: The settings in this section should be set in your `config/application.rb` or your environment config like this: `config.solid_queue.silence_polling = true`
150
200
 
@@ -156,12 +206,6 @@ There are several settings that control how Solid Queue works that you can set a
156
206
  ```ruby
157
207
  -> (exception) { Rails.error.report(exception, handled: false) }
158
208
  ```
159
- - `connects_to`: a custom database configuration that will be used in the abstract `SolidQueue::Record` Active Record model. This is required to use a different database than the main app. For example:
160
-
161
- ```ruby
162
- # Use a separate DB for Solid Queue
163
- config.solid_queue.connects_to = { database: { writing: :solid_queue_primary, reading: :solid_queue_replica } }
164
- ```
165
209
  - `use_skip_locked`: whether to use `FOR UPDATE SKIP LOCKED` when performing locking reads. This will be automatically detected in the future, and for now, you'd only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8, and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential.
166
210
  - `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
167
211
  - `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
@@ -173,6 +217,10 @@ There are several settings that control how Solid Queue works that you can set a
173
217
  - `default_concurrency_control_period`: the value to be used as the default for the `duration` parameter in [concurrency controls](#concurrency-controls). It defaults to 3 minutes.
174
218
  - `enqueue_after_transaction_commit`: whether the job queuing is deferred to after the current Active Record transaction is committed. The default is `false`. [Read more](https://github.com/rails/rails/pull/51426).
175
219
 
220
+ ## Errors when enqueuing
221
+ Solid Queue will raise a `SolidQueue::Job::EnqueueError` for any Active Record errors that happen when enqueuing a job. The reason for not raising `ActiveJob::EnqueueError` is that this one gets handled by Active Job, causing `perform_later` to return `false` and set `job.enqueue_error`, yielding the job to a block that you need to pass to `perform_later`. This works very well for your own jobs, but makes failure very hard to handle for jobs enqueued by Rails or other gems, such as `Turbo::Streams::BroadcastJob` or `ActiveStorage::AnalyzeJob`, because you don't control the call to `perform_later` in that cases.
222
+
223
+ In the case of recurring tasks, if such error is raised when enqueuing the job corresponding to the task, it'll be handled and logged but it won't bubble up.
176
224
 
177
225
  ## Concurrency controls
178
226
  Solid Queue extends Active Job with concurrency controls, that allows you to limit how many jobs of a certain type or with certain arguments can run at the same time. When limited in this way, jobs will be blocked from running, and they'll stay blocked until another job finishes and unblocks them, or after the set expiry time (concurrency limit's _duration_) elapses. Jobs are never discarded or lost, only blocked.
@@ -252,7 +300,7 @@ By default, Solid Queue runs in the same DB as your app, and job enqueuing is _n
252
300
  If you prefer not to rely on this, or avoid relying on it unintentionally, you should make sure that:
253
301
  - You set [`config.active_job.enqueue_after_transaction_commit`](https://edgeguides.rubyonrails.org/configuring.html#config-active-job-enqueue-after-transaction-commit) to `always`, if you're using Rails 7.2+.
254
302
  - Or, your jobs relying on specific records are always enqueued on [`after_commit` callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#after-commit-and-after-rollback) or otherwise from a place where you're certain that whatever data the job will use has been committed to the database before the job is enqueued.
255
- - Or, you configure a database for Solid Queue, even if it's the same as your app, ensuring that a different connection on the thread handling requests or running jobs for your app will be used to enqueue jobs. For example:
303
+ - Or, you configure a different database for Solid Queue, even if it's the same as your app, ensuring that a different connection on the thread handling requests or running jobs for your app will be used to enqueue jobs. For example:
256
304
 
257
305
  ```ruby
258
306
  class ApplicationRecord < ActiveRecord::Base
@@ -288,7 +336,7 @@ Tasks are enqueued at their corresponding times by the dispatcher that owns them
288
336
 
289
337
  It's possible to run multiple dispatchers with the same `recurring_tasks` configuration. To avoid enqueuing duplicate tasks at the same time, an entry in a new `solid_queue_recurring_executions` table is created in the same transaction as the job is enqueued. This table has a unique index on `task_key` and `run_at`, ensuring only one entry per task per time will be created. This only works if you have `preserve_finished_jobs` set to `true` (the default), and the guarantee applies as long as you keep the jobs around.
290
338
 
291
- Finally, it's possible to configure jobs that aren't handled by Solid Queue. That's it, you can a have a job like this in your app:
339
+ Finally, it's possible to configure jobs that aren't handled by Solid Queue. That is, you can have a job like this in your app:
292
340
  ```ruby
293
341
  class MyResqueJob < ApplicationJob
294
342
  self.queue_adapter = :resque
@@ -3,6 +3,8 @@
3
3
  class SolidQueue::ClaimedExecution < SolidQueue::Execution
4
4
  belongs_to :process
5
5
 
6
+ scope :orphaned, -> { where.missing(:process) }
7
+
6
8
  class Result < Struct.new(:success, :error)
7
9
  def success?
8
10
  success
@@ -33,9 +33,45 @@ module SolidQueue
33
33
  end
34
34
 
35
35
  private
36
+ JSON_OVERHEAD = 256
37
+
36
38
  def expand_error_details_from_exception
37
39
  if exception
38
- self.error = { exception_class: exception.class.name, message: exception.message, backtrace: exception.backtrace }
40
+ self.error = { exception_class: exception_class_name, message: exception_message, backtrace: exception_backtrace }
41
+ end
42
+ end
43
+
44
+ def exception_class_name
45
+ exception.class.name
46
+ end
47
+
48
+ def exception_message
49
+ exception.message
50
+ end
51
+
52
+ def exception_backtrace
53
+ if (limit = determine_backtrace_size_limit) && exception.backtrace.to_json.bytesize > limit
54
+ truncate_backtrace(exception.backtrace, limit)
55
+ else
56
+ exception.backtrace
57
+ end
58
+ end
59
+
60
+ def determine_backtrace_size_limit
61
+ column = self.class.connection.schema_cache.columns_hash(self.class.table_name)["error"]
62
+ if column.limit.present?
63
+ column.limit - exception_class_name.bytesize - exception_message.bytesize - JSON_OVERHEAD
64
+ end
65
+ end
66
+
67
+ def truncate_backtrace(lines, limit)
68
+ [].tap do |truncated_backtrace|
69
+ lines.each do |line|
70
+ if (truncated_backtrace << line).to_json.bytesize > limit
71
+ truncated_backtrace.pop
72
+ break
73
+ end
74
+ end
39
75
  end
40
76
  end
41
77
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Job < Record
5
+ class EnqueueError < StandardError; end
6
+
5
7
  include Executable, Clearable, Recurrable
6
8
 
7
9
  serialize :arguments, coder: JSON
@@ -37,6 +39,11 @@ module SolidQueue
37
39
 
38
40
  def create_from_active_job(active_job)
39
41
  create!(**attributes_from_active_job(active_job))
42
+ rescue ActiveRecord::ActiveRecordError => e
43
+ enqueue_error = EnqueueError.new("#{e.class.name}: #{e.message}").tap do |error|
44
+ error.set_backtrace e.backtrace
45
+ end
46
+ raise enqueue_error
40
47
  end
41
48
 
42
49
  def create_all_from_active_jobs(active_jobs)
@@ -2,17 +2,21 @@
2
2
 
3
3
  module SolidQueue
4
4
  class RecurringExecution < Execution
5
+ class AlreadyRecorded < StandardError; end
6
+
5
7
  scope :clearable, -> { where.missing(:job) }
6
8
 
7
9
  class << self
8
10
  def record(task_key, run_at, &block)
9
11
  transaction do
10
12
  block.call.tap do |active_job|
11
- create!(job_id: active_job.provider_job_id, task_key: task_key, run_at: run_at)
13
+ if active_job
14
+ create!(job_id: active_job.provider_job_id, task_key: task_key, run_at: run_at)
15
+ end
12
16
  end
13
17
  end
14
- rescue ActiveRecord::RecordNotUnique
15
- # Task already dispatched
18
+ rescue ActiveRecord::RecordNotUnique => e
19
+ raise AlreadyRecorded
16
20
  end
17
21
 
18
22
  def clear_in_batches(batch_size: 500)
@@ -7,15 +7,15 @@ Puma::Plugin.create do
7
7
  @log_writer = launcher.log_writer
8
8
  @puma_pid = $$
9
9
 
10
+ in_background do
11
+ monitor_solid_queue
12
+ end
13
+
10
14
  launcher.events.on_booted do
11
15
  @solid_queue_pid = fork do
12
16
  Thread.new { monitor_puma }
13
17
  SolidQueue::Supervisor.start(mode: :all)
14
18
  end
15
-
16
- in_background do
17
- monitor_solid_queue
18
- end
19
19
  end
20
20
 
21
21
  launcher.events.on_stopped { stop_solid_queue }
@@ -51,12 +51,18 @@ Puma::Plugin.create do
51
51
  end
52
52
 
53
53
  def solid_queue_dead?
54
- Process.waitpid(solid_queue_pid, Process::WNOHANG)
54
+ if solid_queue_started?
55
+ Process.waitpid(solid_queue_pid, Process::WNOHANG)
56
+ end
55
57
  false
56
58
  rescue Errno::ECHILD, Errno::ESRCH
57
59
  true
58
60
  end
59
61
 
62
+ def solid_queue_started?
63
+ solid_queue_pid.present?
64
+ end
65
+
60
66
  def puma_dead?
61
67
  Process.ppid != puma_pid
62
68
  end
@@ -11,6 +11,10 @@ module SolidQueue
11
11
  @scheduled_tasks = Concurrent::Hash.new
12
12
  end
13
13
 
14
+ def empty?
15
+ configured_tasks.empty?
16
+ end
17
+
14
18
  def load_tasks
15
19
  configured_tasks.each do |task|
16
20
  load_task(task)
@@ -31,15 +31,23 @@ module SolidQueue
31
31
 
32
32
  def enqueue(at:)
33
33
  SolidQueue.instrument(:enqueue_recurring_task, task: key, at: at) do |payload|
34
- if using_solid_queue_adapter?
34
+ active_job = if using_solid_queue_adapter?
35
35
  perform_later_and_record(run_at: at)
36
36
  else
37
37
  payload[:other_adapter] = true
38
38
 
39
- perform_later
40
- end.tap do |active_job|
41
- payload[:active_job_id] = active_job&.job_id
39
+ perform_later do |job|
40
+ unless job.successfully_enqueued?
41
+ payload[:enqueue_error] = job.enqueue_error&.message
42
+ end
43
+ end
42
44
  end
45
+
46
+ payload[:active_job_id] = active_job.job_id if active_job
47
+ rescue RecurringExecution::AlreadyRecorded
48
+ payload[:skipped] = true
49
+ rescue Job::EnqueueError => error
50
+ payload[:enqueue_error] = error.message
43
51
  end
44
52
  end
45
53
 
@@ -68,8 +76,8 @@ module SolidQueue
68
76
  RecurringExecution.record(key, run_at) { perform_later }
69
77
  end
70
78
 
71
- def perform_later
72
- job_class.perform_later(*arguments_with_kwargs)
79
+ def perform_later(&block)
80
+ job_class.perform_later(*arguments_with_kwargs, &block)
73
81
  end
74
82
 
75
83
  def arguments_with_kwargs
@@ -51,6 +51,10 @@ module SolidQueue
51
51
  recurring_schedule.unload_tasks
52
52
  end
53
53
 
54
+ def all_work_completed?
55
+ SolidQueue::ScheduledExecution.none? && recurring_schedule.empty?
56
+ end
57
+
54
58
  def set_procline
55
59
  procline "waiting"
56
60
  end
@@ -40,12 +40,18 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
40
40
  end
41
41
 
42
42
  def enqueue_recurring_task(event)
43
- attributes = event.payload.slice(:task, :at, :active_job_id)
43
+ attributes = event.payload.slice(:task, :active_job_id, :enqueue_error, :at)
44
44
 
45
45
  if event.payload[:other_adapter]
46
- debug formatted_event(event, action: "Enqueued recurring task outside Solid Queue", **attributes)
46
+ action = attributes[:active_job_id].present? ? "Enqueued recurring task outside Solid Queue" : "Error enqueuing recurring task"
47
+ info formatted_event(event, action: action, **attributes)
47
48
  else
48
- action = attributes[:active_job_id].present? ? "Enqueued recurring task" : "Skipped recurring task – already dispatched"
49
+ action = case
50
+ when event.payload[:skipped].present? then "Skipped recurring task – already dispatched"
51
+ when attributes[:active_job_id].nil? then "Error enqueuing recurring task"
52
+ else "Enqueued recurring task"
53
+ end
54
+
49
55
  info formatted_event(event, action: action, **attributes)
50
56
  end
51
57
  end
@@ -44,7 +44,7 @@ module SolidQueue::Processes
44
44
  end
45
45
 
46
46
  def with_polling_volume
47
- if SolidQueue.silence_polling?
47
+ if SolidQueue.silence_polling? && ActiveRecord::Base.logger
48
48
  ActiveRecord::Base.logger.silence { yield }
49
49
  else
50
50
  yield
@@ -22,7 +22,8 @@ module SolidQueue
22
22
  run_callbacks(:boot) { boot }
23
23
 
24
24
  start_forks
25
- launch_process_prune
25
+ launch_maintenance_task
26
+
26
27
  supervise
27
28
  rescue Processes::GracefulTerminationRequested
28
29
  graceful_termination
@@ -65,9 +66,12 @@ module SolidQueue
65
66
  configured_processes.each { |configured_process| start_fork(configured_process) }
66
67
  end
67
68
 
68
- def launch_process_prune
69
- @prune_task = Concurrent::TimerTask.new(run_now: true, execution_interval: SolidQueue.process_alive_threshold) { prune_dead_processes }
70
- @prune_task.execute
69
+ def launch_maintenance_task
70
+ @maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: SolidQueue.process_alive_threshold) do
71
+ prune_dead_processes
72
+ release_orphaned_executions
73
+ end
74
+ @maintenance_task.execute
71
75
  end
72
76
 
73
77
  def shutdown
@@ -106,7 +110,7 @@ module SolidQueue
106
110
  end
107
111
 
108
112
  def stop_process_prune
109
- @prune_task&.shutdown
113
+ @maintenance_task&.shutdown
110
114
  end
111
115
 
112
116
  def delete_pidfile
@@ -117,6 +121,10 @@ module SolidQueue
117
121
  wrap_in_app_executor { SolidQueue::Process.prune }
118
122
  end
119
123
 
124
+ def release_orphaned_executions
125
+ wrap_in_app_executor { SolidQueue::ClaimedExecution.orphaned.release_all }
126
+ end
127
+
120
128
  def start_fork(configured_process)
121
129
  configured_process.supervised_by process
122
130
 
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rosa Gutierrez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-11 00:00:00.000000000 Z
11
+ date: 2024-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -266,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
266
  - !ruby/object:Gem::Version
267
267
  version: '0'
268
268
  requirements: []
269
- rubygems_version: 3.5.9
269
+ rubygems_version: 3.5.16
270
270
  signing_key:
271
271
  specification_version: 4
272
272
  summary: Database-backed Active Job backend.