tumugi 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/.gitignore +9 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/concurrent_task_run.rb +27 -0
- data/examples/data_pipeline.rb +31 -0
- data/examples/simple.rb +21 -0
- data/examples/target.rb +51 -0
- data/examples/task_inheritance.rb +27 -0
- data/exe/tumugi +4 -0
- data/lib/tumugi.rb +4 -0
- data/lib/tumugi/application.rb +40 -0
- data/lib/tumugi/cli.rb +50 -0
- data/lib/tumugi/command/run.rb +63 -0
- data/lib/tumugi/command/show.rb +40 -0
- data/lib/tumugi/config.rb +11 -0
- data/lib/tumugi/dag.rb +35 -0
- data/lib/tumugi/dsl.rb +15 -0
- data/lib/tumugi/helper.rb +15 -0
- data/lib/tumugi/logger.rb +21 -0
- data/lib/tumugi/target/base.rb +9 -0
- data/lib/tumugi/target/file_target.rb +17 -0
- data/lib/tumugi/task.rb +86 -0
- data/lib/tumugi/task_definition.rb +101 -0
- data/lib/tumugi/tumugi_module.rb +23 -0
- data/lib/tumugi/version.rb +3 -0
- data/tumugi.gemspec +32 -0
- metadata +217 -0
data/examples/target.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'tumugi/target/file_target'
|
2
|
+
|
3
|
+
task :task1 do
|
4
|
+
requires [:task2, :task3]
|
5
|
+
|
6
|
+
output do |task|
|
7
|
+
Tumugi::Target::FileTarget.new("/tmp/#{task.id}.txt")
|
8
|
+
end
|
9
|
+
|
10
|
+
run do |task|
|
11
|
+
puts 'task1#run'
|
12
|
+
File.write(task.output.path, 'done')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
task :task2 do
|
17
|
+
requires [:task4]
|
18
|
+
|
19
|
+
output do |task|
|
20
|
+
Tumugi::Target::FileTarget.new("/tmp/#{task.id}.txt")
|
21
|
+
end
|
22
|
+
|
23
|
+
run do |task|
|
24
|
+
puts 'task2#run'
|
25
|
+
File.write(task.output.path, 'done')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
task :task3 do
|
30
|
+
requires [:task4]
|
31
|
+
|
32
|
+
output do |task|
|
33
|
+
Tumugi::Target::FileTarget.new("/tmp/#{task.id}.txt")
|
34
|
+
end
|
35
|
+
|
36
|
+
run do |task|
|
37
|
+
puts 'task3#run'
|
38
|
+
File.write(task.output.path, 'done')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :task4 do
|
43
|
+
output do
|
44
|
+
Tumugi::Target::FileTarget.new('/tmp/task4.txt')
|
45
|
+
end
|
46
|
+
|
47
|
+
run do |task|
|
48
|
+
puts 'task4#run'
|
49
|
+
File.write(task.output.path, 'done')
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'tumugi/target/file_target'
|
2
|
+
|
3
|
+
class FileTask < Tumugi::Task
|
4
|
+
def output
|
5
|
+
Tumugi::Target::FileTarget.new("/tmp/#{self.id}.txt")
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
puts "#{self.id}#run"
|
10
|
+
File.write(output.path, 'done')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
task :task1, type: FileTask do
|
15
|
+
requires [:task2, :task3]
|
16
|
+
end
|
17
|
+
|
18
|
+
task :task2, type: FileTask do
|
19
|
+
requires [:task4]
|
20
|
+
end
|
21
|
+
|
22
|
+
task :task3, type: FileTask do
|
23
|
+
requires [:task4]
|
24
|
+
end
|
25
|
+
|
26
|
+
task :task4, type: FileTask do
|
27
|
+
end
|
data/exe/tumugi
ADDED
data/lib/tumugi.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
|
3
|
+
require 'tumugi/dag'
|
4
|
+
require 'tumugi/dsl'
|
5
|
+
require 'tumugi/command/run'
|
6
|
+
require 'tumugi/command/show'
|
7
|
+
|
8
|
+
module Tumugi
|
9
|
+
class Application
|
10
|
+
def initialize
|
11
|
+
@tasks = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(command, root_task_id, options)
|
15
|
+
load(options[:file], true)
|
16
|
+
dag = create_dag(root_task_id)
|
17
|
+
cmd = "Tumugi::Command::#{command.to_s.classify}".constantize.new
|
18
|
+
cmd.execute(dag, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_task(id, task)
|
22
|
+
@tasks[id.to_s] = task
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_task(id)
|
26
|
+
task = @tasks[id.to_s]
|
27
|
+
raise "Task not found: #{id}" if task.nil?
|
28
|
+
task
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_dag(id)
|
34
|
+
dag = Tumugi::DAG.new
|
35
|
+
task = find_task(id)
|
36
|
+
dag.add_task(task)
|
37
|
+
dag
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/tumugi/cli.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'tumugi'
|
3
|
+
require 'tumugi/tumugi_module'
|
4
|
+
|
5
|
+
module Tumugi
|
6
|
+
class CLI < Thor
|
7
|
+
package_name "tumugi"
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def common_options
|
11
|
+
option :file, aliases: '-f', desc: 'Task definition file name'
|
12
|
+
option :config, aliases: '-c', desc: 'Configuration file name', default: 'tumugi.rb'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "version", "Show version"
|
17
|
+
def version
|
18
|
+
puts "tumugi v#{Tumugi::VERSION}"
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "run", "Run workflow"
|
22
|
+
map "run" => "run_" # run is thor's reserved word, so this trick is needed
|
23
|
+
option :workers, aliases: '-w', type: :numeric, desc: 'Number of workers to run task concurrently'
|
24
|
+
option :quiet, type: :boolean, desc: 'Suppress log', default: false
|
25
|
+
option :verbose, type: :boolean, desc: 'Show verbose log', default: false
|
26
|
+
common_options
|
27
|
+
def run_(task)
|
28
|
+
process_common_options
|
29
|
+
Tumugi.application.execute(:run, task, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "show", "Show DAG of workflow"
|
33
|
+
common_options
|
34
|
+
option :out, aliases: '-o', desc: 'Output file name. If not specified, output result to STDOUT'
|
35
|
+
option :format, aliases: '-t', desc: 'Output file format. Only affected --out option is specified.', enum: ['dot', 'png', 'svg']
|
36
|
+
def show(task)
|
37
|
+
process_common_options
|
38
|
+
Tumugi.application.execute(:show, task, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def process_common_options
|
44
|
+
config_file = options[:config]
|
45
|
+
if config_file && File.exists?(config_file) && File.extname(config_file) == '.rb'
|
46
|
+
load(config_file)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'parallel'
|
2
|
+
require 'retriable'
|
3
|
+
|
4
|
+
module Tumugi
|
5
|
+
module Command
|
6
|
+
class Run
|
7
|
+
def execute(dag, options={})
|
8
|
+
workers = options[:workers] || Tumugi.config.workers
|
9
|
+
settings = { in_threads: workers }
|
10
|
+
|
11
|
+
Tumugi.logger.verbose! if options[:verbose]
|
12
|
+
Tumugi.logger.quiet! if options[:quiet]
|
13
|
+
|
14
|
+
Parallel.each(dag.tsort, settings) do |t|
|
15
|
+
Tumugi.logger.info "start: #{t.id}"
|
16
|
+
until t.ready?
|
17
|
+
sleep 1
|
18
|
+
end
|
19
|
+
unless t.completed?
|
20
|
+
Tumugi.logger.info "run: #{t.id}"
|
21
|
+
t.state = :running
|
22
|
+
|
23
|
+
begin
|
24
|
+
Retriable.retriable retry_options do
|
25
|
+
t.run
|
26
|
+
end
|
27
|
+
rescue => e
|
28
|
+
Tumugi.logger.info "failed: #{t.id}"
|
29
|
+
Tumugi.logger.error "#{e.message}"
|
30
|
+
t.state = :failed
|
31
|
+
else
|
32
|
+
Tumugi.logger.info "completed: #{t.id}"
|
33
|
+
t.state = :completed
|
34
|
+
end
|
35
|
+
else
|
36
|
+
t.state = :skipped
|
37
|
+
Tumugi.logger.info "skip: #{t.id} is already completed"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def retry_options
|
45
|
+
{
|
46
|
+
tries: Tumugi.config.max_retry,
|
47
|
+
base_interval: Tumugi.config.retry_interval,
|
48
|
+
max_interval: Tumugi.config.retry_interval * Tumugi.config.max_retry,
|
49
|
+
max_elapsed_time: Tumugi.config.retry_interval * Tumugi.config.max_retry,
|
50
|
+
multiplier: 1.0,
|
51
|
+
rand_factor: 0.0,
|
52
|
+
on_retry: on_retry
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def on_retry
|
57
|
+
Proc.new do |exception, try, elapsed_time, next_interval|
|
58
|
+
Tumugi.logger.error "#{exception.class}: '#{exception.message}' - #{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'gviz'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Tumugi
|
6
|
+
module Command
|
7
|
+
class Show
|
8
|
+
@@supported_formats = ['dot', 'png', 'jpg', 'svg', 'pdf']
|
9
|
+
|
10
|
+
def execute(dag, options={})
|
11
|
+
out = options[:out]
|
12
|
+
if out
|
13
|
+
ext = File.extname(options[:out])
|
14
|
+
format = ext[1..-1] if ext.start_with?('.')
|
15
|
+
raise "#{format} is not supported format" unless @@supported_formats.include?(format)
|
16
|
+
else
|
17
|
+
format = options[:format]
|
18
|
+
end
|
19
|
+
|
20
|
+
graph = Graph do
|
21
|
+
dag.tsort.each do |task|
|
22
|
+
node task.id
|
23
|
+
route task.id => task._requires.map {|t| t.id}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if out.present?
|
28
|
+
Dir.mktmpdir do |dir|
|
29
|
+
file_base_path = "#{File.dirname(dir)}/#{File.basename(out, '.*')}"
|
30
|
+
graph.save(file_base_path, format == 'dot' ? nil : format)
|
31
|
+
FileUtils.mkdir_p(File.dirname(out))
|
32
|
+
FileUtils.copy("#{file_base_path}.#{format}", out)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
print graph
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/tumugi/dag.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
require 'tumugi/helper'
|
3
|
+
|
4
|
+
module Tumugi
|
5
|
+
class DAG
|
6
|
+
include TSort
|
7
|
+
include Tumugi::Helper
|
8
|
+
|
9
|
+
attr_reader :tasks
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@tasks = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def tsort_each_node(&block)
|
16
|
+
@tasks.each_key(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def tsort_each_child(node, &block)
|
20
|
+
@tasks.fetch(node).each(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_task(task)
|
24
|
+
t = task.instance
|
25
|
+
unless @tasks[t]
|
26
|
+
reqs = list(t._requires).map {|r| r.instance }
|
27
|
+
@tasks[t] = reqs
|
28
|
+
reqs.each do |r|
|
29
|
+
add_task(r)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
task
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/tumugi/dsl.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Tumugi DSL functions.
|
2
|
+
|
3
|
+
require 'tumugi/task_definition'
|
4
|
+
|
5
|
+
module Tumugi
|
6
|
+
module DSL
|
7
|
+
def task(*args, &block)
|
8
|
+
Tumugi::TaskDefinition.define(*args, &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Extend the main object with the DSL commands.
|
14
|
+
# This allows top-level calls to task, etc.
|
15
|
+
extend Tumugi::DSL
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Tumugi
|
4
|
+
class Logger
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@logger, :debug, :error, :fatal, :info, :warn, :level
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@logger = ::Logger.new(STDOUT)
|
10
|
+
@logger.level = ::Logger::INFO
|
11
|
+
end
|
12
|
+
|
13
|
+
def verbose!
|
14
|
+
@logger.level = ::Logger::DEBUG
|
15
|
+
end
|
16
|
+
|
17
|
+
def quiet!
|
18
|
+
@logger = ::Logger.new(nil)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/tumugi/task.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'tumugi/helper'
|
2
|
+
|
3
|
+
module Tumugi
|
4
|
+
class Task
|
5
|
+
include Tumugi::Helper
|
6
|
+
|
7
|
+
attr_accessor :state # :pending, :running, :completed, :skipped
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@logger = Tumugi.logger
|
11
|
+
@state = :pending
|
12
|
+
end
|
13
|
+
|
14
|
+
def id
|
15
|
+
@id ||= self.class.name
|
16
|
+
end
|
17
|
+
|
18
|
+
def id=(s)
|
19
|
+
@id = s
|
20
|
+
end
|
21
|
+
|
22
|
+
def eql?(other)
|
23
|
+
self.hash == other.hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def hash
|
27
|
+
self.id.hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def instance
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# If you need to define task dependencies, override in subclass
|
35
|
+
def requires
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
|
39
|
+
def input
|
40
|
+
@input ||= _input
|
41
|
+
end
|
42
|
+
|
43
|
+
# If you need to define output of task to skip alredy done task,
|
44
|
+
# override in subclass. If not, a task run always.
|
45
|
+
def output
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
|
49
|
+
def run
|
50
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def ready?
|
54
|
+
list(_requires).all? { |t| t.instance.completed? }
|
55
|
+
end
|
56
|
+
|
57
|
+
def completed?
|
58
|
+
outputs = list(output)
|
59
|
+
!outputs.empty? && outputs.all?(&:exist?)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Following methods are internal use only
|
63
|
+
|
64
|
+
def _requires
|
65
|
+
@_requires ||= requires
|
66
|
+
end
|
67
|
+
|
68
|
+
def _output
|
69
|
+
@_output ||= output
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def _input
|
75
|
+
if _requires.nil?
|
76
|
+
[]
|
77
|
+
elsif _requires.is_a?(Array)
|
78
|
+
_requires.map { |t| t.instance._output }
|
79
|
+
elsif _requires.is_a?(Hash)
|
80
|
+
Hash[_requires.map { |k, t| [k, t.instance._output] }]
|
81
|
+
else
|
82
|
+
_requires.instance._output
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|