tumugi 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/.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
|