little_monster 0.1.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 +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
|