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 +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
|