solid_queue 0.2.2 → 0.3.0

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: d907fb9133f2c72a61b586038f05b248a29e7c55f506aa541ca39f35a15d40ff
4
- data.tar.gz: 887ecd7d0a15159ae10845466993699165a531367dbb21151e3ae704e56fa8e9
3
+ metadata.gz: 2c5bed020eb4391f1885c95f2f4373e30d1eacd544e2cfcadf131c8c09079fd0
4
+ data.tar.gz: 749ac036b072622cd2cc299f1fc5b1a7353a44347beccaa8407157eed8af02ef
5
5
  SHA512:
6
- metadata.gz: 665bb353c9cc8c557952ca5e2f63120b0541eef7a20008b6c288147e0350564200be2e6272f9924448867b9b2d09a7e48dc5b2267ec35b91f5dcd38bc409fff1
7
- data.tar.gz: 9e911e1a270b5da57f75011e40ad6949480a8b87a2eb391934697159701273e1f05da9ac5c114e8d9dac492bb7685f6884eacac592ed44a460f513fd7349f85a
6
+ metadata.gz: f39c8aadb36bc8ae5556f3cbd190e6d123ca441b14374b56c207530dd2d37d8dc4880f6e977f6bc2e047b47c886768a3c86d6fefd040a447f6d8f21e28e8a273
7
+ data.tar.gz: d95c4be96c4388c6558a32f44690e9d92913e47bd01a379f09ef8b3e65a9098a2e578dd68885c36374b27e389c65d67e1100ad4d1c6908e6f675ede75bce8d3a
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Solid Queue is a DB-based queuing backend for [Active Job](https://edgeguides.rubyonrails.org/active_job_basics.html), designed with simplicity and performance in mind.
4
4
 
5
- Besides regular job enqueuing and processing, Solid Queue supports delayed jobs, concurrency controls, pausing queues, numeric priorities per job, priorities by queue order, and bulk enqueuing (`enqueue_all` for Active Job's `perform_all_later`). _Improvements to logging and instrumentation, a better CLI tool, a way to run within an existing process in "async" mode, unique jobs and recurring, cron-like tasks are coming very soon._
5
+ Besides regular job enqueuing and processing, Solid Queue supports delayed jobs, concurrency controls, pausing queues, numeric priorities per job, priorities by queue order, and bulk enqueuing (`enqueue_all` for Active Job's `perform_all_later`). _Improvements to logging and instrumentation, a better CLI tool, a way to run within an existing process in "async" mode, and some way of specifying unique jobs are coming very soon._
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
 
@@ -66,6 +66,8 @@ $ bundle exec rake solid_queue:start
66
66
 
67
67
  This will start processing jobs in all queues using the default configuration. See [below](#configuration) to learn more about configuring Solid Queue.
68
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 `bundle exec rake solid_queue:start` on multiple machines at the same time. If you'd like to designate some machines to be only dispatchers or only workers, use `bundle exec rake solid_queue:dispatch` or `bundle exec rake solid_queue:work`, respectively.
70
+
69
71
  ## Requirements
70
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.
71
73
 
@@ -75,7 +77,7 @@ Besides Rails 7.1, Solid Queue works best with MySQL 8+ or PostgreSQL 9.5+, as t
75
77
 
76
78
  We have three types of processes in Solid Queue:
77
79
  - _Workers_ are in charge of picking jobs ready to run from queues and processing them. They work off the `solid_queue_ready_executions` table.
78
- - _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. They also do some maintenance work related to concurrency controls.
80
+ - _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. They're also in charge of managing [recurring tasks](#recurring-tasks), dispatching jobs to process them according to their schedule. On top of that, they do some maintenance work related to [concurrency controls](#concurrency-controls).
79
81
  - The _supervisor_ forks workers and dispatchers according to the configuration, controls their heartbeats, and sends them signals to stop and start them when needed.
80
82
 
81
83
  By default, Solid Queue will try to find your configuration under `config/solid_queue.yml`, but you can set a different path using the environment variable `SOLID_QUEUE_CONFIG`. This is what this configuration looks like:
@@ -115,8 +117,10 @@ Everything is optional. If no configuration is provided, Solid Queue will run wi
115
117
  This will create a worker fetching jobs from all queues starting with `staging`. The wildcard `*` is only allowed on its own or at the end of a queue name; you can't specify queue names such as `*_some_queue`. These will be ignored.
116
118
 
117
119
  Finally, you can combine prefixes with exact names, like `[ staging*, background ]`, and the behaviour with respect to order will be the same as with only exact names.
118
- - `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 `5`. Only workers have this setting.
120
+ - `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.
119
121
  - `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.
122
+ - `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.
123
+ - `recurring_tasks`: a list of recurring tasks the dispatcher will manage. Read more details about this one in the [Recurring tasks](#recurring-tasks) section.
120
124
 
121
125
 
122
126
  ### Queue order and priorities
@@ -131,7 +135,7 @@ We recommend not mixing queue order with priorities but either choosing one or t
131
135
 
132
136
  ### Threads, processes and signals
133
137
 
134
- Workers in Solid Queue use a thread pool to run work in multiple threads, configurable via the `threads` parameter above. Besides this, parallelism can be achieved via multiple processes, configurable via different workers or the `processes` parameter above.
138
+ Workers in Solid Queue use a thread pool to run work in multiple threads, configurable via the `threads` parameter above. 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.
135
139
 
136
140
  The supervisor is in charge of managing these processes, and it responds to the following signals:
137
141
  - `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.
@@ -263,3 +267,48 @@ Solid Queue has been inspired by [resque](https://github.com/resque/resque) and
263
267
 
264
268
  ## License
265
269
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
270
+
271
+ ## Recurring tasks
272
+ 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:
273
+ ```yml
274
+ dispatchers:
275
+ - polling_interval: 1
276
+ batch_size: 500
277
+ recurring_tasks:
278
+ my_periodic_job:
279
+ class: MyJob
280
+ args: [ 42, { status: "custom_status" } ]
281
+ schedule: every second
282
+ ```
283
+ `recurring_tasks` is a hash/dictionary, and the key will be the task key internally. Each task needs to have a class, which will be the job class to enqueue, and a schedule. The schedule is parsed using [Fugit](https://github.com/floraison/fugit), so it accepts anything [that Fugit accepts as a cron](https://github.com/floraison/fugit?tab=readme-ov-file#fugitcron). You can also provide arguments to be passed to the job, as a single argument, a hash, or an array of arguments that can also include kwargs as the last element in the array.
284
+
285
+ The job in the example configuration above will be enqueued every second as:
286
+ ```ruby
287
+ MyJob.perform_later(42, status: "custom_status")
288
+ ```
289
+
290
+ Tasks are enqueued at their corresponding times by the dispatcher that owns them, 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).
291
+
292
+ It's possible to run multiple dispatchers with the same `recurring_tasks` configuration. To avoid enqueuing duplicate tasks at the same time, an entry in a new `solid_queue_recurring_executions` table is created in the same transaction as the job is enqueued. This table has a unique index on `task_key` and `run_at`, ensuring only one entry per task per time will be created. This only works if you have `preserve_finished_jobs` set to `true` (the default), and the guarantee applies as long as you keep the jobs around.
293
+
294
+ Finally, it's possible to configure jobs that aren't handled by Solid Queue. That's it, you can a have a job like this in your app:
295
+ ```ruby
296
+ class MyResqueJob < ApplicationJob
297
+ self.queue_adapter = :resque
298
+
299
+ def perform(arg)
300
+ # ..
301
+ end
302
+ end
303
+ ```
304
+
305
+ You can still configure this in Solid Queue:
306
+ ```yml
307
+ dispatchers:
308
+ - recurring_tasks:
309
+ my_periodic_resque_job:
310
+ class: MyResqueJob
311
+ args: 22
312
+ schedule: "*/5 * * * *"
313
+ ```
314
+ and the job will be enqueued via `perform_later` so it'll run in Resque. However, in this case we won't track any `solid_queue_recurring_execution` record for it and there won't be any guarantees that the job is enqueued only once each time.
@@ -43,7 +43,7 @@ module SolidQueue
43
43
  promote_to_ready
44
44
  destroy!
45
45
 
46
- SolidQueue.logger.info("[SolidQueue] Unblocked job #{job.id} under #{concurrency_key}")
46
+ SolidQueue.logger.debug("[SolidQueue] Unblocked job #{job.id} under #{concurrency_key}")
47
47
  end
48
48
  end
49
49
  end
@@ -16,7 +16,6 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
16
16
  insert_all!(job_data)
17
17
  where(job_id: job_ids, process_id: process_id).load.tap do |claimed|
18
18
  block.call(claimed)
19
- SolidQueue.logger.info("[SolidQueue] Claimed #{claimed.size} jobs")
20
19
  end
21
20
  end
22
21
 
@@ -6,13 +6,13 @@ module SolidQueue
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- scope :clearable, ->(finished_before: SolidQueue.clear_finished_jobs_after.ago) { where.not(finished_at: nil).where(finished_at: ...finished_before) }
9
+ scope :clearable, ->(finished_before: SolidQueue.clear_finished_jobs_after.ago, class_name: nil) { where.not(finished_at: nil).where(finished_at: ...finished_before).where(class_name.present? ? { class_name: class_name } : {}) }
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)
13
+ def clear_finished_in_batches(batch_size: 500, finished_before: SolidQueue.clear_finished_jobs_after.ago, class_name: nil)
14
14
  loop do
15
- records_deleted = clearable(finished_before: finished_before).limit(batch_size).delete_all
15
+ records_deleted = clearable(finished_before: finished_before, class_name: class_name).limit(batch_size).delete_all
16
16
  break if records_deleted == 0
17
17
  end
18
18
  end
@@ -6,7 +6,7 @@ module SolidQueue
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- include Clearable, ConcurrencyControls, Schedulable
9
+ include ConcurrencyControls, Schedulable
10
10
 
11
11
  has_one :ready_execution
12
12
  has_one :claimed_execution
@@ -78,7 +78,7 @@ module SolidQueue
78
78
  end
79
79
 
80
80
  def finished!
81
- if preserve_finished_jobs?
81
+ if SolidQueue.preserve_finished_jobs?
82
82
  touch(:finished_at)
83
83
  else
84
84
  destroy!
@@ -117,10 +117,6 @@ module SolidQueue
117
117
  def execution
118
118
  %w[ ready claimed failed ].reduce(nil) { |acc, status| acc || public_send("#{status}_execution") }
119
119
  end
120
-
121
- def preserve_finished_jobs?
122
- SolidQueue.preserve_finished_jobs
123
- end
124
120
  end
125
121
  end
126
122
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Job
5
+ module Recurrable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ has_one :recurring_execution, dependent: :destroy
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Job < Record
5
- include Executable
5
+ include Executable, Clearable, Recurrable
6
6
 
7
7
  serialize :arguments, coder: JSON
8
8
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class RecurringExecution < Execution
5
+ scope :clearable, -> { where.missing(:job) }
6
+
7
+ class << self
8
+ def record(task_key, run_at, &block)
9
+ transaction do
10
+ if job_id = block.call
11
+ create!(job_id: job_id, task_key: task_key, run_at: run_at)
12
+ end
13
+ end
14
+ rescue ActiveRecord::RecordNotUnique
15
+ SolidQueue.logger.info("[SolidQueue] Skipped recurring task #{task_key} at #{run_at} — already dispatched")
16
+ end
17
+
18
+ def clear_in_batches(batch_size: 500)
19
+ loop do
20
+ records_deleted = clearable.limit(batch_size).delete_all
21
+ break if records_deleted == 0
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
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
@@ -4,7 +4,7 @@ module SolidQueue
4
4
  class Configuration
5
5
  WORKER_DEFAULTS = {
6
6
  queues: "*",
7
- threads: 5,
7
+ threads: 3,
8
8
  processes: 1,
9
9
  polling_interval: 0.1
10
10
  }
@@ -12,7 +12,9 @@ module SolidQueue
12
12
  DISPATCHER_DEFAULTS = {
13
13
  batch_size: 500,
14
14
  polling_interval: 1,
15
- concurrency_maintenance_interval: 600
15
+ concurrency_maintenance: true,
16
+ concurrency_maintenance_interval: 600,
17
+ recurring_tasks: []
16
18
  }
17
19
 
18
20
  def initialize(mode: :work, load_from: nil)
@@ -33,7 +35,7 @@ module SolidQueue
33
35
  if mode.in? %i[ work all]
34
36
  workers_options.flat_map do |worker_options|
35
37
  processes = worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
36
- processes.times.collect { SolidQueue::Worker.new(**worker_options.with_defaults(WORKER_DEFAULTS)) }
38
+ processes.times.map { Worker.new(**worker_options.with_defaults(WORKER_DEFAULTS)) }
37
39
  end
38
40
  else
39
41
  []
@@ -42,8 +44,10 @@ module SolidQueue
42
44
 
43
45
  def dispatchers
44
46
  if mode.in? %i[ dispatch all]
45
- dispatchers_options.flat_map do |dispatcher_options|
46
- SolidQueue::Dispatcher.new(**dispatcher_options)
47
+ dispatchers_options.map do |dispatcher_options|
48
+ recurring_tasks = parse_recurring_tasks dispatcher_options[:recurring_tasks]
49
+
50
+ Dispatcher.new **dispatcher_options.merge(recurring_tasks: recurring_tasks).with_defaults(DISPATCHER_DEFAULTS)
47
51
  end
48
52
  end
49
53
  end
@@ -73,6 +77,11 @@ module SolidQueue
73
77
  .map { |options| options.dup.symbolize_keys }
74
78
  end
75
79
 
80
+ def parse_recurring_tasks(tasks)
81
+ Array(tasks).map do |id, options|
82
+ Dispatcher::RecurringTask.from_configuration(id, **options)
83
+ end.select(&:valid?)
84
+ end
76
85
 
77
86
  def load_config_from(file_or_hash)
78
87
  case file_or_hash
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Dispatcher::ConcurrencyMaintenance
5
+ include AppExecutor
6
+
7
+ attr_reader :interval, :batch_size
8
+
9
+ def initialize(interval, batch_size)
10
+ @interval = interval
11
+ @batch_size = batch_size
12
+ end
13
+
14
+ def start
15
+ @concurrency_maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: interval) do
16
+ expire_semaphores
17
+ unblock_blocked_executions
18
+ end
19
+
20
+ @concurrency_maintenance_task.add_observer do |_, _, error|
21
+ handle_thread_error(error) if error
22
+ end
23
+
24
+ @concurrency_maintenance_task.execute
25
+ end
26
+
27
+ def stop
28
+ @concurrency_maintenance_task.shutdown
29
+ end
30
+
31
+ private
32
+ def expire_semaphores
33
+ wrap_in_app_executor do
34
+ Semaphore.expired.in_batches(of: batch_size, &:delete_all)
35
+ end
36
+ end
37
+
38
+ def unblock_blocked_executions
39
+ wrap_in_app_executor do
40
+ BlockedExecution.unblock(batch_size)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Dispatcher::RecurringSchedule
5
+ include AppExecutor
6
+
7
+ attr_reader :configured_tasks, :scheduled_tasks
8
+
9
+ def initialize(tasks)
10
+ @configured_tasks = Array(tasks).map { |task| Dispatcher::RecurringTask.wrap(task) }
11
+ @scheduled_tasks = Concurrent::Hash.new
12
+ end
13
+
14
+ def load_tasks
15
+ configured_tasks.each do |task|
16
+ load_task(task)
17
+ end
18
+ end
19
+
20
+ def load_task(task)
21
+ scheduled_tasks[task.key] = schedule(task)
22
+ end
23
+
24
+ def unload_tasks
25
+ scheduled_tasks.values.each(&:cancel)
26
+ scheduled_tasks.clear
27
+ end
28
+
29
+ def tasks
30
+ configured_tasks.each_with_object({}) { |task, hsh| hsh[task.key] = task.to_h }
31
+ end
32
+
33
+ def inspect
34
+ configured_tasks.map(&:to_s).join(" | ")
35
+ end
36
+
37
+ private
38
+ def schedule(task)
39
+ scheduled_task = Concurrent::ScheduledTask.new(task.delay_from_now, args: [ self, task, task.next_time ]) do |thread_schedule, thread_task, thread_task_run_at|
40
+ thread_schedule.load_task(thread_task)
41
+
42
+ wrap_in_app_executor do
43
+ thread_task.enqueue(at: thread_task_run_at)
44
+ end
45
+ end
46
+
47
+ scheduled_task.add_observer do |_, _, error|
48
+ # Don't notify on task cancellation before execution, as this will happen normally
49
+ # as part of unloading tasks
50
+ handle_thread_error(error) if error && !error.is_a?(Concurrent::CancelledOperationError)
51
+ end
52
+
53
+ scheduled_task.tap(&:execute)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,85 @@
1
+ require "fugit"
2
+
3
+ module SolidQueue
4
+ class Dispatcher::RecurringTask
5
+ class << self
6
+ def wrap(args)
7
+ args.is_a?(self) ? args : from_configuration(args.first, **args.second)
8
+ end
9
+
10
+ def from_configuration(key, **options)
11
+ new(key, class_name: options[:class], schedule: options[:schedule], arguments: options[:args])
12
+ end
13
+ end
14
+
15
+ attr_reader :key, :schedule, :class_name, :arguments
16
+
17
+ def initialize(key, class_name:, schedule:, arguments: nil)
18
+ @key = key
19
+ @class_name = class_name
20
+ @schedule = schedule
21
+ @arguments = Array(arguments)
22
+ end
23
+
24
+ def delay_from_now
25
+ [ (next_time - Time.current).to_f, 0 ].max
26
+ end
27
+
28
+ def next_time
29
+ parsed_schedule.next_time.utc
30
+ end
31
+
32
+ def enqueue(at:)
33
+ if using_solid_queue_adapter?
34
+ perform_later_and_record(run_at: at)
35
+ else
36
+ perform_later
37
+ end
38
+ end
39
+
40
+ def valid?
41
+ parsed_schedule.instance_of?(Fugit::Cron)
42
+ end
43
+
44
+ def to_s
45
+ "#{class_name}.perform_later(#{arguments.map(&:inspect).join(",")}) [ #{parsed_schedule.original.to_s} ]"
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ schedule: schedule,
51
+ class_name: class_name,
52
+ arguments: arguments
53
+ }
54
+ end
55
+
56
+ private
57
+ def using_solid_queue_adapter?
58
+ job_class.queue_adapter_name.inquiry.solid_queue?
59
+ end
60
+
61
+ def perform_later_and_record(run_at:)
62
+ RecurringExecution.record(key, run_at) { perform_later.provider_job_id }
63
+ end
64
+
65
+ def perform_later
66
+ job_class.perform_later(*arguments_with_kwargs)
67
+ end
68
+
69
+ def arguments_with_kwargs
70
+ if arguments.last.is_a?(Hash)
71
+ arguments[0...-1] + [ Hash.ruby2_keywords_hash(arguments.last) ]
72
+ else
73
+ arguments
74
+ end
75
+ end
76
+
77
+ def parsed_schedule
78
+ @parsed_schedule ||= Fugit.parse(schedule)
79
+ end
80
+
81
+ def job_class
82
+ @job_class ||= class_name.safe_constantize
83
+ end
84
+ end
85
+ end
@@ -2,72 +2,57 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Dispatcher < Processes::Base
5
- include Processes::Runnable, Processes::Poller
5
+ include Processes::Poller
6
6
 
7
- attr_accessor :batch_size, :concurrency_maintenance_interval
7
+ attr_accessor :batch_size, :concurrency_maintenance, :recurring_schedule
8
8
 
9
- set_callback :boot, :after, :launch_concurrency_maintenance
10
- set_callback :shutdown, :before, :stop_concurrency_maintenance
9
+ after_boot :start_concurrency_maintenance, :load_recurring_schedule
10
+ before_shutdown :stop_concurrency_maintenance, :unload_recurring_schedule
11
11
 
12
12
  def initialize(**options)
13
13
  options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS)
14
14
 
15
15
  @batch_size = options[:batch_size]
16
16
  @polling_interval = options[:polling_interval]
17
- @concurrency_maintenance_interval = options[:concurrency_maintenance_interval]
17
+
18
+ @concurrency_maintenance = ConcurrencyMaintenance.new(options[:concurrency_maintenance_interval], options[:batch_size]) if options[:concurrency_maintenance]
19
+ @recurring_schedule = RecurringSchedule.new(options[:recurring_tasks])
18
20
  end
19
21
 
20
22
  private
21
- def run
23
+ def poll
22
24
  batch = dispatch_next_batch
23
-
24
- unless batch.size > 0
25
- procline "waiting"
26
- interruptible_sleep(polling_interval)
27
- end
25
+ batch.size
28
26
  end
29
27
 
30
28
  def dispatch_next_batch
31
29
  with_polling_volume do
32
- SolidQueue::ScheduledExecution.dispatch_next_batch(batch_size)
30
+ ScheduledExecution.dispatch_next_batch(batch_size)
33
31
  end
34
32
  end
35
33
 
36
- def launch_concurrency_maintenance
37
- @concurrency_maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: concurrency_maintenance_interval) do
38
- expire_semaphores
39
- unblock_blocked_executions
40
- end
41
-
42
- @concurrency_maintenance_task.add_observer do |_, _, error|
43
- handle_thread_error(error) if error
44
- end
45
-
46
- @concurrency_maintenance_task.execute
34
+ def start_concurrency_maintenance
35
+ concurrency_maintenance&.start
47
36
  end
48
37
 
49
- def stop_concurrency_maintenance
50
- @concurrency_maintenance_task.shutdown
38
+ def load_recurring_schedule
39
+ recurring_schedule.load_tasks
51
40
  end
52
41
 
53
- def expire_semaphores
54
- wrap_in_app_executor do
55
- Semaphore.expired.in_batches(of: batch_size, &:delete_all)
56
- end
42
+ def stop_concurrency_maintenance
43
+ concurrency_maintenance&.stop
57
44
  end
58
45
 
59
- def unblock_blocked_executions
60
- wrap_in_app_executor do
61
- BlockedExecution.unblock(batch_size)
62
- end
46
+ def unload_recurring_schedule
47
+ recurring_schedule.unload_tasks
63
48
  end
64
49
 
65
- def initial_jitter
66
- Kernel.rand(0...polling_interval)
50
+ def set_procline
51
+ procline "waiting"
67
52
  end
68
53
 
69
54
  def metadata
70
- super.merge(batch_size: batch_size)
55
+ super.merge(batch_size: batch_size, concurrency_maintenance_interval: concurrency_maintenance&.interval, recurring_schedule: recurring_schedule.tasks.presence )
71
56
  end
72
57
  end
73
58
  end
@@ -3,25 +3,8 @@
3
3
  module SolidQueue
4
4
  module Processes
5
5
  class Base
6
- include ActiveSupport::Callbacks
7
- define_callbacks :boot, :shutdown
8
-
6
+ include Callbacks # Defines callbacks needed by other concerns
9
7
  include AppExecutor, Registrable, Interruptible, Procline
10
-
11
- private
12
- def observe_initial_delay
13
- interruptible_sleep(initial_jitter)
14
- end
15
-
16
- def boot
17
- end
18
-
19
- def shutdown
20
- end
21
-
22
- def initial_jitter
23
- 0
24
- end
25
8
  end
26
9
  end
27
10
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue::Processes
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ extend ActiveModel::Callbacks
9
+ define_model_callbacks :boot, :shutdown
10
+ end
11
+
12
+ private
13
+ def boot
14
+ end
15
+
16
+ def shutdown
17
+ end
18
+ end
19
+ end
@@ -4,11 +4,39 @@ module SolidQueue::Processes
4
4
  module Poller
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ include Runnable
8
+
7
9
  included do
8
10
  attr_accessor :polling_interval
9
11
  end
10
12
 
11
13
  private
14
+ def run
15
+ if mode.async?
16
+ @thread = Thread.new { start_loop }
17
+ else
18
+ start_loop
19
+ end
20
+ end
21
+
22
+ def start_loop
23
+ loop do
24
+ break if shutting_down?
25
+
26
+ wrap_in_app_executor do
27
+ unless poll > 0
28
+ interruptible_sleep(polling_interval)
29
+ end
30
+ end
31
+ end
32
+ ensure
33
+ run_callbacks(:shutdown) { shutdown }
34
+ end
35
+
36
+ def poll
37
+ raise NotImplementedError
38
+ end
39
+
12
40
  def with_polling_volume
13
41
  if SolidQueue.silence_polling?
14
42
  ActiveRecord::Base.logger.silence { yield }
@@ -5,11 +5,10 @@ module SolidQueue::Processes
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- set_callback :boot, :after, :register
9
- set_callback :boot, :after, :launch_heartbeat
8
+ after_boot :register, :launch_heartbeat
10
9
 
11
- set_callback :shutdown, :before, :stop_heartbeat
12
- set_callback :shutdown, :after, :deregister
10
+ before_shutdown :stop_heartbeat
11
+ after_shutdown :deregister
13
12
  end
14
13
 
15
14
  def inspect
@@ -26,7 +25,7 @@ module SolidQueue::Processes
26
25
  pid: process_pid,
27
26
  hostname: hostname,
28
27
  supervisor: try(:supervisor),
29
- metadata: metadata
28
+ metadata: metadata.compact
30
29
  end
31
30
 
32
31
  def deregister
@@ -8,11 +8,9 @@ module SolidQueue::Processes
8
8
 
9
9
  def start
10
10
  @stopping = false
11
-
12
- observe_initial_delay
13
11
  run_callbacks(:boot) { boot }
14
12
 
15
- start_loop
13
+ run
16
14
  end
17
15
 
18
16
  def stop
@@ -20,60 +18,47 @@ module SolidQueue::Processes
20
18
  @thread&.join
21
19
  end
22
20
 
23
- private
24
- DEFAULT_MODE = :async
21
+ private
22
+ DEFAULT_MODE = :async
25
23
 
26
- def mode
27
- (@mode || DEFAULT_MODE).to_s.inquiry
28
- end
24
+ def mode
25
+ (@mode || DEFAULT_MODE).to_s.inquiry
26
+ end
29
27
 
30
- def boot
31
- register_signal_handlers if supervised?
32
- SolidQueue.logger.info("[SolidQueue] Starting #{self}")
33
- end
28
+ def boot
29
+ if supervised?
30
+ register_signal_handlers
31
+ set_procline
32
+ end
34
33
 
35
- def start_loop
36
- if mode.async?
37
- @thread = Thread.new { do_start_loop }
38
- else
39
- do_start_loop
34
+ SolidQueue.logger.info("[SolidQueue] Starting #{self}")
40
35
  end
41
- end
42
36
 
43
- def do_start_loop
44
- loop do
45
- break if shutting_down?
46
-
47
- wrap_in_app_executor do
48
- run
49
- end
37
+ def shutting_down?
38
+ stopping? || supervisor_went_away? || finished?
50
39
  end
51
- ensure
52
- run_callbacks(:shutdown) { shutdown }
53
- end
54
40
 
55
- def shutting_down?
56
- stopping? || supervisor_went_away? || finished?
57
- end
41
+ def run
42
+ raise NotImplementedError
43
+ end
58
44
 
59
- def run
60
- raise NotImplementedError
61
- end
45
+ def stopping?
46
+ @stopping
47
+ end
62
48
 
63
- def stopping?
64
- @stopping
65
- end
49
+ def finished?
50
+ running_inline? && all_work_completed?
51
+ end
66
52
 
67
- def finished?
68
- running_inline? && all_work_completed?
69
- end
53
+ def all_work_completed?
54
+ false
55
+ end
70
56
 
71
- def all_work_completed?
72
- false
73
- end
57
+ def set_procline
58
+ end
74
59
 
75
- def running_inline?
76
- mode.inline?
77
- end
60
+ def running_inline?
61
+ mode.inline?
62
+ end
78
63
  end
79
64
  end
@@ -14,6 +14,10 @@ module SolidQueue::Processes
14
14
  end
15
15
 
16
16
  private
17
+ def set_procline
18
+ procline "waiting"
19
+ end
20
+
17
21
  def supervisor_went_away?
18
22
  supervised? && supervisor&.pid != ::Process.ppid
19
23
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module RecurringTasks
5
+ class Manager < Processes::Base
6
+ include Processes::Runnable
7
+
8
+ attr_accessor :schedule
9
+
10
+ after_boot :load_schedule
11
+ before_shutdown :unload_schedule
12
+
13
+ def initialize(tasks)
14
+ @schedule = Schedule.new(tasks)
15
+ end
16
+
17
+ private
18
+ def load_schedule
19
+ schedule.load_tasks
20
+ end
21
+
22
+ def unload_schedule
23
+ schedule.unload_tasks
24
+ end
25
+
26
+ def metadata
27
+ super.merge(schedule: schedule.tasks)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module RecurringTasks
5
+ class Schedule
6
+ include AppExecutor
7
+
8
+ attr_reader :configured_tasks, :scheduled_tasks
9
+
10
+ def initialize(tasks)
11
+ @configured_tasks = Array(tasks).map { |task| Task.wrap(task) }
12
+ @scheduled_tasks = Concurrent::Hash.new
13
+ end
14
+
15
+ def load_tasks
16
+ configured_tasks.each do |task|
17
+ load_task(task)
18
+ end
19
+ end
20
+
21
+ def load_task(task)
22
+ scheduled_tasks[task.key] = schedule(task)
23
+ end
24
+
25
+ def unload_tasks
26
+ scheduled_tasks.values.each(&:cancel)
27
+ scheduled_tasks.clear
28
+ end
29
+
30
+ def tasks
31
+ configured_tasks.each_with_object({}) { |task, hsh| hsh[task.key] = task.to_h }
32
+ end
33
+
34
+ def inspect
35
+ configured_tasks.map(&:to_s).join(" | ")
36
+ end
37
+
38
+ private
39
+ def schedule(task)
40
+ scheduled_task = Concurrent::ScheduledTask.new(task.delay_from_now, args: [ self, task, task.next_time ]) do |thread_schedule, thread_task, thread_task_run_at|
41
+ thread_schedule.load_task(thread_task)
42
+
43
+ wrap_in_app_executor do
44
+ thread_task.enqueue(at: thread_task_run_at)
45
+ end
46
+ end
47
+
48
+ scheduled_task.add_observer do |_, _, error|
49
+ # Don't notify on task cancellation before execution, as this will happen normally
50
+ # as part of unloading tasks
51
+ handle_thread_error(error) if error && !error.is_a?(Concurrent::CancelledOperationError)
52
+ end
53
+
54
+ scheduled_task.tap(&:execute)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,87 @@
1
+ require "fugit"
2
+
3
+ module SolidQueue
4
+ module RecurringTasks
5
+ class Task
6
+ class << self
7
+ def wrap(args)
8
+ args.is_a?(self) ? args : from_configuration(args.first, **args.second)
9
+ end
10
+
11
+ def from_configuration(key, **options)
12
+ new(key, class_name: options[:class], schedule: options[:schedule], arguments: options[:args])
13
+ end
14
+ end
15
+
16
+ attr_reader :key, :schedule, :class_name, :arguments
17
+
18
+ def initialize(key, class_name:, schedule:, arguments: nil)
19
+ @key = key
20
+ @class_name = class_name
21
+ @schedule = schedule
22
+ @arguments = Array(arguments)
23
+ end
24
+
25
+ def delay_from_now
26
+ [ (next_time - Time.current).to_f, 0 ].max
27
+ end
28
+
29
+ def next_time
30
+ parsed_schedule.next_time.utc
31
+ end
32
+
33
+ def enqueue(at:)
34
+ if using_solid_queue_adapter?
35
+ perform_later_and_record(run_at: at)
36
+ else
37
+ perform_later
38
+ end
39
+ end
40
+
41
+ def valid?
42
+ parsed_schedule.instance_of?(Fugit::Cron)
43
+ end
44
+
45
+ def to_s
46
+ "#{class_name}.perform_later(#{arguments.map(&:inspect).join(",")}) [ #{parsed_schedule.original.to_s} ]"
47
+ end
48
+
49
+ def to_h
50
+ {
51
+ schedule: schedule,
52
+ class_name: class_name,
53
+ arguments: arguments
54
+ }
55
+ end
56
+
57
+ private
58
+ def using_solid_queue_adapter?
59
+ job_class.queue_adapter_name.inquiry.solid_queue?
60
+ end
61
+
62
+ def perform_later_and_record(run_at:)
63
+ RecurringExecution.record(key, run_at) { perform_later.provider_job_id }
64
+ end
65
+
66
+ def perform_later
67
+ job_class.perform_later(*arguments_with_kwargs)
68
+ end
69
+
70
+ def arguments_with_kwargs
71
+ if arguments.last.is_a?(Hash)
72
+ arguments[0...-1] + [ Hash.ruby2_keywords_hash(arguments.last) ]
73
+ else
74
+ arguments
75
+ end
76
+ end
77
+
78
+ def parsed_schedule
79
+ @parsed_schedule ||= Fugit.parse(schedule)
80
+ end
81
+
82
+ def job_class
83
+ @job_class ||= class_name.safe_constantize
84
+ end
85
+ end
86
+ end
87
+ end
@@ -4,7 +4,7 @@ module SolidQueue
4
4
  class Supervisor < Processes::Base
5
5
  include Processes::Signals
6
6
 
7
- set_callback :boot, :after, :launch_process_prune
7
+ after_boot :launch_process_prune
8
8
 
9
9
  class << self
10
10
  def start(mode: :work, load_configuration_from: nil)
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Worker < Processes::Base
5
- include Processes::Runnable, Processes::Poller
5
+ include Processes::Poller
6
6
 
7
7
  attr_accessor :queues, :pool
8
8
 
@@ -15,22 +15,17 @@ module SolidQueue
15
15
  end
16
16
 
17
17
  private
18
- def run
19
- polled_executions = poll
20
-
21
- if polled_executions.size > 0
22
- procline "performing #{polled_executions.count} jobs"
23
-
24
- polled_executions.each do |execution|
18
+ def poll
19
+ claim_executions.then do |executions|
20
+ executions.each do |execution|
25
21
  pool.post(execution)
26
22
  end
27
- else
28
- procline "waiting for jobs in #{queues.join(",")}"
29
- interruptible_sleep(polling_interval)
23
+
24
+ executions.size
30
25
  end
31
26
  end
32
27
 
33
- def poll
28
+ def claim_executions
34
29
  with_polling_volume do
35
30
  SolidQueue::ReadyExecution.claim(queues, pool.idle_threads, process.id)
36
31
  end
@@ -47,6 +42,10 @@ module SolidQueue
47
42
  SolidQueue::ReadyExecution.aggregated_count_across(queues).zero?
48
43
  end
49
44
 
45
+ def set_procline
46
+ procline "waiting for jobs in #{queues.join(",")}"
47
+ end
48
+
50
49
  def metadata
51
50
  super.merge(queues: queues.join(","), thread_pool_size: pool.size)
52
51
  end
data/lib/solid_queue.rb CHANGED
@@ -3,24 +3,16 @@
3
3
  require "solid_queue/version"
4
4
  require "solid_queue/engine"
5
5
 
6
- require "active_job/queue_adapters/solid_queue_adapter"
7
- require "active_job/concurrency_controls"
8
-
9
- require "solid_queue/app_executor"
10
- require "solid_queue/processes/supervised"
11
- require "solid_queue/processes/registrable"
12
- require "solid_queue/processes/interruptible"
13
- require "solid_queue/processes/pidfile"
14
- require "solid_queue/processes/procline"
15
- require "solid_queue/processes/poller"
16
- require "solid_queue/processes/base"
17
- require "solid_queue/processes/runnable"
18
- require "solid_queue/processes/signals"
19
- require "solid_queue/configuration"
20
- require "solid_queue/pool"
21
- require "solid_queue/worker"
22
- require "solid_queue/dispatcher"
23
- require "solid_queue/supervisor"
6
+ require "active_job"
7
+ require "active_job/queue_adapters"
8
+
9
+ require "zeitwerk"
10
+
11
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
12
+ loader.ignore("#{__dir__}/solid_queue/tasks.rb")
13
+ loader.ignore("#{__dir__}/generators")
14
+ loader.ignore("#{__dir__}/puma")
15
+ loader.setup
24
16
 
25
17
  module SolidQueue
26
18
  mattr_accessor :logger, default: ActiveSupport::Logger.new($stdout)
@@ -42,11 +34,17 @@ module SolidQueue
42
34
  mattr_accessor :clear_finished_jobs_after, default: 1.day
43
35
  mattr_accessor :default_concurrency_control_period, default: 3.minutes
44
36
 
45
- def self.supervisor?
46
- supervisor
47
- end
37
+ class << self
38
+ def supervisor?
39
+ supervisor
40
+ end
41
+
42
+ def silence_polling?
43
+ silence_polling
44
+ end
48
45
 
49
- def self.silence_polling?
50
- silence_polling
46
+ def preserve_finished_jobs?
47
+ preserve_finished_jobs
48
+ end
51
49
  end
52
50
  end
metadata CHANGED
@@ -1,29 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.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: 2024-03-05 00:00:00.000000000 Z
11
+ date: 2024-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '7.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activejob
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: railties
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
25
53
  - !ruby/object:Gem::Version
26
54
  version: '7.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: fugit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.9.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.9.0
27
83
  - !ruby/object:Gem::Dependency
28
84
  name: debug
29
85
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +122,48 @@ dependencies:
66
122
  - - ">="
67
123
  - !ruby/object:Gem::Version
68
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: mysql2
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pg
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: sqlite3
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
69
167
  description: Database-backed Active Job backend.
70
168
  email:
71
169
  - rosa@37signals.com
@@ -86,6 +184,7 @@ files:
86
184
  - app/models/solid_queue/job/clearable.rb
87
185
  - app/models/solid_queue/job/concurrency_controls.rb
88
186
  - app/models/solid_queue/job/executable.rb
187
+ - app/models/solid_queue/job/recurrable.rb
89
188
  - app/models/solid_queue/job/schedulable.rb
90
189
  - app/models/solid_queue/pause.rb
91
190
  - app/models/solid_queue/process.rb
@@ -94,11 +193,13 @@ files:
94
193
  - app/models/solid_queue/queue_selector.rb
95
194
  - app/models/solid_queue/ready_execution.rb
96
195
  - app/models/solid_queue/record.rb
196
+ - app/models/solid_queue/recurring_execution.rb
97
197
  - app/models/solid_queue/scheduled_execution.rb
98
198
  - app/models/solid_queue/semaphore.rb
99
199
  - config/routes.rb
100
200
  - db/migrate/20231211200639_create_solid_queue_tables.rb
101
201
  - db/migrate/20240110143450_add_missing_index_to_blocked_executions.rb
202
+ - db/migrate/20240218110712_create_recurring_executions.rb
102
203
  - lib/active_job/concurrency_controls.rb
103
204
  - lib/active_job/queue_adapters/solid_queue_adapter.rb
104
205
  - lib/generators/solid_queue/install/USAGE
@@ -109,9 +210,13 @@ files:
109
210
  - lib/solid_queue/app_executor.rb
110
211
  - lib/solid_queue/configuration.rb
111
212
  - lib/solid_queue/dispatcher.rb
213
+ - lib/solid_queue/dispatcher/concurrency_maintenance.rb
214
+ - lib/solid_queue/dispatcher/recurring_schedule.rb
215
+ - lib/solid_queue/dispatcher/recurring_task.rb
112
216
  - lib/solid_queue/engine.rb
113
217
  - lib/solid_queue/pool.rb
114
218
  - lib/solid_queue/processes/base.rb
219
+ - lib/solid_queue/processes/callbacks.rb
115
220
  - lib/solid_queue/processes/interruptible.rb
116
221
  - lib/solid_queue/processes/pidfile.rb
117
222
  - lib/solid_queue/processes/poller.rb
@@ -120,6 +225,9 @@ files:
120
225
  - lib/solid_queue/processes/runnable.rb
121
226
  - lib/solid_queue/processes/signals.rb
122
227
  - lib/solid_queue/processes/supervised.rb
228
+ - lib/solid_queue/recurring_tasks/manager.rb
229
+ - lib/solid_queue/recurring_tasks/schedule.rb
230
+ - lib/solid_queue/recurring_tasks/task.rb
123
231
  - lib/solid_queue/supervisor.rb
124
232
  - lib/solid_queue/tasks.rb
125
233
  - lib/solid_queue/version.rb