solid_queue 0.2.1 → 0.3.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -7
  3. data/app/models/solid_queue/blocked_execution.rb +3 -3
  4. data/app/models/solid_queue/claimed_execution.rb +0 -1
  5. data/app/models/solid_queue/execution/dispatching.rb +1 -1
  6. data/app/models/solid_queue/job/clearable.rb +3 -3
  7. data/app/models/solid_queue/job/executable.rb +2 -6
  8. data/app/models/solid_queue/job/recurrable.rb +13 -0
  9. data/app/models/solid_queue/job.rb +1 -1
  10. data/app/models/solid_queue/recurring_execution.rb +26 -0
  11. data/app/models/solid_queue/scheduled_execution.rb +1 -1
  12. data/app/models/solid_queue/semaphore.rb +5 -22
  13. data/db/migrate/20240218110712_create_recurring_executions.rb +14 -0
  14. data/lib/solid_queue/configuration.rb +14 -5
  15. data/lib/solid_queue/dispatcher/concurrency_maintenance.rb +44 -0
  16. data/lib/solid_queue/dispatcher/recurring_schedule.rb +56 -0
  17. data/lib/solid_queue/dispatcher/recurring_task.rb +85 -0
  18. data/lib/solid_queue/dispatcher.rb +21 -36
  19. data/lib/solid_queue/processes/base.rb +1 -18
  20. data/lib/solid_queue/processes/callbacks.rb +19 -0
  21. data/lib/solid_queue/processes/poller.rb +28 -0
  22. data/lib/solid_queue/processes/registrable.rb +5 -6
  23. data/lib/solid_queue/processes/runnable.rb +31 -46
  24. data/lib/solid_queue/processes/supervised.rb +4 -0
  25. data/lib/solid_queue/recurring_tasks/manager.rb +31 -0
  26. data/lib/solid_queue/recurring_tasks/schedule.rb +58 -0
  27. data/lib/solid_queue/recurring_tasks/task.rb +87 -0
  28. data/lib/solid_queue/supervisor.rb +1 -1
  29. data/lib/solid_queue/version.rb +1 -1
  30. data/lib/solid_queue/worker.rb +11 -12
  31. data/lib/solid_queue.rb +22 -24
  32. metadata +116 -10
  33. data/lib/active_job/uniqueness.rb +0 -41
  34. data/lib/solid_queue/dispatcher/scheduled_executions_dispatcher.rb +0 -6
@@ -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
@@ -55,7 +54,7 @@ module SolidQueue::Processes
55
54
  end
56
55
 
57
56
  def hostname
58
- @hostname ||= Socket.gethostname
57
+ @hostname ||= Socket.gethostname.force_encoding(Encoding::UTF_8)
59
58
  end
60
59
 
61
60
  def process_pid
@@ -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.1"
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)
@@ -33,7 +25,7 @@ module SolidQueue
33
25
 
34
26
  mattr_accessor :shutdown_timeout, default: 5.seconds
35
27
 
36
- mattr_accessor :silence_polling, default: false
28
+ mattr_accessor :silence_polling, default: true
37
29
 
38
30
  mattr_accessor :supervisor_pidfile
39
31
  mattr_accessor :supervisor, default: false
@@ -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