solid_queue 1.1.0 → 1.1.1

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: e843e842397e1d8141e0457b5b278026b71effe6ebf83d8300d2c4098db56adf
4
- data.tar.gz: a5db37bdea8dacec796f2dcb912ab8f2a69c929487dff081d8a53fe10c20086c
3
+ metadata.gz: c36dbf886c5dd03f4dab75e6c28b671fbd24ce011589bbab25ef44f640686c96
4
+ data.tar.gz: de499841947c2a12fa43e0e6e11cf8e8e0789c73ed7290b16ecc32ccfc133283
5
5
  SHA512:
6
- metadata.gz: a6831f7114c24d68ae8ed7b5aebfd4d3df0cc2c7b4e0046e6275a5bf5e46f8da8f5b79e9a303ae908e3581e8b96a132776f4c1a3f55bbf550dd008541e679665
7
- data.tar.gz: e51b90234b3a60355a96c9163bfc7d95a1f5eb95fcf7a2dc1faf553f04a7c242da2d48493ba5b6574c2dbf4b0d259cb69af1dda576475f1510a31f8325be5f8b
6
+ metadata.gz: 07b6b3b7c8a24aafe97b0b02acebd8fb06f2171dbdb41d6d5c779b70422c633df10850fd8dd569e0fc06a99d8ce4a5ecef21a17f0675fb547c3d5495f142473b
7
+ data.tar.gz: 4b8e7fbebd2a03c2fa25d498146d469dda245c9a2a979bb604257e2519d101e6a5e36c85479c7d1b3cafbdb3fbad6d0b8d564061868445a85fa1a361cad0dd70
data/README.md CHANGED
@@ -6,6 +6,31 @@ Besides regular job enqueuing and processing, Solid Queue supports delayed jobs,
6
6
 
7
7
  Solid Queue can be used with SQL databases such as MySQL, PostgreSQL or SQLite, and it leverages the `FOR UPDATE SKIP LOCKED` clause, if available, to avoid blocking and waiting on locks when polling jobs. It relies on Active Job for retries, discarding, error handling, serialization, or delays, and it's compatible with Ruby on Rails's multi-threading.
8
8
 
9
+ ## Table of contents
10
+
11
+ - [Installation](#installation)
12
+ - [Single database configuration](#single-database-configuration)
13
+ - [Incremental adoption](#incremental-adoption)
14
+ - [High performance requirements](#high-performance-requirements)
15
+ - [Configuration](#configuration)
16
+ - [Workers, dispatchers and scheduler](#workers-dispatchers-and-scheduler)
17
+ - [Queue order and priorities](#queue-order-and-priorities)
18
+ - [Queues specification and performance](#queues-specification-and-performance)
19
+ - [Threads, processes and signals](#threads-processes-and-signals)
20
+ - [Database configuration](#database-configuration)
21
+ - [Other configuration settings](#other-configuration-settings)
22
+ - [Lifecycle hooks](#lifecycle-hooks)
23
+ - [Errors when enqueuing](#errors-when-enqueuing)
24
+ - [Concurrency controls](#concurrency-controls)
25
+ - [Failed jobs and retries](#failed-jobs-and-retries)
26
+ - [Error reporting on jobs](#error-reporting-on-jobs)
27
+ - [Puma plugin](#puma-plugin)
28
+ - [Jobs and transactional integrity](#jobs-and-transactional-integrity)
29
+ - [Recurring tasks](#recurring-tasks)
30
+ - [Inspiration](#inspiration)
31
+ - [License](#license)
32
+
33
+
9
34
  ## Installation
10
35
 
11
36
  Solid Queue is configured by default in new Rails 8 applications. But if you're running an earlier version, you can add it manually following these steps:
@@ -43,8 +68,6 @@ production:
43
68
  migrations_paths: db/queue_migrate
44
69
  ```
45
70
 
46
- Note: Calling `bin/rails solid_queue:install` will automatically add `config.solid_queue.connects_to = { database: { writing: :queue } }` to `config/environments/production.rb`, so no additional configuration is needed there (although you must make sure that you use the `queue` name in `database.yml` for this to match!). But if you want to use Solid Queue in a different environment (like staging or even development), you'll have to manually add that `config.solid_queue.connects_to` line to the respective environment file. And, as always, make sure that the name you're using for the database in `config/database.yml` matches the name you use in `config.solid_queue.connects_to`.
47
-
48
71
  Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
49
72
 
50
73
  Now you're ready to start processing jobs by running `bin/jobs` on the server that's doing the work. This will start processing jobs in all queues using the default configuration. See [below](#configuration) to learn more about configuring Solid Queue.
@@ -53,6 +76,72 @@ For small projects, you can run Solid Queue on the same machine as your webserve
53
76
 
54
77
  **Note**: future changes to the schema will come in the form of regular migrations.
55
78
 
79
+ ### Usage in development and other non-production environments
80
+
81
+ Calling `bin/rails solid_queue:install` will automatically add `config.solid_queue.connects_to = { database: { writing: :queue } }` to `config/environments/production.rb`. In order to use Solid Queue in other environments (such as development or staging), you'll need to add a similar configuration(s).
82
+
83
+ For example, if you're using SQLite in development, update `database.yml` as follows:
84
+
85
+ ```diff
86
+ development:
87
+ primary:
88
+ <<: *default
89
+ database: storage/development.sqlite3
90
+ + queue:
91
+ + <<: *default
92
+ + database: storage/development_queue.sqlite3
93
+ + migrations_paths: db/queue_migrate
94
+ ```
95
+
96
+ Next, add the following to `development.rb`
97
+
98
+ ```ruby
99
+ # Use Solid Queue in Development.
100
+ config.active_job.queue_adapter = :solid_queue
101
+ config.solid_queue.connects_to = { database: { writing: :queue } }
102
+ ```
103
+
104
+ Once you've added this, run `db:prepare` to create the Solid Queue database and load the schema.
105
+
106
+ Finally, in order for jobs to be processed, you'll need to have Solid Queue running. In Development, this can be done via [the Puma plugin](#puma-plugin) as well. In `puma.rb` update the following line:
107
+
108
+ ```ruby
109
+ # You can either set the env var, or check for development
110
+ plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] || Rails.env.development?
111
+ ```
112
+
113
+ You can also just use `bin/jobs`, but in this case you might want to [set a different logger for Solid Queue](#other-configuration-settings) because the default logger will log to `log/development.log` and you won't see anything when you run `bin/jobs`. For example:
114
+ ```ruby
115
+ config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT)
116
+ ```
117
+
118
+ **Note about Action Cable**: If you use Action Cable (or anything dependent on Action Cable, such as Turbo Streams), you will also need to update it to use a database.
119
+
120
+ In `config/cable.yml`
121
+
122
+ ```diff
123
+ development:
124
+ - adapter: async
125
+ + adapter: solid_cable
126
+ + connects_to:
127
+ + database:
128
+ + writing: cable
129
+ + polling_interval: 0.1.seconds
130
+ + message_retention: 1.day
131
+ ```
132
+
133
+ In `config/database.yml`
134
+
135
+ ```diff
136
+ development:
137
+ primary:
138
+ <<: *default
139
+ database: storage/development.sqlite3
140
+ + cable:
141
+ + <<: *default
142
+ + database: storage/development_cable.sqlite3
143
+ + migrations_paths: db/cable_migrate
144
+ ```
56
145
 
57
146
  ### Single database configuration
58
147
 
@@ -64,7 +153,7 @@ Running Solid Queue in a separate database is recommended, but it's also possibl
64
153
 
65
154
  You won't have multiple databases, so `database.yml` doesn't need to have primary and queue database.
66
155
 
67
- ## Incremental adoption
156
+ ### Incremental adoption
68
157
 
69
158
  If you're planning to adopt Solid Queue incrementally by switching one job at the time, you can do so by leaving the `config.active_job.queue_adapter` set to your old backend, and then set the `queue_adapter` directly in the jobs you're moving:
70
159
 
@@ -77,7 +166,7 @@ class MyJob < ApplicationJob
77
166
  end
78
167
  ```
79
168
 
80
- ## High performance requirements
169
+ ### High performance requirements
81
170
 
82
171
  Solid Queue was designed for the highest throughput when used with MySQL 8+ or PostgreSQL 9.5+, as they support `FOR UPDATE SKIP LOCKED`. You can use it with older versions, but in that case, you might run into lock waits if you run multiple workers for the same queue. You can also use it with SQLite on smaller applications.
83
172
 
@@ -86,6 +175,7 @@ Solid Queue was designed for the highest throughput when used with MySQL 8+ or P
86
175
  ### Workers, dispatchers and scheduler
87
176
 
88
177
  We have several types of actors in Solid Queue:
178
+
89
179
  - _Workers_ are in charge of picking jobs ready to run from queues and processing them. They work off the `solid_queue_ready_executions` table.
90
180
  - _Dispatchers_ are in charge of selecting jobs scheduled to run in the future that are due and _dispatching_ them, which is simply moving them from the `solid_queue_scheduled_executions` table over to the `solid_queue_ready_executions` table so that workers can pick them up. On top of that, they do some maintenance work related to [concurrency controls](#concurrency-controls).
91
181
  - The _scheduler_ manages [recurring tasks](#recurring-tasks), enqueuing jobs for them when they're due.
@@ -99,7 +189,6 @@ By default, Solid Queue will try to find your configuration under `config/queue.
99
189
  bin/jobs -c config/calendar.yml
100
190
  ```
101
191
 
102
-
103
192
  This is what this configuration looks like:
104
193
 
105
194
  ```yml
@@ -153,6 +242,7 @@ Here's an overview of the different options:
153
242
  Check the sections below on [how queue order behaves combined with priorities](#queue-order-and-priorities), and [how the way you specify the queues per worker might affect performance](#queues-specification-and-performance).
154
243
 
155
244
  - `threads`: this is the max size of the thread pool that each worker will have to run jobs. Each worker will fetch this number of jobs from their queue(s), at most and will post them to the thread pool to be run. By default, this is `3`. Only workers have this setting.
245
+ It is recommended to set this value less than or equal to the queue database's connection pool size minus 2, as each worker thread uses one connection, and two additional connections are reserved for polling and heartbeat.
156
246
  - `processes`: this is the number of worker processes that will be forked by the supervisor with the settings given. By default, this is `1`, just a single process. This setting is useful if you want to dedicate more than one CPU core to a queue or queues with the same configuration. Only workers have this setting.
157
247
  - `concurrency_maintenance`: whether the dispatcher will perform the concurrency maintenance work. This is `true` by default, and it's useful if you don't use any [concurrency controls](#concurrency-controls) and want to disable it or if you run multiple dispatchers and want some of them to just dispatch jobs without doing anything else.
158
248
 
@@ -250,6 +340,32 @@ You can configure the database used by Solid Queue via the `config.solid_queue.c
250
340
 
251
341
  All the options available to Active Record for multiple databases can be used here.
252
342
 
343
+ ### Other configuration settings
344
+
345
+ _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`
346
+
347
+ There are several settings that control how Solid Queue works that you can set as well:
348
+ - `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
349
+ - `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
350
+ - `on_thread_error`: custom lambda/Proc to call when there's an error within a Solid Queue thread that takes the exception raised as argument. Defaults to
351
+
352
+ ```ruby
353
+ -> (exception) { Rails.error.report(exception, handled: false) }
354
+ ```
355
+
356
+ **This is not used for errors raised within a job execution**. Errors happening in jobs are handled by Active Job's `retry_on` or `discard_on`, and ultimately will result in [failed jobs](#failed-jobs-and-retries). This is for errors happening within Solid Queue itself.
357
+
358
+ - `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.
359
+ - `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
360
+ - `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
361
+ - `shutdown_timeout`: time the supervisor will wait since it sent the `TERM` signal to its supervised processes before sending a `QUIT` version to them requesting immediate termination—defaults to 5 seconds.
362
+ - `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
363
+ - `supervisor_pidfile`: path to a pidfile that the supervisor will create when booting to prevent running more than one supervisor in the same host, or in case you want to use it for a health check. It's `nil` by default.
364
+ - `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
365
+ - `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true—defaults to 1 day. **Note:** Right now, there's no automatic cleanup of finished jobs. You'd need to do this by periodically invoking `SolidQueue::Job.clear_finished_in_batches`, which can be configured as [a recurring task](#recurring-tasks).
366
+ - `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.
367
+
368
+
253
369
  ## Lifecycle hooks
254
370
 
255
371
  In Solid queue, you can hook into two different points in the supervisor's life:
@@ -277,30 +393,6 @@ SolidQueue.on_stop { stop_metrics_server }
277
393
 
278
394
  These can be called several times to add multiple hooks, but it needs to happen before Solid Queue is started. An initializer would be a good place to do this.
279
395
 
280
- ### Other configuration settings
281
-
282
- _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`
283
-
284
- There are several settings that control how Solid Queue works that you can set as well:
285
- - `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
286
- - `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
287
- - `on_thread_error`: custom lambda/Proc to call when there's an error within a Solid Queue thread that takes the exception raised as argument. Defaults to
288
-
289
- ```ruby
290
- -> (exception) { Rails.error.report(exception, handled: false) }
291
- ```
292
-
293
- **This is not used for errors raised within a job execution**. Errors happening in jobs are handled by Active Job's `retry_on` or `discard_on`, and ultimately will result in [failed jobs](#failed-jobs-and-retries). This is for errors happening within Solid Queue itself.
294
-
295
- - `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.
296
- - `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
297
- - `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
298
- - `shutdown_timeout`: time the supervisor will wait since it sent the `TERM` signal to its supervised processes before sending a `QUIT` version to them requesting immediate termination—defaults to 5 seconds.
299
- - `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
300
- - `supervisor_pidfile`: path to a pidfile that the supervisor will create when booting to prevent running more than one supervisor in the same host, or in case you want to use it for a health check. It's `nil` by default.
301
- - `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
302
- - `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true—defaults to 1 day. **Note:** Right now, there's no automatic cleanup of finished jobs. You'd need to do this by periodically invoking `SolidQueue::Job.clear_finished_in_batches`, but this will happen automatically in the near future.
303
- - `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.
304
396
 
305
397
  ## Errors when enqueuing
306
398
 
@@ -412,6 +504,12 @@ plugin :solid_queue
412
504
  ```
413
505
  to your `puma.rb` configuration.
414
506
 
507
+ If you're using Puma in development but you don't want to use Solid Queue in development, make sure you avoid the plugin being used, for example using an environment variable like this:
508
+ ```ruby
509
+ plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
510
+ ```
511
+ that you set in production only. This is what Rails 8's default Puma config looks like. Otherwise, if you're using Puma in development but not Solid Queue, starting Puma would start also Solid Queue supervisor and it'll most likely fail because it won't be properly configured.
512
+
415
513
 
416
514
  ## Jobs and transactional integrity
417
515
  :warning: Having your jobs in the same ACID-compliant database as your application data enables a powerful yet sharp tool: taking advantage of transactional integrity to ensure some action in your app is not committed unless your job is also committed and vice versa, and ensuring that your job won't be enqueued until the transaction within which you're enqueuing it is committed. This can be very powerful and useful, but it can also backfire if you base some of your logic on this behaviour, and in the future, you move to another active job backend, or if you simply move Solid Queue to its own database, and suddenly the behaviour changes under you. Because this can be quite tricky and many people shouldn't need to worry about it, by default Solid Queue is configured in a different database as the main app.
@@ -477,9 +575,15 @@ MyJob.perform_later(42, status: "custom_status")
477
575
 
478
576
  - `priority`: a numeric priority value to be used when enqueuing the job.
479
577
 
480
-
481
578
  Tasks are enqueued at their corresponding times by the scheduler, and each task schedules the next one. This is pretty much [inspired by what GoodJob does](https://github.com/bensheldon/good_job/blob/994ecff5323bf0337e10464841128fda100750e6/lib/good_job/cron_manager.rb).
482
579
 
580
+ For recurring tasks defined as a `command`, you can also change the job class that runs them as follows:
581
+ ```ruby
582
+ Rails.application.config.after_initialize do # or to_prepare
583
+ SolidQueue::RecurringTask.default_job_class = MyRecurringCommandJob
584
+ end
585
+ ```
586
+
483
587
  It's possible to run multiple schedulers with the same `recurring_tasks` configuration, for example, if you have multiple servers for redundancy, and you run the `scheduler` in more than one of them. 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.
484
588
 
485
589
  **Note**: a single recurring schedule is supported, so you can have multiple schedulers using the same schedule, but not multiple schedulers using different configurations.
@@ -10,9 +10,10 @@ module SolidQueue
10
10
  end
11
11
 
12
12
  class_methods do
13
- def clear_finished_in_batches(batch_size: 500, finished_before: SolidQueue.clear_finished_jobs_after.ago, class_name: nil)
13
+ def clear_finished_in_batches(batch_size: 500, finished_before: SolidQueue.clear_finished_jobs_after.ago, class_name: nil, sleep_between_batches: 0)
14
14
  loop do
15
15
  records_deleted = clearable(finished_before: finished_before, class_name: class_name).limit(batch_size).delete_all
16
+ sleep(sleep_between_batches) if sleep_between_batches > 0
16
17
  break if records_deleted == 0
17
18
  end
18
19
  end
@@ -12,6 +12,8 @@ module SolidQueue
12
12
 
13
13
  scope :static, -> { where(static: true) }
14
14
 
15
+ has_many :recurring_executions, foreign_key: :task_key, primary_key: :key
16
+
15
17
  mattr_accessor :default_job_class
16
18
  self.default_job_class = RecurringJob
17
19
 
@@ -53,6 +55,18 @@ module SolidQueue
53
55
  parsed_schedule.next_time.utc
54
56
  end
55
57
 
58
+ def previous_time
59
+ parsed_schedule.previous_time.utc
60
+ end
61
+
62
+ def last_enqueued_time
63
+ if recurring_executions.loaded?
64
+ recurring_executions.map(&:run_at).max
65
+ else
66
+ recurring_executions.maximum(:run_at)
67
+ end
68
+ end
69
+
56
70
  def enqueue(at:)
57
71
  SolidQueue.instrument(:enqueue_recurring_task, task: key, at: at) do |payload|
58
72
  active_job = if using_solid_queue_adapter?
@@ -4,7 +4,7 @@ module SolidQueue
4
4
  module AppExecutor
5
5
  def wrap_in_app_executor(&block)
6
6
  if SolidQueue.app_executor
7
- SolidQueue.app_executor.wrap(&block)
7
+ SolidQueue.app_executor.wrap(source: "application.solid_queue", &block)
8
8
  else
9
9
  yield
10
10
  end
@@ -2,6 +2,12 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Configuration
5
+ include ActiveModel::Model
6
+
7
+ validate :ensure_configured_processes
8
+ validate :ensure_valid_recurring_tasks
9
+ validate :ensure_correctly_sized_thread_pool
10
+
5
11
  class Process < Struct.new(:kind, :attributes)
6
12
  def instantiate
7
13
  "SolidQueue::#{kind.to_s.titleize}".safe_constantize.new(**attributes)
@@ -36,14 +42,46 @@ module SolidQueue
36
42
  end
37
43
  end
38
44
 
39
- def max_number_of_threads
40
- # At most "threads" in each worker + 1 thread for the worker + 1 thread for the heartbeat task
41
- workers_options.map { |options| options[:threads] }.max + 2
45
+ def error_messages
46
+ if configured_processes.none?
47
+ "No workers or processed configured. Exiting..."
48
+ else
49
+ error_messages = invalid_tasks.map do |task|
50
+ all_messages = task.errors.full_messages.map { |msg| "\t#{msg}" }.join("\n")
51
+ "#{task.key}:\n#{all_messages}"
52
+ end
53
+ .join("\n")
54
+
55
+ "Invalid processes configured:\n#{error_messages}"
56
+ end
42
57
  end
43
58
 
44
59
  private
45
60
  attr_reader :options
46
61
 
62
+ def ensure_configured_processes
63
+ unless configured_processes.any?
64
+ errors.add(:base, "No processes configured")
65
+ end
66
+ end
67
+
68
+ def ensure_valid_recurring_tasks
69
+ unless skip_recurring_tasks? || invalid_tasks.none?
70
+ error_messages = invalid_tasks.map do |task|
71
+ "- #{task.key}: #{task.errors.full_messages.join(", ")}"
72
+ end
73
+
74
+ errors.add(:base, "Invalid recurring tasks:\n#{error_messages.join("\n")}")
75
+ end
76
+ end
77
+
78
+ def ensure_correctly_sized_thread_pool
79
+ if (db_pool_size = SolidQueue::Record.connection_pool&.size) && db_pool_size < estimated_number_of_threads
80
+ errors.add(:base, "Solid Queue is configured to use #{estimated_number_of_threads} threads but the " +
81
+ "database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
82
+ end
83
+ end
84
+
47
85
  def default_options
48
86
  {
49
87
  config_file: Rails.root.join(ENV["SOLID_QUEUE_CONFIG"] || DEFAULT_CONFIG_FILE_PATH),
@@ -54,6 +92,10 @@ module SolidQueue
54
92
  }
55
93
  end
56
94
 
95
+ def invalid_tasks
96
+ recurring_tasks.select(&:invalid?)
97
+ end
98
+
57
99
  def only_work?
58
100
  options[:only_work]
59
101
  end
@@ -100,7 +142,7 @@ module SolidQueue
100
142
  def recurring_tasks
101
143
  @recurring_tasks ||= recurring_tasks_config.map do |id, options|
102
144
  RecurringTask.from_configuration(id, **options)
103
- end.select(&:valid?)
145
+ end
104
146
  end
105
147
 
106
148
  def processes_config
@@ -147,5 +189,11 @@ module SolidQueue
147
189
  {}
148
190
  end
149
191
  end
192
+
193
+ def estimated_number_of_threads
194
+ # At most "threads" in each worker + 1 thread for the worker + 1 thread for the heartbeat task
195
+ thread_count = workers_options.map { |options| options.fetch(:threads, WORKER_DEFAULTS[:threads]) }.max
196
+ (thread_count || 1) + 2
197
+ end
150
198
  end
151
199
  end
@@ -24,7 +24,8 @@ module SolidQueue
24
24
  private
25
25
  def poll
26
26
  batch = dispatch_next_batch
27
- batch.size
27
+
28
+ batch.size.zero? ? polling_interval : 0.seconds
28
29
  end
29
30
 
30
31
  def dispatch_next_batch
@@ -145,6 +145,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
145
145
  end
146
146
 
147
147
  def replace_fork(event)
148
+ supervisor_pid = event.payload[:supervisor_pid]
148
149
  status = event.payload[:status]
149
150
  attributes = event.payload.slice(:pid).merge \
150
151
  status: (status.exitstatus || "no exit status set"),
@@ -155,7 +156,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
155
156
 
156
157
  if replaced_fork = event.payload[:fork]
157
158
  info formatted_event(event, action: "Replaced terminated #{replaced_fork.kind}", **attributes.merge(hostname: replaced_fork.hostname, name: replaced_fork.name))
158
- else
159
+ elsif supervisor_pid != 1 # Running Docker, possibly having some processes that have been reparented
159
160
  warn formatted_event(event, action: "Tried to replace forked process but it had already died", **attributes)
160
161
  end
161
162
  end
@@ -12,13 +12,17 @@ module SolidQueue::Processes
12
12
  queue << true
13
13
  end
14
14
 
15
+ # Sleeps for 'time'. Can be interrupted asynchronously and return early via wake_up.
16
+ # @param time [Numeric] the time to sleep. 0 returns immediately.
17
+ # @return [true, nil]
18
+ # * returns `true` if an interrupt was requested via #wake_up between the
19
+ # last call to `interruptible_sleep` and now, resulting in an early return.
20
+ # * returns `nil` if it slept the full `time` and was not interrupted.
15
21
  def interruptible_sleep(time)
16
- # Invoking from the main thread can result in a 35% slowdown (at least when running the test suite).
17
- # Using some form of Async (Futures) addresses this performance issue.
22
+ # Invoking this from the main thread may result in significant slowdown.
23
+ # Utilizing asynchronous execution (Futures) addresses this performance issue.
18
24
  Concurrent::Promises.future(time) do |timeout|
19
- if timeout > 0 && queue.pop(timeout:)
20
- queue.clear
21
- end
25
+ queue.pop(timeout:).tap { queue.clear }
22
26
  end.value
23
27
  end
24
28
 
@@ -25,11 +25,11 @@ module SolidQueue::Processes
25
25
  loop do
26
26
  break if shutting_down?
27
27
 
28
- wrap_in_app_executor do
29
- unless poll > 0
30
- interruptible_sleep(polling_interval)
31
- end
28
+ delay = wrap_in_app_executor do
29
+ poll
32
30
  end
31
+
32
+ interruptible_sleep(delay)
33
33
  end
34
34
  ensure
35
35
  SolidQueue.instrument(:shutdown_process, process: self) do
@@ -10,10 +10,10 @@ module SolidQueue
10
10
  SolidQueue.supervisor = true
11
11
  configuration = Configuration.new(**options)
12
12
 
13
- if configuration.configured_processes.any?
13
+ if configuration.valid?
14
14
  new(configuration).tap(&:start)
15
15
  else
16
- abort "No workers or processed configured. Exiting..."
16
+ abort configuration.errors.full_messages.join("\n") + "\nExiting..."
17
17
  end
18
18
  end
19
19
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "1.1.0"
2
+ VERSION = "1.1.1"
3
3
  end
@@ -7,6 +7,7 @@ module SolidQueue
7
7
  after_boot :run_start_hooks
8
8
  before_shutdown :run_stop_hooks
9
9
 
10
+
10
11
  attr_accessor :queues, :pool
11
12
 
12
13
  def initialize(**options)
@@ -29,7 +30,7 @@ module SolidQueue
29
30
  pool.post(execution)
30
31
  end
31
32
 
32
- executions.size
33
+ pool.idle? ? polling_interval : 10.minutes
33
34
  end
34
35
  end
35
36
 
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: 1.1.0
4
+ version: 1.1.1
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-12-05 00:00:00.000000000 Z
11
+ date: 2024-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord