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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -4
- data/README.md +149 -118
- data/docs/reliability.md +73 -91
- data/exe/solid-jobs +2 -3
- data/lib/active_job/queue_adapters/solid_jobs_adapter.rb +7 -8
- data/lib/solid_jobs/{config.rb → blueprint.rb} +51 -51
- data/lib/solid_jobs/catalog.rb +427 -0
- data/lib/solid_jobs/claim.rb +315 -0
- data/lib/solid_jobs/{server.rb → conductor.rb} +10 -10
- data/lib/solid_jobs/{cli.rb → console.rb} +12 -9
- data/lib/solid_jobs/{processor.rb → engine.rb} +33 -33
- data/lib/solid_jobs/errors.rb +1 -1
- 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/{testing.rb → lab.rb} +19 -19
- data/lib/solid_jobs/publisher.rb +186 -0
- data/lib/solid_jobs/rails_adapter.rb +13 -0
- data/lib/solid_jobs/{rails.rb → railtie.rb} +1 -2
- data/lib/solid_jobs/recovery.rb +8 -7
- data/lib/solid_jobs/task.rb +147 -0
- data/lib/solid_jobs/{scheduler.rb → timer.rb} +8 -8
- data/lib/solid_jobs/utilities.rb +5 -11
- data/lib/solid_jobs/version.rb +1 -1
- data/lib/solid_jobs.rb +28 -27
- metadata +19 -18
- data/lib/solid_jobs/active_job.rb +0 -16
- data/lib/solid_jobs/api.rb +0 -457
- 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/recovery.rb
CHANGED
|
@@ -5,12 +5,12 @@ require "socket"
|
|
|
5
5
|
|
|
6
6
|
module SolidJobs
|
|
7
7
|
class Recovery < Service::Base
|
|
8
|
-
|
|
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
|
|
13
|
-
redis.call("rpush", "
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
|
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/utilities.rb
CHANGED
|
@@ -5,20 +5,14 @@ module SolidJobs
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def stringify_keys(value)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
15
|
+
hash.transform_keys(&:to_s)
|
|
22
16
|
end
|
|
23
17
|
|
|
24
18
|
def shareable_copy(value)
|
data/lib/solid_jobs/version.rb
CHANGED
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/
|
|
13
|
-
require_relative "solid_jobs/
|
|
14
|
-
require_relative "solid_jobs/
|
|
15
|
-
require_relative "solid_jobs/
|
|
16
|
-
require_relative "solid_jobs/
|
|
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/
|
|
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/
|
|
26
|
-
require_relative "solid_jobs/
|
|
27
|
-
require_relative "solid_jobs/
|
|
28
|
-
require_relative "solid_jobs/
|
|
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] ||=
|
|
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] =
|
|
54
|
+
Ractor.current[CONFIG_KEY] = Blueprint.new
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def testing!(mode, &block)
|
|
57
|
-
|
|
58
|
+
Lab.testing!(mode, &block)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
def enqueue(
|
|
61
|
-
|
|
61
|
+
def enqueue(task, arguments, channel: "default", **options)
|
|
62
|
+
Publisher.publish(
|
|
62
63
|
options.merge(
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
64
|
+
"task" => task,
|
|
65
|
+
"arguments" => arguments,
|
|
66
|
+
"channel" => channel,
|
|
66
67
|
),
|
|
67
68
|
)
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
def
|
|
71
|
-
|
|
71
|
+
def enqueue_many(task, argument_sets, channel: "default", **options)
|
|
72
|
+
Publisher.publish_many(
|
|
72
73
|
options.merge(
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
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.
|
|
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
|
|
109
|
-
|
|
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/
|
|
123
|
-
- lib/solid_jobs/
|
|
124
|
-
- lib/solid_jobs/
|
|
125
|
-
- lib/solid_jobs/
|
|
126
|
-
- lib/solid_jobs/
|
|
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/
|
|
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/
|
|
132
|
-
- lib/solid_jobs/
|
|
133
|
-
- lib/solid_jobs/
|
|
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/
|
|
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/
|
|
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
|
-
|