solid_queue 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/jobs/solid_queue/recurring_job.rb +9 -0
- data/app/models/solid_queue/recurring_execution.rb +16 -3
- data/app/models/solid_queue/recurring_task/arguments.rb +17 -0
- data/{lib/solid_queue/dispatcher → app/models/solid_queue}/recurring_task.rb +37 -21
- data/app/models/solid_queue/semaphore.rb +18 -5
- data/db/migrate/20240719134516_create_recurring_tasks.rb +20 -0
- data/lib/solid_queue/configuration.rb +1 -1
- data/lib/solid_queue/dispatcher/recurring_schedule.rb +21 -12
- data/lib/solid_queue/dispatcher.rb +7 -7
- data/lib/solid_queue/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 952d693063dae16e88a88acb2f8d77f396472cb29811e7ab2c47c06fc400cf10
|
4
|
+
data.tar.gz: 338ad6a0057939a54997cdef138fa5e62e974112625c43fd65240354a0ba760a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0bddd34b216770c9e0658bb620ae8801ab500074692417aeb0907c1f22f4f9e40a6233266aa69c1aa2b015dc294864d529659f0ee4adbdfdbef647d03fb4d35
|
7
|
+
data.tar.gz: f2323054f8fdc5ee686f738d2a8359b23fe88f00b1b376cc18e99588bacc9df3be2e3a8bb660ce31db854f0fcca91560ad4d703a737b1d05c3a1dea7817c10a2
|
@@ -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
|
-
|
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
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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
|
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.
|
48
|
-
|
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
|
-
|
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
|
@@ -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|
|
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
|
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
|
-
|
25
|
+
schedule_task(task)
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
24
|
-
def
|
29
|
+
def schedule_task(task)
|
25
30
|
scheduled_tasks[task.key] = schedule(task)
|
26
31
|
end
|
27
32
|
|
28
|
-
def
|
33
|
+
def unschedule_tasks
|
29
34
|
scheduled_tasks.values.each(&:cancel)
|
30
35
|
scheduled_tasks.clear
|
31
36
|
end
|
32
37
|
|
33
|
-
def
|
34
|
-
configured_tasks.
|
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.
|
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, :
|
8
|
-
before_shutdown :stop_concurrency_maintenance, :
|
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.
|
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
|
42
|
-
recurring_schedule.
|
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
|
50
|
-
recurring_schedule.
|
49
|
+
def unschedule_recurring_tasks
|
50
|
+
recurring_schedule.unschedule_tasks
|
51
51
|
end
|
52
52
|
|
53
53
|
def all_work_completed?
|
data/lib/solid_queue/version.rb
CHANGED
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
|
+
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-
|
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/
|
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
|