solid-jobs 0.1.3 → 0.2.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.
@@ -21,37 +21,39 @@ module SolidJobs
21
21
  "hostname" => Socket.gethostname,
22
22
  "pid" => ::Process.pid,
23
23
  "started_at" => @started_at,
24
- "queues" => @config.queues,
24
+ "channels" => @config.channels,
25
25
  "labels" => [],
26
26
  "identity" => @identity,
27
27
  }
28
- busy = @config.redis_pool.call("HLEN", "#{@identity}:work")
28
+ node_key = Keyspace.node(@identity)
29
+ work_key = Keyspace.node_work(@identity)
30
+ busy = @config.redis_pool.call("HLEN", work_key)
29
31
  @config.redis_pool.pipelined do |pipeline|
30
- pipeline.call("INCRBY", "stat:processed", processed) if processed.positive?
31
- pipeline.call("INCRBY", "stat:failed", failed) if failed.positive?
32
- pipeline.call("SADD", "processes", @identity)
32
+ pipeline.call("INCRBY", Keyspace::PROCESSED, processed) if processed.positive?
33
+ pipeline.call("INCRBY", Keyspace::FAILED, failed) if failed.positive?
34
+ pipeline.call("SADD", Keyspace::NODES, @identity)
33
35
  pipeline.call(
34
36
  "HSET",
35
- @identity,
37
+ node_key,
36
38
  "info", JSON.generate(info),
37
39
  "beat", now,
38
40
  "busy", busy,
39
41
  "quiet", quiet ? "true" : "false",
40
42
  "concurrency", @concurrency,
41
43
  )
42
- pipeline.call("EXPIRE", @identity, TTL)
43
- pipeline.call("DEL", "#{@identity}:work")
44
- pipeline.call("HSET", "#{@identity}:work", *work.flatten(1)) unless work.empty?
45
- pipeline.call("EXPIRE", "#{@identity}:work", TTL)
44
+ pipeline.call("EXPIRE", node_key, TTL)
45
+ pipeline.call("DEL", work_key)
46
+ pipeline.call("HSET", work_key, *work.flatten(1)) unless work.empty?
47
+ pipeline.call("EXPIRE", work_key, TTL)
46
48
  end
47
49
  now
48
50
  end
49
51
 
50
52
  def cleanup
51
53
  @config.redis_pool.pipelined do |pipeline|
52
- pipeline.call("SREM", "processes", @identity)
53
- pipeline.call("DEL", @identity)
54
- pipeline.call("DEL", "#{@identity}:work")
54
+ pipeline.call("SREM", Keyspace::NODES, @identity)
55
+ pipeline.call("DEL", Keyspace.node(@identity))
56
+ pipeline.call("DEL", Keyspace.node_work(@identity))
55
57
  end
56
58
  ensure
57
59
  @config.close
@@ -5,10 +5,10 @@ require "set"
5
5
 
6
6
  module SolidJobs
7
7
  class IntegrityCheck
8
- ACTIVE_SETS = {
9
- "schedule" => "SCHEDULED",
10
- "retry" => "RETRY",
11
- "dead" => "DEAD",
8
+ ACTIVE_COLLECTIONS = {
9
+ Keyspace::PLANNED => "PLANNED",
10
+ Keyspace::RETRIES => "RETRYING",
11
+ Keyspace::DISCARDED => "DISCARDED",
12
12
  }.freeze
13
13
 
14
14
  Report = Struct.new(
@@ -51,10 +51,10 @@ module SolidJobs
51
51
  end
52
52
 
53
53
  def call
54
- read_queues
55
- read_sorted_sets
56
- reserved = read_reservations
57
- validate_reservation_metadata(reserved)
54
+ read_channels
55
+ read_timed_collections
56
+ claimed = read_claimed
57
+ validate_claim_metadata(claimed)
58
58
  @acked.each { |job_id| add_state(job_id, "ACKED", "acknowledged") }
59
59
 
60
60
  observed = @states.keys.to_set
@@ -71,56 +71,57 @@ module SolidJobs
71
71
 
72
72
  private
73
73
 
74
- def read_queues
75
- scan("queue:*").each do |key|
74
+ def read_channels
75
+ scan("#{Keyspace::PREFIX}:channel:*").each do |key|
76
76
  Array(@redis.call("LRANGE", key, 0, -1)).each_with_index do |raw, index|
77
77
  add_payload(raw, "READY", "#{key}[#{index}]")
78
78
  end
79
79
  end
80
80
  end
81
81
 
82
- def read_sorted_sets
83
- ACTIVE_SETS.each do |key, state|
82
+ def read_timed_collections
83
+ ACTIVE_COLLECTIONS.each do |key, state|
84
84
  Array(@redis.call("ZRANGE", key, 0, -1)).each_with_index do |raw, index|
85
85
  add_payload(raw, state, "#{key}[#{index}]")
86
86
  end
87
87
  end
88
88
  end
89
89
 
90
- def read_reservations
91
- scan("*:reserved:*").to_h do |key|
90
+ def read_claimed
91
+ scan("#{Keyspace::PREFIX}:node:*:claimed:*").to_h do |key|
92
92
  records = Array(@redis.call("LRANGE", key, 0, -1)).each_with_index.map do |raw, index|
93
93
  payload = parse_payload(raw, "#{key}[#{index}]")
94
- add_state(payload["jid"], "RESERVED", "#{key}[#{index}]") if payload
94
+ add_state(payload["id"], "CLAIMED", "#{key}[#{index}]") if payload
95
95
  payload
96
96
  end.compact
97
97
  [key, records]
98
98
  end
99
99
  end
100
100
 
101
- def validate_reservation_metadata(reserved)
102
- metadata = reservation_metadata
103
- reserved.each do |key, payloads|
104
- identity, worker_id = key.split(":reserved:", 2)
105
- entry = metadata.delete([identity, worker_id])
101
+ def validate_claim_metadata(claimed)
102
+ metadata = claim_metadata
103
+ claimed.each do |key, payloads|
104
+ node_key, executor_id = key.split(":claimed:", 2)
105
+ identity = node_key.delete_prefix("#{Keyspace::PREFIX}:node:")
106
+ entry = metadata.delete([identity, executor_id])
106
107
  if payloads.empty?
107
- @invalid_reservations << issue(key, "empty_reserved_list")
108
+ @invalid_reservations << issue(key, "empty_claimed_list")
108
109
  elsif payloads.length > 1
109
- @invalid_reservations << issue(key, "multiple_payloads_for_worker")
110
+ @invalid_reservations << issue(key, "multiple_tasks_for_executor")
110
111
  end
111
112
  payload = payloads.first
112
113
  unless entry
113
- @invalid_reservations << issue(key, "missing_metadata", job_id: payload&.[]("jid"))
114
+ @invalid_reservations << issue(key, "missing_metadata", job_id: payload&.[]("id"))
114
115
  next
115
116
  end
116
117
  next unless payload
117
118
 
118
119
  attributes = entry.fetch(:attributes)
119
120
  {
120
- "job_id" => payload["jid"],
121
- "process_id" => identity,
122
- "worker_id" => worker_id,
123
- "queue" => payload["queue"],
121
+ "task_id" => payload["id"],
122
+ "node_id" => identity,
123
+ "executor_id" => executor_id,
124
+ "channel" => payload["channel"],
124
125
  }.each do |field, expected|
125
126
  next if attributes[field].to_s == expected.to_s
126
127
 
@@ -138,14 +139,16 @@ module SolidJobs
138
139
  entry.fetch(:key),
139
140
  "metadata_without_payload",
140
141
  field: entry.fetch(:field),
141
- job_id: entry.fetch(:attributes)["job_id"],
142
+ job_id: entry.fetch(:attributes)["task_id"],
142
143
  )
143
144
  end
144
145
  end
145
146
 
146
- def reservation_metadata
147
- scan("*:reservations").each_with_object({}) do |key, entries|
148
- identity = key.delete_suffix(":reservations")
147
+ def claim_metadata
148
+ scan("#{Keyspace::PREFIX}:node:*:claims").each_with_object({}) do |key, entries|
149
+ identity = key
150
+ .delete_prefix("#{Keyspace::PREFIX}:node:")
151
+ .delete_suffix(":claims")
149
152
  hash_entries(key).each do |field, raw|
150
153
  attributes = parse_json(raw, "#{key}[#{field}]")
151
154
  next unless attributes
@@ -160,10 +163,10 @@ module SolidJobs
160
163
  end
161
164
 
162
165
  def dangling_attempts
163
- hash_entries("solid-jobs:attempts").filter_map do |job_id, attempt|
166
+ hash_entries(Keyspace::ATTEMPTS).filter_map do |job_id, attempt|
164
167
  next if @states[job_id]&.any? { |location| location.fetch(:state) != "ACKED" }
165
168
 
166
- {job_id: job_id, attempt: Integer(attempt), index: "solid-jobs:attempts"}
169
+ {job_id: job_id, attempt: Integer(attempt), index: Keyspace::ATTEMPTS}
167
170
  end
168
171
  end
169
172
 
@@ -177,15 +180,15 @@ module SolidJobs
177
180
 
178
181
  def add_payload(raw, state, location)
179
182
  payload = parse_payload(raw, location)
180
- add_state(payload["jid"], state, location) if payload
183
+ add_state(payload["id"], state, location) if payload
181
184
  end
182
185
 
183
186
  def parse_payload(raw, location)
184
187
  payload = parse_json(raw, location)
185
188
  return unless payload
186
- return payload if payload["jid"]
189
+ return payload if payload["id"]
187
190
 
188
- @malformed_payloads << {location: location, error: "missing jid", raw: raw}
191
+ @malformed_payloads << {location: location, error: "missing id", raw: raw}
189
192
  nil
190
193
  end
191
194
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidJobs
4
+ class InterceptorRegistry
5
+ Definition = Data.define(:type, :arguments)
6
+
7
+ include Enumerable
8
+
9
+ def initialize(definitions = [])
10
+ @definitions = definitions.map do |definition|
11
+ Definition.new(definition.type, definition.arguments.dup.freeze)
12
+ end
13
+ end
14
+
15
+ def each(&block)
16
+ @definitions.each(&block)
17
+ end
18
+
19
+ def use(type, *arguments)
20
+ discard(type)
21
+ @definitions = (@definitions + [Definition.new(type, arguments.freeze)]).freeze
22
+ self
23
+ end
24
+
25
+ def discard(type)
26
+ @definitions = @definitions.reject { |definition| definition.type == type }.freeze
27
+ self
28
+ end
29
+
30
+ def empty?
31
+ @definitions.empty?
32
+ end
33
+
34
+ def call(context, &operation)
35
+ continuation = operation
36
+ @definitions.reverse_each do |definition|
37
+ interceptor = definition.type.new(*definition.arguments)
38
+ downstream = continuation
39
+ continuation = -> { interceptor.around(context, &downstream) }
40
+ end
41
+ continuation.call
42
+ end
43
+
44
+ def export
45
+ Utilities.shareable_copy(
46
+ @definitions.map { |definition| [definition.type, definition.arguments] },
47
+ )
48
+ end
49
+
50
+ def import(records)
51
+ @definitions = records.map do |type, arguments|
52
+ Definition.new(type, arguments.freeze)
53
+ end.freeze
54
+ self
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidJobs
4
+ module Keyspace
5
+ PREFIX = "solid_jobs".freeze
6
+ CHANNELS = "#{PREFIX}:channels".freeze
7
+ PAUSED_CHANNELS = "#{PREFIX}:paused_channels".freeze
8
+ PLANNED = "#{PREFIX}:planned".freeze
9
+ RETRIES = "#{PREFIX}:retries".freeze
10
+ DISCARDED = "#{PREFIX}:discarded".freeze
11
+ NODES = "#{PREFIX}:nodes".freeze
12
+ ATTEMPTS = "#{PREFIX}:attempts".freeze
13
+ PROCESSED = "#{PREFIX}:metrics:processed".freeze
14
+ FAILED = "#{PREFIX}:metrics:failed".freeze
15
+
16
+ module_function
17
+
18
+ def channel(name)
19
+ "#{PREFIX}:channel:#{name}"
20
+ end
21
+
22
+ def node(identity)
23
+ "#{PREFIX}:node:#{identity}"
24
+ end
25
+
26
+ def node_work(identity)
27
+ "#{node(identity)}:work"
28
+ end
29
+
30
+ def node_signals(identity)
31
+ "#{node(identity)}:signals"
32
+ end
33
+
34
+ def claims(identity)
35
+ "#{node(identity)}:claims"
36
+ end
37
+
38
+ def claimed(identity, executor_id)
39
+ "#{node(identity)}:claimed:#{executor_id}"
40
+ end
41
+ end
42
+ end
@@ -6,8 +6,8 @@ module SolidJobs
6
6
  class Processor
7
7
  def initialize(config, identity: nil, processor_id: nil, heartbeat: nil)
8
8
  @config = config
9
- @fetcher = Fetch.new(config, identity: identity, processor_id: processor_id)
10
- @worker = Worker.new(redis_config: config.redis_config, config: config)
9
+ @claims = Claim.new(config, identity: identity, processor_id: processor_id)
10
+ @executor = Executor.new(redis_config: config.redis_config, config: config)
11
11
  @identity = identity
12
12
  @processor_id = processor_id
13
13
  @heartbeat = heartbeat
@@ -29,12 +29,12 @@ module SolidJobs
29
29
  next
30
30
  end
31
31
 
32
- unit = retrieve
33
- next unless unit
32
+ claim = retrieve
33
+ next unless claim
34
34
 
35
35
  receive_commands(control)
36
36
  if @stopping || @quiet
37
- requeue(unit)
37
+ requeue(claim)
38
38
  break if @stopping
39
39
 
40
40
  next
@@ -42,19 +42,19 @@ module SolidJobs
42
42
 
43
43
  begin
44
44
  @state_mutex.synchronize { @busy = true }
45
- payload = unit.job
46
- register_work(unit, payload)
45
+ envelope = claim.envelope
46
+ register_work(claim, envelope)
47
47
  begin
48
- @worker.perform(payload)
48
+ @executor.execute(envelope)
49
49
  rescue Shutdown
50
- requeue(unit)
50
+ requeue(claim)
51
51
  break
52
52
  rescue StandardError => error
53
- RetryService.call(config: @config, payload: payload, error: error)
54
- acknowledge(unit)
53
+ FailurePolicy.call(config: @config, payload: envelope, error: error)
54
+ complete(claim)
55
55
  failed += 1
56
56
  else
57
- processed += 1 if acknowledge(unit)
57
+ processed += 1 if complete(claim)
58
58
  end
59
59
  ensure
60
60
  @state_mutex.synchronize { @busy = false }
@@ -78,50 +78,50 @@ module SolidJobs
78
78
 
79
79
  private
80
80
 
81
- def acknowledge(unit)
82
- acknowledged = unit.acknowledge
83
- unless acknowledged
81
+ def complete(claim)
82
+ completed = claim.complete
83
+ unless completed
84
84
  @config.logger.warn(
85
- "Reservation fencing rejected stale ACK: #{unit.reservation_id}",
85
+ "Claim fencing rejected stale completion: #{claim.claim_token}",
86
86
  )
87
87
  end
88
- acknowledged
88
+ completed
89
89
  rescue SolidRedis::ConnectionError => error
90
90
  @fetcher.connection_failed!
91
91
  @config.logger.warn(
92
- "Redis ACK failed; reserved job will be replayed: #{error.class}: #{error.message}",
92
+ "Redis completion failed; claimed task will be replayed: #{error.class}: #{error.message}",
93
93
  )
94
94
  false
95
95
  end
96
96
 
97
- def requeue(unit)
98
- requeued = unit.requeue
97
+ def requeue(claim)
98
+ requeued = claim.requeue
99
99
  unless requeued
100
100
  @config.logger.warn(
101
- "Reservation fencing rejected stale requeue: #{unit.reservation_id}",
101
+ "Claim fencing rejected stale requeue: #{claim.claim_token}",
102
102
  )
103
103
  end
104
104
  requeued
105
105
  end
106
106
 
107
107
  def retrieve
108
- @fetcher.retrieve
108
+ @claims.next
109
109
  rescue SolidRedis::ConnectionError => error
110
- @fetcher.connection_failed!
111
- @config.logger.warn("Redis fetch failed, retrying: #{error.class}: #{error.message}")
110
+ @claims.connection_failed!
111
+ @config.logger.warn("Redis claim failed, retrying: #{error.class}: #{error.message}")
112
112
  sleep 0.1
113
113
  nil
114
114
  end
115
115
 
116
- def register_work(unit, payload)
116
+ def register_work(claim, envelope)
117
117
  return unless @identity
118
118
 
119
119
  work = JSON.generate(
120
- "queue" => unit.queue,
121
- "payload" => payload,
122
- "run_at" => Time.now.to_f,
123
- "reservation_id" => unit.reservation_id,
124
- "attempt" => unit.attempt,
120
+ "channel" => claim.channel,
121
+ "envelope" => envelope,
122
+ "started_at" => Time.now.to_f,
123
+ "claim_token" => claim.claim_token,
124
+ "attempt" => claim.attempt,
125
125
  )
126
126
  send_heartbeat(:work, @processor_id, work)
127
127
  end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module SolidJobs
7
+ class Publisher
8
+ DEFAULT_CHUNK_SIZE = 1_000
9
+ PLANNED_CHUNK_SIZE = 100
10
+ Publication = Data.define(:task, :envelope, :redis_pool)
11
+
12
+ def self.publish(envelope)
13
+ new.publish(envelope)
14
+ end
15
+
16
+ def self.publish_many(envelope)
17
+ new.publish_many(envelope)
18
+ end
19
+
20
+ def initialize(config: nil, configuration: nil)
21
+ selected = config || configuration || SolidJobs.config
22
+ @config = selected.is_a?(SolidRedis::Config) ? Config.new(redis: selected) : selected
23
+ end
24
+
25
+ def publish(envelope)
26
+ normalized = normalize(envelope)
27
+ normalized = intercept(normalized)
28
+ return unless normalized
29
+
30
+ validate_arguments!(normalized.fetch("arguments"))
31
+ persist([normalized])
32
+ normalized.fetch("id")
33
+ end
34
+
35
+ def publish_many(envelope)
36
+ raise InvalidJobError, "Task envelope must be a Hash" unless envelope.is_a?(Hash)
37
+
38
+ source = Utilities.stringify_hash_keys(envelope)
39
+ argument_sets = source.delete("arguments")
40
+ unless argument_sets.respond_to?(:each)
41
+ raise InvalidJobError, "Task 'arguments' must be an Array or Enumerable"
42
+ end
43
+
44
+ run_times = source.delete("run_at")
45
+ unless run_times.nil? || run_times.is_a?(Numeric) || run_times.is_a?(Array)
46
+ raise InvalidJobError, "Task 'run_at' must be Numeric or an Array"
47
+ end
48
+ if run_times.is_a?(Array) && argument_sets.respond_to?(:size) && run_times.size != argument_sets.size
49
+ raise InvalidJobError, "Task 'run_at' Array must match the number of argument sets"
50
+ end
51
+
52
+ chunk_size = Integer(
53
+ source.delete("chunk_size") || (run_times ? PLANNED_CHUNK_SIZE : DEFAULT_CHUNK_SIZE),
54
+ )
55
+ raise ArgumentError, "chunk_size must be positive" unless chunk_size.positive?
56
+
57
+ explicit_id = source.delete("id")
58
+ if explicit_id && argument_sets.respond_to?(:size) && argument_sets.size > 1
59
+ raise InvalidJobError, "An explicit id is only valid for a single task"
60
+ end
61
+
62
+ template = normalize(source.merge("arguments" => []))
63
+ template.delete("id")
64
+ ids = []
65
+ argument_sets.each_slice(chunk_size).with_index do |chunk, chunk_index|
66
+ envelopes = chunk.filter_map.with_index do |arguments, index|
67
+ raise InvalidJobError, "Each task's arguments must be an Array" unless arguments.is_a?(Array)
68
+
69
+ item = template.merge(
70
+ "arguments" => arguments,
71
+ "id" => explicit_id || SecureRandom.uuid,
72
+ )
73
+ item["run_at"] = run_times.is_a?(Array) ? run_times[chunk_index * chunk_size + index] : run_times if run_times
74
+ intercepted = intercept(item)
75
+ if intercepted
76
+ validate_arguments!(intercepted.fetch("arguments"))
77
+ ids << intercepted.fetch("id")
78
+ else
79
+ ids << nil
80
+ end
81
+ intercepted
82
+ end
83
+ persist(envelopes)
84
+ end
85
+ ids
86
+ end
87
+
88
+ def persist(envelopes)
89
+ return true if envelopes.empty?
90
+
91
+ if defined?(Testing)
92
+ case Testing.mode
93
+ when :capture
94
+ envelopes.each do |envelope|
95
+ Testing.captured_for(Utilities.constantize(envelope.fetch("task"))) << envelope
96
+ end
97
+ return true
98
+ when :execute
99
+ envelopes.each { |envelope| Testing.execute(envelope) }
100
+ return true
101
+ end
102
+ end
103
+
104
+ @config.redis_pool.pipelined do |pipeline|
105
+ planned, ready = envelopes.partition { |envelope| envelope["run_at"] }
106
+ planned.each do |envelope|
107
+ score = envelope.delete("run_at")
108
+ pipeline.call("ZADD", Keyspace::PLANNED, score, JSON.generate(envelope))
109
+ end
110
+ persist_ready(pipeline, ready)
111
+ end
112
+ true
113
+ end
114
+
115
+ private
116
+
117
+ def normalize(envelope)
118
+ raise InvalidJobError, "Task envelope must be a Hash" unless envelope.is_a?(Hash)
119
+
120
+ item = @config.default_task_options.merge(Utilities.stringify_hash_keys(envelope))
121
+ task = item["task"]
122
+ arguments = item["arguments"]
123
+ unless task.is_a?(Class) || (task.is_a?(String) && !task.empty?)
124
+ raise InvalidJobError, "Task envelope requires a non-empty task"
125
+ end
126
+ raise InvalidJobError, "Task arguments must be an Array" unless arguments.is_a?(Array)
127
+
128
+ item["task"] = task.name if task.is_a?(Class)
129
+ item["channel"] = String(item.fetch("channel"))
130
+ item["id"] ||= SecureRandom.uuid
131
+ item["created_ms"] ||= Utilities.realtime_milliseconds
132
+ item
133
+ end
134
+
135
+ def intercept(envelope)
136
+ context = Publication.new(envelope.fetch("task"), envelope, @config.redis_pool)
137
+ @config.publish_interceptors.call(context) { envelope }
138
+ end
139
+
140
+ def persist_ready(pipeline, envelopes)
141
+ envelopes.group_by { |envelope| envelope.fetch("channel") }.each do |channel, group|
142
+ pipeline.call("SADD", Keyspace::CHANNELS, channel)
143
+ queued_ms = Utilities.realtime_milliseconds
144
+ encoded = group.map do |envelope|
145
+ envelope["queued_ms"] = queued_ms
146
+ JSON.generate(envelope)
147
+ end
148
+ pipeline.call("LPUSH", Keyspace.channel(channel), *encoded)
149
+ end
150
+ end
151
+
152
+ def validate_arguments!(arguments)
153
+ invalid = invalid_json_value(arguments)
154
+ return unless invalid
155
+
156
+ message = "Task arguments must contain JSON-native values with string hash keys; found #{invalid.inspect}"
157
+ case @config.on_complex_arguments
158
+ when :raise then raise InvalidArgumentError, message
159
+ when :warn then @config.logger.warn(message)
160
+ end
161
+ end
162
+
163
+ def invalid_json_value(value)
164
+ case value
165
+ when nil, true, false, String, Integer, Float
166
+ nil
167
+ when Array
168
+ value.each do |nested|
169
+ invalid = invalid_json_value(nested)
170
+ return invalid if invalid
171
+ end
172
+ nil
173
+ when Hash
174
+ value.each do |key, nested|
175
+ return key unless key.is_a?(String)
176
+
177
+ invalid = invalid_json_value(nested)
178
+ return invalid if invalid
179
+ end
180
+ nil
181
+ else
182
+ value
183
+ end
184
+ end
185
+ end
186
+ end
@@ -5,12 +5,12 @@ require "socket"
5
5
 
6
6
  module SolidJobs
7
7
  class Recovery < Service::Base
8
- RESERVED_PATTERN = "*:reserved:*"
8
+ CLAIMED_PATTERN = "#{Keyspace::PREFIX}:node:*:claimed:*".freeze
9
9
  RESTORE_ONE = <<~LUA.freeze
10
10
  local payload = redis.call("rpop", KEYS[1])
11
11
  if payload then
12
- local job = cjson.decode(payload)
13
- redis.call("rpush", "queue:" .. job["queue"], payload)
12
+ local envelope = cjson.decode(payload)
13
+ redis.call("rpush", "#{Keyspace::PREFIX}:channel:" .. envelope["channel"], payload)
14
14
  return payload
15
15
  end
16
16
  LUA
@@ -18,14 +18,15 @@ module SolidJobs
18
18
  def call
19
19
  recovered = 0
20
20
  scan_keys.each do |key|
21
- identity = key.split(":reserved:", 2).first
21
+ node_key = key.split(":claimed:", 2).first
22
+ identity = node_key.delete_prefix("#{Keyspace::PREFIX}:node:")
22
23
  next if alive?(identity)
23
24
 
24
25
  while @config.redis_pool.call("EVAL", RESTORE_ONE, 1, key)
25
26
  recovered += 1
26
27
  end
27
28
  @config.redis_pool.call("DEL", key)
28
- @config.redis_pool.call("DEL", "#{identity}:reservations")
29
+ @config.redis_pool.call("DEL", Keyspace.claims(identity))
29
30
  end
30
31
  recovered
31
32
  end
@@ -36,7 +37,7 @@ module SolidJobs
36
37
  hostname, pid = identity.split(":", 3)
37
38
  return false if hostname == Socket.gethostname && !process_alive?(Integer(pid))
38
39
 
39
- beat = @config.redis_pool.call("HGET", identity, "beat")
40
+ beat = @config.redis_pool.call("HGET", Keyspace.node(identity), "beat")
40
41
  beat && Time.now.to_f - Float(beat) < Heartbeat::TTL
41
42
  end
42
43
 
@@ -54,7 +55,7 @@ module SolidJobs
54
55
  keys = []
55
56
  loop do
56
57
  cursor, found = @config.redis_pool.call(
57
- "SCAN", cursor, "MATCH", RESERVED_PATTERN, "COUNT", 100,
58
+ "SCAN", cursor, "MATCH", CLAIMED_PATTERN, "COUNT", 100,
58
59
  )
59
60
  keys.concat(found)
60
61
  break if cursor == "0"