solid-jobs 0.4.2 → 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/CHANGELOG.md +8 -0
- data/README.md +307 -9
- data/docs/reliability.md +5 -0
- data/lib/solid_jobs/blueprint.rb +14 -2
- data/lib/solid_jobs/claim.rb +74 -25
- data/lib/solid_jobs/conductor.rb +30 -6
- data/lib/solid_jobs/engine.rb +63 -7
- data/lib/solid_jobs/failure_policy.rb +30 -2
- data/lib/solid_jobs/heartbeat.rb +74 -20
- data/lib/solid_jobs/instrumentation.rb +75 -0
- data/lib/solid_jobs/keyspace.rb +5 -0
- data/lib/solid_jobs/observations.rb +177 -0
- data/lib/solid_jobs/publication_barrier.rb +46 -0
- data/lib/solid_jobs/publisher.rb +37 -8
- data/lib/solid_jobs/recovery.rb +31 -2
- data/lib/solid_jobs/task.rb +35 -0
- data/lib/solid_jobs/version.rb +1 -1
- data/lib/solid_jobs.rb +11 -0
- metadata +4 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module SolidJobs
|
|
6
|
+
module PublicationBarrier
|
|
7
|
+
TIMEOUT_MS = 30_000
|
|
8
|
+
FIELD = "_solid_jobs_publication"
|
|
9
|
+
Entry = Data.define(:key, :publication, :owner)
|
|
10
|
+
RELEASE = <<~LUA.freeze
|
|
11
|
+
local released = 0
|
|
12
|
+
for index, key in ipairs(KEYS) do
|
|
13
|
+
if redis.call("get", key) == ARGV[index] then
|
|
14
|
+
released = released + redis.call("del", key)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
return released
|
|
18
|
+
LUA
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def prepare(publication: SecureRandom.hex(16), owner: SecureRandom.hex(16))
|
|
23
|
+
Entry.new(Keyspace.publication(publication), publication, owner)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def mark(target, entry)
|
|
27
|
+
target.call("SET", entry.key, entry.owner, "PX", TIMEOUT_MS)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def release(redis_pool, entries)
|
|
31
|
+
entries = Array(entries)
|
|
32
|
+
return 0 if entries.empty?
|
|
33
|
+
|
|
34
|
+
redis_pool.call(
|
|
35
|
+
"EVAL", RELEASE, entries.size,
|
|
36
|
+
*entries.map(&:key),
|
|
37
|
+
*entries.map(&:owner),
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def strip(envelope)
|
|
42
|
+
envelope.delete(FIELD)
|
|
43
|
+
envelope
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/solid_jobs/publisher.rb
CHANGED
|
@@ -28,7 +28,8 @@ module SolidJobs
|
|
|
28
28
|
return unless normalized
|
|
29
29
|
|
|
30
30
|
validate_arguments!(normalized.fetch("arguments"))
|
|
31
|
-
persist([normalized])
|
|
31
|
+
barriers = persist([normalized])
|
|
32
|
+
emit_persisted([normalized], barriers)
|
|
32
33
|
normalized.fetch("id")
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -80,13 +81,14 @@ module SolidJobs
|
|
|
80
81
|
end
|
|
81
82
|
intercepted
|
|
82
83
|
end
|
|
83
|
-
persist(envelopes)
|
|
84
|
+
barriers = persist(envelopes)
|
|
85
|
+
emit_persisted(envelopes, barriers)
|
|
84
86
|
end
|
|
85
87
|
ids
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
def persist(envelopes)
|
|
89
|
-
return
|
|
91
|
+
return [] if envelopes.empty?
|
|
90
92
|
|
|
91
93
|
if defined?(Lab)
|
|
92
94
|
case Lab.mode
|
|
@@ -94,26 +96,35 @@ module SolidJobs
|
|
|
94
96
|
envelopes.each do |envelope|
|
|
95
97
|
Lab.captured_for(Utilities.constantize(envelope.fetch("task"))) << envelope
|
|
96
98
|
end
|
|
97
|
-
return
|
|
99
|
+
return []
|
|
98
100
|
when :execute
|
|
99
101
|
envelopes.each { |envelope| Lab.execute(envelope) }
|
|
100
|
-
return
|
|
102
|
+
return []
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
planned, ready = envelopes.partition { |envelope| envelope["run_at"] }
|
|
107
|
+
barriers = []
|
|
104
108
|
@config.redis_pool.pipelined do |pipeline|
|
|
105
|
-
planned, ready = envelopes.partition { |envelope| envelope["run_at"] }
|
|
106
109
|
planned.each do |envelope|
|
|
107
110
|
score = envelope.delete("run_at")
|
|
108
111
|
pipeline.call("ZADD", Keyspace::PLANNED, score, JSON.generate(envelope))
|
|
109
112
|
end
|
|
110
|
-
persist_ready(pipeline, ready)
|
|
113
|
+
barriers = persist_ready(pipeline, ready)
|
|
111
114
|
end
|
|
112
|
-
|
|
115
|
+
barriers
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
private
|
|
116
119
|
|
|
120
|
+
def emit_persisted(envelopes, barriers)
|
|
121
|
+
envelopes.each do |envelope|
|
|
122
|
+
Instrumentation.emit(@config, :enqueued, envelope)
|
|
123
|
+
Instrumentation.emit(@config, :journaled, envelope)
|
|
124
|
+
end
|
|
125
|
+
release_publications(barriers)
|
|
126
|
+
end
|
|
127
|
+
|
|
117
128
|
def normalize(envelope)
|
|
118
129
|
raise InvalidJobError, "Task envelope must be a Hash" unless envelope.is_a?(Hash)
|
|
119
130
|
|
|
@@ -138,15 +149,33 @@ module SolidJobs
|
|
|
138
149
|
end
|
|
139
150
|
|
|
140
151
|
def persist_ready(pipeline, envelopes)
|
|
152
|
+
barriers = []
|
|
141
153
|
envelopes.group_by { |envelope| envelope.fetch("channel") }.each do |channel, group|
|
|
142
154
|
pipeline.call("SADD", Keyspace::CHANNELS, channel)
|
|
143
155
|
queued_ms = Utilities.realtime_milliseconds
|
|
144
156
|
encoded = group.map do |envelope|
|
|
145
157
|
envelope["queued_ms"] = queued_ms
|
|
158
|
+
envelope[PublicationBarrier::FIELD] = SecureRandom.hex(16)
|
|
146
159
|
JSON.generate(envelope)
|
|
147
160
|
end
|
|
161
|
+
group.zip(encoded).each do |envelope, _payload|
|
|
162
|
+
barrier = PublicationBarrier.prepare(
|
|
163
|
+
publication: envelope.fetch(PublicationBarrier::FIELD),
|
|
164
|
+
)
|
|
165
|
+
PublicationBarrier.mark(pipeline, barrier)
|
|
166
|
+
barriers << barrier
|
|
167
|
+
end
|
|
148
168
|
pipeline.call("LPUSH", Keyspace.channel(channel), *encoded)
|
|
149
169
|
end
|
|
170
|
+
barriers
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def release_publications(barriers)
|
|
174
|
+
PublicationBarrier.release(@config.redis_pool, barriers)
|
|
175
|
+
rescue SolidRedis::ConnectionError, SolidRedis::CommandError => error
|
|
176
|
+
@config.logger.warn(
|
|
177
|
+
"SolidJobs publication barrier release failed; waiting for expiry: #{error.class}: #{error.message}",
|
|
178
|
+
)
|
|
150
179
|
end
|
|
151
180
|
|
|
152
181
|
def validate_arguments!(arguments)
|
data/lib/solid_jobs/recovery.rb
CHANGED
|
@@ -10,8 +10,21 @@ module SolidJobs
|
|
|
10
10
|
local payload = redis.call("rpop", KEYS[1])
|
|
11
11
|
if payload then
|
|
12
12
|
local envelope = cjson.decode(payload)
|
|
13
|
+
local publication = envelope["#{PublicationBarrier::FIELD}"]
|
|
14
|
+
if not publication then
|
|
15
|
+
publication = ARGV[3]
|
|
16
|
+
envelope["#{PublicationBarrier::FIELD}"] = publication
|
|
17
|
+
payload = cjson.encode(envelope)
|
|
18
|
+
end
|
|
19
|
+
redis.call(
|
|
20
|
+
"set",
|
|
21
|
+
ARGV[1] .. publication,
|
|
22
|
+
ARGV[4],
|
|
23
|
+
"PX",
|
|
24
|
+
ARGV[2]
|
|
25
|
+
)
|
|
13
26
|
redis.call("rpush", "#{Keyspace::PREFIX}:channel:" .. envelope["channel"], payload)
|
|
14
|
-
return payload
|
|
27
|
+
return {payload, publication, ARGV[4]}
|
|
15
28
|
end
|
|
16
29
|
LUA
|
|
17
30
|
|
|
@@ -22,8 +35,15 @@ module SolidJobs
|
|
|
22
35
|
identity = node_key.delete_prefix("#{Keyspace::PREFIX}:node:")
|
|
23
36
|
next if alive?(identity)
|
|
24
37
|
|
|
25
|
-
while @config.redis_pool.call(
|
|
38
|
+
while (restored = @config.redis_pool.call(
|
|
39
|
+
"EVAL", RESTORE_ONE, 1, key,
|
|
40
|
+
Keyspace::PUBLICATION_PREFIX, PublicationBarrier::TIMEOUT_MS,
|
|
41
|
+
SecureRandom.hex(16), SecureRandom.hex(16),
|
|
42
|
+
))
|
|
43
|
+
payload, publication, owner = restored
|
|
26
44
|
recovered += 1
|
|
45
|
+
Instrumentation.emit(@config, :recovered, payload)
|
|
46
|
+
release_publication(publication, owner)
|
|
27
47
|
end
|
|
28
48
|
@config.redis_pool.call("DEL", key)
|
|
29
49
|
@config.redis_pool.call("DEL", Keyspace.claims(identity))
|
|
@@ -33,6 +53,15 @@ module SolidJobs
|
|
|
33
53
|
|
|
34
54
|
private
|
|
35
55
|
|
|
56
|
+
def release_publication(publication, owner)
|
|
57
|
+
entry = PublicationBarrier.prepare(publication: publication, owner: owner)
|
|
58
|
+
PublicationBarrier.release(@config.redis_pool, [entry])
|
|
59
|
+
rescue SolidRedis::ConnectionError, SolidRedis::CommandError => error
|
|
60
|
+
@config.logger.warn(
|
|
61
|
+
"SolidJobs recovery barrier release failed; waiting for expiry: #{error.class}: #{error.message}",
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
36
65
|
def alive?(identity)
|
|
37
66
|
hostname, pid = identity.split(":", 3)
|
|
38
67
|
return false if hostname == Socket.gethostname && !process_alive?(Integer(pid))
|
data/lib/solid_jobs/task.rb
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module SolidJobs
|
|
4
6
|
module Task
|
|
7
|
+
# Intercepts `enqueue` and reroutes non Ractor-safe tasks to the warm pool.
|
|
8
|
+
module WarmPoolRouting
|
|
9
|
+
NON_SHAREABLE_TYPES = [Mutex, Thread, Fiber].freeze
|
|
10
|
+
|
|
11
|
+
def enqueue(*arguments)
|
|
12
|
+
return super unless requires_warm_pool?(arguments)
|
|
13
|
+
|
|
14
|
+
require_relative "../../app/jobs/solid_jobs_generic_executor"
|
|
15
|
+
SolidJobsGenericExecutor.enqueue(
|
|
16
|
+
SolidJobsGenericExecutor.worker_id_for(name),
|
|
17
|
+
name,
|
|
18
|
+
Ractor.make_shareable(JSON.parse(JSON.generate(arguments))),
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def requires_warm_pool?(args)
|
|
23
|
+
return false if equal?(defined?(::SolidJobsGenericExecutor) && ::SolidJobsGenericExecutor)
|
|
24
|
+
return false unless defined?(::Rails)
|
|
25
|
+
|
|
26
|
+
!args.all? { |argument| Ractor.shareable?(argument) } && holds_blocking_state?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def holds_blocking_state?
|
|
32
|
+
instance_variables.any? do |ivar|
|
|
33
|
+
value = instance_variable_get(ivar)
|
|
34
|
+
NON_SHAREABLE_TYPES.any? { |type| value.is_a?(type) } || value.class.name.to_s.include?("Pool")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
5
39
|
def self.included(base)
|
|
6
40
|
base.extend(TaskMethods)
|
|
41
|
+
base.singleton_class.prepend(WarmPoolRouting)
|
|
7
42
|
base.include(InstanceMethods)
|
|
8
43
|
base.task_options
|
|
9
44
|
end
|
data/lib/solid_jobs/version.rb
CHANGED
data/lib/solid_jobs.rb
CHANGED
|
@@ -11,6 +11,8 @@ require_relative "solid_jobs/keyspace"
|
|
|
11
11
|
require_relative "solid_jobs/ractor_support"
|
|
12
12
|
require_relative "solid_jobs/startup_barrier"
|
|
13
13
|
require_relative "solid_jobs/interceptor_registry"
|
|
14
|
+
require_relative "solid_jobs/instrumentation"
|
|
15
|
+
require_relative "solid_jobs/publication_barrier"
|
|
14
16
|
require_relative "solid_jobs/blueprint"
|
|
15
17
|
require_relative "solid_jobs/publisher"
|
|
16
18
|
require_relative "solid_jobs/lab"
|
|
@@ -20,6 +22,7 @@ require_relative "solid_jobs/claim"
|
|
|
20
22
|
require_relative "solid_jobs/failure_policy"
|
|
21
23
|
require_relative "solid_jobs/timer"
|
|
22
24
|
require_relative "solid_jobs/engine"
|
|
25
|
+
require_relative "solid_jobs/observations"
|
|
23
26
|
require_relative "solid_jobs/heartbeat"
|
|
24
27
|
require_relative "solid_jobs/recovery"
|
|
25
28
|
require_relative "solid_jobs/integrity_check"
|
|
@@ -44,6 +47,14 @@ module SolidJobs
|
|
|
44
47
|
config
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
def instrumenter
|
|
51
|
+
config.instrumenter
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def instrumenter=(value)
|
|
55
|
+
config.instrumenter = value
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
def use_config(configuration)
|
|
48
59
|
Ractor.current[CONFIG_KEY]&.close
|
|
49
60
|
Ractor.current[CONFIG_KEY] = configuration
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid-jobs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -129,10 +129,13 @@ files:
|
|
|
129
129
|
- lib/solid_jobs/executor.rb
|
|
130
130
|
- lib/solid_jobs/failure_policy.rb
|
|
131
131
|
- lib/solid_jobs/heartbeat.rb
|
|
132
|
+
- lib/solid_jobs/instrumentation.rb
|
|
132
133
|
- lib/solid_jobs/integrity_check.rb
|
|
133
134
|
- lib/solid_jobs/interceptor_registry.rb
|
|
134
135
|
- lib/solid_jobs/keyspace.rb
|
|
135
136
|
- lib/solid_jobs/lab.rb
|
|
137
|
+
- lib/solid_jobs/observations.rb
|
|
138
|
+
- lib/solid_jobs/publication_barrier.rb
|
|
136
139
|
- lib/solid_jobs/publisher.rb
|
|
137
140
|
- lib/solid_jobs/ractor_support.rb
|
|
138
141
|
- lib/solid_jobs/rails_adapter.rb
|