solid_queue 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e12cccf2a1485f92c30925675e6a91bb36b063057390e57250a582440a046c8a
4
- data.tar.gz: a1c857b509ff15124eed754cb8ed8613d95764f97e2f06758baf4031ea10d387
3
+ metadata.gz: 952d693063dae16e88a88acb2f8d77f396472cb29811e7ab2c47c06fc400cf10
4
+ data.tar.gz: 338ad6a0057939a54997cdef138fa5e62e974112625c43fd65240354a0ba760a
5
5
  SHA512:
6
- metadata.gz: bf861243ec274d583b29222275303a3cb8383803291e911f8d2872ba110d92705978d31ceada27e246368410668fc7f2ba8a9bb368c19cdfff666ed74636ea3c
7
- data.tar.gz: a6ccfc868515c105f67bbcbdcb9cda4b4ea790e77a61d16e8dd310e6e68244791f1fb2a0e693cec5c2af0c2ba93056b078269d97673884c4e80ade876d48fcc5
6
+ metadata.gz: b0bddd34b216770c9e0658bb620ae8801ab500074692417aeb0907c1f22f4f9e40a6233266aa69c1aa2b015dc294864d529659f0ee4adbdfdbef647d03fb4d35
7
+ data.tar.gz: f2323054f8fdc5ee686f738d2a8359b23fe88f00b1b376cc18e99588bacc9df3be2e3a8bb660ce31db854f0fcca91560ad4d703a737b1d05c3a1dea7817c10a2
@@ -0,0 +1,9 @@
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
@@ -7,16 +7,29 @@ module SolidQueue
7
7
  scope :clearable, -> { where.missing(:job) }
8
8
 
9
9
  class << self
10
+ def create_or_insert!(**attributes)
11
+ if connection.supports_insert_conflict_target?
12
+ # PostgreSQL fails and aborts the current transaction when it hits a duplicate key conflict
13
+ # during two concurrent INSERTs for the same value of an unique index. We need to explicitly
14
+ # indicate unique_by to ignore duplicate rows by this value when inserting
15
+ unless insert(attributes, unique_by: [ :task_key, :run_at ]).any?
16
+ raise AlreadyRecorded
17
+ end
18
+ else
19
+ create!(**attributes)
20
+ end
21
+ rescue ActiveRecord::RecordNotUnique
22
+ raise AlreadyRecorded
23
+ end
24
+
10
25
  def record(task_key, run_at, &block)
11
26
  transaction do
12
27
  block.call.tap do |active_job|
13
28
  if active_job
14
- create!(job_id: active_job.provider_job_id, task_key: task_key, run_at: run_at)
29
+ create_or_insert!(job_id: active_job.provider_job_id, task_key: task_key, run_at: run_at)
15
30
  end
16
31
  end
17
32
  end
18
- rescue ActiveRecord::RecordNotUnique => e
19
- raise AlreadyRecorded
20
33
  end
21
34
 
22
35
  def clear_in_batches(batch_size: 500)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/arguments"
4
+
5
+ module SolidQueue
6
+ class RecurringTask::Arguments
7
+ class << self
8
+ def load(data)
9
+ data.nil? ? [] : ActiveJob::Arguments.deserialize(ActiveSupport::JSON.load(data))
10
+ end
11
+
12
+ def dump(data)
13
+ ActiveSupport::JSON.dump(ActiveJob::Arguments.serialize(Array(data)))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,24 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "fugit"
2
4
 
3
5
  module SolidQueue
4
- class Dispatcher::RecurringTask
6
+ class RecurringTask < Record
7
+ serialize :arguments, coder: Arguments, default: []
8
+
9
+ validate :supported_schedule
10
+ validate :existing_job_class
11
+
12
+ scope :static, -> { where(static: true) }
13
+
5
14
  class << self
6
15
  def wrap(args)
7
16
  args.is_a?(self) ? args : from_configuration(args.first, **args.second)
8
17
  end
9
18
 
10
19
  def from_configuration(key, **options)
11
- new(key, class_name: options[:class], schedule: options[:schedule], arguments: options[:args])
20
+ new(key: key, class_name: options[:class], schedule: options[:schedule], arguments: options[:args])
12
21
  end
13
- end
14
22
 
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)
23
+ def create_or_update_all(tasks)
24
+ if connection.supports_insert_conflict_target?
25
+ # PostgreSQL fails and aborts the current transaction when it hits a duplicate key conflict
26
+ # during two concurrent INSERTs for the same value of an unique index. We need to explicitly
27
+ # indicate unique_by to ignore duplicate rows by this value when inserting
28
+ upsert_all tasks.map(&:attributes_for_upsert), unique_by: :key
29
+ else
30
+ upsert_all tasks.map(&:attributes_for_upsert)
31
+ end
32
+ end
22
33
  end
23
34
 
24
35
  def delay_from_now
@@ -51,23 +62,27 @@ module SolidQueue
51
62
  end
52
63
  end
53
64
 
54
- def valid?
55
- parsed_schedule.instance_of?(Fugit::Cron)
56
- end
57
-
58
65
  def to_s
59
66
  "#{class_name}.perform_later(#{arguments.map(&:inspect).join(",")}) [ #{parsed_schedule.original} ]"
60
67
  end
61
68
 
62
- def to_h
63
- {
64
- schedule: schedule,
65
- class_name: class_name,
66
- arguments: arguments
67
- }
69
+ def attributes_for_upsert
70
+ attributes.without("id", "created_at", "updated_at")
68
71
  end
69
72
 
70
73
  private
74
+ def supported_schedule
75
+ unless parsed_schedule.instance_of?(Fugit::Cron)
76
+ errors.add :schedule, :unsupported, message: "is not a supported recurring schedule"
77
+ end
78
+ end
79
+
80
+ def existing_job_class
81
+ unless job_class.present?
82
+ errors.add :class_name, :undefined, message: "doesn't correspond to an existing class"
83
+ end
84
+ end
85
+
71
86
  def using_solid_queue_adapter?
72
87
  job_class.queue_adapter_name.inquiry.solid_queue?
73
88
  end
@@ -88,12 +103,13 @@ module SolidQueue
88
103
  end
89
104
  end
90
105
 
106
+
91
107
  def parsed_schedule
92
108
  @parsed_schedule ||= Fugit.parse(schedule)
93
109
  end
94
110
 
95
111
  def job_class
96
- @job_class ||= class_name.safe_constantize
112
+ @job_class ||= class_name&.safe_constantize
97
113
  end
98
114
  end
99
115
  end
@@ -17,6 +17,17 @@ module SolidQueue
17
17
  def signal_all(jobs)
18
18
  Proxy.signal_all(jobs)
19
19
  end
20
+
21
+ # Requires a unique index on key
22
+ def create_unique_by(attributes)
23
+ if connection.supports_insert_conflict_target?
24
+ insert({ **attributes }, unique_by: :key).any?
25
+ else
26
+ create!(**attributes)
27
+ end
28
+ rescue ActiveRecord::RecordNotUnique
29
+ false
30
+ end
20
31
  end
21
32
 
22
33
  class Proxy
@@ -44,15 +55,17 @@ module SolidQueue
44
55
  attr_accessor :job
45
56
 
46
57
  def attempt_creation
47
- Semaphore.create!(key: key, value: limit - 1, expires_at: expires_at)
48
- true
49
- rescue ActiveRecord::RecordNotUnique
50
- if limit == 1 then false
58
+ if Semaphore.create_unique_by(key: key, value: limit - 1, expires_at: expires_at)
59
+ true
51
60
  else
52
- attempt_decrement
61
+ check_limit_or_decrement
53
62
  end
54
63
  end
55
64
 
65
+ def check_limit_or_decrement
66
+ limit == 1 ? false : attempt_decrement
67
+ end
68
+
56
69
  def attempt_decrement
57
70
  Semaphore.available.where(key: key).update_all([ "value = value - 1, expires_at = ?", expires_at ]) > 0
58
71
  end
@@ -0,0 +1,20 @@
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
@@ -75,7 +75,7 @@ module SolidQueue
75
75
 
76
76
  def parse_recurring_tasks(tasks)
77
77
  Array(tasks).map do |id, options|
78
- Dispatcher::RecurringTask.from_configuration(id, **options)
78
+ RecurringTask.from_configuration(id, **options)
79
79
  end.select(&:valid?)
80
80
  end
81
81
 
@@ -7,7 +7,7 @@ module SolidQueue
7
7
  attr_reader :configured_tasks, :scheduled_tasks
8
8
 
9
9
  def initialize(tasks)
10
- @configured_tasks = Array(tasks).map { |task| Dispatcher::RecurringTask.wrap(task) }
10
+ @configured_tasks = Array(tasks).map { |task| SolidQueue::RecurringTask.wrap(task) }.select(&:valid?)
11
11
  @scheduled_tasks = Concurrent::Hash.new
12
12
  end
13
13
 
@@ -15,33 +15,42 @@ module SolidQueue
15
15
  configured_tasks.empty?
16
16
  end
17
17
 
18
- def load_tasks
18
+ def schedule_tasks
19
+ wrap_in_app_executor do
20
+ persist_tasks
21
+ reload_tasks
22
+ end
23
+
19
24
  configured_tasks.each do |task|
20
- load_task(task)
25
+ schedule_task(task)
21
26
  end
22
27
  end
23
28
 
24
- def load_task(task)
29
+ def schedule_task(task)
25
30
  scheduled_tasks[task.key] = schedule(task)
26
31
  end
27
32
 
28
- def unload_tasks
33
+ def unschedule_tasks
29
34
  scheduled_tasks.values.each(&:cancel)
30
35
  scheduled_tasks.clear
31
36
  end
32
37
 
33
- def tasks
34
- configured_tasks.each_with_object({}) { |task, hsh| hsh[task.key] = task.to_h }
35
- end
36
-
37
- def inspect
38
- configured_tasks.map(&:to_s).join(" | ")
38
+ def task_keys
39
+ configured_tasks.map(&:key)
39
40
  end
40
41
 
41
42
  private
43
+ def persist_tasks
44
+ SolidQueue::RecurringTask.create_or_update_all configured_tasks
45
+ end
46
+
47
+ def reload_tasks
48
+ @configured_tasks = SolidQueue::RecurringTask.where(key: task_keys)
49
+ end
50
+
42
51
  def schedule(task)
43
52
  scheduled_task = Concurrent::ScheduledTask.new(task.delay_from_now, args: [ self, task, task.next_time ]) do |thread_schedule, thread_task, thread_task_run_at|
44
- thread_schedule.load_task(thread_task)
53
+ thread_schedule.schedule_task(thread_task)
45
54
 
46
55
  wrap_in_app_executor do
47
56
  thread_task.enqueue(at: thread_task_run_at)
@@ -4,8 +4,8 @@ module SolidQueue
4
4
  class Dispatcher < Processes::Poller
5
5
  attr_accessor :batch_size, :concurrency_maintenance, :recurring_schedule
6
6
 
7
- after_boot :start_concurrency_maintenance, :load_recurring_schedule
8
- before_shutdown :stop_concurrency_maintenance, :unload_recurring_schedule
7
+ after_boot :start_concurrency_maintenance, :schedule_recurring_tasks
8
+ before_shutdown :stop_concurrency_maintenance, :unschedule_recurring_tasks
9
9
 
10
10
  def initialize(**options)
11
11
  options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS)
@@ -19,7 +19,7 @@ module SolidQueue
19
19
  end
20
20
 
21
21
  def metadata
22
- super.merge(batch_size: batch_size, concurrency_maintenance_interval: concurrency_maintenance&.interval, recurring_schedule: recurring_schedule.tasks.presence)
22
+ super.merge(batch_size: batch_size, concurrency_maintenance_interval: concurrency_maintenance&.interval, recurring_schedule: recurring_schedule.task_keys.presence)
23
23
  end
24
24
 
25
25
  private
@@ -38,16 +38,16 @@ module SolidQueue
38
38
  concurrency_maintenance&.start
39
39
  end
40
40
 
41
- def load_recurring_schedule
42
- recurring_schedule.load_tasks
41
+ def schedule_recurring_tasks
42
+ recurring_schedule.schedule_tasks
43
43
  end
44
44
 
45
45
  def stop_concurrency_maintenance
46
46
  concurrency_maintenance&.stop
47
47
  end
48
48
 
49
- def unload_recurring_schedule
50
- recurring_schedule.unload_tasks
49
+ def unschedule_recurring_tasks
50
+ recurring_schedule.unschedule_tasks
51
51
  end
52
52
 
53
53
  def all_work_completed?
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  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.4.0
4
+ version: 0.5.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-08-05 00:00:00.000000000 Z
11
+ date: 2024-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -188,6 +188,7 @@ files:
188
188
  - MIT-LICENSE
189
189
  - README.md
190
190
  - Rakefile
191
+ - app/jobs/solid_queue/recurring_job.rb
191
192
  - app/models/solid_queue/blocked_execution.rb
192
193
  - app/models/solid_queue/claimed_execution.rb
193
194
  - app/models/solid_queue/execution.rb
@@ -210,12 +211,15 @@ files:
210
211
  - app/models/solid_queue/ready_execution.rb
211
212
  - app/models/solid_queue/record.rb
212
213
  - app/models/solid_queue/recurring_execution.rb
214
+ - app/models/solid_queue/recurring_task.rb
215
+ - app/models/solid_queue/recurring_task/arguments.rb
213
216
  - app/models/solid_queue/scheduled_execution.rb
214
217
  - app/models/solid_queue/semaphore.rb
215
218
  - config/routes.rb
216
219
  - db/migrate/20231211200639_create_solid_queue_tables.rb
217
220
  - db/migrate/20240110143450_add_missing_index_to_blocked_executions.rb
218
221
  - db/migrate/20240218110712_create_recurring_executions.rb
222
+ - db/migrate/20240719134516_create_recurring_tasks.rb
219
223
  - lib/active_job/concurrency_controls.rb
220
224
  - lib/active_job/queue_adapters/solid_queue_adapter.rb
221
225
  - lib/generators/solid_queue/install/USAGE
@@ -228,7 +232,6 @@ files:
228
232
  - lib/solid_queue/dispatcher.rb
229
233
  - lib/solid_queue/dispatcher/concurrency_maintenance.rb
230
234
  - lib/solid_queue/dispatcher/recurring_schedule.rb
231
- - lib/solid_queue/dispatcher/recurring_task.rb
232
235
  - lib/solid_queue/engine.rb
233
236
  - lib/solid_queue/log_subscriber.rb
234
237
  - lib/solid_queue/pool.rb
@@ -259,7 +262,7 @@ metadata:
259
262
  source_code_uri: https://github.com/rails/solid_queue
260
263
  post_install_message: |
261
264
  Upgrading to Solid Queue 0.4.x? There are some breaking changes about how Solid Queue is started. Check
262
- https://github.com/rails/solid_cache/blob/main/UPGRADING.md for upgrade instructions.
265
+ https://github.com/rails/solid_queue/blob/main/UPGRADING.md for upgrade instructions.
263
266
  rdoc_options: []
264
267
  require_paths:
265
268
  - lib