tumugi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
+ require 'tumugi/cli'
4
+ Tumugi::CLI.start(ARGV)
@@ -0,0 +1,4 @@
1
+ require 'tumugi/version'
2
+
3
+ module Tumugi
4
+ end
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ module Tumugi
2
+ class Config
3
+ attr_accessor :workers, :max_retry, :retry_interval
4
+
5
+ def initialize
6
+ @workers = 1
7
+ @max_retry = 3
8
+ @retry_interval = 300 #seconds
9
+ end
10
+ end
11
+ end
@@ -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
@@ -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,15 @@
1
+ module Tumugi
2
+ module Helper
3
+ def list(obj)
4
+ if obj.nil?
5
+ []
6
+ elsif obj.is_a?(Array)
7
+ obj
8
+ elsif obj.is_a?(Hash)
9
+ obj.map { |k,v| v }
10
+ else
11
+ [obj]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ module Tumugi
2
+ module Target
3
+ class Base
4
+ def exist?
5
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ require 'tumugi/target/base'
2
+
3
+ module Tumugi
4
+ module Target
5
+ class FileTarget < Base
6
+ attr_reader :path
7
+
8
+ def initialize(path)
9
+ @path = path
10
+ end
11
+
12
+ def exist?
13
+ ::File.exist?(path)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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