solid_queue 0.4.1 → 0.5.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: cb47bf3ee9dffa1300c093abb260394d088f100b3c16a026d755f706d9f7a852
4
- data.tar.gz: 10cc1b6f866d148d0c1cefce7587614aaca3e938f3f04370c15318e5b8d3509f
3
+ metadata.gz: 952d693063dae16e88a88acb2f8d77f396472cb29811e7ab2c47c06fc400cf10
4
+ data.tar.gz: 338ad6a0057939a54997cdef138fa5e62e974112625c43fd65240354a0ba760a
5
5
  SHA512:
6
- metadata.gz: 71054017fcd26421f25140db9c929518ee011210fd6fea51e77352abf2a128289e965843dc1cb1fe97b62fa003d571e1de3631fcab329914b069ef9e0621806b
7
- data.tar.gz: d08497fc98f9498aeaa3449182fc13cb90f71c859c67d55e0e5c14803bb1d5445e7717e1e3aca578fcb22a09c47126d465306e0220f54d6af4a8f8027fef0072
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.1"
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.1
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