solid_queue 1.5.0 → 1.6.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.
- checksums.yaml +4 -4
- data/README.md +35 -5
- data/app/models/solid_queue/record/distinct_values.rb +12 -10
- data/app/models/solid_queue/record.rb +10 -0
- data/lib/generators/solid_queue/update/update_generator.rb +19 -0
- data/lib/solid_queue/cli.rb +4 -0
- data/lib/solid_queue/configuration.rb +83 -11
- data/lib/solid_queue/engine.rb +4 -0
- data/lib/solid_queue/fiber_pool.rb +130 -0
- data/lib/solid_queue/fork_supervisor.rb +23 -3
- data/lib/solid_queue/log_subscriber.rb +5 -0
- data/lib/solid_queue/pool.rb +46 -25
- data/lib/solid_queue/processes/runnable.rb +86 -1
- data/lib/solid_queue/tasks.rb +5 -0
- data/lib/solid_queue/thread_pool.rb +28 -0
- data/lib/solid_queue/timer.rb +3 -4
- data/lib/solid_queue/version.rb +5 -1
- data/lib/solid_queue/worker.rb +9 -3
- data/lib/solid_queue.rb +5 -0
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6cda6edb92a7805d6c2c0fc13b2649c2c2df26a4d65e555049efde022cb8d14
|
|
4
|
+
data.tar.gz: c6d0036cbeb743e56faa4ed9fe6756f1a8e3ae3705a402f55440bb14cb3174dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f7420684f8aa122d12e2314aa2445fb31a3f18f2a7bc53b4ceffccccd131a7f6d9408ef776da33dbbfe437be4c835437524279ac6150e57d134cf85698caf7a
|
|
7
|
+
data.tar.gz: cd02de9753ce345ec599d72230f634df0e633f278871df5da4330318fcb92b8820d99612f43fb8d89be348460d1cb198a5bfab34fdce5c6f639709ab8dd426a2
|
data/README.md
CHANGED
|
@@ -204,6 +204,10 @@ Or you can also set the environment variable `SOLID_QUEUE_SUPERVISOR_MODE` to `a
|
|
|
204
204
|
|
|
205
205
|
**The recommended and default mode is `fork`. Only use `async` if you know what you're doing and have strong reasons to**
|
|
206
206
|
|
|
207
|
+
This supervisor mode is separate from a worker's concurrency model. Supervisor mode decides whether supervised processes live in forks or threads. Worker configuration decides whether claimed jobs run in a thread pool (`threads: N`) or as fibers on a single fiber reactor thread (`fibers: N`).
|
|
208
|
+
|
|
209
|
+
Because these are separate concerns, you can combine the default `fork` supervisor mode with fiber workers. In that setup, each worker process gets its own fiber reactor and bounded fiber count.
|
|
210
|
+
|
|
207
211
|
## Configuration
|
|
208
212
|
|
|
209
213
|
By default, Solid Queue will try to find your configuration under `config/queue.yml`, but you can set a different path using the environment variable `SOLID_QUEUE_CONFIG` or by using the `-c/--config_file` option with `bin/jobs`, like this:
|
|
@@ -214,6 +218,8 @@ bin/jobs -c config/calendar.yml
|
|
|
214
218
|
|
|
215
219
|
You can also skip the scheduler process by setting the environment variable `SOLID_QUEUE_SKIP_RECURRING=true`. This is useful for environments like staging, review apps, or development where you don't want any recurring jobs to run. This is equivalent to using the `--skip-recurring` option with `bin/jobs`.
|
|
216
220
|
|
|
221
|
+
To run **only** the scheduler (no workers or dispatchers)—for example to isolate recurring tasks on a dedicated process—set `SOLID_QUEUE_ONLY_RECURRING=true` or use the `--only-recurring` option with `bin/jobs`.
|
|
222
|
+
|
|
217
223
|
This is what this configuration looks like:
|
|
218
224
|
|
|
219
225
|
```yml
|
|
@@ -230,6 +236,9 @@ production:
|
|
|
230
236
|
threads: 5
|
|
231
237
|
polling_interval: 0.1
|
|
232
238
|
processes: 3
|
|
239
|
+
- queues: "api*"
|
|
240
|
+
fibers: 100
|
|
241
|
+
polling_interval: 0.05
|
|
233
242
|
scheduler:
|
|
234
243
|
dynamic_tasks_enabled: true
|
|
235
244
|
polling_interval: 5
|
|
@@ -272,9 +281,11 @@ Here's an overview of the different options:
|
|
|
272
281
|
|
|
273
282
|
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).
|
|
274
283
|
|
|
275
|
-
- `threads`:
|
|
276
|
-
It is recommended to set this value less than or equal to the queue database's connection pool size minus 2, as each worker
|
|
277
|
-
- `
|
|
284
|
+
- `threads`: configures a worker to execute jobs in a thread pool of this size. By default, workers use `threads: 3`. Only workers have this setting, and it can't be combined with `fibers`.
|
|
285
|
+
It is recommended to set this value less than or equal to the queue database's connection pool size minus 2, as each worker uses connections for polling and heartbeat and thread mode may use additional connections for job execution.
|
|
286
|
+
- `fibers`: configures a worker to execute jobs as fibers on a single fiber reactor thread, with this value as the maximum number of in-flight jobs. It can't be combined with `threads`.
|
|
287
|
+
Fiber workers require fiber-scoped isolated execution state. In Rails apps, set `config.active_support.isolation_level = :fiber` before using `fibers`. Solid Queue refuses to boot fiber workers when isolation remains thread-scoped. On Rails 7.2 and later, a practical starting point is usually `3-5` queue database connections per worker process rather than matching the `fibers` value, because ordinary Active Record query paths can release connections between non-blocking waits. On Rails 7.1, size the queue database pool more conservatively, as in-flight fiber jobs may still retain connections roughly in proportion to `fibers`.
|
|
288
|
+
- `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. This works with both `threads` and `fibers` workers as long as the supervisor is running in the default `fork` mode. **Note**: this option is ignored only when the supervisor itself is [running in `async` mode](#fork-vs-async-mode).
|
|
278
289
|
- `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.
|
|
279
290
|
|
|
280
291
|
|
|
@@ -365,7 +376,17 @@ queues: back*
|
|
|
365
376
|
|
|
366
377
|
### Threads, processes, and signals
|
|
367
378
|
|
|
368
|
-
|
|
379
|
+
By default, workers in Solid Queue use a thread pool to run work in multiple threads, configurable via the `threads` parameter above. Workers can also be configured with `fibers`, in which case claimed jobs are executed as fibers on a single reactor thread and bounded by the worker's fiber count. Besides this, parallelism can be achieved via multiple processes on one machine (configurable via different workers or the `processes` parameter above) or by horizontal scaling.
|
|
380
|
+
|
|
381
|
+
Fiber worker execution is best suited for cooperative, mostly I/O-bound jobs. Blocking or CPU-heavy work still blocks the single reactor thread, so it should not be expected to outperform thread mode for every workload.
|
|
382
|
+
|
|
383
|
+
Because fiber workers run multiple fibers on a single thread, Rails must also isolate execution state per fiber rather than per thread. If your app keeps the default thread-scoped isolation level, Solid Queue will raise a boot-time error instead of running fiber workers with shared Active Record state.
|
|
384
|
+
|
|
385
|
+
Keep in mind that `config.active_support.isolation_level = :fiber` applies to your whole application, not just to Solid Queue: if you run Solid Queue inside Puma via [the plugin](#puma-plugin), or combine fiber workers with thread workers in the same process using the supervisor's `async` mode, everything in that process will use fiber-scoped execution state. This is fully supported by Rails, but it's a global setting worth being deliberate about.
|
|
386
|
+
|
|
387
|
+
On Rails 7.2 and later, fiber workers can often use a much smaller queue database pool than an equivalent thread pool. A practical starting point is `3-5` queue database connections per worker process: one for job execution, one for polling, one for heartbeats, plus some headroom. In the default `fork` supervisor mode, that guidance applies per worker process. In supervisor `async` mode, all workers share one process, so add together the requirements for the workers running there.
|
|
388
|
+
|
|
389
|
+
That lower-pool guidance depends on job code not holding connections open across non-blocking waits. APIs such as `ActiveRecord::Base.connection`, `lease_connection`, `connection_pool.checkout`, or long-lived `with_connection` / transaction blocks can pin connections and push fiber workers back toward thread-like pool usage. On Rails 7.1, plan conservatively and assume the configured fiber count can still grow queue database connection usage.
|
|
369
390
|
|
|
370
391
|
The supervisor is in charge of managing these processes, and it responds to the following signals when running in its own process via `bin/jobs` or with [the Puma plugin](#puma-plugin) with the default `fork` mode:
|
|
371
392
|
- `TERM`, `INT`: starts graceful termination. The supervisor will send a `TERM` signal to its supervised processes, and it'll wait up to `SolidQueue.shutdown_timeout` time until they're done. If any supervised processes are still around by then, it'll send a `QUIT` signal to them to indicate they must exit.
|
|
@@ -377,6 +398,10 @@ On Windows, the `QUIT` signal can't be trapped, so the supervisor only responds
|
|
|
377
398
|
|
|
378
399
|
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. Jobs that were claimed by processes with an expired heartbeat will be marked as failed with a `SolidQueue::Processes::ProcessPrunedError`. You can configure both the frequency of heartbeats and the threshold to consider a process dead. See the section below for this.
|
|
379
400
|
|
|
401
|
+
Worker heartbeats are driven by a separate timer task, not by the worker execution backend itself. This means fiber workers do not rely on the reactor loop to prove liveness. However, liveness is still tracked at the worker-process level, not at the individual thread or fiber level.
|
|
402
|
+
|
|
403
|
+
This means finished and failed jobs still follow the normal Solid Queue lifecycle, but a single stuck job can remain claimed if the worker process itself is still alive. If you need stronger stuck-job detection, that requires an explicit timeout or watchdog mechanism on top of process heartbeats.
|
|
404
|
+
|
|
380
405
|
In a similar way, if a worker is terminated in any other way not initiated by the above signals (e.g. a worker is sent a `KILL` signal), jobs in progress will be marked as failed so that they can be inspected, with a `SolidQueue::Processes::ProcessExitError`. Sometimes a job in particular is responsible for this, for example, if it has a memory leak and you have a mechanism to kill processes over a certain memory threshold, so this will help identifying this kind of situation.
|
|
381
406
|
|
|
382
407
|
|
|
@@ -392,7 +417,7 @@ _Note_: The settings in this section should be set in your `config/application.r
|
|
|
392
417
|
|
|
393
418
|
There are several settings that control how Solid Queue works that you can set as well:
|
|
394
419
|
- `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
|
|
395
|
-
- `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap
|
|
420
|
+
- `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap background operations, defaults to the app executor
|
|
396
421
|
- `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
|
|
397
422
|
|
|
398
423
|
```ruby
|
|
@@ -404,6 +429,7 @@ There are several settings that control how Solid Queue works that you can set a
|
|
|
404
429
|
- `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 only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8; for MariaDB, versions < 10.6; and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential.
|
|
405
430
|
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
|
406
431
|
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
|
432
|
+
- `fork_boot_timeout`: how long a forked process can take to finish booting before the supervisor replaces it—defaults to 5 minutes. It only applies in the default `fork` mode.
|
|
407
433
|
- `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.
|
|
408
434
|
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
|
|
409
435
|
- `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.
|
|
@@ -709,6 +735,8 @@ bin/jobs --recurring_schedule_file=config/schedule.yml
|
|
|
709
735
|
|
|
710
736
|
You can completely disable recurring tasks by setting the environment variable `SOLID_QUEUE_SKIP_RECURRING=true` or by using the `--skip-recurring` option with `bin/jobs`.
|
|
711
737
|
|
|
738
|
+
To run only the scheduler (no workers or dispatchers), set `SOLID_QUEUE_ONLY_RECURRING=true` or use `--only-recurring` with `bin/jobs`.
|
|
739
|
+
|
|
712
740
|
The configuration itself looks like this:
|
|
713
741
|
|
|
714
742
|
```yml
|
|
@@ -802,6 +830,8 @@ SolidQueue.unschedule_recurring_task("my_dynamic_task")
|
|
|
802
830
|
|
|
803
831
|
Only dynamic tasks can be unscheduled at runtime. Attempting to unschedule a static task (defined in `config/recurring.yml`) will raise an `ActiveRecord::RecordNotFound` error.
|
|
804
832
|
|
|
833
|
+
To update an existing dynamic task, unschedule it and then schedule it again with the new options. A running scheduler only detects dynamic tasks being created and deleted, so updating a `SolidQueue::RecurringTask` record in place (for example, changing its `schedule` with `update!`) won't be picked up until the scheduler restarts.
|
|
834
|
+
|
|
805
835
|
Tasks scheduled like this persist between Solid Queue's restarts and won't stop running until you manually unschedule them.
|
|
806
836
|
|
|
807
837
|
## Inspiration
|
|
@@ -19,23 +19,25 @@ module SolidQueue
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
def loose_index_scan_emulation_needed?
|
|
22
|
-
connection.adapter_name == "PostgreSQL"
|
|
22
|
+
connection_pool.with_connection { |connection| connection.adapter_name == "PostgreSQL" }
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# Emulates a loose index scan, honoring the current scope (e.g. LIKE prefixes)
|
|
26
26
|
# by building the anchor and the recursive step as scoped relations, whose
|
|
27
27
|
# #to_sql inlines any bind parameters so they can be embedded in the raw CTE.
|
|
28
28
|
def loose_distinct_via_recursive_cte(column)
|
|
29
|
-
|
|
29
|
+
connection_pool.with_connection do |connection|
|
|
30
|
+
col = connection.quote_column_name(column)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
connection.select_values(<<~SQL.squish)
|
|
33
|
+
WITH RECURSIVE t AS (
|
|
34
|
+
(#{next_distinct_value(col, "#{col} IS NOT NULL")})
|
|
35
|
+
UNION ALL
|
|
36
|
+
SELECT (#{next_distinct_value(col, "#{col} > t.#{col}")}) FROM t WHERE t.#{col} IS NOT NULL
|
|
37
|
+
)
|
|
38
|
+
SELECT #{col} FROM t WHERE #{col} IS NOT NULL
|
|
39
|
+
SQL
|
|
40
|
+
end
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Smallest value of `col` within the current scope that matches `condition`.
|
|
@@ -24,6 +24,16 @@ module SolidQueue
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def warn_about_pending_migrations
|
|
28
|
+
SolidQueue.deprecator.warn(<<~DEPRECATION)
|
|
29
|
+
Solid Queue has pending database migrations. To get the new migration files, run:
|
|
30
|
+
rails solid_queue:update
|
|
31
|
+
And then:
|
|
32
|
+
rails db:migrate
|
|
33
|
+
These migrations will be required after version #{SolidQueue.next_major_version}.0
|
|
34
|
+
DEPRECATION
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
# Pass index hints to the query optimizer using SQL comment hints.
|
|
28
38
|
# Uses MySQL 8 optimizer hint query comments, which SQLite and
|
|
29
39
|
# PostgreSQL ignore.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
class SolidQueue::UpdateGenerator < Rails::Generators::Base
|
|
6
|
+
include ActiveRecord::Generators::Migration
|
|
7
|
+
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
class_option :database, type: :string, aliases: %i[ --db ], default: "queue",
|
|
11
|
+
desc: "The database that Solid Queue uses. Defaults to `queue`"
|
|
12
|
+
|
|
13
|
+
def copy_new_migrations
|
|
14
|
+
Dir.glob(File.join(self.class.source_root, "db", "*.rb")).each do |migration_file|
|
|
15
|
+
name = File.basename(migration_file)
|
|
16
|
+
migration_template File.join("db", name), File.join(db_migrate_path, name), skip: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/solid_queue/cli.rb
CHANGED
|
@@ -20,6 +20,10 @@ module SolidQueue
|
|
|
20
20
|
desc: "Whether to skip recurring tasks scheduling",
|
|
21
21
|
banner: "SOLID_QUEUE_SKIP_RECURRING"
|
|
22
22
|
|
|
23
|
+
class_option :only_recurring, type: :boolean,
|
|
24
|
+
desc: "Whether to run only the scheduler process for recurring tasks",
|
|
25
|
+
banner: "SOLID_QUEUE_ONLY_RECURRING"
|
|
26
|
+
|
|
23
27
|
def self.exit_on_failure?
|
|
24
28
|
true
|
|
25
29
|
end
|
|
@@ -6,7 +6,9 @@ module SolidQueue
|
|
|
6
6
|
include ActiveModel::Validations::Callbacks
|
|
7
7
|
|
|
8
8
|
validate :ensure_configured_processes, :ensure_valid_recurring_tasks
|
|
9
|
-
validate :
|
|
9
|
+
validate :ensure_valid_worker_execution_options
|
|
10
|
+
validate :ensure_fiber_workers_have_required_dependency, :ensure_fiber_workers_use_supported_isolation_level
|
|
11
|
+
validate :warn_about_incorrectly_sized_database_pool, :warn_about_missing_config_files
|
|
10
12
|
|
|
11
13
|
before_validation { warnings.clear }
|
|
12
14
|
|
|
@@ -37,13 +39,17 @@ module SolidQueue
|
|
|
37
39
|
|
|
38
40
|
DEFAULT_CONFIG_FILE_PATH = "config/queue.yml"
|
|
39
41
|
DEFAULT_RECURRING_SCHEDULE_FILE_PATH = "config/recurring.yml"
|
|
42
|
+
FIBER_QUERY_SCOPED_CONNECTIONS_VERSION = Gem::Version.new("7.2.0")
|
|
40
43
|
|
|
41
44
|
def initialize(**options)
|
|
42
45
|
@options = options.with_defaults(default_options)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def configured_processes
|
|
46
|
-
if only_work?
|
|
49
|
+
if only_work?
|
|
50
|
+
workers
|
|
51
|
+
elsif only_recurring?
|
|
52
|
+
schedulers
|
|
47
53
|
else
|
|
48
54
|
dispatchers + workers + schedulers
|
|
49
55
|
end
|
|
@@ -96,12 +102,12 @@ module SolidQueue
|
|
|
96
102
|
end
|
|
97
103
|
end
|
|
98
104
|
|
|
99
|
-
def
|
|
105
|
+
def warn_about_incorrectly_sized_database_pool
|
|
100
106
|
db_pool_size = SolidQueue::Record.connection_pool&.size
|
|
101
107
|
|
|
102
|
-
if db_pool_size && db_pool_size <
|
|
103
|
-
warnings.add(:base, "Warning: Solid Queue
|
|
104
|
-
"database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
|
|
108
|
+
if db_pool_size && db_pool_size < estimated_database_pool_size
|
|
109
|
+
warnings.add(:base, "Warning: Solid Queue needs at least #{estimated_database_pool_size} database connections " \
|
|
110
|
+
"for the configured workers but the database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
|
|
105
111
|
end
|
|
106
112
|
rescue ActiveRecord::ActiveRecordError
|
|
107
113
|
# No usable database connection. Skip the pool-size warning in that case.
|
|
@@ -118,6 +124,34 @@ module SolidQueue
|
|
|
118
124
|
end
|
|
119
125
|
end
|
|
120
126
|
|
|
127
|
+
def ensure_valid_worker_execution_options
|
|
128
|
+
workers_options.each do |options|
|
|
129
|
+
if options.key?(:threads) && options.key?(:fibers)
|
|
130
|
+
errors.add(:base, "Workers can specify either `threads` or `fibers`, but not both.")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ensure_fiber_workers_have_required_dependency
|
|
136
|
+
return unless workers_options.any? { |options| fiber_worker?(options) }
|
|
137
|
+
|
|
138
|
+
require "async"
|
|
139
|
+
require "async/semaphore"
|
|
140
|
+
rescue LoadError
|
|
141
|
+
errors.add(:base, "Fiber workers require the `async` gem. " \
|
|
142
|
+
"Add `gem \"async\"` to your Gemfile to configure workers with `fibers`.")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ensure_fiber_workers_use_supported_isolation_level
|
|
146
|
+
return unless workers_options.any? { |options| fiber_worker?(options) }
|
|
147
|
+
|
|
148
|
+
unless ActiveSupport::IsolatedExecutionState.isolation_level == :fiber
|
|
149
|
+
errors.add(:base, "Fiber workers require fiber-scoped isolated execution state. " \
|
|
150
|
+
"Set `config.active_support.isolation_level = :fiber` in your Rails configuration " \
|
|
151
|
+
"(or `ActiveSupport::IsolatedExecutionState.isolation_level = :fiber` outside Rails).")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
121
155
|
def default_options
|
|
122
156
|
{
|
|
123
157
|
mode: ENV["SOLID_QUEUE_SUPERVISOR_MODE"] || :fork,
|
|
@@ -126,6 +160,7 @@ module SolidQueue
|
|
|
126
160
|
recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
|
|
127
161
|
only_work: false,
|
|
128
162
|
only_dispatch: false,
|
|
163
|
+
only_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_ONLY_RECURRING"]),
|
|
129
164
|
skip_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_SKIP_RECURRING"])
|
|
130
165
|
}
|
|
131
166
|
end
|
|
@@ -142,6 +177,10 @@ module SolidQueue
|
|
|
142
177
|
options[:only_dispatch]
|
|
143
178
|
end
|
|
144
179
|
|
|
180
|
+
def only_recurring?
|
|
181
|
+
options[:only_recurring]
|
|
182
|
+
end
|
|
183
|
+
|
|
145
184
|
def skip_recurring_tasks?
|
|
146
185
|
options[:skip_recurring] || only_work?
|
|
147
186
|
end
|
|
@@ -154,7 +193,8 @@ module SolidQueue
|
|
|
154
193
|
1
|
|
155
194
|
end
|
|
156
195
|
|
|
157
|
-
|
|
196
|
+
defaults = worker_defaults_for(worker_options)
|
|
197
|
+
processes.times.map { Process.new(:worker, worker_options.with_defaults(defaults)) }
|
|
158
198
|
end
|
|
159
199
|
end
|
|
160
200
|
|
|
@@ -248,10 +288,42 @@ module SolidQueue
|
|
|
248
288
|
end
|
|
249
289
|
end
|
|
250
290
|
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
291
|
+
def estimated_database_pool_size
|
|
292
|
+
worker_pool_size = workers_options.map { |options| estimated_database_pool_size_for_worker(options) }.max
|
|
293
|
+
worker_pool_size || 1
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def estimated_database_pool_size_for_worker(options)
|
|
297
|
+
# Connections used to execute jobs + 1 for the worker's polling thread + 1 for the heartbeat task
|
|
298
|
+
estimated_execution_connections_for_worker(options) + 2
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def worker_capacity(options)
|
|
302
|
+
options[:fibers] || options[:threads] || WORKER_DEFAULTS[:threads]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def estimated_execution_connections_for_worker(options)
|
|
306
|
+
fiber_worker?(options) ? fiber_execution_connections_for_worker(options) : worker_capacity(options)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def fiber_execution_connections_for_worker(options)
|
|
310
|
+
fiber_jobs_release_connections_between_queries? ? 1 : worker_capacity(options)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def fiber_jobs_release_connections_between_queries?
|
|
314
|
+
ActiveRecord.gem_version >= FIBER_QUERY_SCOPED_CONNECTIONS_VERSION
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def fiber_worker?(options)
|
|
318
|
+
options.key?(:fibers)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def worker_defaults_for(options)
|
|
322
|
+
if fiber_worker?(options)
|
|
323
|
+
WORKER_DEFAULTS.except(:threads)
|
|
324
|
+
else
|
|
325
|
+
WORKER_DEFAULTS
|
|
326
|
+
end
|
|
255
327
|
end
|
|
256
328
|
end
|
|
257
329
|
end
|
data/lib/solid_queue/engine.rb
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class FiberPool < Pool
|
|
5
|
+
def initialize(size, on_idle: nil)
|
|
6
|
+
super
|
|
7
|
+
|
|
8
|
+
@state_mutex = Mutex.new
|
|
9
|
+
@shutdown = false
|
|
10
|
+
@fatal_error = nil
|
|
11
|
+
@boot_queue = Thread::Queue.new
|
|
12
|
+
@pending_executions = Thread::Queue.new
|
|
13
|
+
@reactor_thread = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def post(execution)
|
|
17
|
+
raise_if_fatal_error!
|
|
18
|
+
raise RuntimeError, "Execution pool is shutting down" if shutdown?
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def available_capacity
|
|
24
|
+
raise_if_fatal_error!
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shutdown
|
|
29
|
+
state_mutex.synchronize do
|
|
30
|
+
next false if @shutdown
|
|
31
|
+
|
|
32
|
+
@shutdown = true
|
|
33
|
+
end.tap do |shut_down|
|
|
34
|
+
# Wake the reactor: already-queued executions are drained before the
|
|
35
|
+
# blocked pop in +wait_for_executions+ returns nil
|
|
36
|
+
pending_executions.close if shut_down
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def shutdown?
|
|
41
|
+
state_mutex.synchronize { @shutdown }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def wait_for_termination(timeout)
|
|
45
|
+
reactor_thread&.join(timeout)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
attr_reader :boot_queue, :pending_executions, :reactor_thread, :state_mutex
|
|
50
|
+
|
|
51
|
+
def name
|
|
52
|
+
@name ||= "solid_queue-fiber-pool-#{object_id}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def schedule(execution)
|
|
56
|
+
start_reactor_if_needed
|
|
57
|
+
pending_executions << execution
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The reactor thread is started lazily, when the first execution is posted,
|
|
61
|
+
# so that the pool can be safely built before forking: in the default fork
|
|
62
|
+
# supervisor mode, workers are instantiated in the supervisor process, and
|
|
63
|
+
# a thread started there wouldn't survive the fork. The async gem is also
|
|
64
|
+
# required lazily here, so that setups without fiber workers never load it.
|
|
65
|
+
def start_reactor_if_needed
|
|
66
|
+
@reactor_thread ||= begin
|
|
67
|
+
require "async"
|
|
68
|
+
require "async/semaphore"
|
|
69
|
+
|
|
70
|
+
start_reactor.tap do
|
|
71
|
+
boot_result = boot_queue.pop
|
|
72
|
+
raise boot_result if boot_result.is_a?(Exception)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def start_reactor
|
|
78
|
+
create_thread do
|
|
79
|
+
Async do |task|
|
|
80
|
+
semaphore = Async::Semaphore.new(size, parent: task)
|
|
81
|
+
boot_queue << :ready
|
|
82
|
+
|
|
83
|
+
# The reactor exits when all in-flight execution fibers, children
|
|
84
|
+
# of this task, have finished
|
|
85
|
+
wait_for_executions(semaphore)
|
|
86
|
+
end
|
|
87
|
+
rescue Exception => error
|
|
88
|
+
register_fatal_error(error)
|
|
89
|
+
raise
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def wait_for_executions(semaphore)
|
|
94
|
+
# Thread::Queue#pop is fiber-scheduler-aware: it suspends this fiber, letting
|
|
95
|
+
# execution fibers run, and wakes the reactor when the poller thread pushes new
|
|
96
|
+
# work or closes the queue on shutdown, after which it drains any remaining
|
|
97
|
+
# executions and returns nil
|
|
98
|
+
while execution = pending_executions.pop
|
|
99
|
+
semaphore.async(execution) do |_execution_task, scheduled_execution|
|
|
100
|
+
perform_execution(scheduled_execution)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def perform_execution(execution)
|
|
106
|
+
wrap_in_app_executor { execution.perform }
|
|
107
|
+
rescue Async::Stop => error
|
|
108
|
+
handle_thread_error(error)
|
|
109
|
+
register_fatal_error(error)
|
|
110
|
+
rescue Exception => error
|
|
111
|
+
handle_thread_error(error)
|
|
112
|
+
ensure
|
|
113
|
+
restore_capacity
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def register_fatal_error(error)
|
|
117
|
+
state_mutex.synchronize do
|
|
118
|
+
@fatal_error ||= error
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
boot_queue << error if boot_queue.empty?
|
|
122
|
+
on_idle&.call
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def raise_if_fatal_error!
|
|
126
|
+
error = state_mutex.synchronize { @fatal_error }
|
|
127
|
+
raise error if error
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -31,6 +31,21 @@ module SolidQueue
|
|
|
31
31
|
|
|
32
32
|
replace_fork(pid, status)
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
check_boot_timeouts
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_boot_timeouts
|
|
39
|
+
process_instances.each do |pid, instance|
|
|
40
|
+
terminate_unready_process(pid) if instance.boot_timed_out?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def terminate_unready_process(pid)
|
|
45
|
+
SolidQueue.instrument(:fork_boot_timeout, process: process_instances[pid], pid: pid) do
|
|
46
|
+
# A child stuck in boot cannot reach its run loop to stop gracefully
|
|
47
|
+
signal_process(pid, :KILL)
|
|
48
|
+
end
|
|
34
49
|
end
|
|
35
50
|
|
|
36
51
|
def reap_terminated_forks
|
|
@@ -38,9 +53,13 @@ module SolidQueue
|
|
|
38
53
|
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
39
54
|
break unless pid
|
|
40
55
|
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
if terminated_fork = process_instances.delete(pid)
|
|
57
|
+
terminated_fork.mark_as_reaped
|
|
58
|
+
|
|
59
|
+
if !status.exited? || status.exitstatus.to_i > 0
|
|
60
|
+
error = Processes::ProcessExitError.new(status)
|
|
61
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
62
|
+
end
|
|
44
63
|
end
|
|
45
64
|
|
|
46
65
|
configured_processes.delete(pid)
|
|
@@ -52,6 +71,7 @@ module SolidQueue
|
|
|
52
71
|
def replace_fork(pid, status)
|
|
53
72
|
SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
|
54
73
|
if terminated_fork = process_instances.delete(pid)
|
|
74
|
+
terminated_fork.mark_as_reaped
|
|
55
75
|
payload[:fork] = terminated_fork
|
|
56
76
|
error = Processes::ProcessExitError.new(status)
|
|
57
77
|
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
@@ -161,6 +161,11 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
+
def fork_boot_timeout(event)
|
|
165
|
+
process = event.payload[:process]
|
|
166
|
+
warn formatted_event(event, action: "Terminate #{process.kind} that failed to boot in time", **event.payload.slice(:pid).merge(hostname: process.hostname, name: process.name))
|
|
167
|
+
end
|
|
168
|
+
|
|
164
169
|
private
|
|
165
170
|
def formatted_event(event, action:, **attributes)
|
|
166
171
|
"SolidQueue-#{SolidQueue::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
|
data/lib/solid_queue/pool.rb
CHANGED
|
@@ -4,51 +4,72 @@ module SolidQueue
|
|
|
4
4
|
class Pool
|
|
5
5
|
include AppExecutor
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
def self.build(type:, size:, on_idle: nil)
|
|
8
|
+
SolidQueue.const_get("#{type.to_s.camelize}Pool").new(size, on_idle: on_idle)
|
|
9
|
+
end
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
attr_reader :size
|
|
10
12
|
|
|
11
13
|
def initialize(size, on_idle: nil)
|
|
12
14
|
@size = size
|
|
13
15
|
@on_idle = on_idle
|
|
14
|
-
@
|
|
16
|
+
@available_capacity = size
|
|
15
17
|
@mutex = Mutex.new
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
def type
|
|
21
|
+
self.class.name.demodulize.delete_suffix("Pool").underscore.to_sym
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
def post(execution)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
mutex.synchronize { on_idle.try(:call) if idle? }
|
|
27
|
-
end
|
|
28
|
-
end.on_rejection! do |e|
|
|
29
|
-
handle_thread_error(e)
|
|
25
|
+
reserve_capacity!
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
schedule(execution)
|
|
29
|
+
rescue Exception
|
|
30
|
+
restore_capacity
|
|
31
|
+
raise
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
+
def available_capacity
|
|
36
|
+
mutex.synchronize { @available_capacity }
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def idle?
|
|
38
|
-
|
|
40
|
+
available_capacity.positive?
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
private
|
|
42
|
-
attr_reader :
|
|
44
|
+
attr_reader :mutex, :on_idle
|
|
45
|
+
|
|
46
|
+
def schedule(execution)
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def perform_execution(execution)
|
|
51
|
+
wrap_in_app_executor { execution.perform }
|
|
52
|
+
rescue Exception => error
|
|
53
|
+
handle_thread_error(error)
|
|
54
|
+
ensure
|
|
55
|
+
restore_capacity
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reserve_capacity!
|
|
59
|
+
mutex.synchronize do
|
|
60
|
+
raise RuntimeError, "Execution pool is at capacity" if @available_capacity <= 0
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
@available_capacity -= 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def restore_capacity
|
|
67
|
+
should_notify = mutex.synchronize do
|
|
68
|
+
@available_capacity += 1
|
|
69
|
+
@available_capacity.positive?
|
|
70
|
+
end
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
@executor ||= Concurrent::ThreadPoolExecutor.new DEFAULT_OPTIONS.merge(max_threads: size, max_queue: size)
|
|
72
|
+
on_idle&.call if should_notify
|
|
52
73
|
end
|
|
53
74
|
end
|
|
54
75
|
end
|
|
@@ -31,6 +31,14 @@ module SolidQueue::Processes
|
|
|
31
31
|
!running_async? || @thread&.alive?
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def boot_timed_out?
|
|
35
|
+
@boot_guard.timed_out?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_as_reaped
|
|
39
|
+
@boot_guard.close
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
private
|
|
35
43
|
DEFAULT_MODE = :async
|
|
36
44
|
|
|
@@ -41,11 +49,14 @@ module SolidQueue::Processes
|
|
|
41
49
|
def run_in_mode(&block)
|
|
42
50
|
case
|
|
43
51
|
when running_as_fork?
|
|
44
|
-
|
|
52
|
+
@boot_guard = BootGuards::ForkGuard.new
|
|
53
|
+
fork(&block).tap { @boot_guard.start }
|
|
45
54
|
when running_async?
|
|
55
|
+
@boot_guard = BootGuards::NullGuard.new
|
|
46
56
|
@thread = create_thread(&block)
|
|
47
57
|
@thread.object_id
|
|
48
58
|
else
|
|
59
|
+
@boot_guard = BootGuards::NullGuard.new
|
|
49
60
|
block.call
|
|
50
61
|
end
|
|
51
62
|
end
|
|
@@ -59,6 +70,8 @@ module SolidQueue::Processes
|
|
|
59
70
|
end
|
|
60
71
|
end
|
|
61
72
|
end
|
|
73
|
+
|
|
74
|
+
@boot_guard.complete
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
def shutting_down?
|
|
@@ -95,4 +108,76 @@ module SolidQueue::Processes
|
|
|
95
108
|
mode.fork?
|
|
96
109
|
end
|
|
97
110
|
end
|
|
111
|
+
|
|
112
|
+
module BootGuards
|
|
113
|
+
# Tracks a process that shares memory with its supervisor, whose boot time
|
|
114
|
+
# doesn't need monitoring.
|
|
115
|
+
class NullGuard
|
|
116
|
+
def complete
|
|
117
|
+
@completed = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def start
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def completed?
|
|
124
|
+
@completed
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def timed_out?
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def close
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Tracks a forked process from the moment it's started until its boot
|
|
136
|
+
# callbacks finish, over a pipe that survives forking: the forked process
|
|
137
|
+
# writes to it when it's done booting, and its supervisor reads from it to
|
|
138
|
+
# decide whether the process is taking too long to boot and needs replacing.
|
|
139
|
+
class ForkGuard
|
|
140
|
+
def initialize
|
|
141
|
+
@reader, @writer = IO.pipe
|
|
142
|
+
@created_at = SolidQueue::Timer.monotonic_time_now
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Runs in the forked process when it has finished booting
|
|
146
|
+
def complete
|
|
147
|
+
reader.close
|
|
148
|
+
writer.write(".")
|
|
149
|
+
rescue Errno::EPIPE
|
|
150
|
+
# The supervisor stopped waiting while this process finished booting
|
|
151
|
+
ensure
|
|
152
|
+
writer.close
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Runs in the parent process right after forking
|
|
156
|
+
def start
|
|
157
|
+
writer.close
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A byte means boot completed; EOF means the process exited before
|
|
161
|
+
# finishing its boot, and will be replaced when it's reaped
|
|
162
|
+
def completed?
|
|
163
|
+
@completed ||= begin
|
|
164
|
+
completed = reader.read_nonblock(1, exception: false) != :wait_readable
|
|
165
|
+
reader.close if completed
|
|
166
|
+
completed
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def timed_out?
|
|
171
|
+
!completed? && SolidQueue::Timer.monotonic_time_now - created_at >= SolidQueue.fork_boot_timeout
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def close
|
|
175
|
+
reader.close unless reader.closed?
|
|
176
|
+
writer.close unless writer.closed?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
attr_reader :reader, :writer, :created_at
|
|
181
|
+
end
|
|
182
|
+
end
|
|
98
183
|
end
|
data/lib/solid_queue/tasks.rb
CHANGED
|
@@ -4,6 +4,11 @@ namespace :solid_queue do
|
|
|
4
4
|
Rails::Command.invoke :generate, [ "solid_queue:install" ]
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
desc "Copy any new Solid Queue migrations to the application"
|
|
8
|
+
task :update do
|
|
9
|
+
Rails::Command.invoke :generate, [ "solid_queue:update" ]
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
desc "start solid_queue supervisor to dispatch and process jobs"
|
|
8
13
|
task start: :environment do
|
|
9
14
|
SolidQueue::Supervisor.start
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class ThreadPool < Pool
|
|
5
|
+
delegate :shutdown, :shutdown?, :wait_for_termination, to: :executor
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
DEFAULT_OPTIONS = {
|
|
9
|
+
min_threads: 0,
|
|
10
|
+
idletime: 60,
|
|
11
|
+
fallback_policy: :abort
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def schedule(execution)
|
|
15
|
+
Concurrent::Promises.future_on(executor, execution) do |thread_execution|
|
|
16
|
+
perform_execution(thread_execution)
|
|
17
|
+
end.on_rejection! do |error|
|
|
18
|
+
# Backstop for errors raised outside perform_execution's own rescue,
|
|
19
|
+
# such as when restoring capacity or waking up the worker
|
|
20
|
+
handle_thread_error(error)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def executor
|
|
25
|
+
@executor ||= Concurrent::ThreadPoolExecutor.new DEFAULT_OPTIONS.merge(max_threads: size, max_queue: size)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/solid_queue/timer.rb
CHANGED
data/lib/solid_queue/version.rb
CHANGED
data/lib/solid_queue/worker.rb
CHANGED
|
@@ -11,18 +11,24 @@ module SolidQueue
|
|
|
11
11
|
attr_reader :queues, :pool
|
|
12
12
|
|
|
13
13
|
def initialize(**options)
|
|
14
|
+
execution_pool_type = options.key?(:fibers) ? :fiber : :thread
|
|
15
|
+
|
|
14
16
|
options = options.dup.with_defaults(SolidQueue::Configuration::WORKER_DEFAULTS)
|
|
17
|
+
execution_pool_size = execution_pool_type == :fiber ? options[:fibers] : options[:threads]
|
|
15
18
|
|
|
16
19
|
# Ensure that the queues array is deep frozen to prevent accidental modification
|
|
17
20
|
@queues = Array(options[:queues]).map(&:freeze).freeze
|
|
18
21
|
|
|
19
|
-
@pool = Pool.
|
|
22
|
+
@pool = Pool.build \
|
|
23
|
+
type: execution_pool_type,
|
|
24
|
+
size: execution_pool_size,
|
|
25
|
+
on_idle: -> { wake_up }
|
|
20
26
|
|
|
21
27
|
super(**options)
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def metadata
|
|
25
|
-
super.merge(queues: queues.join(","),
|
|
31
|
+
super.merge(queues: queues.join(","), pool_type: pool.type, pool_size: pool.size)
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
private
|
|
@@ -38,7 +44,7 @@ module SolidQueue
|
|
|
38
44
|
|
|
39
45
|
def claim_executions
|
|
40
46
|
with_polling_volume do
|
|
41
|
-
SolidQueue::ReadyExecution.claim(queues, pool.
|
|
47
|
+
SolidQueue::ReadyExecution.claim(queues, pool.available_capacity, process_id)
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
|
data/lib/solid_queue.rb
CHANGED
|
@@ -29,6 +29,7 @@ module SolidQueue
|
|
|
29
29
|
|
|
30
30
|
mattr_accessor :process_heartbeat_interval, default: 60.seconds
|
|
31
31
|
mattr_accessor :process_alive_threshold, default: 5.minutes
|
|
32
|
+
mattr_accessor :fork_boot_timeout, default: 5.minutes
|
|
32
33
|
|
|
33
34
|
mattr_accessor :shutdown_timeout, default: 5.seconds
|
|
34
35
|
|
|
@@ -86,6 +87,10 @@ module SolidQueue
|
|
|
86
87
|
preserve_finished_jobs
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
def deprecator
|
|
91
|
+
@deprecator ||= ActiveSupport::Deprecation.new(next_major_version, "SolidQueue")
|
|
92
|
+
end
|
|
93
|
+
|
|
89
94
|
def instrument(channel, **options, &block)
|
|
90
95
|
ActiveSupport::Notifications.instrument("#{channel}.solid_queue", **options, &block)
|
|
91
96
|
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: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rosa Gutierrez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -122,6 +122,20 @@ dependencies:
|
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '1.9'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: async
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '2.24'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '2.24'
|
|
125
139
|
- !ruby/object:Gem::Dependency
|
|
126
140
|
name: minitest
|
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -309,6 +323,7 @@ files:
|
|
|
309
323
|
- lib/generators/solid_queue/install/templates/config/queue.yml
|
|
310
324
|
- lib/generators/solid_queue/install/templates/config/recurring.yml
|
|
311
325
|
- lib/generators/solid_queue/install/templates/db/queue_schema.rb
|
|
326
|
+
- lib/generators/solid_queue/update/update_generator.rb
|
|
312
327
|
- lib/puma/plugin/solid_queue.rb
|
|
313
328
|
- lib/solid_queue.rb
|
|
314
329
|
- lib/solid_queue/app_executor.rb
|
|
@@ -318,6 +333,7 @@ files:
|
|
|
318
333
|
- lib/solid_queue/dispatcher.rb
|
|
319
334
|
- lib/solid_queue/dispatcher/concurrency_maintenance.rb
|
|
320
335
|
- lib/solid_queue/engine.rb
|
|
336
|
+
- lib/solid_queue/fiber_pool.rb
|
|
321
337
|
- lib/solid_queue/fork_supervisor.rb
|
|
322
338
|
- lib/solid_queue/lifecycle_hooks.rb
|
|
323
339
|
- lib/solid_queue/log_subscriber.rb
|
|
@@ -342,6 +358,7 @@ files:
|
|
|
342
358
|
- lib/solid_queue/supervisor/pidfiled.rb
|
|
343
359
|
- lib/solid_queue/supervisor/signals.rb
|
|
344
360
|
- lib/solid_queue/tasks.rb
|
|
361
|
+
- lib/solid_queue/thread_pool.rb
|
|
345
362
|
- lib/solid_queue/timer.rb
|
|
346
363
|
- lib/solid_queue/version.rb
|
|
347
364
|
- lib/solid_queue/worker.rb
|