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.
@@ -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