jiggler 0.1.0.rc2
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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE +4 -0
- data/README.md +423 -0
- data/bin/jiggler +31 -0
- data/lib/jiggler/cleaner.rb +130 -0
- data/lib/jiggler/cli.rb +263 -0
- data/lib/jiggler/config.rb +165 -0
- data/lib/jiggler/core.rb +22 -0
- data/lib/jiggler/errors.rb +5 -0
- data/lib/jiggler/job.rb +116 -0
- data/lib/jiggler/launcher.rb +69 -0
- data/lib/jiggler/manager.rb +73 -0
- data/lib/jiggler/redis_store.rb +55 -0
- data/lib/jiggler/retrier.rb +122 -0
- data/lib/jiggler/scheduled/enqueuer.rb +78 -0
- data/lib/jiggler/scheduled/poller.rb +97 -0
- data/lib/jiggler/stats/collection.rb +26 -0
- data/lib/jiggler/stats/monitor.rb +103 -0
- data/lib/jiggler/summary.rb +101 -0
- data/lib/jiggler/support/helper.rb +35 -0
- data/lib/jiggler/version.rb +5 -0
- data/lib/jiggler/web/assets/stylesheets/application.css +64 -0
- data/lib/jiggler/web/views/application.erb +329 -0
- data/lib/jiggler/web.rb +80 -0
- data/lib/jiggler/worker.rb +179 -0
- data/lib/jiggler.rb +10 -0
- data/spec/examples.txt +79 -0
- data/spec/fixtures/config/jiggler.yml +4 -0
- data/spec/fixtures/jobs.rb +5 -0
- data/spec/fixtures/my_failed_job.rb +10 -0
- data/spec/fixtures/my_job.rb +9 -0
- data/spec/fixtures/my_job_with_args.rb +18 -0
- data/spec/jiggler/cleaner_spec.rb +171 -0
- data/spec/jiggler/cli_spec.rb +87 -0
- data/spec/jiggler/config_spec.rb +56 -0
- data/spec/jiggler/core_spec.rb +34 -0
- data/spec/jiggler/job_spec.rb +99 -0
- data/spec/jiggler/launcher_spec.rb +66 -0
- data/spec/jiggler/manager_spec.rb +52 -0
- data/spec/jiggler/redis_store_spec.rb +20 -0
- data/spec/jiggler/retrier_spec.rb +55 -0
- data/spec/jiggler/scheduled/enqueuer_spec.rb +81 -0
- data/spec/jiggler/scheduled/poller_spec.rb +40 -0
- data/spec/jiggler/stats/monitor_spec.rb +40 -0
- data/spec/jiggler/summary_spec.rb +168 -0
- data/spec/jiggler/web_spec.rb +37 -0
- data/spec/jiggler/worker_spec.rb +110 -0
- data/spec/spec_helper.rb +54 -0
- metadata +230 -0
data/lib/jiggler/cli.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
require 'async'
|
7
|
+
require 'async/io/trap'
|
8
|
+
require 'async/pool'
|
9
|
+
|
10
|
+
module Jiggler
|
11
|
+
class CLI
|
12
|
+
include Singleton
|
13
|
+
CONTEXT_SWITCHER_THRESHOLD = 0.5
|
14
|
+
|
15
|
+
attr_reader :logger, :config, :environment
|
16
|
+
|
17
|
+
SIGNAL_HANDLERS = {
|
18
|
+
:INT => ->(cli) {
|
19
|
+
cli.logger.fatal('Received INT, shutting down')
|
20
|
+
cli.stop
|
21
|
+
},
|
22
|
+
:TERM => ->(cli) {
|
23
|
+
cli.logger.fatal('Received TERM, shutting down')
|
24
|
+
cli.stop
|
25
|
+
},
|
26
|
+
:TSTP => ->(cli) {
|
27
|
+
cli.logger.info('Received TSTP, no longer accepting new work')
|
28
|
+
cli.suspend
|
29
|
+
}
|
30
|
+
}
|
31
|
+
UNHANDLED_SIGNAL_HANDLER = ->(cli) { cli.logger.info('No signal handler registered, ignoring') }
|
32
|
+
SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
|
33
|
+
SIGNAL_HANDLERS.freeze
|
34
|
+
|
35
|
+
def parse_and_init(args = ARGV.dup)
|
36
|
+
@config ||= Jiggler.config
|
37
|
+
|
38
|
+
setup_options(args)
|
39
|
+
initialize_logger
|
40
|
+
validate!
|
41
|
+
load_app
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
return unless ping_redis
|
46
|
+
@cond = Async::Condition.new
|
47
|
+
Async do
|
48
|
+
setup_signal_handlers
|
49
|
+
patch_scheduler
|
50
|
+
@launcher = Launcher.new(config)
|
51
|
+
@launcher.start
|
52
|
+
Async do
|
53
|
+
@cond.wait
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@switcher&.exit
|
57
|
+
end
|
58
|
+
|
59
|
+
def stop
|
60
|
+
@launcher.stop
|
61
|
+
logger.info('Jiggler is stopped, bye!')
|
62
|
+
@cond.signal
|
63
|
+
end
|
64
|
+
|
65
|
+
def suspend
|
66
|
+
@launcher.suspend
|
67
|
+
logger.info('Jiggler is suspended')
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# forces scheduler to switch fibers if they take more than threshold to execute
|
73
|
+
def patch_scheduler
|
74
|
+
@switcher = Thread.new(Fiber.scheduler) do |scheduler|
|
75
|
+
loop do
|
76
|
+
sleep(CONTEXT_SWITCHER_THRESHOLD)
|
77
|
+
switch = scheduler.context_switch
|
78
|
+
next if switch.nil?
|
79
|
+
next if Process.clock_gettime(Process::CLOCK_MONOTONIC) - switch < CONTEXT_SWITCHER_THRESHOLD
|
80
|
+
|
81
|
+
Process.kill('URG', Process.pid)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Signal.trap('URG') do
|
86
|
+
next Fiber.scheduler.context_switch!(nil) unless Async::Task.current?
|
87
|
+
Async::Task.current.yield
|
88
|
+
end
|
89
|
+
|
90
|
+
Fiber.scheduler.instance_eval do
|
91
|
+
def context_switch
|
92
|
+
@context_switch
|
93
|
+
end
|
94
|
+
|
95
|
+
def context_switch!(value = Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
96
|
+
@context_switch = value
|
97
|
+
end
|
98
|
+
|
99
|
+
def block(...)
|
100
|
+
context_switch!(nil)
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
104
|
+
def kernel_sleep(...)
|
105
|
+
context_switch!(nil)
|
106
|
+
super
|
107
|
+
end
|
108
|
+
|
109
|
+
def resume(fiber, *args)
|
110
|
+
context_switch!
|
111
|
+
super
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_signal_handlers
|
117
|
+
SIGNAL_HANDLERS.each do |signal, handler|
|
118
|
+
trap = Async::IO::Trap.new(signal)
|
119
|
+
trap.install!
|
120
|
+
Async(transient: true) do
|
121
|
+
trap.wait
|
122
|
+
invoked_traps[signal] += 1
|
123
|
+
handler.call(self)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def invoked_traps
|
129
|
+
@invoked_traps ||= Hash.new { |h, k| h[k] = 0 }
|
130
|
+
end
|
131
|
+
|
132
|
+
def validate!
|
133
|
+
if config[:queues].any? { |q| q.include?(':') }
|
134
|
+
raise ArgumentError, 'Queue names cannot contain colons'
|
135
|
+
end
|
136
|
+
|
137
|
+
[:concurrency, :client_concurrency, :timeout].each do |opt|
|
138
|
+
raise ArgumentError, "#{opt}: #{config[opt]} is not a valid value" if config[opt].to_i <= 0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_options(argv)
|
143
|
+
opts = {}
|
144
|
+
@parser = option_parser(opts)
|
145
|
+
@parser.parse!(argv)
|
146
|
+
opts
|
147
|
+
end
|
148
|
+
|
149
|
+
def option_parser(opts)
|
150
|
+
parser = OptionParser.new do |o|
|
151
|
+
o.on '-c', '--concurrency INT', 'Number of fibers to use on the server' do |arg|
|
152
|
+
opts[:concurrency] = Integer(arg)
|
153
|
+
end
|
154
|
+
|
155
|
+
o.on '-e', '--environment ENV', 'Application environment' do |arg|
|
156
|
+
opts[:environment] = arg
|
157
|
+
end
|
158
|
+
|
159
|
+
o.on '-q', '--queue QUEUE1,QUEUE2', 'Queues to process' do |arg|
|
160
|
+
opts[:queues] ||= []
|
161
|
+
arg.split(',').each do |queue|
|
162
|
+
opts[:queues] << queue
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
o.on '-r', '--require PATH', 'File to require' do |arg|
|
167
|
+
opts[:require] = arg
|
168
|
+
end
|
169
|
+
|
170
|
+
o.on '-t', '--timeout NUM', 'Shutdown timeout' do |arg|
|
171
|
+
opts[:timeout] = Integer(arg)
|
172
|
+
end
|
173
|
+
|
174
|
+
o.on '-v', '--verbose', 'Print more verbose output' do |arg|
|
175
|
+
opts[:verbose] = arg
|
176
|
+
end
|
177
|
+
|
178
|
+
o.on '-C', '--config PATH', 'Path to YAML config file' do |arg|
|
179
|
+
opts[:config_file] = arg
|
180
|
+
end
|
181
|
+
|
182
|
+
o.on '-V', '--version', 'Print version and exit' do
|
183
|
+
puts("Jiggler #{Jiggler::VERSION}")
|
184
|
+
exit(0)
|
185
|
+
end
|
186
|
+
|
187
|
+
o.on_tail '-h', '--help', 'Show help' do
|
188
|
+
puts o
|
189
|
+
exit(0)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
parser.banner = 'Jiggler [options]'
|
193
|
+
parser
|
194
|
+
end
|
195
|
+
|
196
|
+
def setup_options(args)
|
197
|
+
opts = parse_options(args)
|
198
|
+
|
199
|
+
set_environment(opts)
|
200
|
+
|
201
|
+
opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
|
202
|
+
opts[:queues] = [Jiggler::Config::DEFAULT_QUEUE] if opts[:queues].nil?
|
203
|
+
opts[:server_mode] = true # cli starts only in server mode
|
204
|
+
config.merge!(opts)
|
205
|
+
end
|
206
|
+
|
207
|
+
def set_environment(opts)
|
208
|
+
opts[:environment] ||= ENV['APP_ENV'] || 'development'
|
209
|
+
@environment = opts[:environment]
|
210
|
+
end
|
211
|
+
|
212
|
+
def initialize_logger
|
213
|
+
@logger = config.logger
|
214
|
+
logger.level = ::Logger::DEBUG if config[:verbose]
|
215
|
+
end
|
216
|
+
|
217
|
+
def symbolize_keys_deep!(hash)
|
218
|
+
hash.keys.each do |k|
|
219
|
+
symkey = k.respond_to?(:to_sym) ? k.to_sym : k
|
220
|
+
hash[symkey] = hash.delete k
|
221
|
+
symbolize_keys_deep! hash[symkey] if hash[symkey].is_a? Hash
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_config(path)
|
226
|
+
erb = ERB.new(File.read(path))
|
227
|
+
erb.filename = File.expand_path(path)
|
228
|
+
opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
|
229
|
+
|
230
|
+
symbolize_keys_deep!(opts)
|
231
|
+
|
232
|
+
opts = opts.merge(opts.delete(environment.to_sym) || {})
|
233
|
+
opts.delete(:strict)
|
234
|
+
|
235
|
+
opts
|
236
|
+
rescue => error
|
237
|
+
raise ArgumentError, "Error parsing config file: #{error.message}"
|
238
|
+
end
|
239
|
+
|
240
|
+
def ping_redis
|
241
|
+
config.with_sync_redis { |conn| conn.call('PING') }
|
242
|
+
true
|
243
|
+
rescue => err
|
244
|
+
logger.fatal("Redis connection error: #{err.message}")
|
245
|
+
false
|
246
|
+
end
|
247
|
+
|
248
|
+
def load_app
|
249
|
+
if config[:require].nil? || config[:require].empty?
|
250
|
+
logger.warn('No require option specified. Please specify a Ruby file to require with --require')
|
251
|
+
# allow to start empty server
|
252
|
+
return
|
253
|
+
end
|
254
|
+
# the code required by this file is expected to call Jiggler.configure
|
255
|
+
# thus it'll be executed in the context of the current process
|
256
|
+
# and apply the configuration for the server
|
257
|
+
require config[:require]
|
258
|
+
rescue LoadError => e
|
259
|
+
logger.fatal("Could not load jobs: #{e.message}")
|
260
|
+
exit(1)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Jiggler
|
7
|
+
class Config
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
DEFAULT_QUEUE = 'default'
|
11
|
+
QUEUE_PREFIX = 'jiggler:list:'
|
12
|
+
SERVER_PREFIX = 'jiggler:svr:'
|
13
|
+
RETRIES_SET = 'jiggler:set:retries'
|
14
|
+
SCHEDULED_SET = 'jiggler:set:scheduled'
|
15
|
+
DEAD_SET = 'jiggler:set:dead'
|
16
|
+
PROCESSED_COUNTER = 'jiggler:stats:processed_counter'
|
17
|
+
FAILURES_COUNTER = 'jiggler:stats:failures_counter'
|
18
|
+
|
19
|
+
DEFAULTS = {
|
20
|
+
require: nil,
|
21
|
+
environment: 'development',
|
22
|
+
concurrency: 10,
|
23
|
+
timeout: 25,
|
24
|
+
max_dead_jobs: 10_000,
|
25
|
+
stats_interval: 10,
|
26
|
+
poller_enabled: true,
|
27
|
+
poll_interval: 5,
|
28
|
+
dead_timeout: 180 * 24 * 60 * 60, # 6 months in seconds
|
29
|
+
# client settings
|
30
|
+
client_concurrency: 10,
|
31
|
+
client_redis_pool: nil,
|
32
|
+
client_async: false,
|
33
|
+
}
|
34
|
+
|
35
|
+
def initialize(options = {})
|
36
|
+
@options = DEFAULTS.merge(options)
|
37
|
+
@options[:redis_url] = ENV['REDIS_URL'] if @options[:redis_url].nil? && ENV['REDIS_URL']
|
38
|
+
@options[:queues] ||= [DEFAULT_QUEUE]
|
39
|
+
@directory = {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def queue_prefix
|
43
|
+
QUEUE_PREFIX
|
44
|
+
end
|
45
|
+
|
46
|
+
def retries_set
|
47
|
+
RETRIES_SET
|
48
|
+
end
|
49
|
+
|
50
|
+
def scheduled_set
|
51
|
+
SCHEDULED_SET
|
52
|
+
end
|
53
|
+
|
54
|
+
def dead_set
|
55
|
+
DEAD_SET
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_queue
|
59
|
+
DEFAULT_QUEUE
|
60
|
+
end
|
61
|
+
|
62
|
+
# jiggler main process prefix
|
63
|
+
def server_prefix
|
64
|
+
SERVER_PREFIX
|
65
|
+
end
|
66
|
+
|
67
|
+
def processed_counter
|
68
|
+
PROCESSED_COUNTER
|
69
|
+
end
|
70
|
+
|
71
|
+
def failures_counter
|
72
|
+
FAILURES_COUNTER
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_scan_key
|
76
|
+
@process_scan_key ||= "#{server_prefix}*"
|
77
|
+
end
|
78
|
+
|
79
|
+
def queue_scan_key
|
80
|
+
@queue_scan_key ||= "#{queue_prefix}*"
|
81
|
+
end
|
82
|
+
|
83
|
+
def prefixed_queues
|
84
|
+
@prefixed_queues ||= @options[:queues].map do |name|
|
85
|
+
"#{QUEUE_PREFIX}#{name}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def with_async_redis
|
90
|
+
Async do
|
91
|
+
redis_pool.acquire do |conn|
|
92
|
+
yield conn
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def with_sync_redis
|
98
|
+
Sync do
|
99
|
+
redis_pool.acquire do |conn|
|
100
|
+
yield conn
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def redis_options
|
106
|
+
@redis_options ||= begin
|
107
|
+
opts = @options.slice(
|
108
|
+
:concurrency,
|
109
|
+
:redis_url
|
110
|
+
)
|
111
|
+
|
112
|
+
opts[:concurrency] += 2 # monitor + safety margin
|
113
|
+
opts[:concurrency] += 1 if @options[:poller_enabled]
|
114
|
+
opts[:async] = true
|
115
|
+
|
116
|
+
opts
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def client_redis_options
|
121
|
+
@client_redis_options ||= begin
|
122
|
+
opts = @options.slice(
|
123
|
+
:redis_url,
|
124
|
+
:client_redis_pool
|
125
|
+
)
|
126
|
+
|
127
|
+
opts[:concurrency] = @options[:client_concurrency]
|
128
|
+
opts[:async] = @options[:client_async]
|
129
|
+
opts
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def redis_pool
|
134
|
+
@redis_pool ||= Jiggler::RedisStore.new(redis_options).pool
|
135
|
+
end
|
136
|
+
|
137
|
+
def client_redis_pool
|
138
|
+
@client_redis_pool ||= begin
|
139
|
+
@options[:client_redis_pool] || Jiggler::RedisStore.new(client_redis_options).pool
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def client_redis_pool=(new_pool)
|
144
|
+
@client_redis_pool = new_pool
|
145
|
+
end
|
146
|
+
|
147
|
+
def cleaner
|
148
|
+
@cleaner ||= Jiggler::Cleaner.new(self)
|
149
|
+
end
|
150
|
+
|
151
|
+
def summary
|
152
|
+
@summary ||= Jiggler::Summary.new(self)
|
153
|
+
end
|
154
|
+
|
155
|
+
def logger=(new_logger)
|
156
|
+
@logger = new_logger
|
157
|
+
end
|
158
|
+
|
159
|
+
def logger
|
160
|
+
@logger ||= ::Logger.new(STDOUT, level: :info)
|
161
|
+
end
|
162
|
+
|
163
|
+
def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :delete, :slice
|
164
|
+
end
|
165
|
+
end
|
data/lib/jiggler/core.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oj'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Jiggler
|
7
|
+
def self.config
|
8
|
+
@config ||= Jiggler::Config.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.logger
|
12
|
+
config.logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configure(&block)
|
16
|
+
block.call(config)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.summary
|
20
|
+
config.summary.all
|
21
|
+
end
|
22
|
+
end
|
data/lib/jiggler/job.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiggler
|
4
|
+
module Job
|
5
|
+
module ClassMethods
|
6
|
+
def enqueue(*args)
|
7
|
+
Enqueuer.new(self).enqueue(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def enqueue_in(seconds, *args)
|
11
|
+
Enqueuer.new(self).enqueue_in(seconds, *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
# MyJob.with_options(queue: 'custom', retries: 3).enqueue(*args)
|
15
|
+
def with_options(options)
|
16
|
+
Enqueuer.new(self, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def enqueue_bulk(args_arr)
|
20
|
+
Enqueuer.new(self).enqueue_bulk(args_arr)
|
21
|
+
end
|
22
|
+
|
23
|
+
def queue
|
24
|
+
@queue || Jiggler::Config::DEFAULT_QUEUE
|
25
|
+
end
|
26
|
+
|
27
|
+
def retry_queue
|
28
|
+
@retry_queue || Jiggler::Config::DEFAULT_QUEUE
|
29
|
+
end
|
30
|
+
|
31
|
+
def retries
|
32
|
+
@retries || 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def job_options(queue: nil, retries: nil, retry_queue: nil)
|
36
|
+
@queue = queue || Jiggler::Config::DEFAULT_QUEUE
|
37
|
+
@retries = retries || 0
|
38
|
+
@retry_queue = retry_queue || queue
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Enqueuer
|
43
|
+
def initialize(klass, options = {})
|
44
|
+
@options = options
|
45
|
+
@klass = klass
|
46
|
+
end
|
47
|
+
|
48
|
+
def with_options(options)
|
49
|
+
@options.merge(options)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def enqueue(*args)
|
54
|
+
Jiggler.config.client_redis_pool.acquire do |conn|
|
55
|
+
conn.call('LPUSH', list_name, job_args(args))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def enqueue_bulk(args_arr)
|
60
|
+
Jiggler.config.client_redis_pool.acquire do |conn|
|
61
|
+
conn.pipelined do |pipeline|
|
62
|
+
args_arr.each do |args|
|
63
|
+
pipeline.call('LPUSH', list_name, job_args(args))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def enqueue_in(seconds, *args)
|
70
|
+
timestamp = Time.now.to_f + seconds
|
71
|
+
Jiggler.config.client_redis_pool.acquire do |conn|
|
72
|
+
conn.call(
|
73
|
+
'ZADD',
|
74
|
+
config.scheduled_set,
|
75
|
+
timestamp,
|
76
|
+
job_args(args)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def list_name
|
82
|
+
"#{config.queue_prefix}#{@options.fetch(:queue, @klass.queue)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def job_args(raw_args)
|
86
|
+
Oj.dump({ name: @klass.name, args: raw_args, **job_options }, mode: :compat)
|
87
|
+
end
|
88
|
+
|
89
|
+
def job_options
|
90
|
+
retries = @options.fetch(:retries, @klass.retries)
|
91
|
+
jid = @options.fetch(:jid, SecureRandom.hex(8))
|
92
|
+
{ retries: retries, jid: jid }
|
93
|
+
end
|
94
|
+
|
95
|
+
def config
|
96
|
+
@config ||= Jiggler.config
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.included(base)
|
101
|
+
base.extend(ClassMethods)
|
102
|
+
end
|
103
|
+
|
104
|
+
def enqueue(*args)
|
105
|
+
Enqueuer.new(self.class).enqueue(*args)
|
106
|
+
end
|
107
|
+
|
108
|
+
def enqueue_in(seconds, *args)
|
109
|
+
Enqueuer.new(self.class).enqueue_in(seconds, *args)
|
110
|
+
end
|
111
|
+
|
112
|
+
def perform(**args)
|
113
|
+
raise "#{self.class} must implement 'perform' method"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiggler
|
4
|
+
class Launcher
|
5
|
+
include Support::Helper
|
6
|
+
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@done = false
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
poller.start if config[:poller_enabled]
|
16
|
+
monitor.start
|
17
|
+
manager.start
|
18
|
+
end
|
19
|
+
|
20
|
+
def suspend
|
21
|
+
return if @done
|
22
|
+
|
23
|
+
@done = true
|
24
|
+
manager.suspend
|
25
|
+
|
26
|
+
poller.terminate if config[:poller_enabled]
|
27
|
+
monitor.terminate
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
suspend
|
32
|
+
manager.terminate
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def uuid
|
38
|
+
@uuid ||= begin
|
39
|
+
data_str = [
|
40
|
+
SecureRandom.hex(6),
|
41
|
+
config[:concurrency],
|
42
|
+
config[:timeout],
|
43
|
+
config[:queues].join(','),
|
44
|
+
config[:poller_enabled] ? '1' : '0',
|
45
|
+
Time.now.to_i,
|
46
|
+
Process.pid,
|
47
|
+
ENV['DYNO'] || Socket.gethostname
|
48
|
+
].join(':')
|
49
|
+
"#{config.server_prefix}#{data_str}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def collection
|
54
|
+
@collection ||= Stats::Collection.new(uuid)
|
55
|
+
end
|
56
|
+
|
57
|
+
def manager
|
58
|
+
@manager ||= Manager.new(config, collection)
|
59
|
+
end
|
60
|
+
|
61
|
+
def poller
|
62
|
+
@poller ||= Scheduled::Poller.new(config)
|
63
|
+
end
|
64
|
+
|
65
|
+
def monitor
|
66
|
+
@monitor ||= Stats::Monitor.new(config, collection)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiggler
|
4
|
+
class Manager
|
5
|
+
include Support::Helper
|
6
|
+
|
7
|
+
def initialize(config, collection)
|
8
|
+
@workers = Set.new
|
9
|
+
@done = false
|
10
|
+
@config = config
|
11
|
+
@timeout = @config[:timeout]
|
12
|
+
@collection = collection
|
13
|
+
@config[:concurrency].times do
|
14
|
+
@workers << init_worker
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
@workers.each(&:run)
|
20
|
+
end
|
21
|
+
|
22
|
+
def suspend
|
23
|
+
return if @done
|
24
|
+
|
25
|
+
@done = true
|
26
|
+
@workers.each(&:suspend)
|
27
|
+
end
|
28
|
+
|
29
|
+
def terminate
|
30
|
+
suspend
|
31
|
+
schedule_shutdown
|
32
|
+
wait_for_workers
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def wait_for_workers
|
38
|
+
logger.info('Waiting for workers to finish...')
|
39
|
+
@workers.each(&:wait)
|
40
|
+
@shutdown_task.stop
|
41
|
+
end
|
42
|
+
|
43
|
+
def schedule_shutdown
|
44
|
+
@shutdown_task = Async do
|
45
|
+
sleep(@timeout)
|
46
|
+
|
47
|
+
next if @workers.empty?
|
48
|
+
|
49
|
+
hard_shutdown
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def init_worker
|
54
|
+
Jiggler::Worker.new(
|
55
|
+
@config, @collection, &method(:process_worker_result)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_worker_result(worker, reason = nil)
|
60
|
+
@workers.delete(worker)
|
61
|
+
unless @done
|
62
|
+
new_worker = init_worker
|
63
|
+
@workers << new_worker
|
64
|
+
new_worker.run
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def hard_shutdown
|
69
|
+
logger.warn('Hard shutdown, terminating workers...')
|
70
|
+
@workers.each(&:terminate)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|