solid-jobs 0.1.2 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -2
- data/README.md +137 -117
- data/docs/reliability.md +71 -89
- data/lib/active_job/queue_adapters/solid_jobs_adapter.rb +6 -7
- data/lib/solid_jobs/active_job.rb +2 -5
- data/lib/solid_jobs/api.rb +133 -156
- data/lib/solid_jobs/claim.rb +315 -0
- data/lib/solid_jobs/cli.rb +9 -6
- data/lib/solid_jobs/config.rb +50 -50
- data/lib/solid_jobs/executor.rb +34 -0
- data/lib/solid_jobs/failure_policy.rb +98 -0
- data/lib/solid_jobs/heartbeat.rb +15 -13
- data/lib/solid_jobs/integrity_check.rb +39 -36
- data/lib/solid_jobs/interceptor_registry.rb +57 -0
- data/lib/solid_jobs/keyspace.rb +42 -0
- data/lib/solid_jobs/processor.rb +30 -30
- data/lib/solid_jobs/publisher.rb +186 -0
- data/lib/solid_jobs/recovery.rb +8 -7
- data/lib/solid_jobs/server.rb +2 -2
- data/lib/solid_jobs/task.rb +147 -0
- data/lib/solid_jobs/testing.rb +18 -18
- data/lib/solid_jobs/{scheduler.rb → timer.rb} +8 -8
- data/lib/solid_jobs/version.rb +1 -2
- data/lib/solid_jobs.rb +18 -18
- metadata +13 -32
- data/lib/solid_jobs/client.rb +0 -202
- data/lib/solid_jobs/fetch.rb +0 -310
- data/lib/solid_jobs/job.rb +0 -156
- data/lib/solid_jobs/middleware/chain.rb +0 -104
- data/lib/solid_jobs/retry_service.rb +0 -100
- data/lib/solid_jobs/worker.rb +0 -35
data/lib/solid_jobs/server.rb
CHANGED
|
@@ -163,7 +163,7 @@ module SolidJobs
|
|
|
163
163
|
started = SolidJobs::StartupBarrier.boot(ready) do
|
|
164
164
|
local_config = SolidJobs::Config.from_ractor_snapshot(settings)
|
|
165
165
|
SolidJobs.use_config(local_config)
|
|
166
|
-
scheduler = SolidJobs::
|
|
166
|
+
scheduler = SolidJobs::Timer.new(local_config)
|
|
167
167
|
local_config.redis_pool.call("PING")
|
|
168
168
|
end
|
|
169
169
|
next :aborted unless started
|
|
@@ -265,7 +265,7 @@ module SolidJobs
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
def remote_signal
|
|
268
|
-
config.redis_pool.call("RPOP",
|
|
268
|
+
config.redis_pool.call("RPOP", Keyspace.node_signals(identity))
|
|
269
269
|
end
|
|
270
270
|
|
|
271
271
|
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidJobs
|
|
4
|
+
module Task
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
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 ClassMethods
|
|
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
|
+
enqueue_at(Time.now.to_f + Float(delay), *arguments)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def enqueue_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
|
+
Testing.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
|
+
Testing.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 EmptyQueueError, "No captured task for #{name}" unless envelope
|
|
94
|
+
|
|
95
|
+
Testing.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
|
+
enqueue_at(time, *arguments)
|
|
118
|
+
else
|
|
119
|
+
publish(arguments)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def enqueue_after(delay, *arguments)
|
|
124
|
+
enqueue_at(Time.now.to_f + Float(delay), *arguments)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def enqueue_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 EmptyQueueError < Error; end
|
|
147
|
+
end
|
data/lib/solid_jobs/testing.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module SolidJobs
|
|
4
6
|
module Testing
|
|
5
|
-
MODES = %i[disable
|
|
6
|
-
STORAGE_KEY = :
|
|
7
|
+
MODES = %i[disable capture execute].freeze
|
|
8
|
+
STORAGE_KEY = :solid_jobs_testing_captured
|
|
7
9
|
MODE_KEY = :solid_jobs_testing_mode
|
|
8
10
|
|
|
9
11
|
module_function
|
|
@@ -29,14 +31,14 @@ module SolidJobs
|
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
def
|
|
33
|
-
testing!(:
|
|
34
|
-
testing!(:
|
|
34
|
+
def capture!
|
|
35
|
+
testing!(:capture) { yield } if block_given?
|
|
36
|
+
testing!(:capture) unless block_given?
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
def
|
|
38
|
-
testing!(:
|
|
39
|
-
testing!(:
|
|
39
|
+
def execute!
|
|
40
|
+
testing!(:execute) { yield } if block_given?
|
|
41
|
+
testing!(:execute) unless block_given?
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def disable!
|
|
@@ -44,22 +46,20 @@ module SolidJobs
|
|
|
44
46
|
testing!(:disable) unless block_given?
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
def
|
|
48
|
-
storage[
|
|
49
|
+
def captured_for(task)
|
|
50
|
+
storage[task.name] ||= []
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def clear_all
|
|
52
54
|
storage.clear
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
SolidJobs.config.
|
|
61
|
-
instance.perform(*payload.fetch("args"))
|
|
62
|
-
end
|
|
57
|
+
def execute(envelope)
|
|
58
|
+
normalized = Utilities.stringify_hash_keys(envelope)
|
|
59
|
+
normalized["task"] = normalized.fetch("task").name if normalized["task"].is_a?(Class)
|
|
60
|
+
normalized["channel"] ||= "default"
|
|
61
|
+
normalized["id"] ||= SecureRandom.uuid
|
|
62
|
+
Executor.new(redis_config: SolidJobs.config.redis_config).execute(normalized)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def storage
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module SolidJobs
|
|
6
|
-
class
|
|
7
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
24
|
-
while (raw = pop_due(
|
|
25
|
-
@
|
|
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(
|
|
49
|
-
@config.redis_pool.call("EVAL", POP_DUE, 1,
|
|
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
|
data/lib/solid_jobs/version.rb
CHANGED
data/lib/solid_jobs.rb
CHANGED
|
@@ -3,22 +3,22 @@
|
|
|
3
3
|
require "base_service"
|
|
4
4
|
require "callback_collection"
|
|
5
5
|
require "solid_redis"
|
|
6
|
-
require "solid_resp_ractor"
|
|
7
6
|
|
|
8
7
|
require_relative "solid_jobs/version"
|
|
9
8
|
require_relative "solid_jobs/errors"
|
|
10
9
|
require_relative "solid_jobs/utilities"
|
|
10
|
+
require_relative "solid_jobs/keyspace"
|
|
11
11
|
require_relative "solid_jobs/ractor_support"
|
|
12
12
|
require_relative "solid_jobs/startup_barrier"
|
|
13
|
-
require_relative "solid_jobs/
|
|
13
|
+
require_relative "solid_jobs/interceptor_registry"
|
|
14
14
|
require_relative "solid_jobs/config"
|
|
15
|
-
require_relative "solid_jobs/
|
|
15
|
+
require_relative "solid_jobs/publisher"
|
|
16
16
|
require_relative "solid_jobs/testing"
|
|
17
|
-
require_relative "solid_jobs/
|
|
18
|
-
require_relative "solid_jobs/
|
|
19
|
-
require_relative "solid_jobs/
|
|
20
|
-
require_relative "solid_jobs/
|
|
21
|
-
require_relative "solid_jobs/
|
|
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
22
|
require_relative "solid_jobs/processor"
|
|
23
23
|
require_relative "solid_jobs/heartbeat"
|
|
24
24
|
require_relative "solid_jobs/recovery"
|
|
@@ -58,22 +58,22 @@ module SolidJobs
|
|
|
58
58
|
Testing.testing!(mode, &block)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
def enqueue(
|
|
62
|
-
|
|
61
|
+
def enqueue(task, arguments, channel: "default", **options)
|
|
62
|
+
Publisher.publish(
|
|
63
63
|
options.merge(
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
64
|
+
"task" => task,
|
|
65
|
+
"arguments" => arguments,
|
|
66
|
+
"channel" => channel,
|
|
67
67
|
),
|
|
68
68
|
)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def
|
|
72
|
-
|
|
71
|
+
def enqueue_many(task, argument_sets, channel: "default", **options)
|
|
72
|
+
Publisher.publish_many(
|
|
73
73
|
options.merge(
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
74
|
+
"task" => task,
|
|
75
|
+
"arguments" => argument_sets,
|
|
76
|
+
"channel" => channel,
|
|
77
77
|
),
|
|
78
78
|
)
|
|
79
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -60,7 +60,7 @@ dependencies:
|
|
|
60
60
|
version: '1.0'
|
|
61
61
|
- - ">="
|
|
62
62
|
- !ruby/object:Gem::Version
|
|
63
|
-
version: 1.0.
|
|
63
|
+
version: 1.0.11
|
|
64
64
|
type: :runtime
|
|
65
65
|
prerelease: false
|
|
66
66
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -70,27 +70,7 @@ dependencies:
|
|
|
70
70
|
version: '1.0'
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: 1.0.
|
|
74
|
-
- !ruby/object:Gem::Dependency
|
|
75
|
-
name: solid-resp-ractor
|
|
76
|
-
requirement: !ruby/object:Gem::Requirement
|
|
77
|
-
requirements:
|
|
78
|
-
- - "~>"
|
|
79
|
-
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0.1'
|
|
81
|
-
- - ">="
|
|
82
|
-
- !ruby/object:Gem::Version
|
|
83
|
-
version: 0.1.3
|
|
84
|
-
type: :runtime
|
|
85
|
-
prerelease: false
|
|
86
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
87
|
-
requirements:
|
|
88
|
-
- - "~>"
|
|
89
|
-
- !ruby/object:Gem::Version
|
|
90
|
-
version: '0.1'
|
|
91
|
-
- - ">="
|
|
92
|
-
- !ruby/object:Gem::Version
|
|
93
|
-
version: 0.1.3
|
|
73
|
+
version: 1.0.11
|
|
94
74
|
- !ruby/object:Gem::Dependency
|
|
95
75
|
name: minitest
|
|
96
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -125,8 +105,8 @@ dependencies:
|
|
|
125
105
|
- - "~>"
|
|
126
106
|
- !ruby/object:Gem::Version
|
|
127
107
|
version: '13.0'
|
|
128
|
-
description: Ractor-local
|
|
129
|
-
|
|
108
|
+
description: A Ractor-local Redis task runtime with its own envelope, channels, claims,
|
|
109
|
+
interceptors, scheduling, and failure handling.
|
|
130
110
|
executables:
|
|
131
111
|
- solid-jobs
|
|
132
112
|
extensions: []
|
|
@@ -141,27 +121,28 @@ files:
|
|
|
141
121
|
- lib/solid_jobs.rb
|
|
142
122
|
- lib/solid_jobs/active_job.rb
|
|
143
123
|
- lib/solid_jobs/api.rb
|
|
124
|
+
- lib/solid_jobs/claim.rb
|
|
144
125
|
- lib/solid_jobs/cli.rb
|
|
145
|
-
- lib/solid_jobs/client.rb
|
|
146
126
|
- lib/solid_jobs/config.rb
|
|
147
127
|
- lib/solid_jobs/errors.rb
|
|
148
|
-
- lib/solid_jobs/
|
|
128
|
+
- lib/solid_jobs/executor.rb
|
|
129
|
+
- lib/solid_jobs/failure_policy.rb
|
|
149
130
|
- lib/solid_jobs/heartbeat.rb
|
|
150
131
|
- lib/solid_jobs/integrity_check.rb
|
|
151
|
-
- lib/solid_jobs/
|
|
152
|
-
- lib/solid_jobs/
|
|
132
|
+
- lib/solid_jobs/interceptor_registry.rb
|
|
133
|
+
- lib/solid_jobs/keyspace.rb
|
|
153
134
|
- lib/solid_jobs/processor.rb
|
|
135
|
+
- lib/solid_jobs/publisher.rb
|
|
154
136
|
- lib/solid_jobs/ractor_support.rb
|
|
155
137
|
- lib/solid_jobs/rails.rb
|
|
156
138
|
- lib/solid_jobs/recovery.rb
|
|
157
|
-
- lib/solid_jobs/retry_service.rb
|
|
158
|
-
- lib/solid_jobs/scheduler.rb
|
|
159
139
|
- lib/solid_jobs/server.rb
|
|
160
140
|
- lib/solid_jobs/startup_barrier.rb
|
|
141
|
+
- lib/solid_jobs/task.rb
|
|
161
142
|
- lib/solid_jobs/testing.rb
|
|
143
|
+
- lib/solid_jobs/timer.rb
|
|
162
144
|
- lib/solid_jobs/utilities.rb
|
|
163
145
|
- lib/solid_jobs/version.rb
|
|
164
|
-
- lib/solid_jobs/worker.rb
|
|
165
146
|
homepage: https://github.com/nicolasva/solid-jobs
|
|
166
147
|
licenses:
|
|
167
148
|
- MIT
|
data/lib/solid_jobs/client.rb
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
|
|
6
|
-
module SolidJobs
|
|
7
|
-
class Client
|
|
8
|
-
DEFAULT_BATCH_SIZE = 1_000
|
|
9
|
-
SCHEDULED_BATCH_SIZE = 100
|
|
10
|
-
|
|
11
|
-
def self.push(item)
|
|
12
|
-
new.push(item)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.push_bulk(item)
|
|
16
|
-
new.push_bulk(item)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def initialize(config: nil, configuration: nil)
|
|
20
|
-
selected = config || configuration || SolidJobs.config
|
|
21
|
-
@config = selected.is_a?(SolidRedis::Config) ? Config.new(redis: selected) : selected
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def push(item)
|
|
25
|
-
payload = normalize_item(item)
|
|
26
|
-
payload = invoke_middleware(payload)
|
|
27
|
-
return unless payload
|
|
28
|
-
|
|
29
|
-
verify_json!(payload)
|
|
30
|
-
raw_push([payload])
|
|
31
|
-
payload["jid"]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def push_bulk(item)
|
|
35
|
-
raise InvalidJobError, "Job payload must be a Hash" unless item.is_a?(Hash)
|
|
36
|
-
|
|
37
|
-
source = Utilities.stringify_hash_keys(item)
|
|
38
|
-
args = source.delete("args")
|
|
39
|
-
raise InvalidJobError, "Job 'args' must be an Array or Enumerable" unless args.respond_to?(:each)
|
|
40
|
-
|
|
41
|
-
ats = source.delete("at")
|
|
42
|
-
if ats && !ats.is_a?(Numeric) && !ats.is_a?(Array)
|
|
43
|
-
raise InvalidJobError, "Job 'at' must be a Numeric or an Array"
|
|
44
|
-
end
|
|
45
|
-
if ats.is_a?(Array) && args.respond_to?(:size) && ats.size != args.size
|
|
46
|
-
raise InvalidJobError, "Job 'at' Array must have the same size as 'args'"
|
|
47
|
-
end
|
|
48
|
-
batch_size = Integer(source.delete("batch_size") || (ats ? SCHEDULED_BATCH_SIZE : DEFAULT_BATCH_SIZE))
|
|
49
|
-
raise ArgumentError, "batch_size must be positive" unless batch_size.positive?
|
|
50
|
-
|
|
51
|
-
explicit_jid = source.delete("jid")
|
|
52
|
-
if explicit_jid && args.respond_to?(:size) && args.size > 1
|
|
53
|
-
raise InvalidJobError, "Explicit jid is only supported for one bulk job"
|
|
54
|
-
end
|
|
55
|
-
base = normalize_item(source.merge("args" => []))
|
|
56
|
-
base.delete("jid")
|
|
57
|
-
result = []
|
|
58
|
-
args.each_slice(batch_size).with_index do |batch, batch_index|
|
|
59
|
-
payloads = []
|
|
60
|
-
batch.each_with_index do |job_args, index|
|
|
61
|
-
raise InvalidJobError, "Bulk job arguments must be Arrays" unless job_args.is_a?(Array)
|
|
62
|
-
|
|
63
|
-
payload = base.merge(
|
|
64
|
-
"args" => job_args,
|
|
65
|
-
"jid" => explicit_jid || SecureRandom.hex(12),
|
|
66
|
-
)
|
|
67
|
-
payload["at"] = ats.is_a?(Array) ? ats[batch_index * batch_size + index] : ats if ats
|
|
68
|
-
normalized = invoke_middleware(payload)
|
|
69
|
-
if normalized
|
|
70
|
-
verify_json!(normalized)
|
|
71
|
-
payloads << normalized
|
|
72
|
-
result << normalized["jid"]
|
|
73
|
-
else
|
|
74
|
-
result << nil
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
raw_push(payloads)
|
|
78
|
-
end
|
|
79
|
-
result
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def raw_push(payloads)
|
|
83
|
-
return true if payloads.empty?
|
|
84
|
-
|
|
85
|
-
if defined?(Testing)
|
|
86
|
-
case Testing.mode
|
|
87
|
-
when :fake
|
|
88
|
-
payloads.each do |payload|
|
|
89
|
-
Testing.jobs_for(Utilities.constantize(payload["class"])) << payload
|
|
90
|
-
end
|
|
91
|
-
return true
|
|
92
|
-
when :inline
|
|
93
|
-
payloads.each { |payload| Testing.execute_inline(payload) }
|
|
94
|
-
return true
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
@config.redis_pool.pipelined do |pipeline|
|
|
99
|
-
if payloads.all? { |payload| payload["at"] }
|
|
100
|
-
payloads.each do |payload|
|
|
101
|
-
score = payload.delete("at")
|
|
102
|
-
pipeline.call("ZADD", "schedule", score, JSON.generate(payload))
|
|
103
|
-
end
|
|
104
|
-
elsif payloads.none? { |payload| payload["at"] }
|
|
105
|
-
push_immediate(pipeline, payloads)
|
|
106
|
-
else
|
|
107
|
-
scheduled, immediate = payloads.partition { |payload| payload["at"] }
|
|
108
|
-
scheduled.each do |payload|
|
|
109
|
-
score = payload.delete("at")
|
|
110
|
-
pipeline.call("ZADD", "schedule", score, JSON.generate(payload))
|
|
111
|
-
end
|
|
112
|
-
push_immediate(pipeline, immediate)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
true
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
private
|
|
119
|
-
|
|
120
|
-
def normalize_item(item)
|
|
121
|
-
raise InvalidJobError, "Job payload must be a Hash" unless item.is_a?(Hash)
|
|
122
|
-
|
|
123
|
-
payload = @config.default_job_options.merge(Utilities.stringify_hash_keys(item))
|
|
124
|
-
payload["class"] ||= payload.delete("job_class")
|
|
125
|
-
payload["jid"] ||= payload.delete("job_id")
|
|
126
|
-
job_class = payload["class"]
|
|
127
|
-
args = payload["args"]
|
|
128
|
-
unless job_class.is_a?(Class) || (job_class.is_a?(String) && !job_class.empty?)
|
|
129
|
-
raise InvalidJobError, "Job must include a non-empty class"
|
|
130
|
-
end
|
|
131
|
-
raise InvalidJobError, "Job args must be an Array" unless args.is_a?(Array)
|
|
132
|
-
|
|
133
|
-
payload["class"] = job_class.name if job_class.is_a?(Class)
|
|
134
|
-
payload["queue"] = String(payload.fetch("queue"))
|
|
135
|
-
payload["jid"] ||= SecureRandom.hex(12)
|
|
136
|
-
payload["created_at"] ||= Utilities.realtime_milliseconds
|
|
137
|
-
payload
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def invoke_middleware(payload)
|
|
141
|
-
job_class = payload["class"]
|
|
142
|
-
@config.client_middleware.invoke(job_class, payload, payload["queue"], @config.redis_pool) do
|
|
143
|
-
payload
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def push_immediate(pipeline, payloads)
|
|
148
|
-
return if payloads.empty?
|
|
149
|
-
|
|
150
|
-
groups = if payloads.all? { |payload| payload["queue"] == payloads.first["queue"] }
|
|
151
|
-
[[payloads.first["queue"], payloads]]
|
|
152
|
-
else
|
|
153
|
-
payloads.group_by { |payload| payload["queue"] }
|
|
154
|
-
end
|
|
155
|
-
groups.each do |queue, jobs|
|
|
156
|
-
pipeline.call("SADD", "queues", queue)
|
|
157
|
-
now = Utilities.realtime_milliseconds
|
|
158
|
-
encoded = jobs.map do |payload|
|
|
159
|
-
payload["enqueued_at"] = now
|
|
160
|
-
JSON.generate(payload)
|
|
161
|
-
end
|
|
162
|
-
pipeline.call("LPUSH", "queue:#{queue}", *encoded)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def verify_json!(payload)
|
|
167
|
-
invalid = find_invalid_json_value(payload["args"])
|
|
168
|
-
return unless invalid
|
|
169
|
-
|
|
170
|
-
message = "Job arguments must contain only JSON-native values with string hash keys; found #{invalid.inspect}"
|
|
171
|
-
case @config.on_complex_arguments
|
|
172
|
-
when :raise
|
|
173
|
-
raise InvalidArgumentError, message
|
|
174
|
-
when :warn
|
|
175
|
-
@config.logger.warn(message)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def find_invalid_json_value(value)
|
|
180
|
-
case value
|
|
181
|
-
when nil, true, false, String, Integer, Float
|
|
182
|
-
nil
|
|
183
|
-
when Array
|
|
184
|
-
value.each do |nested|
|
|
185
|
-
invalid = find_invalid_json_value(nested)
|
|
186
|
-
return invalid if invalid
|
|
187
|
-
end
|
|
188
|
-
nil
|
|
189
|
-
when Hash
|
|
190
|
-
value.each do |key, nested|
|
|
191
|
-
return key unless key.is_a?(String)
|
|
192
|
-
|
|
193
|
-
invalid = find_invalid_json_value(nested)
|
|
194
|
-
return invalid if invalid
|
|
195
|
-
end
|
|
196
|
-
nil
|
|
197
|
-
else
|
|
198
|
-
value
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|