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.
@@ -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 true if envelopes.empty?
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 true
99
+ return []
98
100
  when :execute
99
101
  envelopes.each { |envelope| Lab.execute(envelope) }
100
- return true
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
- true
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)
@@ -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("EVAL", RESTORE_ONE, 1, key)
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))
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidJobs
4
- VERSION = "0.4.2"
4
+ VERSION = "0.6.0"
5
5
  end
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.2
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