little_monster 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +6 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +124 -0
- data/README.md +5 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/exe/lm +4 -0
- data/lib/little_monster.rb +67 -0
- data/lib/little_monster/all.rb +4 -0
- data/lib/little_monster/config.rb +29 -0
- data/lib/little_monster/core.rb +20 -0
- data/lib/little_monster/core/api.rb +74 -0
- data/lib/little_monster/core/counters.rb +50 -0
- data/lib/little_monster/core/errors/api_unreachable_error.rb +4 -0
- data/lib/little_monster/core/errors/callback_failed_error.rb +4 -0
- data/lib/little_monster/core/errors/cancel_error.rb +4 -0
- data/lib/little_monster/core/errors/fatal_task_error.rb +4 -0
- data/lib/little_monster/core/errors/job_already_locked_error.rb +4 -0
- data/lib/little_monster/core/errors/job_not_found_error.rb +15 -0
- data/lib/little_monster/core/errors/job_retry_error.rb +4 -0
- data/lib/little_monster/core/errors/max_retries_error.rb +4 -0
- data/lib/little_monster/core/errors/task_error.rb +4 -0
- data/lib/little_monster/core/job.rb +188 -0
- data/lib/little_monster/core/job_data.rb +47 -0
- data/lib/little_monster/core/job_factory.rb +139 -0
- data/lib/little_monster/core/job_orchrestator.rb +194 -0
- data/lib/little_monster/core/loggable.rb +7 -0
- data/lib/little_monster/core/runner.rb +39 -0
- data/lib/little_monster/core/tagged_logger.rb +66 -0
- data/lib/little_monster/core/task.rb +41 -0
- data/lib/little_monster/generators/cli.rb +75 -0
- data/lib/little_monster/generators/conf_gen.rb +28 -0
- data/lib/little_monster/generators/generate.rb +35 -0
- data/lib/little_monster/generators/templates/config/application.rb +15 -0
- data/lib/little_monster/generators/templates/config/enviroments/development.rb +1 -0
- data/lib/little_monster/generators/templates/config/enviroments/production.rb +1 -0
- data/lib/little_monster/generators/templates/config/enviroments/test.rb +1 -0
- data/lib/little_monster/generators/templates/config/toiler.yml +3 -0
- data/lib/little_monster/generators/templates/jobs_spec_temp.erb +11 -0
- data/lib/little_monster/generators/templates/jobs_temp.erb +16 -0
- data/lib/little_monster/generators/templates/lib/.keep +0 -0
- data/lib/little_monster/generators/templates/log/.keep +0 -0
- data/lib/little_monster/generators/templates/spec_helper_temp.erb +22 -0
- data/lib/little_monster/generators/templates/tasks_spec_temp.erb +11 -0
- data/lib/little_monster/generators/templates/tasks_temp.erb +5 -0
- data/lib/little_monster/rspec.rb +20 -0
- data/lib/little_monster/rspec/helpers/job_helper.rb +61 -0
- data/lib/little_monster/rspec/helpers/task_helper.rb +46 -0
- data/lib/little_monster/rspec/matchers/have_data.rb +24 -0
- data/lib/little_monster/rspec/matchers/have_ended_with_status.rb +24 -0
- data/lib/little_monster/rspec/matchers/have_run.rb +28 -0
- data/lib/little_monster/rspec/matchers/have_run_task.rb +47 -0
- data/lib/little_monster/version.rb +3 -0
- data/lib/little_monster/worker.rb +27 -0
- data/little_monster.gemspec +48 -0
- metadata +343 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
# TODO : don't send data on callback fail
|
2
|
+
module LittleMonster::Core
|
3
|
+
class Job::Orchrestator
|
4
|
+
attr_reader :logger
|
5
|
+
attr_reader :job
|
6
|
+
|
7
|
+
def initialize(job)
|
8
|
+
@job = job
|
9
|
+
@logger = @job.logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
# notifies status as running and then restores old_status if it is an ending status
|
14
|
+
last_status = @job.status
|
15
|
+
@job.status = :running
|
16
|
+
@job.notify_status
|
17
|
+
|
18
|
+
if Job::ENDED_STATUS.include? last_status
|
19
|
+
@job.status = last_status
|
20
|
+
else
|
21
|
+
run_tasks
|
22
|
+
|
23
|
+
logger.default_tags.delete(:current_task)
|
24
|
+
# reset retries so retries don't mix between tasks and callbacks
|
25
|
+
@job.retries = 0
|
26
|
+
end
|
27
|
+
|
28
|
+
run_callback
|
29
|
+
logger.info "[type:job_finish] [status:#{@job.status}] data: #{@job.data.to_h[:outputs]}"
|
30
|
+
ensure
|
31
|
+
options = {}
|
32
|
+
options[:data] = @job.data if @job.ended_status?
|
33
|
+
@job.notify_status options
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_tasks
|
37
|
+
@job.tasks_to_run.each do |task_name|
|
38
|
+
@job.current_action = task_name
|
39
|
+
@job.notify_task :running
|
40
|
+
|
41
|
+
logger.default_tags[:current_task] = @job.current_action
|
42
|
+
logger.info "[type:start_task] data: #{@job.data.to_h[:outputs]}"
|
43
|
+
|
44
|
+
begin
|
45
|
+
raise LittleMonster::CancelError if @job.is_cancelled?
|
46
|
+
|
47
|
+
task = build_task(task_name)
|
48
|
+
task.run
|
49
|
+
|
50
|
+
# data is sent only on task success
|
51
|
+
@job.notify_task :success, data: @job.data
|
52
|
+
|
53
|
+
logger.info "[type:finish_task] [status:success] data: #{@job.data.to_h[:outputs]}"
|
54
|
+
|
55
|
+
if @job.mock?
|
56
|
+
@job.runned_tasks[task_name] = {}
|
57
|
+
@job.runned_tasks[task_name][:instance] = task
|
58
|
+
@job.runned_tasks[task_name][:data] = @job.data.to_h[:outputs].to_h.dup
|
59
|
+
end
|
60
|
+
rescue APIUnreachableError => e
|
61
|
+
logger.error "[type:api_unreachable] [message:#{e.message}]"
|
62
|
+
raise e
|
63
|
+
rescue CancelError => e
|
64
|
+
logger.info '[type:cancel] job was cancelled'
|
65
|
+
cancel
|
66
|
+
return
|
67
|
+
rescue StandardError => e
|
68
|
+
logger.debug "[type:standard_error] an error was catched with [message:#{e.message}]"
|
69
|
+
task.error e unless e.is_a? NameError
|
70
|
+
handle_error e
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
@job.retries = 0 # Hago esto para que despues de succesful un task resete retries
|
75
|
+
end
|
76
|
+
|
77
|
+
@job.current_action = nil
|
78
|
+
@job.status = :success
|
79
|
+
end
|
80
|
+
|
81
|
+
def run_callback
|
82
|
+
@job.current_action = @job.callback_to_run
|
83
|
+
|
84
|
+
return if @job.current_action.nil?
|
85
|
+
|
86
|
+
logger.default_tags[:callback] = @job.current_action
|
87
|
+
@job.notify_callback :running
|
88
|
+
|
89
|
+
logger.info "[type:start_callback] data: #{@job.data.to_h[:outputs]}"
|
90
|
+
begin
|
91
|
+
logger.default_tags[:type] = 'callback_log'
|
92
|
+
@job.public_send(@job.current_action)
|
93
|
+
ensure
|
94
|
+
logger.default_tags.delete(:type)
|
95
|
+
end
|
96
|
+
logger.info "[type:finish_callback] [status:success] data: #{@job.data.to_h[:outputs]}"
|
97
|
+
|
98
|
+
@job.notify_callback :success
|
99
|
+
|
100
|
+
@job.current_action = nil
|
101
|
+
@job.retries = 0
|
102
|
+
logger.default_tags.delete(:callback)
|
103
|
+
rescue APIUnreachableError => e
|
104
|
+
logger.error "[type:api_unreachable] [message:#{e.message}]"
|
105
|
+
raise e
|
106
|
+
rescue StandardError => e
|
107
|
+
logger.debug "[type:standard_error] an error was catched with [message:#{e.message}]"
|
108
|
+
handle_error e
|
109
|
+
end
|
110
|
+
|
111
|
+
def build_task(task_symbol)
|
112
|
+
task = @job.task_class_for(task_symbol).new(@job.data)
|
113
|
+
task.send(:set_default_values, @job.data, @job.id, logger, @job.method(:is_cancelled?))
|
114
|
+
task
|
115
|
+
end
|
116
|
+
|
117
|
+
def cancel
|
118
|
+
logger.debug 'notifiying cancel...'
|
119
|
+
|
120
|
+
@job.notify_task :cancelled
|
121
|
+
logger.info "[type:finish_task] [status:cancelled] data: #{@job.data.to_h[:outputs]}"
|
122
|
+
|
123
|
+
@job.status = :cancelled
|
124
|
+
end
|
125
|
+
|
126
|
+
# Methods that work both on tasks and callbacks
|
127
|
+
|
128
|
+
def abort_job(_e)
|
129
|
+
logger.debug 'notifiying abort...'
|
130
|
+
|
131
|
+
if @job.callback_running?
|
132
|
+
logger.info "[type:finish_callback] [status:error] data: #{@job.data.to_h[:outputs]}"
|
133
|
+
@job.notify_callback :error
|
134
|
+
|
135
|
+
# if callback is not on_error, raise exception to run on_error
|
136
|
+
if @job.current_action != :on_error
|
137
|
+
# set status on pending because we are sending the job back to the queue
|
138
|
+
@job.status = :pending
|
139
|
+
raise CallbackFailedError, '[type:callback_fail_error]'
|
140
|
+
end
|
141
|
+
else
|
142
|
+
@job.notify_task :error
|
143
|
+
logger.info "[type:finish_task] [status:error] data: #{@job.data.to_h[:outputs]}"
|
144
|
+
end
|
145
|
+
|
146
|
+
@job.status = :error
|
147
|
+
end
|
148
|
+
|
149
|
+
def handle_error(e)
|
150
|
+
raise e if LittleMonster.env.development?
|
151
|
+
logger.error "[type:error] [error_type:#{e.class}][message:#{e.message}] \n #{e.backtrace.to_a.join("\n\t")}"
|
152
|
+
|
153
|
+
if e.is_a?(FatalTaskError) || e.is_a?(NameError)
|
154
|
+
logger.debug 'error is fatal, aborting run'
|
155
|
+
return abort_job(e)
|
156
|
+
end
|
157
|
+
|
158
|
+
do_retry
|
159
|
+
end
|
160
|
+
|
161
|
+
def do_retry
|
162
|
+
if @job.retry?
|
163
|
+
logger.debug "Retry ##{@job.retries} of #{@job.max_retries}"
|
164
|
+
|
165
|
+
@job.retries += 1
|
166
|
+
|
167
|
+
logger.debug 'notifiying retry'
|
168
|
+
if @job.callback_running?
|
169
|
+
@job.notify_callback :pending, retries: @job.retries
|
170
|
+
logger.info '[type:callback_retry]'
|
171
|
+
else
|
172
|
+
@job.notify_task :pending, retries: @job.retries
|
173
|
+
logger.info '[type:task_retry]'
|
174
|
+
end
|
175
|
+
|
176
|
+
@job.status = :pending
|
177
|
+
|
178
|
+
logger.info "[type:job_retry] data: #{@job.data.to_h[:outputs]}"
|
179
|
+
raise JobRetryError, "doing retry #{@job.retries} of #{@job.max_retries}"
|
180
|
+
else
|
181
|
+
logger.debug 'job has reached max retries'
|
182
|
+
|
183
|
+
if @job.callback_running?
|
184
|
+
logger.info '[type:callback_max_retries]'
|
185
|
+
else
|
186
|
+
logger.info '[type:task_max_retries]'
|
187
|
+
end
|
188
|
+
|
189
|
+
logger.info "[type:job_max_retries] [retries:#{@job.max_retries}]"
|
190
|
+
abort_job(MaxRetriesError.new)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module LittleMonster::Core
|
2
|
+
class Runner
|
3
|
+
include Loggable
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
@params = params
|
7
|
+
|
8
|
+
@heartbeat_task = Concurrent::TimerTask.new(execution_interval: LittleMonster.heartbeat_execution_interval) do
|
9
|
+
send_heartbeat!
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
send_heartbeat!
|
15
|
+
|
16
|
+
@heartbeat_task.execute unless LittleMonster.disable_requests?
|
17
|
+
|
18
|
+
job = LittleMonster::Job::Factory.new(@params).build
|
19
|
+
job.run unless job.nil?
|
20
|
+
rescue JobNotFoundError => e
|
21
|
+
logger.error "[id:#{@params[:id]}][type:job_not_found] [message:#{e.message}] \n #{e.backtrace.to_a.join("\n\t")}"
|
22
|
+
ensure
|
23
|
+
@heartbeat_task.shutdown
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_heartbeat!
|
27
|
+
return if LittleMonster.disable_requests?
|
28
|
+
|
29
|
+
res = LittleMonster::API.put "/jobs/#{@params[:id]}/worker", body: {
|
30
|
+
ip: Socket.gethostname,
|
31
|
+
host: Socket.gethostname,
|
32
|
+
pid: Process.pid
|
33
|
+
}
|
34
|
+
|
35
|
+
raise LittleMonster::JobAlreadyLockedError, "job [id:#{@params[:id]}] is already locked, discarding" if res.code == 401
|
36
|
+
res.success?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module LittleMonster::Core
|
2
|
+
class TaggedLogger
|
3
|
+
attr_accessor :parent_logger
|
4
|
+
attr_reader :tags
|
5
|
+
|
6
|
+
LEVELS = [:unknown, :fatal, :error, :warn, :info, :debug].freeze
|
7
|
+
|
8
|
+
def self.tags_to_string(hash)
|
9
|
+
hash.map { |k, v| "[#{k}:#{v}]" }.join
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@tags = Hash.new({})
|
14
|
+
@parent_logger = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(method, *args, &block)
|
18
|
+
if method.to_s.ends_with? 'tags='
|
19
|
+
tag_key = method.to_s.split('_').first.to_sym
|
20
|
+
return public_send('tags_for', tag_key, *args) if LEVELS.include? tag_key
|
21
|
+
end
|
22
|
+
|
23
|
+
if method.to_s.ends_with? 'tags'
|
24
|
+
tag_key = method.to_s.split('_').first.to_sym
|
25
|
+
return @tags[tag_key] if LEVELS.include? tag_key
|
26
|
+
end
|
27
|
+
|
28
|
+
if LEVELS.include? method.to_sym
|
29
|
+
return LittleMonster.logger.public_send method, tag_message(method.to_sym, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
super method, *args, &block
|
33
|
+
end
|
34
|
+
|
35
|
+
def tags_for(key, t = {})
|
36
|
+
@tags[key] = t
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_tags
|
40
|
+
@tags[:default]
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_tags=(t)
|
44
|
+
tags_for(:default, t)
|
45
|
+
end
|
46
|
+
|
47
|
+
def tag_message(level, message = '')
|
48
|
+
prefix_string = tags_to_string @tags[:default].merge(@tags[level])
|
49
|
+
prefix_string << ' -- ' unless prefix_string.blank?
|
50
|
+
|
51
|
+
unless @parent_logger.nil?
|
52
|
+
prefix_string = @parent_logger.tag_message level, prefix_string
|
53
|
+
end
|
54
|
+
|
55
|
+
[prefix_string, message].join
|
56
|
+
end
|
57
|
+
|
58
|
+
def log_tags(level, tags_hash)
|
59
|
+
public_send(level, tags_to_string(tags_hash))
|
60
|
+
end
|
61
|
+
|
62
|
+
def tags_to_string(hash)
|
63
|
+
self.class.tags_to_string hash
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module LittleMonster::Core
|
2
|
+
class Task
|
3
|
+
include Loggable
|
4
|
+
|
5
|
+
attr_reader :data
|
6
|
+
attr_reader :job_id
|
7
|
+
|
8
|
+
def initialize(data, job_id = nil)
|
9
|
+
@data = data
|
10
|
+
@job_id = job_id
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
raise NotImplementedError, 'You must implement the run method'
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_error(error)
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(e)
|
21
|
+
logger.error e
|
22
|
+
on_error e
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_cancelled!
|
26
|
+
is_cancelled = false
|
27
|
+
is_cancelled = @cancelled_callback.call unless @cancelled_callback.nil?
|
28
|
+
raise CancelError if is_cancelled
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def set_default_values(data, job_id = nil, job_logger = nil, cancelled_callback = nil)
|
34
|
+
@cancelled_callback = cancelled_callback
|
35
|
+
@job_id = job_id
|
36
|
+
@data = data
|
37
|
+
logger.parent_logger = job_logger if job_logger
|
38
|
+
logger.default_tags.merge!(type: 'task_log')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require_relative './conf_gen'
|
3
|
+
require_relative './generate'
|
4
|
+
|
5
|
+
module LittleMonster
|
6
|
+
class Cli < Thor
|
7
|
+
desc 'show version', 'version'
|
8
|
+
map %w(-v --version) => :version
|
9
|
+
|
10
|
+
def version
|
11
|
+
say LittleMonster::VERSION
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'exec <job>', 'runs a job'
|
15
|
+
option :message,
|
16
|
+
type: :hash,
|
17
|
+
aliases: :m,
|
18
|
+
default: {}
|
19
|
+
|
20
|
+
method_option :message,
|
21
|
+
aliases: '-m',
|
22
|
+
type: :string,
|
23
|
+
default: '{}',
|
24
|
+
desc: 'Message that will be send as parameter (must be a JSON format)'
|
25
|
+
|
26
|
+
method_option :record_mode,
|
27
|
+
aliases: '-r',
|
28
|
+
type: :string,
|
29
|
+
enum: %w(none new reload),
|
30
|
+
default: 'none',
|
31
|
+
desc: 'Recording mocks mode none|new|reload',
|
32
|
+
banner: 'Recording type could be none,new or reload on default assume none'
|
33
|
+
|
34
|
+
def exec(job)
|
35
|
+
ENV['LITTLE_MONSTER_ENV'] = options[:environment]
|
36
|
+
require_relative "#{Dir.pwd}/config/application.rb"
|
37
|
+
require 'webmock'
|
38
|
+
require 'vcr'
|
39
|
+
|
40
|
+
msg = MultiJson.load(options[:message], symbolize_keys: true)
|
41
|
+
params = { data: { outputs: msg }, name: job }
|
42
|
+
vcr_mode = { 'none' => :none,
|
43
|
+
'new' => :new_episodes,
|
44
|
+
'reload' => :all }.fetch(options[:record_mode], :none)
|
45
|
+
VCR.configure do |config|
|
46
|
+
config.cassette_library_dir = 'mocks/vcr_cassettes'
|
47
|
+
config.hook_into :webmock # or :fakeweb
|
48
|
+
end
|
49
|
+
|
50
|
+
VCR.use_cassette(job.to_s, record: vcr_mode) do
|
51
|
+
LittleMonster::Runner.new(params).run
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'start', 'starts the little monster worker'
|
56
|
+
option :daemonize,
|
57
|
+
type: :boolean,
|
58
|
+
default: false,
|
59
|
+
aliases: :d
|
60
|
+
|
61
|
+
def start
|
62
|
+
require_relative "#{Dir.pwd}/config/application.rb"
|
63
|
+
|
64
|
+
toiler_args = ['-C', "#{Dir.pwd}/config/toiler.yml"]
|
65
|
+
toiler_args += ['-d', '-L', 'log/little_monster.log'] if options[:daemonize]
|
66
|
+
Toiler::CLI.instance.run(toiler_args)
|
67
|
+
end
|
68
|
+
|
69
|
+
register(LittleMonster::ConfGen, 'init', 'init', 'Creates new Little Monster Schema app')
|
70
|
+
register(LittleMonster::Generate,
|
71
|
+
'generate',
|
72
|
+
'generate <job_name> <task_list>...',
|
73
|
+
'Creates a job with his respective tasks.')
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'active_support/core_ext/string'
|
3
|
+
|
4
|
+
module LittleMonster
|
5
|
+
class ConfGen < Thor::Group
|
6
|
+
include Thor::Actions
|
7
|
+
|
8
|
+
def self.source_root
|
9
|
+
File.dirname(__FILE__)
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_conf_files
|
13
|
+
directory('./templates/config', 'config')
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_lib_files
|
17
|
+
directory('./templates/lib', 'lib')
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_log_files
|
21
|
+
directory('./templates/log', 'log')
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_specs_files
|
25
|
+
template 'templates/spec_helper_temp.erb', 'spec/spec_helper.rb'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|