solid_queue 0.7.0 → 0.8.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: 23836c755cda6dd9bec594148ad92ea19740da4125e9115d17c44bd2543d752c
4
- data.tar.gz: 40d91182c13f155a86879f04abcf4232a50f3502f4038b5e9e07a3c15da8def2
3
+ metadata.gz: '053856cc5d0d234f55fa42184371360bca7278a37130bc4230656c48ac0f8e74'
4
+ data.tar.gz: f31c7c8e48f67874365e788cb301c41e5a3be8dc77f28bf5e848f01645076928
5
5
  SHA512:
6
- metadata.gz: f8ac683f1a0e635762d0d93b9b74f712c5fc5465381ea51230448dc2f176de67aafb0ee7031ae9e9cfee73edcee7bd70352e78aefd49ae25ea71a7485af95aff
7
- data.tar.gz: b02eac304a30fb8c7e9e2627446a31bb56d22740e863b1d2749838429416f886da78d19d2000b533e3672576387ee075b8efb3c330f6177b5cd76a27955cd433
6
+ metadata.gz: cdd486cd413c30d10deea24ba32f8f9e06000263a619f0fb175f08695a53f83a55eae56a19aa626e97af1e37a882c16be40441a158569ca06cb3140681414c8e
7
+ data.tar.gz: 1545db515ce86ad1f5e327533be2cfeba19a2b5bb162139dfa40584482a9d1b576775c97d79d4aa6baa5aff36fb3a7579088e9e9ef4852e03f79544fc5a0091e
data/README.md CHANGED
@@ -6,43 +6,52 @@ 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 multi-threading.
8
8
 
9
- ## Installation and usage
10
- Add this line to your application's Gemfile:
9
+ ## Installation
11
10
 
12
- ```ruby
13
- gem "solid_queue"
14
- ```
11
+ 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:
15
12
 
16
- And then execute:
17
- ```bash
18
- $ bundle
19
- ```
13
+ 1. `bundle add solid_queue`
14
+ 2. `bin/rails solid_queue:install`
20
15
 
21
- Or install it yourself as:
22
- ```bash
23
- $ gem install solid_queue
24
- ```
16
+ This will configure Solid Queue as the production Active Job backend, create `config/solid_queue.yml`, and create the `db/queue_schema.rb`.
25
17
 
26
- Now, you need to install the necessary migrations and configure the Active Job's adapter. You can do both at once using the provided generator:
18
+ You will then have to add the configuration for the queue database in `config/database.yml`. If you're using sqlite, it'll look like this:
27
19
 
28
- ```bash
29
- $ bin/rails generate solid_queue:install
20
+ ```yaml
21
+ production:
22
+ primary:
23
+ <<: *default
24
+ database: storage/production.sqlite3
25
+ queue:
26
+ <<: *default
27
+ database: storage/production_queue.sqlite3
28
+ migrations_paths: db/queue_migrate
30
29
  ```
31
30
 
32
- This will set `solid_queue` as the Active Job's adapter in production, and will copy the required migration over to your app.
31
+ ...or if you're using MySQL/PostgreSQL/Trilogy:
33
32
 
34
- Alternatively, you can skip setting the Active Job's adapter with:
35
- ```bash
36
- $ bin/rails generate solid_queue:install --skip_adapter
33
+ ```yaml
34
+ production:
35
+ primary: &primary_production
36
+ <<: *default
37
+ database: app_production
38
+ username: app
39
+ password: <%= ENV["APP_DATABASE_PASSWORD"] %>
40
+ queue:
41
+ <<: *primary_production
42
+ database: app_production_queue
43
+ migrations_paths: db/queue_migrate
37
44
  ```
38
45
 
39
- And set Solid Queue as your Active Job's queue backend manually, in your environment config:
40
- ```ruby
41
- # config/environments/production.rb
42
- config.active_job.queue_adapter = :solid_queue
43
- ```
46
+ Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
47
+
48
+ 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.
49
+
50
+ For small projects, you can run Solid Queue on the same machine as your webserver. When you're ready to scale, Solid Queue supports horizontal scaling out-of-the-box. You can run Solid Queue on a separate server from your webserver, or even run `bin/jobs` on multiple machines at the same time. Depending on the configuration, you can designate some machines to run only dispatchers or only workers. See the [configuration](#configuration) section for more details on this.
44
51
 
45
- Or you can set only specific jobs to use Solid Queue as their backend if you're migrating from another adapter and want to move jobs progressively:
52
+ ## Incremental adoption
53
+
54
+ 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:
46
55
 
47
56
  ```ruby
48
57
  # app/jobs/my_job.rb
@@ -52,24 +61,9 @@ class MyJob < ApplicationJob
52
61
  # ...
53
62
  end
54
63
  ```
64
+ ## High performance requirements
55
65
 
56
- Finally, you need to run the migrations:
57
-
58
- ```bash
59
- $ bin/rails db:migrate
60
- ```
61
-
62
- After this, you'll be ready to enqueue jobs using Solid Queue, but you need to start Solid Queue's supervisor to run them. You can use the provided binstub:`
63
- ```bash
64
- $ bin/jobs
65
- ```
66
-
67
- This will start processing jobs in all queues using the default configuration. See [below](#configuration) to learn more about configuring Solid Queue.
68
-
69
- For small projects, you can run Solid Queue on the same machine as your webserver. When you're ready to scale, Solid Queue supports horizontal scaling out-of-the-box. You can run Solid Queue on a separate server from your webserver, or even run `bin/jobs` on multiple machines at the same time. Depending on the configuration, you can designate some machines to run only dispatchers or only workers. See the [configuration](#configuration) section for more details on this.
70
-
71
- ## Requirements
72
- Besides Rails 7.1, Solid Queue works best 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.
66
+ 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.
73
67
 
74
68
  ## Configuration
75
69
 
@@ -135,8 +129,8 @@ Here's an overview of the different options:
135
129
  - `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.
136
130
  - `recurring_tasks`: a list of recurring tasks the dispatcher will manage. Read more details about this one in the [Recurring tasks](#recurring-tasks) section.
137
131
 
138
-
139
132
  ### Queue order and priorities
133
+
140
134
  As mentioned above, if you specify a list of queues for a worker, these will be polled in the order given, such as for the list `real_time,background`, no jobs will be taken from `background` unless there aren't any more jobs waiting in `real_time`.
141
135
 
142
136
  Active Job also supports positive integer priorities when enqueuing jobs. In Solid Queue, the smaller the value, the higher the priority. The default is `0`.
@@ -159,66 +153,53 @@ When receiving a `QUIT` signal, if workers still have jobs in-flight, these will
159
153
  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.
160
154
 
161
155
 
162
- ### Dedicated database configuration
156
+ ### Database configuration
163
157
 
164
- Solid Queue can be configured to run on a different database than the main application.
158
+ You can configure the database used by Solid Queue via the `config.solid_queue.connects_to` option in the `config/application.rb` or `config/environments/production.rb` config files. By default, a single database is used for both writing and reading called `queue` to match the database configuration you set up during the install.
165
159
 
166
- 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.
160
+ All the options available to Active Record for multiple databases can be used here.
167
161
 
168
- ```ruby
169
- # Use a separate DB for Solid Queue
170
- config.solid_queue.connects_to = { database: { writing: :solid_queue_primary, reading: :solid_queue_replica } }
171
- ```
162
+ ## Lifecycle hooks
172
163
 
173
- 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.
164
+ In Solid queue, you can hook into two different points in the supervisor's life:
165
+ - `start`: after the supervisor has finished booting and right before it forks workers and dispatchers.
166
+ - `stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown.
174
167
 
175
- ```yml
176
- default: &default
177
- adapter: sqlite3
178
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
179
- timeout: 5000
168
+ And into two different points in a worker's life:
169
+ - `worker_start`: after the worker has finished booting and right before it starts the polling loop.
170
+ - `worker_stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown (which is just `exit!`).
180
171
 
181
- solid_queue: &solid_queue
182
- <<: *default
183
- migrations_paths: db/solid_queue_migrate
172
+ You can use the following methods with a block to do this:
173
+ ```ruby
174
+ SolidQueue.on_start
175
+ SolidQueue.on_stop
184
176
 
185
- development:
186
- primary:
187
- <<: *default
188
- # ...
189
- solid_queue_primary:
190
- <<: *solid_queue
191
- # ...
192
- solid_queue_replica:
193
- <<: *solid_queue
194
- # ...
177
+ SolidQueue.on_worker_start
178
+ SolidQueue.on_worker_stop
195
179
  ```
196
180
 
197
- 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`.
198
-
199
- ```bash
200
- $ bin/rails g solid_queue:install --database solid_queue
181
+ For example:
182
+ ```ruby
183
+ SolidQueue.on_start { start_metrics_server }
184
+ SolidQueue.on_stop { stop_metrics_server }
201
185
  ```
202
186
 
203
- Note: If you've already run the solid queue install command (`bin/rails generate solid_queue:install`) without a `--database` option, 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.
204
-
205
- Finally, run the migrations:
206
-
207
- ```bash
208
- $ bin/rails db:migrate
209
- ```
187
+ 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.
210
188
 
211
189
  ### Other configuration settings
190
+
212
191
  _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`
213
192
 
214
193
  There are several settings that control how Solid Queue works that you can set as well:
215
194
  - `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
216
195
  - `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
217
- - `on_thread_error`: custom lambda/Proc to call when there's an error within a thread that takes the exception raised as argument. Defaults to
196
+ - `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
218
197
 
219
198
  ```ruby
220
199
  -> (exception) { Rails.error.report(exception, handled: false) }
221
200
  ```
201
+ **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.
202
+
222
203
  - `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.
223
204
  - `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
224
205
  - `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
@@ -231,11 +212,13 @@ There are several settings that control how Solid Queue works that you can set a
231
212
  - `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).
232
213
 
233
214
  ## Errors when enqueuing
215
+
234
216
  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.
235
217
 
236
218
  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.
237
219
 
238
220
  ## Concurrency controls
221
+
239
222
  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.
240
223
 
241
224
  ```ruby
@@ -283,6 +266,8 @@ In this case, if we have a `Box::MovePostingsByContactToDesignatedBoxJob` job en
283
266
 
284
267
  Note that the `duration` setting depends indirectly on the value for `concurrency_maintenance_interval` that you set for your dispatcher(s), as that'd be the frequency with which blocked jobs are checked and unblocked. In general, you should set `duration` in a way that all your jobs would finish well under that duration and think of the concurrency maintenance task as a failsafe in case something goes wrong.
285
268
 
269
+ Jobs are unblocked in order of priority but queue order is not taken into account for unblocking jobs. That means that if you have a group of jobs that share a concurrency group but are in different queues, or jobs of the same class that you enqueue in different queues, the queue order you set for a worker is not taken into account when unblocking blocked ones. The reason is that a job that runs unblocks the next one, and the job itself doesn't know about a particular worker's queue order (you could even have different workers with different queue orders), it can only know about priority. Once blocked jobs are unblocked and available for polling, they'll be picked up by a worker following its queue order.
270
+
286
271
  Finally, failed jobs that are automatically or manually retried work in the same way as new jobs that get enqueued: they get in the queue for gaining the lock, and whenever they get it, they'll be run. It doesn't matter if they had gained the lock already in the past.
287
272
 
288
273
  ## Failed jobs and retries
@@ -299,35 +284,15 @@ failed_execution.discard # This will delete the job from the system
299
284
  However, we recommend taking a look at [mission_control-jobs](https://github.com/rails/mission_control-jobs), a dashboard where, among other things, you can examine and retry/discard failed jobs.
300
285
 
301
286
  ## Puma plugin
287
+
302
288
  We provide a Puma plugin if you want to run the Solid Queue's supervisor together with Puma and have Puma monitor and manage it. You just need to add
303
289
  ```ruby
304
290
  plugin :solid_queue
305
291
  ```
306
292
  to your `puma.rb` configuration.
307
293
 
308
-
309
- ## Jobs and transactional integrity
310
- :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. 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.
311
-
312
- By default, Solid Queue runs in the same DB as your app, and job enqueuing is _not_ deferred until any ongoing transaction is committed, which means that by default, you'll be taking advantage of this transactional integrity.
313
-
314
- If you prefer not to rely on this, or avoid relying on it unintentionally, you should make sure that:
315
- - 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+.
316
- - 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.
317
- - 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:
318
-
319
- ```ruby
320
- class ApplicationRecord < ActiveRecord::Base
321
- self.abstract_class = true
322
-
323
- connects_to database: { writing: :primary, reading: :replica }
324
- ```
325
-
326
- ```ruby
327
- config.solid_queue.connects_to = { database: { writing: :primary, reading: :replica } }
328
- ```
329
-
330
294
  ## Recurring tasks
295
+
331
296
  Solid Queue supports defining recurring tasks that run at specific times in the future, on a regular basis like cron jobs. These are managed by dispatcher processes and as such, they can be defined in the dispatcher's configuration like this:
332
297
  ```yml
333
298
  dispatchers:
data/UPGRADING.md CHANGED
@@ -1,6 +1,9 @@
1
+ # Upgrading to version 0.8.x
2
+ *IMPORTANT*: This version collapsed all migrations into a single `db/queue_schema.rb`, that will use a separate `queue` database. If you're upgrading from a version < 0.6.0, you need to upgrade to 0.6.0 first, ensure all migrations are up-to-date, and then upgrade further.
3
+
1
4
  # Upgrading to version 0.7.x
2
5
 
3
- This version removed the new async mode introduced in version 0.4.0 and introduced a new binstub that can be used to start Solid Queue's supervisor. It includes also a minor migration.
6
+ This version removed the new async mode introduced in version 0.4.0 and introduced a new binstub that can be used to start Solid Queue's supervisor.
4
7
 
5
8
  To install both the binstub `bin/jobs` and the migration, you can just run
6
9
  ```
@@ -5,6 +5,6 @@ Example:
5
5
  bin/rails generate solid_queue:install
6
6
 
7
7
  This will perform the following:
8
- Installs solid_queue migrations
8
+ Adds solid_queue db schema
9
9
  Replaces Active Job's adapter in environment configuration
10
10
  Installs bin/jobs binstub to start the supervisor
@@ -3,33 +3,17 @@
3
3
  class SolidQueue::InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
- class_option :skip_adapter, type: :boolean, default: nil, desc: "Skip setting Solid Queue as the Active Job's adapter"
7
- class_option :database, type: :string, default: nil, desc: "The database to use for migrations, if different from the primary one."
8
-
9
- def add_solid_queue
10
- unless options[:skip_adapter]
11
- if (env_config = Pathname(destination_root).join("config/environments/production.rb")).exist?
12
- say "Setting solid_queue as Active Job's queue adapter"
13
- gsub_file env_config, /(# )?config\.active_job\.queue_adapter\s+=.*/, "config.active_job.queue_adapter = :solid_queue"
14
- end
15
- end
16
-
17
- if File.exist?("config/solid_queue.yml")
18
- say "Skipping sample configuration as config/solid_queue.yml exists"
19
- else
20
- say "Copying sample configuration"
21
- copy_file "config.yml", "config/solid_queue.yml"
22
- end
23
-
24
- say "Copying binstub"
25
- copy_file "jobs", "bin/jobs"
6
+ def copy_files
7
+ template "config/solid_queue.yml"
8
+ template "db/queue_schema.rb"
9
+ template "bin/jobs"
26
10
  chmod "bin/jobs", 0755 & ~File.umask, verbose: false
27
11
  end
28
12
 
29
- def create_migrations
30
- say "Installing database migrations"
31
- arguments = [ "FROM=solid_queue" ]
32
- arguments << "DATABASE=#{options[:database]}" if options[:database].present?
33
- rails_command "railties:install:migrations #{arguments.join(" ")}", inline: true
13
+ def configure_active_job_adapter
14
+ gsub_file Pathname(destination_root).join("config/environments/production.rb"),
15
+ /(# )?config\.active_job\.queue_adapter\s+=.*/,
16
+ "config.active_job.queue_adapter = :solid_queue\n" +
17
+ " config.solid_queue.connects_to = { database: { writing: :queue } }\n"
34
18
  end
35
19
  end
@@ -0,0 +1,18 @@
1
+ default: &default
2
+ dispatchers:
3
+ - polling_interval: 1
4
+ batch_size: 500
5
+ workers:
6
+ - queues: "*"
7
+ threads: 3
8
+ processes: <%%= ENV.fetch("JOB_CONCURRENCY", 1) %>
9
+ polling_interval: 0.1
10
+
11
+ development:
12
+ <<: *default
13
+
14
+ test:
15
+ <<: *default
16
+
17
+ production:
18
+ <<: *default
@@ -0,0 +1,129 @@
1
+ ActiveRecord::Schema[7.1].define(version: 2024_09_04_193154) do
2
+ create_table "solid_queue_blocked_executions", force: :cascade do |t|
3
+ t.integer "job_id", null: false
4
+ t.string "queue_name", null: false
5
+ t.integer "priority", default: 0, null: false
6
+ t.string "concurrency_key", null: false
7
+ t.datetime "expires_at", null: false
8
+ t.datetime "created_at", null: false
9
+ t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
10
+ t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
11
+ t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
12
+ end
13
+
14
+ create_table "solid_queue_claimed_executions", force: :cascade do |t|
15
+ t.integer "job_id", null: false
16
+ t.bigint "process_id"
17
+ t.datetime "created_at", null: false
18
+ t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
19
+ t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
20
+ end
21
+
22
+ create_table "solid_queue_failed_executions", force: :cascade do |t|
23
+ t.integer "job_id", null: false
24
+ t.text "error"
25
+ t.datetime "created_at", null: false
26
+ t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
27
+ end
28
+
29
+ create_table "solid_queue_jobs", force: :cascade do |t|
30
+ t.string "queue_name", null: false
31
+ t.string "class_name", null: false
32
+ t.text "arguments"
33
+ t.integer "priority", default: 0, null: false
34
+ t.string "active_job_id"
35
+ t.datetime "scheduled_at"
36
+ t.datetime "finished_at"
37
+ t.string "concurrency_key"
38
+ t.datetime "created_at", null: false
39
+ t.datetime "updated_at", null: false
40
+ t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
41
+ t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
42
+ t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
43
+ t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
44
+ t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
45
+ end
46
+
47
+ create_table "solid_queue_pauses", force: :cascade do |t|
48
+ t.string "queue_name", null: false
49
+ t.datetime "created_at", null: false
50
+ t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
51
+ end
52
+
53
+ create_table "solid_queue_processes", force: :cascade do |t|
54
+ t.string "kind", null: false
55
+ t.datetime "last_heartbeat_at", null: false
56
+ t.bigint "supervisor_id"
57
+ t.integer "pid", null: false
58
+ t.string "hostname"
59
+ t.text "metadata"
60
+ t.datetime "created_at", null: false
61
+ t.string "name", null: false
62
+ t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
63
+ t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
64
+ t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
65
+ end
66
+
67
+ create_table "solid_queue_ready_executions", force: :cascade do |t|
68
+ t.integer "job_id", null: false
69
+ t.string "queue_name", null: false
70
+ t.integer "priority", default: 0, null: false
71
+ t.datetime "created_at", null: false
72
+ t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
73
+ t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
74
+ t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
75
+ end
76
+
77
+ create_table "solid_queue_recurring_executions", force: :cascade do |t|
78
+ t.integer "job_id", null: false
79
+ t.string "task_key", null: false
80
+ t.datetime "run_at", null: false
81
+ t.datetime "created_at", null: false
82
+ t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
83
+ t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
84
+ end
85
+
86
+ create_table "solid_queue_recurring_tasks", force: :cascade do |t|
87
+ t.string "key", null: false
88
+ t.string "schedule", null: false
89
+ t.string "command", limit: 2048
90
+ t.string "class_name"
91
+ t.text "arguments"
92
+ t.string "queue_name"
93
+ t.integer "priority", default: 0
94
+ t.boolean "static", default: true, null: false
95
+ t.text "description"
96
+ t.datetime "created_at", null: false
97
+ t.datetime "updated_at", null: false
98
+ t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
99
+ t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
100
+ end
101
+
102
+ create_table "solid_queue_scheduled_executions", force: :cascade do |t|
103
+ t.integer "job_id", null: false
104
+ t.string "queue_name", null: false
105
+ t.integer "priority", default: 0, null: false
106
+ t.datetime "scheduled_at", null: false
107
+ t.datetime "created_at", null: false
108
+ t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
109
+ t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
110
+ end
111
+
112
+ create_table "solid_queue_semaphores", force: :cascade do |t|
113
+ t.string "key", null: false
114
+ t.integer "value", default: 1, null: false
115
+ t.datetime "expires_at", null: false
116
+ t.datetime "created_at", null: false
117
+ t.datetime "updated_at", null: false
118
+ t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
119
+ t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
120
+ t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
121
+ end
122
+
123
+ add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
124
+ add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
125
+ add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
126
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
127
+ add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
128
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
129
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module LifecycleHooks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ mattr_reader :lifecycle_hooks, default: { start: [], stop: [] }
9
+ end
10
+
11
+ class_methods do
12
+ def on_start(&block)
13
+ self.lifecycle_hooks[:start] << block
14
+ end
15
+
16
+ def on_stop(&block)
17
+ self.lifecycle_hooks[:stop] << block
18
+ end
19
+
20
+ def clear_hooks
21
+ self.lifecycle_hooks[:start] = []
22
+ self.lifecycle_hooks[:stop] = []
23
+ end
24
+ end
25
+
26
+ private
27
+ def run_start_hooks
28
+ run_hooks_for :start
29
+ end
30
+
31
+ def run_stop_hooks
32
+ run_hooks_for :stop
33
+ end
34
+
35
+ def run_hooks_for(event)
36
+ self.class.lifecycle_hooks.fetch(event, []).each do |block|
37
+ block.call
38
+ rescue Exception => exception
39
+ handle_thread_error(exception)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -7,11 +7,7 @@ module SolidQueue::Processes
7
7
  attr_writer :mode
8
8
 
9
9
  def start
10
- @stopped = false
11
-
12
- SolidQueue.instrument(:start_process, process: self) do
13
- run_callbacks(:boot) { boot }
14
- end
10
+ boot
15
11
 
16
12
  if running_async?
17
13
  @thread = create_thread { run }
@@ -25,10 +21,6 @@ module SolidQueue::Processes
25
21
  @thread&.join
26
22
  end
27
23
 
28
- def alive?
29
- !running_async? || @thread.alive?
30
- end
31
-
32
24
  private
33
25
  DEFAULT_MODE = :async
34
26
 
@@ -37,9 +29,15 @@ module SolidQueue::Processes
37
29
  end
38
30
 
39
31
  def boot
40
- if running_as_fork?
41
- register_signal_handlers
42
- set_procline
32
+ SolidQueue.instrument(:start_process, process: self) do
33
+ run_callbacks(:boot) do
34
+ @stopped = false
35
+
36
+ if running_as_fork?
37
+ register_signal_handlers
38
+ set_procline
39
+ end
40
+ end
43
41
  end
44
42
  end
45
43
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Supervisor < Processes::Base
5
+ include LifecycleHooks
5
6
  include Maintenance, Signals, Pidfiled
6
7
 
7
8
  class << self
@@ -27,6 +28,7 @@ module SolidQueue
27
28
 
28
29
  def start
29
30
  boot
31
+ run_start_hooks
30
32
 
31
33
  start_processes
32
34
  launch_maintenance_task
@@ -36,6 +38,7 @@ module SolidQueue
36
38
 
37
39
  def stop
38
40
  @stopped = true
41
+ run_stop_hooks
39
42
  end
40
43
 
41
44
  private
@@ -1,4 +1,9 @@
1
1
  namespace :solid_queue do
2
+ desc "Install Solid Queue"
3
+ task :install do
4
+ Rails::Command.invoke :generate, [ "solid_queue:install" ]
5
+ end
6
+
2
7
  desc "start solid_queue supervisor to dispatch and process jobs"
3
8
  task start: :environment do
4
9
  SolidQueue::Supervisor.start
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.1"
3
3
  end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Worker < Processes::Poller
5
+ include LifecycleHooks
6
+
7
+ after_boot :run_start_hooks
8
+ before_shutdown :run_stop_hooks
9
+
5
10
  attr_accessor :queues, :pool
6
11
 
7
12
  def initialize(**options)
data/lib/solid_queue.rb CHANGED
@@ -43,6 +43,16 @@ module SolidQueue
43
43
  mattr_accessor :clear_finished_jobs_after, default: 1.day
44
44
  mattr_accessor :default_concurrency_control_period, default: 3.minutes
45
45
 
46
+ delegate :on_start, :on_stop, to: Supervisor
47
+
48
+ def on_worker_start(...)
49
+ Worker.on_start(...)
50
+ end
51
+
52
+ def on_worker_stop(...)
53
+ Worker.on_stop(...)
54
+ end
55
+
46
56
  def supervisor?
47
57
  supervisor
48
58
  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.7.0
4
+ version: 0.8.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-09-02 00:00:00.000000000 Z
11
+ date: 2024-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -203,7 +203,6 @@ files:
203
203
  - README.md
204
204
  - Rakefile
205
205
  - UPGRADING.md
206
- - app/jobs/solid_queue/recurring_job.rb
207
206
  - app/models/solid_queue/blocked_execution.rb
208
207
  - app/models/solid_queue/claimed_execution.rb
209
208
  - app/models/solid_queue/execution.rb
@@ -231,19 +230,13 @@ files:
231
230
  - app/models/solid_queue/scheduled_execution.rb
232
231
  - app/models/solid_queue/semaphore.rb
233
232
  - config/routes.rb
234
- - db/migrate/20231211200639_create_solid_queue_tables.rb
235
- - db/migrate/20240110143450_add_missing_index_to_blocked_executions.rb
236
- - db/migrate/20240218110712_create_recurring_executions.rb
237
- - db/migrate/20240719134516_create_recurring_tasks.rb
238
- - db/migrate/20240811173327_add_name_to_processes.rb
239
- - db/migrate/20240813160053_make_name_not_null.rb
240
- - db/migrate/20240819165045_change_solid_queue_recurring_tasks_static_to_not_null.rb
241
233
  - lib/active_job/concurrency_controls.rb
242
234
  - lib/active_job/queue_adapters/solid_queue_adapter.rb
243
235
  - lib/generators/solid_queue/install/USAGE
244
236
  - lib/generators/solid_queue/install/install_generator.rb
245
- - lib/generators/solid_queue/install/templates/config.yml
246
- - lib/generators/solid_queue/install/templates/jobs
237
+ - lib/generators/solid_queue/install/templates/bin/jobs
238
+ - lib/generators/solid_queue/install/templates/config/solid_queue.yml
239
+ - lib/generators/solid_queue/install/templates/db/queue_schema.rb
247
240
  - lib/puma/plugin/solid_queue.rb
248
241
  - lib/solid_queue.rb
249
242
  - lib/solid_queue/app_executor.rb
@@ -253,6 +246,7 @@ files:
253
246
  - lib/solid_queue/dispatcher/concurrency_maintenance.rb
254
247
  - lib/solid_queue/dispatcher/recurring_schedule.rb
255
248
  - lib/solid_queue/engine.rb
249
+ - lib/solid_queue/lifecycle_hooks.rb
256
250
  - lib/solid_queue/log_subscriber.rb
257
251
  - lib/solid_queue/pool.rb
258
252
  - lib/solid_queue/processes/base.rb
@@ -282,9 +276,11 @@ metadata:
282
276
  homepage_uri: https://github.com/rails/solid_queue
283
277
  source_code_uri: https://github.com/rails/solid_queue
284
278
  post_install_message: |
285
- Upgrading to Solid Queue 0.4.x, 0.5.x, 0.6.x or 0.7.x? There are some breaking changes about how Solid Queue is started,
286
- configuration and new migrations. Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
279
+ Upgrading to Solid Queue 0.8.0 from < 0.6.0? You need to upgrade to 0.6.0 first. Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
287
280
  for upgrade instructions.
281
+
282
+ Upgrading to Solid Queue 0.4.x, 0.5.x, 0.6.x or 0.7.x? There are some breaking changes about how Solid Queue is started,
283
+ configuration and new migrations.
288
284
  rdoc_options: []
289
285
  require_paths:
290
286
  - lib
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class SolidQueue::RecurringJob < ActiveJob::Base
4
- def perform(command)
5
- SolidQueue.instrument(:run_command, command: command) do
6
- eval(command, TOPLEVEL_BINDING, __FILE__, __LINE__)
7
- end
8
- end
9
- end
@@ -1,100 +0,0 @@
1
- class CreateSolidQueueTables < ActiveRecord::Migration[7.0]
2
- def change
3
- create_table :solid_queue_jobs do |t|
4
- t.string :queue_name, null: false
5
- t.string :class_name, null: false, index: true
6
- t.text :arguments
7
- t.integer :priority, default: 0, null: false
8
- t.string :active_job_id, index: true
9
- t.datetime :scheduled_at
10
- t.datetime :finished_at, index: true
11
- t.string :concurrency_key
12
-
13
- t.timestamps
14
-
15
- t.index [ :queue_name, :finished_at ], name: "index_solid_queue_jobs_for_filtering"
16
- t.index [ :scheduled_at, :finished_at ], name: "index_solid_queue_jobs_for_alerting"
17
- end
18
-
19
- create_table :solid_queue_scheduled_executions do |t|
20
- t.references :job, index: { unique: true }, null: false
21
- t.string :queue_name, null: false
22
- t.integer :priority, default: 0, null: false
23
- t.datetime :scheduled_at, null: false
24
-
25
- t.datetime :created_at, null: false
26
-
27
- t.index [ :scheduled_at, :priority, :job_id ], name: "index_solid_queue_dispatch_all"
28
- end
29
-
30
- create_table :solid_queue_ready_executions do |t|
31
- t.references :job, index: { unique: true }, null: false
32
- t.string :queue_name, null: false
33
- t.integer :priority, default: 0, null: false
34
-
35
- t.datetime :created_at, null: false
36
-
37
- t.index [ :priority, :job_id ], name: "index_solid_queue_poll_all"
38
- t.index [ :queue_name, :priority, :job_id ], name: "index_solid_queue_poll_by_queue"
39
- end
40
-
41
- create_table :solid_queue_claimed_executions do |t|
42
- t.references :job, index: { unique: true }, null: false
43
- t.bigint :process_id
44
- t.datetime :created_at, null: false
45
-
46
- t.index [ :process_id, :job_id ]
47
- end
48
-
49
- create_table :solid_queue_blocked_executions do |t|
50
- t.references :job, index: { unique: true }, null: false
51
- t.string :queue_name, null: false
52
- t.integer :priority, default: 0, null: false
53
- t.string :concurrency_key, null: false
54
- t.datetime :expires_at, null: false
55
-
56
- t.datetime :created_at, null: false
57
-
58
- t.index [ :expires_at, :concurrency_key ], name: "index_solid_queue_blocked_executions_for_maintenance"
59
- end
60
-
61
- create_table :solid_queue_failed_executions do |t|
62
- t.references :job, index: { unique: true }, null: false
63
- t.text :error
64
- t.datetime :created_at, null: false
65
- end
66
-
67
- create_table :solid_queue_pauses do |t|
68
- t.string :queue_name, null: false, index: { unique: true }
69
- t.datetime :created_at, null: false
70
- end
71
-
72
- create_table :solid_queue_processes do |t|
73
- t.string :kind, null: false
74
- t.datetime :last_heartbeat_at, null: false, index: true
75
- t.bigint :supervisor_id, index: true
76
-
77
- t.integer :pid, null: false
78
- t.string :hostname
79
- t.text :metadata
80
-
81
- t.datetime :created_at, null: false
82
- end
83
-
84
- create_table :solid_queue_semaphores do |t|
85
- t.string :key, null: false, index: { unique: true }
86
- t.integer :value, default: 1, null: false
87
- t.datetime :expires_at, null: false, index: true
88
-
89
- t.timestamps
90
-
91
- t.index [ :key, :value ], name: "index_solid_queue_semaphores_on_key_and_value"
92
- end
93
-
94
- add_foreign_key :solid_queue_blocked_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
95
- add_foreign_key :solid_queue_claimed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
96
- add_foreign_key :solid_queue_failed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
97
- add_foreign_key :solid_queue_ready_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
98
- add_foreign_key :solid_queue_scheduled_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
99
- end
100
- end
@@ -1,5 +0,0 @@
1
- class AddMissingIndexToBlockedExecutions < ActiveRecord::Migration[7.1]
2
- def change
3
- add_index :solid_queue_blocked_executions, [ :concurrency_key, :priority, :job_id ], name: "index_solid_queue_blocked_executions_for_release"
4
- end
5
- end
@@ -1,14 +0,0 @@
1
- class CreateRecurringExecutions < ActiveRecord::Migration[7.1]
2
- def change
3
- create_table :solid_queue_recurring_executions do |t|
4
- t.references :job, index: { unique: true }, null: false
5
- t.string :task_key, null: false
6
- t.datetime :run_at, null: false
7
- t.datetime :created_at, null: false
8
-
9
- t.index [ :task_key, :run_at ], unique: true
10
- end
11
-
12
- add_foreign_key :solid_queue_recurring_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
13
- end
14
- end
@@ -1,20 +0,0 @@
1
- class CreateRecurringTasks < ActiveRecord::Migration[7.1]
2
- def change
3
- create_table :solid_queue_recurring_tasks do |t|
4
- t.string :key, null: false, index: { unique: true }
5
- t.string :schedule, null: false
6
- t.string :command, limit: 2048
7
- t.string :class_name
8
- t.text :arguments
9
-
10
- t.string :queue_name
11
- t.integer :priority, default: 0
12
-
13
- t.boolean :static, default: true, index: true
14
-
15
- t.text :description
16
-
17
- t.timestamps
18
- end
19
- end
20
- end
@@ -1,5 +0,0 @@
1
- class AddNameToProcesses < ActiveRecord::Migration[7.1]
2
- def change
3
- add_column :solid_queue_processes, :name, :string
4
- end
5
- end
@@ -1,16 +0,0 @@
1
- class MakeNameNotNull < ActiveRecord::Migration[7.1]
2
- def up
3
- SolidQueue::Process.where(name: nil).find_each do |process|
4
- process.name ||= [ process.kind.downcase, SecureRandom.hex(10) ].join("-")
5
- process.save!
6
- end
7
-
8
- change_column :solid_queue_processes, :name, :string, null: false
9
- add_index :solid_queue_processes, [ :name, :supervisor_id ], unique: true
10
- end
11
-
12
- def down
13
- remove_index :solid_queue_processes, [ :name, :supervisor_id ]
14
- change_column :solid_queue_processes, :name, :string, null: true
15
- end
16
- end
@@ -1,5 +0,0 @@
1
- class ChangeSolidQueueRecurringTasksStaticToNotNull < ActiveRecord::Migration[7.1]
2
- def change
3
- change_column_null :solid_queue_recurring_tasks, :static, false, true
4
- end
5
- end
@@ -1,18 +0,0 @@
1
- # default: &default
2
- # dispatchers:
3
- # - polling_interval: 1
4
- # batch_size: 500
5
- # workers:
6
- # - queues: "*"
7
- # threads: 3
8
- # processes: 1
9
- # polling_interval: 0.1
10
- #
11
- # development:
12
- # <<: *default
13
- #
14
- # test:
15
- # <<: *default
16
- #
17
- # production:
18
- # <<: *default