solid-jobs 0.4.2 → 0.6.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 +25 -0
- data/README.md +540 -10
- data/docs/reliability.md +5 -0
- data/lib/solid_jobs/backends/base.rb +58 -0
- data/lib/solid_jobs/backends/redis.rb +80 -0
- data/lib/solid_jobs/backends/solid_engine.rb +288 -0
- data/lib/solid_jobs/backends/worker.rb +166 -0
- data/lib/solid_jobs/blueprint.rb +126 -3
- data/lib/solid_jobs/claim.rb +74 -25
- data/lib/solid_jobs/conductor.rb +145 -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 +53 -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 +15 -0
- metadata +8 -1
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,51 @@ 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
|
+
return persist_redis(envelopes) unless @config.routed?
|
|
107
|
+
|
|
108
|
+
envelopes.group_by { |envelope| @config.backend_name_for(envelope.fetch("channel")) }
|
|
109
|
+
.flat_map do |name, group|
|
|
110
|
+
adapter = @config.backend_adapter(name)
|
|
111
|
+
if adapter.is_a?(Backends::Redis) && adapter.config.equal?(@config)
|
|
112
|
+
persist_redis(group)
|
|
113
|
+
else
|
|
114
|
+
adapter.enqueue_many(group.each { |envelope| envelope["queued_ms"] = Utilities.realtime_milliseconds })
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def persist_redis(envelopes)
|
|
120
|
+
return [] if envelopes.empty?
|
|
121
|
+
|
|
122
|
+
planned, ready = envelopes.partition { |envelope| envelope["run_at"] }
|
|
123
|
+
barriers = []
|
|
104
124
|
@config.redis_pool.pipelined do |pipeline|
|
|
105
|
-
planned, ready = envelopes.partition { |envelope| envelope["run_at"] }
|
|
106
125
|
planned.each do |envelope|
|
|
107
126
|
score = envelope.delete("run_at")
|
|
108
127
|
pipeline.call("ZADD", Keyspace::PLANNED, score, JSON.generate(envelope))
|
|
109
128
|
end
|
|
110
|
-
persist_ready(pipeline, ready)
|
|
129
|
+
barriers = persist_ready(pipeline, ready)
|
|
111
130
|
end
|
|
112
|
-
|
|
131
|
+
barriers
|
|
113
132
|
end
|
|
114
133
|
|
|
115
134
|
private
|
|
116
135
|
|
|
136
|
+
def emit_persisted(envelopes, barriers)
|
|
137
|
+
envelopes.each do |envelope|
|
|
138
|
+
Instrumentation.emit(@config, :enqueued, envelope)
|
|
139
|
+
Instrumentation.emit(@config, :journaled, envelope)
|
|
140
|
+
end
|
|
141
|
+
release_publications(barriers)
|
|
142
|
+
end
|
|
143
|
+
|
|
117
144
|
def normalize(envelope)
|
|
118
145
|
raise InvalidJobError, "Task envelope must be a Hash" unless envelope.is_a?(Hash)
|
|
119
146
|
|
|
@@ -138,15 +165,33 @@ module SolidJobs
|
|
|
138
165
|
end
|
|
139
166
|
|
|
140
167
|
def persist_ready(pipeline, envelopes)
|
|
168
|
+
barriers = []
|
|
141
169
|
envelopes.group_by { |envelope| envelope.fetch("channel") }.each do |channel, group|
|
|
142
170
|
pipeline.call("SADD", Keyspace::CHANNELS, channel)
|
|
143
171
|
queued_ms = Utilities.realtime_milliseconds
|
|
144
172
|
encoded = group.map do |envelope|
|
|
145
173
|
envelope["queued_ms"] = queued_ms
|
|
174
|
+
envelope[PublicationBarrier::FIELD] = SecureRandom.hex(16)
|
|
146
175
|
JSON.generate(envelope)
|
|
147
176
|
end
|
|
177
|
+
group.zip(encoded).each do |envelope, _payload|
|
|
178
|
+
barrier = PublicationBarrier.prepare(
|
|
179
|
+
publication: envelope.fetch(PublicationBarrier::FIELD),
|
|
180
|
+
)
|
|
181
|
+
PublicationBarrier.mark(pipeline, barrier)
|
|
182
|
+
barriers << barrier
|
|
183
|
+
end
|
|
148
184
|
pipeline.call("LPUSH", Keyspace.channel(channel), *encoded)
|
|
149
185
|
end
|
|
186
|
+
barriers
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def release_publications(barriers)
|
|
190
|
+
PublicationBarrier.release(@config.redis_pool, barriers)
|
|
191
|
+
rescue SolidRedis::ConnectionError, SolidRedis::CommandError => error
|
|
192
|
+
@config.logger.warn(
|
|
193
|
+
"SolidJobs publication barrier release failed; waiting for expiry: #{error.class}: #{error.message}",
|
|
194
|
+
)
|
|
150
195
|
end
|
|
151
196
|
|
|
152
197
|
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,7 +11,12 @@ 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"
|
|
16
|
+
require_relative "solid_jobs/backends/base"
|
|
17
|
+
require_relative "solid_jobs/backends/solid_engine"
|
|
14
18
|
require_relative "solid_jobs/blueprint"
|
|
19
|
+
require_relative "solid_jobs/backends/redis"
|
|
15
20
|
require_relative "solid_jobs/publisher"
|
|
16
21
|
require_relative "solid_jobs/lab"
|
|
17
22
|
require_relative "solid_jobs/task"
|
|
@@ -20,9 +25,11 @@ require_relative "solid_jobs/claim"
|
|
|
20
25
|
require_relative "solid_jobs/failure_policy"
|
|
21
26
|
require_relative "solid_jobs/timer"
|
|
22
27
|
require_relative "solid_jobs/engine"
|
|
28
|
+
require_relative "solid_jobs/observations"
|
|
23
29
|
require_relative "solid_jobs/heartbeat"
|
|
24
30
|
require_relative "solid_jobs/recovery"
|
|
25
31
|
require_relative "solid_jobs/integrity_check"
|
|
32
|
+
require_relative "solid_jobs/backends/worker"
|
|
26
33
|
require_relative "solid_jobs/conductor"
|
|
27
34
|
require_relative "solid_jobs/catalog"
|
|
28
35
|
require_relative "solid_jobs/rails_adapter" if defined?(::ActiveJob::Base)
|
|
@@ -44,6 +51,14 @@ module SolidJobs
|
|
|
44
51
|
config
|
|
45
52
|
end
|
|
46
53
|
|
|
54
|
+
def instrumenter
|
|
55
|
+
config.instrumenter
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def instrumenter=(value)
|
|
59
|
+
config.instrumenter = value
|
|
60
|
+
end
|
|
61
|
+
|
|
47
62
|
def use_config(configuration)
|
|
48
63
|
Ractor.current[CONFIG_KEY]&.close
|
|
49
64
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -119,6 +119,10 @@ files:
|
|
|
119
119
|
- exe/solid-jobs
|
|
120
120
|
- lib/active_job/queue_adapters/solid_jobs_adapter.rb
|
|
121
121
|
- lib/solid_jobs.rb
|
|
122
|
+
- lib/solid_jobs/backends/base.rb
|
|
123
|
+
- lib/solid_jobs/backends/redis.rb
|
|
124
|
+
- lib/solid_jobs/backends/solid_engine.rb
|
|
125
|
+
- lib/solid_jobs/backends/worker.rb
|
|
122
126
|
- lib/solid_jobs/blueprint.rb
|
|
123
127
|
- lib/solid_jobs/catalog.rb
|
|
124
128
|
- lib/solid_jobs/claim.rb
|
|
@@ -129,10 +133,13 @@ files:
|
|
|
129
133
|
- lib/solid_jobs/executor.rb
|
|
130
134
|
- lib/solid_jobs/failure_policy.rb
|
|
131
135
|
- lib/solid_jobs/heartbeat.rb
|
|
136
|
+
- lib/solid_jobs/instrumentation.rb
|
|
132
137
|
- lib/solid_jobs/integrity_check.rb
|
|
133
138
|
- lib/solid_jobs/interceptor_registry.rb
|
|
134
139
|
- lib/solid_jobs/keyspace.rb
|
|
135
140
|
- lib/solid_jobs/lab.rb
|
|
141
|
+
- lib/solid_jobs/observations.rb
|
|
142
|
+
- lib/solid_jobs/publication_barrier.rb
|
|
136
143
|
- lib/solid_jobs/publisher.rb
|
|
137
144
|
- lib/solid_jobs/ractor_support.rb
|
|
138
145
|
- lib/solid_jobs/rails_adapter.rb
|