solid-jobs 0.1.3 → 0.4.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -4
  3. data/README.md +149 -118
  4. data/docs/reliability.md +73 -91
  5. data/exe/solid-jobs +2 -3
  6. data/lib/active_job/queue_adapters/solid_jobs_adapter.rb +7 -8
  7. data/lib/solid_jobs/{config.rb → blueprint.rb} +51 -51
  8. data/lib/solid_jobs/catalog.rb +427 -0
  9. data/lib/solid_jobs/claim.rb +315 -0
  10. data/lib/solid_jobs/{server.rb → conductor.rb} +10 -10
  11. data/lib/solid_jobs/{cli.rb → console.rb} +12 -9
  12. data/lib/solid_jobs/{processor.rb → engine.rb} +33 -33
  13. data/lib/solid_jobs/errors.rb +1 -1
  14. data/lib/solid_jobs/executor.rb +34 -0
  15. data/lib/solid_jobs/failure_policy.rb +98 -0
  16. data/lib/solid_jobs/heartbeat.rb +15 -13
  17. data/lib/solid_jobs/integrity_check.rb +39 -36
  18. data/lib/solid_jobs/interceptor_registry.rb +57 -0
  19. data/lib/solid_jobs/keyspace.rb +42 -0
  20. data/lib/solid_jobs/{testing.rb → lab.rb} +19 -19
  21. data/lib/solid_jobs/publisher.rb +186 -0
  22. data/lib/solid_jobs/rails_adapter.rb +13 -0
  23. data/lib/solid_jobs/{rails.rb → railtie.rb} +1 -2
  24. data/lib/solid_jobs/recovery.rb +8 -7
  25. data/lib/solid_jobs/task.rb +147 -0
  26. data/lib/solid_jobs/{scheduler.rb → timer.rb} +8 -8
  27. data/lib/solid_jobs/utilities.rb +5 -11
  28. data/lib/solid_jobs/version.rb +1 -1
  29. data/lib/solid_jobs.rb +28 -27
  30. metadata +19 -18
  31. data/lib/solid_jobs/active_job.rb +0 -16
  32. data/lib/solid_jobs/api.rb +0 -457
  33. data/lib/solid_jobs/client.rb +0 -202
  34. data/lib/solid_jobs/fetch.rb +0 -310
  35. data/lib/solid_jobs/job.rb +0 -156
  36. data/lib/solid_jobs/middleware/chain.rb +0 -104
  37. data/lib/solid_jobs/retry_service.rb +0 -100
  38. data/lib/solid_jobs/worker.rb +0 -35
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "active_job"
3
+ require_relative "rails_adapter"
4
4
 
5
5
  if defined?(Rails::Railtie)
6
6
  module SolidJobs
@@ -13,4 +13,3 @@ if defined?(Rails::Railtie)
13
13
  end
14
14
  end
15
15
  end
16
-
@@ -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"
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidJobs
4
+ module Task
5
+ def self.included(base)
6
+ base.extend(TaskMethods)
7
+ base.include(InstanceMethods)
8
+ base.task_options
9
+ end
10
+
11
+ module InstanceMethods
12
+ attr_accessor :task_id
13
+ attr_writer :_context
14
+
15
+ def _context
16
+ @_context ||= {}
17
+ end
18
+
19
+ def logger
20
+ SolidJobs.config.logger
21
+ end
22
+
23
+ def interrupted?
24
+ _context[:interrupted]&.call || false
25
+ end
26
+ end
27
+
28
+ module TaskMethods
29
+ def task_options(options = nil)
30
+ if options
31
+ merged = current_task_options.merge(Utilities.stringify_keys(options))
32
+ @task_options = Utilities.shareable_copy(merged)
33
+ else
34
+ @task_options ||= Utilities.shareable_copy(SolidJobs.config.default_task_options)
35
+ end
36
+ end
37
+
38
+ def current_task_options
39
+ task_options
40
+ end
41
+
42
+ def retry_delay(&block)
43
+ define_singleton_method(:retry_delay_callback, &block) if block
44
+ method(:retry_delay_callback) if respond_to?(:retry_delay_callback)
45
+ end
46
+
47
+ def after_final_failure(&block)
48
+ define_singleton_method(:final_failure_callback, &block) if block
49
+ method(:final_failure_callback) if respond_to?(:final_failure_callback)
50
+ end
51
+
52
+ def enqueue(*arguments)
53
+ publish("task" => self, "arguments" => arguments)
54
+ end
55
+
56
+ def enqueue_many(argument_sets, chunk_size: Publisher::DEFAULT_CHUNK_SIZE)
57
+ Publisher.new.publish_many(
58
+ current_task_options.merge(
59
+ "task" => self,
60
+ "arguments" => argument_sets,
61
+ "chunk_size" => chunk_size,
62
+ ),
63
+ )
64
+ end
65
+
66
+ def enqueue_after(delay, *arguments)
67
+ schedule_at(Time.now.to_f + Float(delay), *arguments)
68
+ end
69
+
70
+ def schedule_at(time, *arguments)
71
+ timestamp = time.is_a?(Time) ? time.to_f : Float(time)
72
+ publish("task" => self, "arguments" => arguments, "run_at" => timestamp)
73
+ end
74
+
75
+ def execute(*arguments)
76
+ Lab.execute(current_task_options.merge("task" => self, "arguments" => arguments))
77
+ end
78
+
79
+ def with_options(options)
80
+ Submission.new(self, options)
81
+ end
82
+
83
+ def captured
84
+ Lab.captured_for(self)
85
+ end
86
+
87
+ def clear_captured
88
+ captured.clear
89
+ end
90
+
91
+ def execute_next
92
+ envelope = captured.shift
93
+ raise NoCapturedTask, "No captured task for #{name}" unless envelope
94
+
95
+ Lab.execute(envelope)
96
+ end
97
+
98
+ private
99
+
100
+ def publish(envelope)
101
+ Publisher.new.publish(current_task_options.merge(envelope))
102
+ end
103
+ end
104
+
105
+ class Submission
106
+ def initialize(task, options)
107
+ @task = task
108
+ @options = Utilities.stringify_keys(options)
109
+ end
110
+
111
+ def enqueue(*arguments)
112
+ return @task.execute(*arguments) if @options.delete("execute")
113
+
114
+ if (delay = @options.delete("delay"))
115
+ enqueue_after(delay, *arguments)
116
+ elsif (time = @options.delete("run_at"))
117
+ schedule_at(time, *arguments)
118
+ else
119
+ publish(arguments)
120
+ end
121
+ end
122
+
123
+ def enqueue_after(delay, *arguments)
124
+ schedule_at(Time.now.to_f + Float(delay), *arguments)
125
+ end
126
+
127
+ def schedule_at(time, *arguments)
128
+ timestamp = time.is_a?(Time) ? time.to_f : Float(time)
129
+ publish(arguments, "run_at" => timestamp)
130
+ end
131
+
132
+ private
133
+
134
+ def publish(arguments, extra = {})
135
+ Publisher.new.publish(
136
+ @task.current_task_options.merge(@options).merge(
137
+ extra,
138
+ "task" => @task,
139
+ "arguments" => arguments,
140
+ ),
141
+ )
142
+ end
143
+ end
144
+ end
145
+
146
+ class NoCapturedTask < Error; end
147
+ end
@@ -3,8 +3,8 @@
3
3
  require "json"
4
4
 
5
5
  module SolidJobs
6
- class Scheduler
7
- SETS = %w[retry schedule].freeze
6
+ class Timer
7
+ COLLECTIONS = [Keyspace::RETRIES, Keyspace::PLANNED].freeze
8
8
  POP_DUE = <<~LUA.freeze
9
9
  local jobs = redis.call("zrange", KEYS[1], "-inf", ARGV[1], "byscore", "limit", 0, 1)
10
10
  if jobs[1] then
@@ -15,14 +15,14 @@ module SolidJobs
15
15
 
16
16
  def initialize(config)
17
17
  @config = config
18
- @client = Client.new(config: config)
18
+ @publisher = Publisher.new(config: config)
19
19
  end
20
20
 
21
21
  def enqueue_due(now = Time.now.to_f)
22
22
  count = 0
23
- SETS.each do |set|
24
- while (raw = pop_due(set, now))
25
- @client.push(JSON.parse(raw))
23
+ COLLECTIONS.each do |collection|
24
+ while (raw = pop_due(collection, now))
25
+ @publisher.publish(JSON.parse(raw))
26
26
  count += 1
27
27
  end
28
28
  end
@@ -45,8 +45,8 @@ module SolidJobs
45
45
 
46
46
  private
47
47
 
48
- def pop_due(set, now)
49
- @config.redis_pool.call("EVAL", POP_DUE, 1, set, now)
48
+ def pop_due(collection, now)
49
+ @config.redis_pool.call("EVAL", POP_DUE, 1, collection, now)
50
50
  end
51
51
 
52
52
  def randomized_interval
@@ -5,20 +5,14 @@ module SolidJobs
5
5
  module_function
6
6
 
7
7
  def stringify_keys(value)
8
- case value
9
- when Hash
10
- value.each_with_object({}) do |(key, nested), result|
11
- result[key.to_s] = stringify_keys(nested)
12
- end
13
- when Array
14
- value.map { |nested| stringify_keys(nested) }
15
- else
16
- value
17
- end
8
+ return stringify_hash_keys(value).transform_values { |nested| stringify_keys(nested) } if value.is_a?(Hash)
9
+ return value.map { |nested| stringify_keys(nested) } if value.is_a?(Array)
10
+
11
+ value
18
12
  end
19
13
 
20
14
  def stringify_hash_keys(hash)
21
- hash.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
15
+ hash.transform_keys(&:to_s)
22
16
  end
23
17
 
24
18
  def shareable_copy(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidJobs
4
- VERSION = "0.1.3"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/solid_jobs.rb CHANGED
@@ -7,25 +7,26 @@ require "solid_redis"
7
7
  require_relative "solid_jobs/version"
8
8
  require_relative "solid_jobs/errors"
9
9
  require_relative "solid_jobs/utilities"
10
+ require_relative "solid_jobs/keyspace"
10
11
  require_relative "solid_jobs/ractor_support"
11
12
  require_relative "solid_jobs/startup_barrier"
12
- require_relative "solid_jobs/middleware/chain"
13
- require_relative "solid_jobs/config"
14
- require_relative "solid_jobs/client"
15
- require_relative "solid_jobs/testing"
16
- require_relative "solid_jobs/job"
17
- require_relative "solid_jobs/worker"
18
- require_relative "solid_jobs/fetch"
19
- require_relative "solid_jobs/retry_service"
20
- require_relative "solid_jobs/scheduler"
21
- require_relative "solid_jobs/processor"
13
+ require_relative "solid_jobs/interceptor_registry"
14
+ require_relative "solid_jobs/blueprint"
15
+ require_relative "solid_jobs/publisher"
16
+ require_relative "solid_jobs/lab"
17
+ require_relative "solid_jobs/task"
18
+ require_relative "solid_jobs/executor"
19
+ require_relative "solid_jobs/claim"
20
+ require_relative "solid_jobs/failure_policy"
21
+ require_relative "solid_jobs/timer"
22
+ require_relative "solid_jobs/engine"
22
23
  require_relative "solid_jobs/heartbeat"
23
24
  require_relative "solid_jobs/recovery"
24
25
  require_relative "solid_jobs/integrity_check"
25
- require_relative "solid_jobs/server"
26
- require_relative "solid_jobs/api"
27
- require_relative "solid_jobs/active_job" if defined?(::ActiveJob::Base)
28
- require_relative "solid_jobs/rails" if defined?(::Rails::Railtie)
26
+ require_relative "solid_jobs/conductor"
27
+ require_relative "solid_jobs/catalog"
28
+ require_relative "solid_jobs/rails_adapter" if defined?(::ActiveJob::Base)
29
+ require_relative "solid_jobs/railtie" if defined?(::Rails::Railtie)
29
30
 
30
31
  module SolidJobs
31
32
  CONFIG_KEY = :solid_jobs_config
@@ -35,7 +36,7 @@ module SolidJobs
35
36
  # Configuration is Ractor-local: every thread and fiber inside a Ractor
36
37
  # shares it, while each Ractor keeps its own isolated configuration.
37
38
  def config
38
- Ractor.current[CONFIG_KEY] ||= Config.new
39
+ Ractor.current[CONFIG_KEY] ||= Blueprint.new
39
40
  end
40
41
 
41
42
  def configure
@@ -50,29 +51,29 @@ module SolidJobs
50
51
 
51
52
  def reset!
52
53
  Ractor.current[CONFIG_KEY]&.close
53
- Ractor.current[CONFIG_KEY] = Config.new
54
+ Ractor.current[CONFIG_KEY] = Blueprint.new
54
55
  end
55
56
 
56
57
  def testing!(mode, &block)
57
- Testing.testing!(mode, &block)
58
+ Lab.testing!(mode, &block)
58
59
  end
59
60
 
60
- def enqueue(job_class, args, queue: "default", **options)
61
- Client.push(
61
+ def enqueue(task, arguments, channel: "default", **options)
62
+ Publisher.publish(
62
63
  options.merge(
63
- "class" => job_class,
64
- "args" => args,
65
- "queue" => queue,
64
+ "task" => task,
65
+ "arguments" => arguments,
66
+ "channel" => channel,
66
67
  ),
67
68
  )
68
69
  end
69
70
 
70
- def enqueue_bulk(job_class, arguments, queue: "default", **options)
71
- Client.push_bulk(
71
+ def enqueue_many(task, argument_sets, channel: "default", **options)
72
+ Publisher.publish_many(
72
73
  options.merge(
73
- "class" => job_class,
74
- "args" => arguments,
75
- "queue" => queue,
74
+ "task" => task,
75
+ "arguments" => argument_sets,
76
+ "channel" => channel,
76
77
  ),
77
78
  )
78
79
  end
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.1.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Vandenbogaerde
@@ -105,8 +105,8 @@ dependencies:
105
105
  - - "~>"
106
106
  - !ruby/object:Gem::Version
107
107
  version: '13.0'
108
- description: Ractor-local job clients and workers with Sidekiq-compatible Redis payloads,
109
- queues, scheduling, and retries.
108
+ description: A Ractor-local Redis task runtime with its own envelope, channels, claims,
109
+ interceptors, scheduling, and failure handling.
110
110
  executables:
111
111
  - solid-jobs
112
112
  extensions: []
@@ -119,29 +119,30 @@ 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/active_job.rb
123
- - lib/solid_jobs/api.rb
124
- - lib/solid_jobs/cli.rb
125
- - lib/solid_jobs/client.rb
126
- - lib/solid_jobs/config.rb
122
+ - lib/solid_jobs/blueprint.rb
123
+ - lib/solid_jobs/catalog.rb
124
+ - lib/solid_jobs/claim.rb
125
+ - lib/solid_jobs/conductor.rb
126
+ - lib/solid_jobs/console.rb
127
+ - lib/solid_jobs/engine.rb
127
128
  - lib/solid_jobs/errors.rb
128
- - lib/solid_jobs/fetch.rb
129
+ - lib/solid_jobs/executor.rb
130
+ - lib/solid_jobs/failure_policy.rb
129
131
  - lib/solid_jobs/heartbeat.rb
130
132
  - lib/solid_jobs/integrity_check.rb
131
- - lib/solid_jobs/job.rb
132
- - lib/solid_jobs/middleware/chain.rb
133
- - lib/solid_jobs/processor.rb
133
+ - lib/solid_jobs/interceptor_registry.rb
134
+ - lib/solid_jobs/keyspace.rb
135
+ - lib/solid_jobs/lab.rb
136
+ - lib/solid_jobs/publisher.rb
134
137
  - lib/solid_jobs/ractor_support.rb
135
- - lib/solid_jobs/rails.rb
138
+ - lib/solid_jobs/rails_adapter.rb
139
+ - lib/solid_jobs/railtie.rb
136
140
  - lib/solid_jobs/recovery.rb
137
- - lib/solid_jobs/retry_service.rb
138
- - lib/solid_jobs/scheduler.rb
139
- - lib/solid_jobs/server.rb
140
141
  - lib/solid_jobs/startup_barrier.rb
141
- - lib/solid_jobs/testing.rb
142
+ - lib/solid_jobs/task.rb
143
+ - lib/solid_jobs/timer.rb
142
144
  - lib/solid_jobs/utilities.rb
143
145
  - lib/solid_jobs/version.rb
144
- - lib/solid_jobs/worker.rb
145
146
  homepage: https://github.com/nicolasva/solid-jobs
146
147
  licenses:
147
148
  - MIT
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidJobs
4
- module ActiveJob
5
- class Wrapper
6
- include SolidJobs::Job
7
- solid_jobs_options retry: true
8
-
9
- def perform(job_data)
10
- ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
11
- end
12
- end
13
- JobWrapper = Wrapper
14
- end
15
- end
16
-