solid_queue_mongoid 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.
- checksums.yaml +7 -0
- data/.claude/settings.local.json +38 -0
- data/.idea/copilot.data.migration.ask2agent.xml +6 -0
- data/.idea/inspectionProfiles/Project_Default.xml +5 -0
- data/.idea/jsLibraryMappings.xml +6 -0
- data/.idea/misc.xml +17 -0
- data/.idea/modules/bigdecimal-4.0.iml +18 -0
- data/.idea/modules/builder-3.3.iml +18 -0
- data/.idea/modules/concurrent-ruby-1.3.iml +21 -0
- data/.idea/modules/connection_pool-3.0.iml +18 -0
- data/.idea/modules/crass-1.0.iml +19 -0
- data/.idea/modules/docile-1.4.iml +20 -0
- data/.idea/modules/drb-2.2.iml +18 -0
- data/.idea/modules/erb-6.0.iml +23 -0
- data/.idea/modules/et-orbi-1.4.iml +20 -0
- data/.idea/modules/fugit-1.12.iml +18 -0
- data/.idea/modules/irb-1.17.iml +26 -0
- data/.idea/modules/json-2.18.iml +18 -0
- data/.idea/modules/lint_roller-1.1.iml +18 -0
- data/.idea/modules/mongo-2.23.iml +19 -0
- data/.idea/modules/nokogiri-1.19.iml +19 -0
- data/.idea/modules/parser-3.3.10.iml +19 -0
- data/.idea/modules/pp-0.6.iml +18 -0
- data/.idea/modules/prettyprint-0.2.iml +22 -0
- data/.idea/modules/prism-1.9.iml +20 -0
- data/.idea/modules/raabro-1.4.iml +18 -0
- data/.idea/modules/rake-13.3.iml +22 -0
- data/.idea/modules/rdoc-7.2.iml +22 -0
- data/.idea/modules/regexp_parser-2.11.iml +20 -0
- data/.idea/modules/specifications.iml +18 -0
- data/.idea/modules/thor-1.5.iml +20 -0
- data/.idea/modules/timeout-0.6.iml +22 -0
- data/.idea/modules/tsort-0.2.iml +22 -0
- data/.idea/modules/unicode-emoji-4.2.iml +19 -0
- data/.idea/modules.xml +36 -0
- data/.idea/solid_queue_mongoid.iml +3297 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +353 -0
- data/.rspec +3 -0
- data/.rubocop.yml +47 -0
- data/ARCHITECTURE.md +91 -0
- data/CHANGELOG.md +27 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +249 -0
- data/Rakefile +12 -0
- data/lib/solid_queue_mongoid/models/blocked_execution.rb +125 -0
- data/lib/solid_queue_mongoid/models/claimed_execution.rb +134 -0
- data/lib/solid_queue_mongoid/models/classes.rb +32 -0
- data/lib/solid_queue_mongoid/models/execution/dispatching.rb +23 -0
- data/lib/solid_queue_mongoid/models/execution/job_attributes.rb +54 -0
- data/lib/solid_queue_mongoid/models/execution.rb +65 -0
- data/lib/solid_queue_mongoid/models/failed_execution.rb +74 -0
- data/lib/solid_queue_mongoid/models/job/clearable.rb +28 -0
- data/lib/solid_queue_mongoid/models/job/concurrency_controls.rb +93 -0
- data/lib/solid_queue_mongoid/models/job/executable.rb +142 -0
- data/lib/solid_queue_mongoid/models/job/recurrable.rb +14 -0
- data/lib/solid_queue_mongoid/models/job/retryable.rb +51 -0
- data/lib/solid_queue_mongoid/models/job/schedulable.rb +55 -0
- data/lib/solid_queue_mongoid/models/job.rb +103 -0
- data/lib/solid_queue_mongoid/models/pause.rb +25 -0
- data/lib/solid_queue_mongoid/models/process/executor.rb +30 -0
- data/lib/solid_queue_mongoid/models/process/prunable.rb +49 -0
- data/lib/solid_queue_mongoid/models/process.rb +73 -0
- data/lib/solid_queue_mongoid/models/queue.rb +65 -0
- data/lib/solid_queue_mongoid/models/queue_selector.rb +101 -0
- data/lib/solid_queue_mongoid/models/ready_execution.rb +70 -0
- data/lib/solid_queue_mongoid/models/record.rb +147 -0
- data/lib/solid_queue_mongoid/models/recurring_execution.rb +62 -0
- data/lib/solid_queue_mongoid/models/recurring_task/arguments.rb +29 -0
- data/lib/solid_queue_mongoid/models/recurring_task.rb +194 -0
- data/lib/solid_queue_mongoid/models/scheduled_execution.rb +43 -0
- data/lib/solid_queue_mongoid/models/semaphore.rb +179 -0
- data/lib/solid_queue_mongoid/railtie.rb +29 -0
- data/lib/solid_queue_mongoid/version.rb +5 -0
- data/lib/solid_queue_mongoid.rb +136 -0
- data/lib/tasks/solid_queue_mongoid.rake +51 -0
- data/release.sh +13 -0
- data/sig/solid_queue_mongoid.rbs +4 -0
- metadata +173 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fugit"
|
|
4
|
+
|
|
5
|
+
module SolidQueue
|
|
6
|
+
class RecurringTask < Record
|
|
7
|
+
field :key, type: String
|
|
8
|
+
field :schedule, type: String
|
|
9
|
+
field :command, type: String
|
|
10
|
+
field :class_name, type: String
|
|
11
|
+
field :arguments, type: Array, default: []
|
|
12
|
+
field :queue_name, type: String
|
|
13
|
+
field :priority, type: Integer, default: 0
|
|
14
|
+
field :description, type: String
|
|
15
|
+
field :static, type: Boolean, default: false
|
|
16
|
+
|
|
17
|
+
index({ key: 1 }, { unique: true })
|
|
18
|
+
|
|
19
|
+
scope :static, -> { where(static: true) }
|
|
20
|
+
scope :dynamic, -> { where(static: false) }
|
|
21
|
+
|
|
22
|
+
validates :key, presence: true
|
|
23
|
+
|
|
24
|
+
validate :ensure_schedule_supported
|
|
25
|
+
validate :ensure_command_or_class_present
|
|
26
|
+
validate :ensure_existing_job_class
|
|
27
|
+
|
|
28
|
+
has_many :recurring_executions, foreign_key: :task_key, primary_key: :key,
|
|
29
|
+
class_name: "SolidQueue::RecurringExecution"
|
|
30
|
+
|
|
31
|
+
mattr_accessor :default_job_class
|
|
32
|
+
self.default_job_class = "SolidQueue::RecurringJob".safe_constantize
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def wrap(args)
|
|
36
|
+
args.is_a?(self) ? args : from_configuration(args.first, **args.second)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def from_configuration(key, **options)
|
|
40
|
+
new(
|
|
41
|
+
key: key,
|
|
42
|
+
class_name: options[:class],
|
|
43
|
+
command: options[:command],
|
|
44
|
+
arguments: Array(options[:args]),
|
|
45
|
+
schedule: options[:schedule],
|
|
46
|
+
queue_name: options[:queue].presence,
|
|
47
|
+
priority: options[:priority].presence,
|
|
48
|
+
description: options[:description],
|
|
49
|
+
static: options.fetch(:static, true)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_dynamic_task(key, **options)
|
|
54
|
+
from_configuration(key, **options.merge(static: false)).save!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete_dynamic_task(key)
|
|
58
|
+
RecurringTask.dynamic.find_by!(key: key).destroy
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Upsert all static tasks; used by Scheduler::RecurringSchedule#persist_tasks.
|
|
62
|
+
def create_or_update_all(tasks)
|
|
63
|
+
tasks.each do |task|
|
|
64
|
+
existing = where(key: task.key).first
|
|
65
|
+
if existing
|
|
66
|
+
existing.update!(task.attributes_for_upsert)
|
|
67
|
+
else
|
|
68
|
+
create!(task.attributes_for_upsert.merge(key: task.key))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delay_from_now
|
|
75
|
+
[(next_time - Time.current).to_f, 0.1].max
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def next_time
|
|
79
|
+
parsed_schedule.next_time.utc
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def previous_time
|
|
83
|
+
parsed_schedule.previous_time.utc
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def last_enqueued_time
|
|
87
|
+
recurring_executions.maximum(:run_at)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def enqueue(at:)
|
|
91
|
+
SolidQueue.instrument(:enqueue_recurring_task, task: key, at: at) do |payload|
|
|
92
|
+
active_job = if using_solid_queue_adapter?
|
|
93
|
+
enqueue_and_record(run_at: at)
|
|
94
|
+
else
|
|
95
|
+
payload[:other_adapter] = true
|
|
96
|
+
perform_later.tap do |job|
|
|
97
|
+
payload[:enqueue_error] = job.enqueue_error&.message unless job.successfully_enqueued?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
active_job.tap do |enqueued_job|
|
|
102
|
+
payload[:active_job_id] = enqueued_job.job_id if enqueued_job
|
|
103
|
+
end
|
|
104
|
+
rescue RecurringExecution::AlreadyRecorded
|
|
105
|
+
payload[:skipped] = true
|
|
106
|
+
false
|
|
107
|
+
rescue Job::EnqueueError => e
|
|
108
|
+
payload[:enqueue_error] = e.message
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_s
|
|
114
|
+
"#{class_name}.perform_later(#{arguments.map(&:inspect).join(",")}) [ #{parsed_schedule.original} ]"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def attributes_for_upsert
|
|
118
|
+
attrs = attributes.except("_id", "id", "created_at", "updated_at")
|
|
119
|
+
attrs.delete("key")
|
|
120
|
+
attrs
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def ensure_schedule_supported
|
|
126
|
+
unless parsed_schedule.instance_of?(Fugit::Cron)
|
|
127
|
+
errors.add :schedule, :unsupported, message: "is not a supported recurring schedule"
|
|
128
|
+
end
|
|
129
|
+
rescue ArgumentError => e
|
|
130
|
+
message = if e.message.include?("multiple crons")
|
|
131
|
+
"generates multiple cron schedules. Please use separate recurring tasks for each schedule, " \
|
|
132
|
+
"or use explicit cron syntax (e.g., '40 0,15 * * *' for multiple times with the same minutes)"
|
|
133
|
+
else
|
|
134
|
+
e.message
|
|
135
|
+
end
|
|
136
|
+
errors.add :schedule, :unsupported, message: message
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def ensure_command_or_class_present
|
|
140
|
+
return if command.present? || class_name.present?
|
|
141
|
+
|
|
142
|
+
errors.add :base, :command_and_class_blank, message: "either command or class must be present"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ensure_existing_job_class
|
|
146
|
+
return unless class_name.present? && job_class.nil?
|
|
147
|
+
|
|
148
|
+
errors.add :class_name, :undefined, message: "doesn't correspond to an existing class"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def using_solid_queue_adapter?
|
|
152
|
+
job_class.respond_to?(:queue_adapter_name) &&
|
|
153
|
+
job_class.queue_adapter_name.inquiry.solid_queue?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def enqueue_and_record(run_at:)
|
|
157
|
+
RecurringExecution.record(key, run_at) do
|
|
158
|
+
job_class.new(*arguments_with_kwargs).set(enqueue_options).tap do |active_job|
|
|
159
|
+
active_job.run_callbacks(:enqueue) do
|
|
160
|
+
Job.enqueue(active_job)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def perform_later
|
|
167
|
+
job_class.new(*arguments_with_kwargs).tap do |active_job|
|
|
168
|
+
active_job.enqueue(enqueue_options)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def arguments_with_kwargs
|
|
173
|
+
if class_name.nil?
|
|
174
|
+
command
|
|
175
|
+
elsif arguments.last.is_a?(Hash)
|
|
176
|
+
arguments[0...-1] + [Hash.ruby2_keywords_hash(arguments.last)]
|
|
177
|
+
else
|
|
178
|
+
arguments
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def parsed_schedule
|
|
183
|
+
@parsed_schedule ||= Fugit.parse(schedule, multi: :fail)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def job_class
|
|
187
|
+
@job_class ||= class_name.present? ? class_name.safe_constantize : self.class.default_job_class
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def enqueue_options
|
|
191
|
+
{ queue: queue_name, priority: priority }.compact
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class ScheduledExecution < Execution
|
|
5
|
+
include Dispatching
|
|
6
|
+
|
|
7
|
+
assumes_attributes_from_job :scheduled_at
|
|
8
|
+
|
|
9
|
+
field :scheduled_at, type: Time
|
|
10
|
+
|
|
11
|
+
scope :due, -> { where(:scheduled_at.lte => Time.current) }
|
|
12
|
+
scope :due_order, -> { order_by(scheduled_at: :asc, priority: :asc, job_id: :asc) }
|
|
13
|
+
scope :next_batch, ->(batch_size) { due.due_order.limit(batch_size) }
|
|
14
|
+
|
|
15
|
+
index({ scheduled_at: 1, priority: 1 })
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def dispatch_next_batch(batch_size)
|
|
19
|
+
Mongoid.transaction do
|
|
20
|
+
SolidQueue.instrument(:dispatch_scheduled, batch_size: batch_size) do |payload|
|
|
21
|
+
job_ids = next_batch(batch_size).pluck(:job_id)
|
|
22
|
+
payload[:size] = if job_ids.empty?
|
|
23
|
+
0
|
|
24
|
+
else
|
|
25
|
+
dispatch_jobs(job_ids)
|
|
26
|
+
end
|
|
27
|
+
payload[:size]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias dispatch_due_batch dispatch_next_batch
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Instance method: dispatch this single execution if the job is due now.
|
|
36
|
+
# Mirrors the AR behaviour used in tests.
|
|
37
|
+
def dispatch
|
|
38
|
+
return unless job.due?
|
|
39
|
+
|
|
40
|
+
self.class.dispatch_jobs([job_id])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
# Semaphore for concurrency control.
|
|
5
|
+
#
|
|
6
|
+
# Convention (matches spec expectations):
|
|
7
|
+
# value = number of USED (acquired) slots (0 = none in use)
|
|
8
|
+
# limit = maximum concurrent slots
|
|
9
|
+
#
|
|
10
|
+
# wait: acquire a slot — succeeds when value < limit (increments value)
|
|
11
|
+
# signal: release a slot — increments value (marks another slot returned)
|
|
12
|
+
#
|
|
13
|
+
# NOTE: This intentionally differs from the ActiveRecord original which uses
|
|
14
|
+
# value = remaining available slots. The specs and BlockedExecution logic here
|
|
15
|
+
# both expect the "used slots" convention.
|
|
16
|
+
class Semaphore < Record
|
|
17
|
+
field :key, type: String
|
|
18
|
+
field :value, type: Integer, default: 0 # number of currently USED slots
|
|
19
|
+
field :limit, type: Integer, default: 1
|
|
20
|
+
field :expires_at, type: Time
|
|
21
|
+
|
|
22
|
+
index({ key: 1 }, { unique: true })
|
|
23
|
+
index({ expires_at: 1 })
|
|
24
|
+
|
|
25
|
+
validates :key, presence: true, uniqueness: true
|
|
26
|
+
|
|
27
|
+
# available: value < limit (at least one slot still free to acquire)
|
|
28
|
+
scope :available, -> { where("$expr" => { "$lt" => ["$value", "$limit"] }) }
|
|
29
|
+
scope :expired, -> { where(:expires_at.lt => Time.current) }
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def wait(job)
|
|
33
|
+
Proxy.new(job).wait
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def signal(job)
|
|
37
|
+
Proxy.new(job).signal
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def signal_all(jobs)
|
|
41
|
+
Proxy.signal_all(jobs)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Requires a unique index on key.
|
|
45
|
+
# Returns true if created/inserted; false on duplicate.
|
|
46
|
+
def create_unique_by(attributes)
|
|
47
|
+
create!(attributes)
|
|
48
|
+
true
|
|
49
|
+
rescue Mongoid::Errors::Validations, Mongo::Error::OperationFailure => e
|
|
50
|
+
raise unless duplicate_key_error?(e)
|
|
51
|
+
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def duplicate_key_error?(err)
|
|
58
|
+
err.message.to_s.include?("E11000") || err.message.to_s.include?("duplicate key")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ── Instance methods ──────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
# Atomically acquire one slot (increment value if value < limit).
|
|
65
|
+
# Returns true on success, false when at limit.
|
|
66
|
+
def acquire
|
|
67
|
+
result = self.class.collection.find_one_and_update(
|
|
68
|
+
{ _id: id, "value" => { "$lt" => limit } },
|
|
69
|
+
{ "$inc" => { "value" => 1 } },
|
|
70
|
+
return_document: :after
|
|
71
|
+
)
|
|
72
|
+
result.present?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Release one slot (decrement value). No-op if already at 0.
|
|
76
|
+
def release
|
|
77
|
+
self.class.collection.find_one_and_update(
|
|
78
|
+
{ _id: id, "value" => { "$gt" => 0 } },
|
|
79
|
+
{ "$inc" => { "value" => -1 } },
|
|
80
|
+
return_document: :after
|
|
81
|
+
)
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# True when there is still room to acquire (value < limit).
|
|
86
|
+
def available?
|
|
87
|
+
reload
|
|
88
|
+
value < limit
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ── Proxy inner class ─────────────────────────────────────────────────────
|
|
92
|
+
class Proxy
|
|
93
|
+
# Decrement value for every job's semaphore key (signal = release a used slot).
|
|
94
|
+
def self.signal_all(jobs)
|
|
95
|
+
keys = jobs.map(&:concurrency_key)
|
|
96
|
+
return if keys.empty?
|
|
97
|
+
|
|
98
|
+
Semaphore.in(key: keys).each do |sem|
|
|
99
|
+
Semaphore.collection.find_one_and_update(
|
|
100
|
+
{ _id: sem.id, "value" => { "$gt" => 0 } },
|
|
101
|
+
{ "$inc" => { "value" => -1 } }
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def initialize(job)
|
|
107
|
+
@job = job
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Acquire a slot: succeeds when value < limit.
|
|
111
|
+
# Creates the semaphore document on first use.
|
|
112
|
+
def wait
|
|
113
|
+
semaphore = Semaphore.where(key: key).first
|
|
114
|
+
|
|
115
|
+
if semaphore
|
|
116
|
+
# Atomically increment if value < limit
|
|
117
|
+
attempt_acquire(semaphore.id, semaphore.limit)
|
|
118
|
+
else
|
|
119
|
+
attempt_creation
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Release a slot: decrement value (marks one used slot as freed).
|
|
124
|
+
def signal
|
|
125
|
+
attempt_release_slot
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
attr_reader :job
|
|
131
|
+
|
|
132
|
+
# Try to create the semaphore with value=1 (one slot in use).
|
|
133
|
+
def attempt_creation
|
|
134
|
+
lim = limit
|
|
135
|
+
if Semaphore.create_unique_by(key: key, value: 1, limit: lim, expires_at: expires_at)
|
|
136
|
+
true
|
|
137
|
+
else
|
|
138
|
+
# Race: someone else created it first — try to acquire from existing
|
|
139
|
+
sem = Semaphore.where(key: key).first
|
|
140
|
+
return false unless sem
|
|
141
|
+
|
|
142
|
+
attempt_acquire(sem.id, sem.limit)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Atomically increment value if currently < limit.
|
|
147
|
+
def attempt_acquire(semaphore_id, lim)
|
|
148
|
+
result = Semaphore.collection.find_one_and_update(
|
|
149
|
+
{ _id: semaphore_id, "value" => { "$lt" => lim } },
|
|
150
|
+
{ "$inc" => { "value" => 1 }, "$set" => { "expires_at" => expires_at } },
|
|
151
|
+
return_document: :after
|
|
152
|
+
)
|
|
153
|
+
result.present?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Atomically decrement value if currently > 0 (release one used slot).
|
|
157
|
+
def attempt_release_slot
|
|
158
|
+
result = Semaphore.collection.find_one_and_update(
|
|
159
|
+
{ "key" => key, "value" => { "$gt" => 0 } },
|
|
160
|
+
{ "$inc" => { "value" => -1 }, "$set" => { "expires_at" => expires_at } },
|
|
161
|
+
return_document: :after
|
|
162
|
+
)
|
|
163
|
+
result.present?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def key
|
|
167
|
+
job.concurrency_key
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def expires_at
|
|
171
|
+
job.respond_to?(:concurrency_duration) ? job.concurrency_duration.from_now : 5.minutes.from_now
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def limit
|
|
175
|
+
(job.respond_to?(:concurrency_limit) ? job.concurrency_limit : nil) || 1
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMongoid
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
rake_tasks do
|
|
6
|
+
load "tasks/solid_queue_mongoid.rake"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Prevent SolidQueue's AR models from loading via Zeitwerk.
|
|
10
|
+
#
|
|
11
|
+
# Must run BEFORE :set_eager_load_paths (which freezes eager_load_paths)
|
|
12
|
+
# and before SolidQueue's own Railtie initializer adds its app/ path.
|
|
13
|
+
initializer "solid_queue_mongoid.shim",
|
|
14
|
+
before: :set_eager_load_paths do |app|
|
|
15
|
+
next unless defined?(SolidQueue::Engine)
|
|
16
|
+
|
|
17
|
+
sq_app_path = SolidQueue::Engine.root.join("app").to_s
|
|
18
|
+
|
|
19
|
+
# Tell Zeitwerk to ignore SolidQueue's app/ tree so it never autoloads
|
|
20
|
+
# the AR model files.
|
|
21
|
+
Rails.autoloaders.each do |loader|
|
|
22
|
+
loader.ignore(sq_app_path) if loader.respond_to?(:ignore)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Remove from eager_load_paths before Rails freezes it.
|
|
26
|
+
app.config.eager_load_paths.delete_if { |p| p.start_with?(sq_app_path) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "solid_queue_mongoid/version"
|
|
4
|
+
require "mongoid"
|
|
5
|
+
require "active_support/all"
|
|
6
|
+
|
|
7
|
+
# Load Railtie if in Rails — must happen before SolidQueue loads its AR models
|
|
8
|
+
require_relative "solid_queue_mongoid/railtie" if defined?(Rails::Railtie)
|
|
9
|
+
|
|
10
|
+
module SolidQueueMongoid
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Configuration
|
|
14
|
+
mattr_accessor :client, :collection_prefix
|
|
15
|
+
|
|
16
|
+
@@client = :default
|
|
17
|
+
@@collection_prefix = "solid_queue_"
|
|
18
|
+
|
|
19
|
+
def self.configure
|
|
20
|
+
yield self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.create_indexes
|
|
24
|
+
all_models.each(&:create_indexes)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.remove_indexes
|
|
28
|
+
all_models.each(&:remove_indexes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.all_models
|
|
32
|
+
[
|
|
33
|
+
SolidQueue::Job,
|
|
34
|
+
SolidQueue::ReadyExecution,
|
|
35
|
+
SolidQueue::ClaimedExecution,
|
|
36
|
+
SolidQueue::BlockedExecution,
|
|
37
|
+
SolidQueue::ScheduledExecution,
|
|
38
|
+
SolidQueue::FailedExecution,
|
|
39
|
+
SolidQueue::RecurringExecution,
|
|
40
|
+
SolidQueue::Process,
|
|
41
|
+
SolidQueue::Pause,
|
|
42
|
+
SolidQueue::Semaphore,
|
|
43
|
+
SolidQueue::RecurringTask
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ─── Shim: inject SolidQueue namespace helpers before SolidQueue runtime loads ───
|
|
49
|
+
# SolidQueue runtime calls SolidQueue.client, SolidQueue.collection_prefix, etc.
|
|
50
|
+
# We also need clear_finished_jobs_after, process_alive_threshold, preserve_finished_jobs?
|
|
51
|
+
# — if SolidQueue gem is present those will already exist; if not we stub sensible defaults.
|
|
52
|
+
module SolidQueue
|
|
53
|
+
def self.client
|
|
54
|
+
SolidQueueMongoid.client
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.collection_prefix
|
|
58
|
+
SolidQueueMongoid.collection_prefix
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# These defaults mirror SolidQueue::Configuration defaults and are only
|
|
62
|
+
# active when solid_queue itself is not loaded.
|
|
63
|
+
unless respond_to?(:clear_finished_jobs_after)
|
|
64
|
+
def self.clear_finished_jobs_after
|
|
65
|
+
1.day
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless respond_to?(:process_alive_threshold)
|
|
70
|
+
def self.process_alive_threshold
|
|
71
|
+
5.minutes
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless respond_to?(:preserve_finished_jobs?)
|
|
76
|
+
def self.preserve_finished_jobs?
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Stub for SolidQueue.instrument — the real implementation in solid_queue
|
|
82
|
+
# uses ActiveSupport::Notifications. When solid_queue is loaded it already
|
|
83
|
+
# defines this, so we only add it when missing.
|
|
84
|
+
unless respond_to?(:instrument)
|
|
85
|
+
def self.instrument(_event, payload = {}, &block)
|
|
86
|
+
yield(payload) if block_given?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ─── Load order ───────────────────────────────────────────────────────────────
|
|
92
|
+
# 1. Base record class
|
|
93
|
+
require_relative "solid_queue_mongoid/models/record"
|
|
94
|
+
|
|
95
|
+
# 2. Pre-declare all classes (avoids superclass mismatch)
|
|
96
|
+
require_relative "solid_queue_mongoid/models/classes"
|
|
97
|
+
|
|
98
|
+
# 3. Execution concerns
|
|
99
|
+
require_relative "solid_queue_mongoid/models/execution/job_attributes"
|
|
100
|
+
require_relative "solid_queue_mongoid/models/execution/dispatching"
|
|
101
|
+
|
|
102
|
+
# 4. Base execution
|
|
103
|
+
require_relative "solid_queue_mongoid/models/execution"
|
|
104
|
+
|
|
105
|
+
# 5. Job concerns (order matters — ConcurrencyControls/Schedulable/Retryable
|
|
106
|
+
# must exist before Executable includes them)
|
|
107
|
+
require_relative "solid_queue_mongoid/models/job/clearable"
|
|
108
|
+
require_relative "solid_queue_mongoid/models/job/recurrable"
|
|
109
|
+
require_relative "solid_queue_mongoid/models/job/schedulable"
|
|
110
|
+
require_relative "solid_queue_mongoid/models/job/retryable"
|
|
111
|
+
require_relative "solid_queue_mongoid/models/job/concurrency_controls"
|
|
112
|
+
require_relative "solid_queue_mongoid/models/job/executable"
|
|
113
|
+
|
|
114
|
+
# 6. Concrete models
|
|
115
|
+
require_relative "solid_queue_mongoid/models/job"
|
|
116
|
+
require_relative "solid_queue_mongoid/models/semaphore" # needed by BlockedExecution
|
|
117
|
+
require_relative "solid_queue_mongoid/models/ready_execution"
|
|
118
|
+
require_relative "solid_queue_mongoid/models/claimed_execution"
|
|
119
|
+
require_relative "solid_queue_mongoid/models/blocked_execution"
|
|
120
|
+
require_relative "solid_queue_mongoid/models/scheduled_execution"
|
|
121
|
+
require_relative "solid_queue_mongoid/models/failed_execution"
|
|
122
|
+
require_relative "solid_queue_mongoid/models/recurring_task/arguments"
|
|
123
|
+
require_relative "solid_queue_mongoid/models/recurring_task"
|
|
124
|
+
require_relative "solid_queue_mongoid/models/recurring_execution"
|
|
125
|
+
require_relative "solid_queue_mongoid/models/process/executor"
|
|
126
|
+
require_relative "solid_queue_mongoid/models/process/prunable"
|
|
127
|
+
require_relative "solid_queue_mongoid/models/process"
|
|
128
|
+
require_relative "solid_queue_mongoid/models/pause"
|
|
129
|
+
require_relative "solid_queue_mongoid/models/queue"
|
|
130
|
+
require_relative "solid_queue_mongoid/models/queue_selector"
|
|
131
|
+
|
|
132
|
+
# Pull in SolidQueue's runtime (engine, workers, dispatcher, etc.) after our
|
|
133
|
+
# Mongoid models are defined so they claim the SolidQueue::* namespace first.
|
|
134
|
+
# SolidQueue's AR model files live in app/models/ which is never required by
|
|
135
|
+
# solid_queue.rb itself — they're Rails-autoloaded and blocked by our Railtie.
|
|
136
|
+
require "solid_queue"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :solid_queue_mongoid do
|
|
4
|
+
desc "Create MongoDB indexes for SolidQueue models"
|
|
5
|
+
task create_indexes: :environment do
|
|
6
|
+
require "solid_queue_mongoid"
|
|
7
|
+
|
|
8
|
+
puts "Creating indexes for SolidQueue Mongoid models..."
|
|
9
|
+
SolidQueueMongoid.create_indexes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "Remove MongoDB indexes for SolidQueue models"
|
|
13
|
+
task remove_indexes: :environment do
|
|
14
|
+
require "solid_queue_mongoid"
|
|
15
|
+
|
|
16
|
+
puts "Removing indexes for SolidQueue Mongoid models..."
|
|
17
|
+
SolidQueueMongoid.remove_indexes
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Show collection names for SolidQueue models"
|
|
21
|
+
task show_collections: :environment do
|
|
22
|
+
require "solid_queue_mongoid"
|
|
23
|
+
|
|
24
|
+
puts "\nSolidQueue Mongoid Collections:"
|
|
25
|
+
puts "--------------------------------"
|
|
26
|
+
puts "Configuration:"
|
|
27
|
+
puts " Client: #{SolidQueue.client}"
|
|
28
|
+
puts " Prefix: #{SolidQueue.collection_prefix}"
|
|
29
|
+
puts "\nCollections:"
|
|
30
|
+
|
|
31
|
+
models = [
|
|
32
|
+
SolidQueue::Job,
|
|
33
|
+
SolidQueue::ReadyExecution,
|
|
34
|
+
SolidQueue::ClaimedExecution,
|
|
35
|
+
SolidQueue::BlockedExecution,
|
|
36
|
+
SolidQueue::ScheduledExecution,
|
|
37
|
+
SolidQueue::FailedExecution,
|
|
38
|
+
SolidQueue::RecurringExecution,
|
|
39
|
+
SolidQueue::Process,
|
|
40
|
+
SolidQueue::Pause,
|
|
41
|
+
SolidQueue::Semaphore,
|
|
42
|
+
SolidQueue::RecurringTask,
|
|
43
|
+
SolidQueue::Queue
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
models.each do |model|
|
|
47
|
+
puts " #{model.name.ljust(45)} => #{model.collection.name}"
|
|
48
|
+
end
|
|
49
|
+
puts ""
|
|
50
|
+
end
|
|
51
|
+
end
|
data/release.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Extract version from version.rb
|
|
5
|
+
VERSION=$(ruby -r ./lib/solid_queue_mongoid/version.rb -e "puts SolidQueueMongoid::VERSION")
|
|
6
|
+
|
|
7
|
+
echo "Building gem version ${VERSION}..."
|
|
8
|
+
gem build solid_queue_mongoid.gemspec
|
|
9
|
+
|
|
10
|
+
echo "Pushing to RubyGems..."
|
|
11
|
+
gem push solid_queue_mongoid-${VERSION}.gem
|
|
12
|
+
|
|
13
|
+
echo "Done!"
|