solid_queue 1.7.1 → 1.7.2
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d1bbdf59a147fbbf0dbb46fe048353340c9f78ee31a5285318b244d3dc4fa7c
|
|
4
|
+
data.tar.gz: 804fe9402e26203920158d705ef2d0353c5d99dba05ae156c3a4ee148b155cdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6d1a1426fd0be06daf3c0b7ac0b508cc7eaa75a32024d89088ac19d5abc2373c8b75ad6795a248e007bd376ebcf2c6ef10a30649625c6f3555703090a75d9fb
|
|
7
|
+
data.tar.gz: 5f57c878e6876d91ec56fd4492f9db5b190071db5efd12ff618ab566c618fc03720c4f76bb522758a9f4f58b258313fc0255020d05aa4cb5815fa041f1fec9bd
|
data/UPGRADING.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# Upgrading to version 1.7.2
|
|
2
|
+
Jobs with [concurrency controls](README.md#concurrency-controls) now remember which semaphore they got their slot from, in their serialized job data, and give the slot back only to that semaphore. Before, a job that ran past its `duration` gave its slot back to the semaphore a later job had created after the job's own one expired, so one more job than the limit allowed could run at the same time, and keep doing so until the jobs waiting for that key ran out.
|
|
3
|
+
|
|
4
|
+
There's nothing to change or migrate. Jobs that got their slot before you upgrade have nothing recorded and give it back as before, so old and new versions can run side by side while you deploy.
|
|
5
|
+
|
|
6
|
+
Solid Queue 2.0 will stop giving back slots that weren't recorded. Before upgrading to 2.0, make sure the concurrency-limited jobs that were ready or running when you upgraded to 1.7.2 have finished. Otherwise their slots won't be given back: a slot like that only comes back when its semaphore expires, and every job that takes or gives back a slot for the same key pushes that expiry out by another `duration`, so on a busy key with a limit above 1 it can stay lost for as long as the key stays busy.
|
|
7
|
+
|
|
1
8
|
# Upgrading to version 1.7.x
|
|
2
9
|
This version introduces support for grouping jobs into batches, which needs new tables. Fresh installs get them with the base schema; existing installations need to copy the migration that adds them and run it:
|
|
3
10
|
|
|
@@ -51,7 +51,7 @@ module SolidQueue
|
|
|
51
51
|
destroy!
|
|
52
52
|
|
|
53
53
|
payload[:failed] = true
|
|
54
|
-
elsif acquire_concurrency_lock
|
|
54
|
+
elsif job.acquire_concurrency_lock
|
|
55
55
|
promote_to_ready
|
|
56
56
|
destroy!
|
|
57
57
|
|
|
@@ -63,12 +63,7 @@ module SolidQueue
|
|
|
63
63
|
|
|
64
64
|
private
|
|
65
65
|
def set_expires_at
|
|
66
|
-
|
|
67
|
-
self.expires_at = duration.from_now
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def acquire_concurrency_lock
|
|
71
|
-
Semaphore.wait(job)
|
|
66
|
+
self.expires_at = job.concurrency_duration.from_now
|
|
72
67
|
end
|
|
73
68
|
|
|
74
69
|
def promote_to_ready
|
|
@@ -5,11 +5,12 @@ module SolidQueue
|
|
|
5
5
|
module ConcurrencyControls
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
|
+
# Stored in the job's serialized Active Job data, which Active Job ignores when running the job
|
|
9
|
+
CONCURRENCY_LOCK_KEY = "solid_queue_concurrency_lock"
|
|
10
|
+
|
|
8
11
|
included do
|
|
9
12
|
has_one :blocked_execution
|
|
10
13
|
|
|
11
|
-
delegate :concurrency_limit, :concurrency_duration, to: :job_class
|
|
12
|
-
|
|
13
14
|
before_destroy :unblock_next_blocked_job, if: -> { concurrency_limited? && ready? }
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -25,8 +26,39 @@ module SolidQueue
|
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def acquire_concurrency_lock
|
|
30
|
+
return true unless concurrency_limited?
|
|
31
|
+
|
|
32
|
+
if job_class.nil?
|
|
33
|
+
# A job whose class doesn't load skips its limits. Record that it holds no slot,
|
|
34
|
+
# so it doesn't give one back if it runs somewhere its class does load.
|
|
35
|
+
record_concurrency_lock(semaphore_id: nil)
|
|
36
|
+
elsif semaphore_id = Semaphore.wait(self)
|
|
37
|
+
record_concurrency_lock(semaphore_id:, limit: concurrency_limit)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
28
41
|
def concurrency_limited?
|
|
29
|
-
concurrency_key.present?
|
|
42
|
+
concurrency_key.present?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A job that kept its concurrency key after its class dropped limits_concurrency gets a limit of 1
|
|
46
|
+
def concurrency_limit
|
|
47
|
+
job_class.concurrency_limit || 1 if concurrency_limited?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def concurrency_duration
|
|
51
|
+
job_class ? job_class.concurrency_duration : SolidQueue.default_concurrency_control_period
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The semaphore the job got its concurrency slot from
|
|
55
|
+
def acquired_semaphore_id
|
|
56
|
+
concurrency_lock&.dig("semaphore_id")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The concurrency limit the job had when it got its slot
|
|
60
|
+
def acquired_concurrency_limit
|
|
61
|
+
concurrency_lock&.dig("limit")
|
|
30
62
|
end
|
|
31
63
|
|
|
32
64
|
def blocked?
|
|
@@ -38,20 +70,35 @@ module SolidQueue
|
|
|
38
70
|
end
|
|
39
71
|
|
|
40
72
|
private
|
|
41
|
-
def
|
|
42
|
-
|
|
73
|
+
def release_concurrency_lock
|
|
74
|
+
holds_concurrency_slot? && Semaphore.signal(self)
|
|
43
75
|
end
|
|
44
76
|
|
|
45
|
-
def
|
|
46
|
-
|
|
77
|
+
def holds_concurrency_slot?
|
|
78
|
+
if concurrency_lock_recorded?
|
|
79
|
+
acquired_semaphore_id.present?
|
|
80
|
+
else
|
|
81
|
+
# Jobs dispatched before Solid Queue recorded their semaphore, which need their class to know their limit
|
|
82
|
+
concurrency_limited? && job_class.present?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
47
85
|
|
|
48
|
-
|
|
86
|
+
def concurrency_lock_recorded?
|
|
87
|
+
arguments.key?(CONCURRENCY_LOCK_KEY)
|
|
49
88
|
end
|
|
50
89
|
|
|
51
|
-
def
|
|
52
|
-
|
|
90
|
+
def release_next_blocked_job
|
|
91
|
+
BlockedExecution.release_one(concurrency_key)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def record_concurrency_lock(semaphore_id:, limit: nil)
|
|
95
|
+
arguments[CONCURRENCY_LOCK_KEY] = { "semaphore_id" => semaphore_id, "limit" => limit }
|
|
96
|
+
update_column :arguments, arguments
|
|
97
|
+
true
|
|
98
|
+
end
|
|
53
99
|
|
|
54
|
-
|
|
100
|
+
def concurrency_lock
|
|
101
|
+
arguments[CONCURRENCY_LOCK_KEY]
|
|
55
102
|
end
|
|
56
103
|
|
|
57
104
|
def handle_concurrency_conflict
|
|
@@ -62,12 +109,12 @@ module SolidQueue
|
|
|
62
109
|
end
|
|
63
110
|
end
|
|
64
111
|
|
|
65
|
-
def
|
|
66
|
-
|
|
112
|
+
def concurrency_on_conflict
|
|
113
|
+
job_class.concurrency_on_conflict.to_s.inquiry
|
|
67
114
|
end
|
|
68
115
|
|
|
69
|
-
def
|
|
70
|
-
BlockedExecution.
|
|
116
|
+
def block
|
|
117
|
+
BlockedExecution.create_or_find_by!(job: self)
|
|
71
118
|
end
|
|
72
119
|
|
|
73
120
|
def execution
|
|
@@ -31,9 +31,10 @@ module SolidQueue
|
|
|
31
31
|
@job = job
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Returns the id of the semaphore the job got a slot on, or nil if there was none free
|
|
34
35
|
def wait
|
|
35
36
|
if semaphore = Semaphore.lock.find_by(key: key)
|
|
36
|
-
semaphore.value > 0 && attempt_decrement
|
|
37
|
+
semaphore.id if semaphore.value > 0 && attempt_decrement
|
|
37
38
|
else
|
|
38
39
|
attempt_creation
|
|
39
40
|
end
|
|
@@ -46,24 +47,46 @@ module SolidQueue
|
|
|
46
47
|
private
|
|
47
48
|
attr_accessor :job
|
|
48
49
|
|
|
50
|
+
def attempt_decrement
|
|
51
|
+
Semaphore.available.where(key: key).update_all([ "value = value - 1, expires_at = ?", expires_at ]) > 0
|
|
52
|
+
end
|
|
53
|
+
|
|
49
54
|
def attempt_creation
|
|
50
55
|
if Semaphore.create_unique_by(key: key, value: limit - 1, expires_at: expires_at)
|
|
51
|
-
|
|
56
|
+
acquired_semaphore_id
|
|
52
57
|
else
|
|
53
58
|
check_limit_or_decrement
|
|
54
59
|
end
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
# A locking read, so it sees the latest committed rows rather than the transaction's
|
|
63
|
+
# snapshot, which on MySQL can still include a semaphore that expired and was deleted
|
|
64
|
+
def acquired_semaphore_id
|
|
65
|
+
Semaphore.lock.where(key: key).pick(:id)
|
|
59
66
|
end
|
|
60
67
|
|
|
61
|
-
def
|
|
62
|
-
|
|
68
|
+
def check_limit_or_decrement
|
|
69
|
+
acquired_semaphore_id if limit > 1 && attempt_decrement
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
def attempt_increment
|
|
66
|
-
|
|
73
|
+
semaphore_to_release.where(value: ...limit_to_release).update_all([ "value = value + 1, expires_at = ?", expires_at ]) > 0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A job only gives its slot back to the semaphore it got it from. If that one expired
|
|
77
|
+
# and was deleted, a semaphore for the same key now belongs to other jobs.
|
|
78
|
+
# Jobs dispatched before Solid Queue recorded their semaphore give it back by key.
|
|
79
|
+
def semaphore_to_release
|
|
80
|
+
if job.acquired_semaphore_id
|
|
81
|
+
Semaphore.where(id: job.acquired_semaphore_id)
|
|
82
|
+
else
|
|
83
|
+
Semaphore.where(key: key)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Recorded when the job got its slot, so it's known even if the job's class no longer loads
|
|
88
|
+
def limit_to_release
|
|
89
|
+
job.acquired_concurrency_limit || limit
|
|
67
90
|
end
|
|
68
91
|
|
|
69
92
|
def key
|
|
@@ -75,7 +98,7 @@ module SolidQueue
|
|
|
75
98
|
end
|
|
76
99
|
|
|
77
100
|
def limit
|
|
78
|
-
job.concurrency_limit
|
|
101
|
+
job.concurrency_limit
|
|
79
102
|
end
|
|
80
103
|
end
|
|
81
104
|
end
|
data/lib/solid_queue/version.rb
CHANGED