fasten 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -2
- data/fasten.gemspec +2 -0
- data/lib/fasten/dag.rb +1 -1
- data/lib/fasten/executor.rb +61 -19
- data/lib/fasten/load_save.rb +9 -5
- data/lib/fasten/{log_support.rb → logger.rb} +4 -4
- data/lib/fasten/stats.rb +55 -6
- data/lib/fasten/task.rb +1 -1
- data/lib/fasten/ui/console.rb +58 -0
- data/lib/fasten/ui/curses.rb +256 -0
- data/lib/fasten/ui.rb +7 -155
- data/lib/fasten/version.rb +1 -1
- data/lib/fasten/worker.rb +43 -13
- data/lib/fasten.rb +18 -2
- metadata +33 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba214b0518a9968fceffca5ba9ce8ed32e845ecbacb56f153e14cd63fded465b
|
4
|
+
data.tar.gz: 6fb2041f89edda667b3b35522ac50503e90e93e7cbe6ecb5e2e93de46b45171d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e49e576f5ed462ec03884818a1fb24b39177bdc0661b5a028338c1160a1be301e71fdb7f109dfdcb306adae2c0153266be0245b650ecb8c1d2c23e5438f1708c
|
7
|
+
data.tar.gz: 674d4a02af2c36b36a2581f609ec13ba71ca8430ab0dd10060f2d0995d0fe5221969b70a9becf606abd7901f88fbbee63816eefc7482866faccc2d323b612303
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fasten (0.
|
4
|
+
fasten (0.5.0)
|
5
5
|
binding_of_caller
|
6
6
|
curses
|
7
|
+
hirb
|
8
|
+
parallel
|
7
9
|
|
8
10
|
GEM
|
9
11
|
remote: https://rubygems.org/
|
@@ -12,9 +14,10 @@ GEM
|
|
12
14
|
binding_of_caller (0.8.0)
|
13
15
|
debug_inspector (>= 0.0.1)
|
14
16
|
coderay (1.1.2)
|
15
|
-
curses (1.2.
|
17
|
+
curses (1.2.5)
|
16
18
|
debug_inspector (0.0.3)
|
17
19
|
diff-lcs (1.3)
|
20
|
+
hirb (0.7.3)
|
18
21
|
jaro_winkler (1.5.1)
|
19
22
|
method_source (0.9.0)
|
20
23
|
parallel (1.12.1)
|
data/fasten.gemspec
CHANGED
@@ -30,6 +30,8 @@ Gem::Specification.new do |spec|
|
|
30
30
|
|
31
31
|
spec.add_runtime_dependency 'binding_of_caller'
|
32
32
|
spec.add_runtime_dependency 'curses'
|
33
|
+
spec.add_runtime_dependency 'hirb'
|
34
|
+
spec.add_runtime_dependency 'parallel'
|
33
35
|
|
34
36
|
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
|
35
37
|
|
data/lib/fasten/dag.rb
CHANGED
data/lib/fasten/executor.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
module Fasten
|
2
2
|
class Executor < Task
|
3
|
-
include Fasten::
|
3
|
+
include Fasten::Logger
|
4
4
|
include Fasten::DAG
|
5
5
|
include Fasten::UI
|
6
6
|
include Fasten::LoadSave
|
7
7
|
include Fasten::Stats
|
8
8
|
|
9
|
-
def initialize(name: nil, workers:
|
9
|
+
def initialize(name: nil, developer: STDIN.tty? && STDOUT.tty?, workers: Parallel.physical_processor_count, worker_class: Fasten::Worker, fasten_dir: '.fasten')
|
10
10
|
setup_stats(name)
|
11
|
-
super name: name || "#{self.class} #{$PID}", workers: workers, pid: $PID, state: :IDLE, worker_class: worker_class, fasten_dir: fasten_dir
|
11
|
+
super name: name || "#{self.class} #{$PID}", workers: workers, pid: $PID, state: :IDLE, worker_class: worker_class, fasten_dir: fasten_dir, developer: developer
|
12
12
|
initialize_dag
|
13
13
|
|
14
14
|
self.worker_list = []
|
@@ -30,7 +30,7 @@ module Fasten
|
|
30
30
|
self.state = task_list.map(&:state).all?(:DONE) ? :DONE : :FAIL
|
31
31
|
log_fin self, running_counters
|
32
32
|
|
33
|
-
stats_add_entry(
|
33
|
+
stats_add_entry(state, self)
|
34
34
|
save_stats
|
35
35
|
end
|
36
36
|
|
@@ -47,46 +47,80 @@ module Fasten
|
|
47
47
|
wait_for_running_tasks
|
48
48
|
raise_error_in_failure
|
49
49
|
remove_workers_as_needed
|
50
|
-
|
50
|
+
if %i[PAUSING PAUSED QUITTING].include?(state)
|
51
|
+
check_state
|
52
|
+
else
|
53
|
+
dispatch_pending_tasks
|
54
|
+
end
|
51
55
|
|
52
|
-
break if no_running_tasks? && no_waiting_tasks?
|
56
|
+
break if no_running_tasks? && no_waiting_tasks? || state == :QUIT
|
53
57
|
end
|
54
58
|
|
55
59
|
remove_all_workers
|
56
60
|
end
|
57
61
|
|
62
|
+
def check_state
|
63
|
+
if state == :PAUSING && no_running_tasks?
|
64
|
+
self.state = :PAUSED
|
65
|
+
self.ui.message = nil
|
66
|
+
ui.force_clear
|
67
|
+
elsif state == :QUITTING && no_running_tasks?
|
68
|
+
self.state = :QUIT
|
69
|
+
ui.force_clear
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def should_wait_for_running_tasks?
|
74
|
+
tasks_running? && (no_waiting_tasks? || tasks_failed? || %i[PAUSING QUITTING].include?(state)) || task_running_list.count >= workers
|
75
|
+
end
|
76
|
+
|
58
77
|
def wait_for_running_tasks
|
59
|
-
while
|
60
|
-
|
78
|
+
while should_wait_for_running_tasks?
|
79
|
+
ui.update
|
61
80
|
reads = worker_list.map(&:parent_read)
|
62
|
-
reads, _writes, _errors = IO.select(reads, [], [],
|
81
|
+
reads, _writes, _errors = IO.select(reads, [], [], 1)
|
63
82
|
|
64
83
|
receive_workers_tasks(reads)
|
65
84
|
end
|
66
|
-
|
85
|
+
|
86
|
+
ui.update
|
67
87
|
end
|
68
88
|
|
69
89
|
def receive_workers_tasks(reads)
|
70
90
|
reads&.each do |read|
|
71
|
-
worker = worker_list.find { |item| item.parent_read == read }
|
72
|
-
|
91
|
+
next unless (worker = worker_list.find { |item| item.parent_read == read })
|
92
|
+
|
93
|
+
task = worker.receive_response
|
73
94
|
|
74
95
|
task_running_list.delete task
|
75
96
|
|
76
97
|
update_task task
|
77
98
|
|
78
99
|
log_fin task, done_counters
|
100
|
+
ui.force_clear
|
79
101
|
end
|
80
102
|
end
|
81
103
|
|
82
104
|
def raise_error_in_failure
|
83
105
|
return unless tasks_failed?
|
84
106
|
|
85
|
-
|
107
|
+
task_error_list.each do |task|
|
108
|
+
log_info "task: #{task} error:#{task.error}\n#{task.error&.backtrace&.join("\n")}"
|
109
|
+
end
|
86
110
|
|
87
|
-
|
111
|
+
if developer
|
112
|
+
ui.cleanup
|
113
|
+
puts "Stopping because the following tasks failed:\n"
|
114
|
+
task_error_list.map(&:to_s).each { |x| puts " #{x}" }
|
88
115
|
|
89
|
-
|
116
|
+
puts 'Entering development console'
|
117
|
+
|
118
|
+
Kernel.binding.pry # rubocop:disable Lint/Debugger
|
119
|
+
else
|
120
|
+
remove_all_workers
|
121
|
+
|
122
|
+
raise "Stopping because the following tasks failed: #{task_error_list.map(&:to_s).join(', ')}"
|
123
|
+
end
|
90
124
|
end
|
91
125
|
|
92
126
|
def remove_workers_as_needed
|
@@ -95,6 +129,8 @@ module Fasten
|
|
95
129
|
|
96
130
|
worker.kill
|
97
131
|
worker_list.delete worker
|
132
|
+
|
133
|
+
ui.force_clear
|
98
134
|
end
|
99
135
|
end
|
100
136
|
|
@@ -104,10 +140,13 @@ module Fasten
|
|
104
140
|
unless worker
|
105
141
|
@worker_id = (@worker_id || 0) + 1
|
106
142
|
worker = worker_class.new executor: self, name: "#{worker_class} #{format '%02X', @worker_id}"
|
143
|
+
worker.block = block if block
|
107
144
|
worker.fork
|
108
145
|
worker_list << worker
|
109
146
|
|
110
147
|
log_info "Worker created: #{worker}"
|
148
|
+
|
149
|
+
ui.force_clear
|
111
150
|
end
|
112
151
|
|
113
152
|
worker
|
@@ -119,15 +158,18 @@ module Fasten
|
|
119
158
|
|
120
159
|
task = next_task
|
121
160
|
log_ini task, "on worker #{worker}"
|
122
|
-
worker.
|
161
|
+
worker.send_request(task)
|
123
162
|
task_running_list << task
|
163
|
+
|
164
|
+
ui.force_clear
|
124
165
|
end
|
125
166
|
end
|
126
167
|
|
127
168
|
def remove_all_workers
|
128
|
-
|
129
|
-
|
130
|
-
|
169
|
+
worker_list.each(&:kill)
|
170
|
+
worker_list.clear
|
171
|
+
|
172
|
+
ui.force_clear
|
131
173
|
end
|
132
174
|
end
|
133
175
|
end
|
data/lib/fasten/load_save.rb
CHANGED
@@ -4,13 +4,17 @@ module Fasten
|
|
4
4
|
|
5
5
|
def load(path)
|
6
6
|
items = YAML.safe_load(File.read(path)).each do |name, params|
|
7
|
-
params.
|
8
|
-
|
9
|
-
|
10
|
-
params
|
7
|
+
if params.is_a? String
|
8
|
+
params = { after: params }
|
9
|
+
else
|
10
|
+
params&.each do |key, val|
|
11
|
+
next unless val.is_a?(String) && (match = %r{^/(.+)/$}.match(val))
|
12
|
+
|
13
|
+
params[key] = Regexp.new(match[1])
|
14
|
+
end
|
11
15
|
end
|
12
16
|
|
13
|
-
add Fasten::Task.new({ name: name }.merge(params))
|
17
|
+
add Fasten::Task.new({ name: name }.merge(params || {}))
|
14
18
|
end
|
15
19
|
|
16
20
|
log_info "Loaded #{items.count} tasks from #{path}"
|
@@ -3,7 +3,7 @@ module Fasten
|
|
3
3
|
attr_accessor :logger
|
4
4
|
end
|
5
5
|
|
6
|
-
module
|
6
|
+
module Logger
|
7
7
|
%w[debug info error].each do |method|
|
8
8
|
define_method "log_#{method}" do |msg|
|
9
9
|
return unless Fasten.logger.respond_to?(method)
|
@@ -21,9 +21,9 @@ module Fasten
|
|
21
21
|
|
22
22
|
def log_fin(object, message = nil)
|
23
23
|
object.fin ||= Time.new
|
24
|
-
|
24
|
+
object.dif = object.fin - object.ini
|
25
25
|
|
26
|
-
log_info "Done #{object.class} #{object} #{message} in #{
|
26
|
+
log_info "Done #{object.class} #{object} #{message} in #{object.dif}"
|
27
27
|
end
|
28
28
|
|
29
29
|
def redirect_std(path)
|
@@ -47,7 +47,7 @@ end
|
|
47
47
|
|
48
48
|
Fasten.logger ||=
|
49
49
|
begin
|
50
|
-
Logger.new(STDOUT, level: Logger::
|
50
|
+
Logger.new(STDOUT, level: Logger::DEBUG, progname: $PROGRAM_NAME)
|
51
51
|
rescue StandardError
|
52
52
|
nil
|
53
53
|
end
|
data/lib/fasten/stats.rb
CHANGED
@@ -11,21 +11,70 @@ module Fasten
|
|
11
11
|
}
|
12
12
|
end
|
13
13
|
|
14
|
-
def stats_add_entry(
|
14
|
+
def stats_add_entry(state, target)
|
15
15
|
return unless target.ini && target.fin
|
16
16
|
|
17
17
|
entry = stats_create_entry(state, target)
|
18
|
-
|
19
|
-
|
18
|
+
self.stats_data ||= []
|
19
|
+
self.stats_entries ||= []
|
20
|
+
stats_data << entry
|
21
|
+
stats_entries << entry
|
20
22
|
|
21
|
-
history = stats_history(
|
23
|
+
history = stats_history(entry)
|
22
24
|
|
25
|
+
update_cnt(history, entry)
|
23
26
|
update_avg(history, entry)
|
24
27
|
update_std(history, entry)
|
25
28
|
end
|
26
29
|
|
27
|
-
|
28
|
-
|
30
|
+
FLOAT_FORMATTER = ->(f) { format('%7.3f', f) }
|
31
|
+
|
32
|
+
def stats_table_run
|
33
|
+
sub = stats_entries.select { |x| x['kind'] == 'task' }.map { |x| x['run'] }.sum
|
34
|
+
tot = stats_entries.select { |x| x['kind'] == 'executor' }.map { |x| x['run'] }.sum
|
35
|
+
|
36
|
+
[sub, tot]
|
37
|
+
end
|
38
|
+
|
39
|
+
def split_time(time)
|
40
|
+
sign = time.negative? ? '-' : ''
|
41
|
+
time = -time if time.negative?
|
42
|
+
|
43
|
+
hours, seconds = time.divmod(3600)
|
44
|
+
minutes, seconds = seconds.divmod(60)
|
45
|
+
seconds, decimal = seconds.divmod(1)
|
46
|
+
milliseconds, _ignored = (decimal.round(4) * 1000).divmod(1)
|
47
|
+
|
48
|
+
[sign, hours, minutes, seconds, milliseconds]
|
49
|
+
end
|
50
|
+
|
51
|
+
def hformat(time, total = nil)
|
52
|
+
sign, hours, minutes, seconds, milliseconds = split_time time
|
53
|
+
|
54
|
+
str = hours.zero? ? format('%.1s%02d:%02d.%03d', sign, minutes, seconds, milliseconds) : format('%.1s%02d:%02d:%02d.%03d', sign, hours, minutes, seconds, milliseconds)
|
55
|
+
str += format(' (%.1f%%)', 100.0 * time / total) if total
|
56
|
+
|
57
|
+
str
|
58
|
+
end
|
59
|
+
|
60
|
+
def stats_table
|
61
|
+
sub, tot = stats_table_run
|
62
|
+
|
63
|
+
Hirb::Console.render_output(stats_entries,
|
64
|
+
fields: %w[state kind name run cnt avg std], unicode: true, class: 'Hirb::Helpers::AutoTable',
|
65
|
+
filters: { 'run' => FLOAT_FORMATTER, 'avg' => FLOAT_FORMATTER, 'std' => FLOAT_FORMATTER },
|
66
|
+
description: false)
|
67
|
+
|
68
|
+
puts format('∑tasks: %<task>s ∑executed: %<executed>s saved: %<saved>s workers: %<workers>s',
|
69
|
+
task: hformat(sub), executed: hformat(tot, sub), saved: hformat(sub - tot, sub), workers: workers.to_s)
|
70
|
+
end
|
71
|
+
|
72
|
+
def stats_history(entry)
|
73
|
+
stats_data.select { |e| e['state'] == entry['state'] && e['kind'] == entry['kind'] && e['name'] == entry['name'] }
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_cnt(history, entry)
|
77
|
+
entry['cnt'] = history.size
|
29
78
|
end
|
30
79
|
|
31
80
|
def update_avg(history, entry)
|
data/lib/fasten/task.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Fasten
|
4
|
+
module UI
|
5
|
+
class Console
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :executor, :worker_list, :task_list, :task_done_list, :task_error_list, :task_running_list, :task_waiting_list, :worker_list
|
8
|
+
def_delegators :executor, :name, :workers, :workers=, :state, :state=, :hformat
|
9
|
+
|
10
|
+
attr_accessor :executor
|
11
|
+
|
12
|
+
def initialize(executor:)
|
13
|
+
@executor = executor
|
14
|
+
@old = {
|
15
|
+
task_done_list: [],
|
16
|
+
task_error_list: []
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup
|
21
|
+
puts <<~FIN
|
22
|
+
|
23
|
+
= == === ==== ===== ====== ======= ======== ========= ==========
|
24
|
+
Fasten your seatbelts! #{'💺' * workers}
|
25
|
+
|
26
|
+
#{name}
|
27
|
+
FIN
|
28
|
+
|
29
|
+
$stdout.sync = true
|
30
|
+
@setup_done = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def update
|
34
|
+
setup unless @setup_done
|
35
|
+
display_task_message(task_done_list, @old[:task_done_list], 'Done in')
|
36
|
+
display_task_message(task_error_list, @old[:task_error_list], 'Fail in')
|
37
|
+
end
|
38
|
+
|
39
|
+
def cleanup
|
40
|
+
puts '========== ========= ======== ======= ====== ===== ==== === == ='
|
41
|
+
@setup_done = false
|
42
|
+
end
|
43
|
+
|
44
|
+
def force_clear; end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def display_task_message(orig, old, message)
|
49
|
+
return unless old.count != orig.count
|
50
|
+
|
51
|
+
(orig - old).each do |task|
|
52
|
+
puts "Time: #{hformat Time.new - executor.ini} #{message} #{hformat task.dif} Task #{task}"
|
53
|
+
old << task
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Fasten
|
6
|
+
module UI
|
7
|
+
class Curses
|
8
|
+
include ::Curses
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :executor, :worker_list, :task_list, :task_done_list, :task_error_list, :task_running_list, :task_waiting_list, :worker_list
|
11
|
+
def_delegators :executor, :name, :workers, :workers=, :state, :state=
|
12
|
+
|
13
|
+
attr_accessor :n_rows, :n_cols, :clear_needed, :message, :executor
|
14
|
+
|
15
|
+
SPINNER_STR = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
16
|
+
SPINNER_LEN = SPINNER_STR.length
|
17
|
+
PROGRESSBAR_STR = ' ▏▎▍▌▋▊▉'
|
18
|
+
PROGRESSBAR_LEN = PROGRESSBAR_STR.length
|
19
|
+
|
20
|
+
def initialize(executor:)
|
21
|
+
@executor = executor
|
22
|
+
end
|
23
|
+
|
24
|
+
def update
|
25
|
+
setup unless @setup_done
|
26
|
+
ui_keyboard
|
27
|
+
clear if clear_needed
|
28
|
+
draw_title
|
29
|
+
ui_workers
|
30
|
+
ui_tasks
|
31
|
+
|
32
|
+
refresh
|
33
|
+
self.clear_needed = false
|
34
|
+
end
|
35
|
+
|
36
|
+
def draw_title
|
37
|
+
ui_text_aligned(0, :left, 'Fasten your seatbelts!')
|
38
|
+
ui_text_aligned(0, :center, name.to_s)
|
39
|
+
ui_text_aligned(0, :right, Time.new.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def cleanup
|
43
|
+
close_screen
|
44
|
+
@setup_done = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup
|
48
|
+
init_screen
|
49
|
+
self.n_rows = lines
|
50
|
+
self.n_cols = cols
|
51
|
+
stdscr.keypad = true
|
52
|
+
stdscr.nodelay = true
|
53
|
+
setup_color
|
54
|
+
noecho
|
55
|
+
cbreak
|
56
|
+
nonl
|
57
|
+
curs_set 0
|
58
|
+
@setup_done = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def setup_color
|
62
|
+
start_color
|
63
|
+
use_default_colors
|
64
|
+
|
65
|
+
init_pair 1, Curses::COLOR_YELLOW, -1
|
66
|
+
init_pair 2, Curses::COLOR_GREEN, -1
|
67
|
+
init_pair 3, Curses::COLOR_RED, -1
|
68
|
+
init_pair 4, Curses::COLOR_WHITE, -1
|
69
|
+
end
|
70
|
+
|
71
|
+
def ui_text_aligned(row, align, str, attrs = nil)
|
72
|
+
if align == :center
|
73
|
+
setpos row, (n_cols - str.length) / 2
|
74
|
+
elsif align == :right
|
75
|
+
setpos row, n_cols - str.length
|
76
|
+
else
|
77
|
+
setpos row, 0
|
78
|
+
end
|
79
|
+
|
80
|
+
attrset attrs if attrs
|
81
|
+
addstr str
|
82
|
+
attroff attrs if attrs
|
83
|
+
|
84
|
+
str.length
|
85
|
+
end
|
86
|
+
|
87
|
+
def force_clear
|
88
|
+
self.clear_needed = true
|
89
|
+
end
|
90
|
+
|
91
|
+
def ui_keyboard
|
92
|
+
return unless (key = stdscr.getch)
|
93
|
+
|
94
|
+
self.message = nil
|
95
|
+
|
96
|
+
if key == Curses::Key::LEFT
|
97
|
+
if workers <= 1
|
98
|
+
self.message = "Can't remove 1 worker left, press [P] to pause"
|
99
|
+
else
|
100
|
+
self.workers -= 1
|
101
|
+
self.message = "Decreasing max workers to #{workers}"
|
102
|
+
end
|
103
|
+
elsif key == Curses::Key::RIGHT
|
104
|
+
self.workers += 1
|
105
|
+
self.message = "Increasing max workers to #{workers}"
|
106
|
+
elsif key == 'q'
|
107
|
+
self.message = 'Will quit when running tasks end'
|
108
|
+
self.state = :QUITTING
|
109
|
+
elsif key == 'p'
|
110
|
+
self.message = 'Will pause when running tasks end'
|
111
|
+
self.state = :PAUSING
|
112
|
+
elsif key == 'r'
|
113
|
+
self.state = :RUNNING
|
114
|
+
end
|
115
|
+
|
116
|
+
force_clear
|
117
|
+
end
|
118
|
+
|
119
|
+
def ui_workers_summary
|
120
|
+
running_count = task_running_list.count
|
121
|
+
waiting_count = task_waiting_list.count
|
122
|
+
workers_count = worker_list.count
|
123
|
+
|
124
|
+
"Procs: #{running_count} run #{workers_count - running_count} idle #{workers} max #{waiting_count} wait"
|
125
|
+
end
|
126
|
+
|
127
|
+
def ui_workers
|
128
|
+
l = ui_text_aligned(1, :left, ui_workers_summary) + 1
|
129
|
+
|
130
|
+
worker_list.each_with_index do |worker, index|
|
131
|
+
setpos 1, l + index
|
132
|
+
attrs = worker.running? ? A_STANDOUT : color_pair(4) | A_DIM
|
133
|
+
attrset attrs
|
134
|
+
addstr worker.running? ? 'R' : '_'
|
135
|
+
attroff attrs
|
136
|
+
end
|
137
|
+
|
138
|
+
ui_state
|
139
|
+
end
|
140
|
+
|
141
|
+
def ui_state
|
142
|
+
if state == :RUNNING
|
143
|
+
attrs = color_pair(2)
|
144
|
+
elsif state == :PAUSING
|
145
|
+
attrs = color_pair(1) | A_BLINK | A_STANDOUT
|
146
|
+
elsif state == :PAUSED
|
147
|
+
attrs = color_pair(1) | A_STANDOUT
|
148
|
+
elsif state == :QUITTING
|
149
|
+
attrs = color_pair(3) | A_BLINK | A_STANDOUT
|
150
|
+
end
|
151
|
+
|
152
|
+
l = ui_text_aligned(1, :right, state.to_s, attrs)
|
153
|
+
return unless message
|
154
|
+
|
155
|
+
setpos 1, n_cols - l - message.length - 1
|
156
|
+
addstr message
|
157
|
+
end
|
158
|
+
|
159
|
+
def ui_progressbar(row, col_ini, col_fin, count, total)
|
160
|
+
slice = total.to_f / (col_fin - col_ini + 1)
|
161
|
+
col_ini.upto col_fin do |col|
|
162
|
+
setpos row, col
|
163
|
+
count -= slice
|
164
|
+
if count.positive?
|
165
|
+
addstr PROGRESSBAR_STR[-1]
|
166
|
+
elsif count > -slice
|
167
|
+
addstr PROGRESSBAR_STR[(count * PROGRESSBAR_LEN / slice) % PROGRESSBAR_LEN]
|
168
|
+
else
|
169
|
+
addstr '.'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def ui_task_icon(task)
|
175
|
+
case task.state
|
176
|
+
when :RUNNING
|
177
|
+
SPINNER_STR[task.worker&.spinner]
|
178
|
+
when :FAIL
|
179
|
+
'✘'
|
180
|
+
when :DONE
|
181
|
+
'✔'
|
182
|
+
else
|
183
|
+
'…'
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def ui_task_color(task)
|
188
|
+
case task.state
|
189
|
+
when :RUNNING
|
190
|
+
color_pair(1) | A_TOP
|
191
|
+
when :FAIL
|
192
|
+
color_pair(3) | A_TOP
|
193
|
+
when :DONE
|
194
|
+
color_pair(2) | A_TOP
|
195
|
+
else
|
196
|
+
color_pair(4) | A_TOP
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def ui_task_string(task, y, x, icon: nil, str: nil)
|
201
|
+
setpos y, x
|
202
|
+
|
203
|
+
attrs = ui_task_color(task)
|
204
|
+
icon = ui_task_icon(task) if icon
|
205
|
+
|
206
|
+
str ||= icon ? "#{icon} #{task}" : task.to_s
|
207
|
+
|
208
|
+
attrset attrs if attrs
|
209
|
+
addstr str
|
210
|
+
attroff attrs if attrs
|
211
|
+
|
212
|
+
x + str.length
|
213
|
+
end
|
214
|
+
|
215
|
+
def ui_tasks
|
216
|
+
worker_list.each do |worker|
|
217
|
+
worker.spinner = (worker.spinner + 1) % SPINNER_LEN if worker.running?
|
218
|
+
end
|
219
|
+
|
220
|
+
count_done = task_done_list.count
|
221
|
+
count_total = task_list.count
|
222
|
+
tl = count_total.to_s.length
|
223
|
+
col_ini = ui_text_aligned(2, :left, format("Tasks: %#{tl}d/%d", count_done, count_total)) + 1
|
224
|
+
col_fin = n_cols - 5
|
225
|
+
ui_text_aligned(2, :right, "#{(count_done * 100 / count_total).to_i}%") if count_total.positive?
|
226
|
+
|
227
|
+
ui_progressbar(2, col_ini, col_fin, count_done, count_total)
|
228
|
+
|
229
|
+
max = 2
|
230
|
+
list = task_list.sort_by(&:run_score)
|
231
|
+
list.each_with_index do |task, index|
|
232
|
+
next if 3 + index >= n_rows
|
233
|
+
|
234
|
+
x = ui_task_string(task, 3 + index, 2, icon: true)
|
235
|
+
max = x if x > max
|
236
|
+
end
|
237
|
+
|
238
|
+
list.each_with_index do |task, index|
|
239
|
+
next if 3 + index >= n_rows
|
240
|
+
|
241
|
+
if task.dif
|
242
|
+
setpos 3 + index, max + 2
|
243
|
+
ui_task_string(task, 3 + index, max + 2, str: format('%.2f s', task.dif))
|
244
|
+
elsif task.depends && !task.depends.empty?
|
245
|
+
setpos 3 + index, max
|
246
|
+
x = max + 2
|
247
|
+
addstr ':'
|
248
|
+
task.depends.each do |dependant_task|
|
249
|
+
x = ui_task_string(dependant_task, 3 + index, x) + 1
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
data/lib/fasten/ui.rb
CHANGED
@@ -1,166 +1,18 @@
|
|
1
|
-
|
1
|
+
require 'fasten/ui/console'
|
2
|
+
require 'fasten/ui/curses'
|
2
3
|
|
3
4
|
module Fasten
|
4
5
|
module UI
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
SPINNER_LEN = SPINNER_STR.length
|
9
|
-
PROGRESSBAR_STR = ' ▏▎▍▌▋▊▉'
|
10
|
-
PROGRESSBAR_LEN = PROGRESSBAR_STR.length
|
6
|
+
def ui
|
7
|
+
@ui ||= STDIN.tty? && STDOUT.tty? ? Fasten::UI::Curses.new(executor: self) : Fasten::UI::Console.new(executor: self)
|
8
|
+
end
|
11
9
|
|
12
10
|
def run_ui
|
13
|
-
|
14
|
-
ui_title
|
15
|
-
ui_workers
|
11
|
+
ui.update
|
16
12
|
|
17
13
|
yield
|
18
14
|
ensure
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def ui_setup
|
23
|
-
init_screen
|
24
|
-
self.ui_rows = lines
|
25
|
-
self.ui_cols = cols
|
26
|
-
ui_setup_color
|
27
|
-
noecho
|
28
|
-
cbreak
|
29
|
-
nonl
|
30
|
-
curs_set 0
|
31
|
-
end
|
32
|
-
|
33
|
-
def ui_setup_color
|
34
|
-
start_color
|
35
|
-
use_default_colors
|
36
|
-
init_pair 1, Curses::COLOR_YELLOW, -1
|
37
|
-
init_pair 2, Curses::COLOR_GREEN, -1
|
38
|
-
init_pair 3, Curses::COLOR_RED, -1
|
39
|
-
init_pair 4, Curses::COLOR_WHITE, -1
|
40
|
-
end
|
41
|
-
|
42
|
-
def ui_text_aligned(row, align, str, attrs = nil)
|
43
|
-
if align == :center
|
44
|
-
setpos row, (ui_cols - str.length) / 2
|
45
|
-
elsif align == :right
|
46
|
-
setpos row, ui_cols - str.length
|
47
|
-
else
|
48
|
-
setpos row, 0
|
49
|
-
end
|
50
|
-
|
51
|
-
attrset attrs if attrs
|
52
|
-
addstr str
|
53
|
-
attroff attrs if attrs
|
54
|
-
|
55
|
-
str.length
|
56
|
-
end
|
57
|
-
|
58
|
-
def ui_title
|
59
|
-
ui_text_aligned(0, :left, 'Fasten your seatbelts!')
|
60
|
-
ui_text_aligned(0, :center, name.to_s)
|
61
|
-
ui_text_aligned(0, :right, Time.new.to_s)
|
62
|
-
end
|
63
|
-
|
64
|
-
def ui_update
|
65
|
-
clear
|
66
|
-
ui_title
|
67
|
-
ui_workers
|
68
|
-
ui_tasks
|
69
|
-
|
70
|
-
refresh
|
71
|
-
end
|
72
|
-
|
73
|
-
def ui_workers_summary
|
74
|
-
"Procs: #{task_running_list.count} run #{worker_list.count - task_running_list.count} idle #{workers} max"
|
75
|
-
end
|
76
|
-
|
77
|
-
def ui_workers
|
78
|
-
l = ui_text_aligned(1, :left, ui_workers_summary) + 1
|
79
|
-
|
80
|
-
worker_list.each_with_index do |worker, index|
|
81
|
-
setpos 1, l + index
|
82
|
-
attrs = worker.running? ? A_STANDOUT : color_pair(4) | A_DIM
|
83
|
-
attrset attrs
|
84
|
-
addstr worker.running? ? 'R' : '_'
|
85
|
-
attroff attrs
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def ui_progressbar(row, col_ini, col_fin, count, total)
|
90
|
-
slice = total.to_f / (col_fin - col_ini + 1)
|
91
|
-
col_ini.upto col_fin do |col|
|
92
|
-
setpos row, col
|
93
|
-
count -= slice
|
94
|
-
if count.positive?
|
95
|
-
addstr PROGRESSBAR_STR[-1]
|
96
|
-
elsif count > -slice
|
97
|
-
addstr PROGRESSBAR_STR[(count * PROGRESSBAR_LEN / slice) % PROGRESSBAR_LEN]
|
98
|
-
else
|
99
|
-
addstr '.'
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def ui_task_string(task, y, x, icon: nil)
|
105
|
-
setpos y, x
|
106
|
-
|
107
|
-
case task.state
|
108
|
-
when :RUNNING
|
109
|
-
attrs = color_pair(1) | A_TOP
|
110
|
-
icon = SPINNER_STR[task.worker&.spinner] if icon
|
111
|
-
when :FAIL
|
112
|
-
attrs = color_pair(3)
|
113
|
-
icon = '✘︎' if icon
|
114
|
-
when :DONE
|
115
|
-
attrs = color_pair(2)
|
116
|
-
icon = '✔︎' if icon
|
117
|
-
else
|
118
|
-
attrs = color_pair(4) | A_DIM
|
119
|
-
icon = '…' if icon
|
120
|
-
end
|
121
|
-
|
122
|
-
str = icon ? "#{icon} #{task}" : task.to_s
|
123
|
-
|
124
|
-
attrset attrs if attrs
|
125
|
-
addstr str
|
126
|
-
attroff attrs if attrs
|
127
|
-
|
128
|
-
x + str.length
|
129
|
-
end
|
130
|
-
|
131
|
-
def ui_tasks
|
132
|
-
worker_list.each do |worker|
|
133
|
-
worker.spinner = (worker.spinner + 1) % SPINNER_LEN if worker.running?
|
134
|
-
end
|
135
|
-
|
136
|
-
count_done = task_done_list.count
|
137
|
-
count_total = task_list.count
|
138
|
-
tl = count_total.to_s.length
|
139
|
-
col_ini = ui_text_aligned(2, :left, format("Tasks: %#{tl}d/%s", count_done, count_total)) + 1
|
140
|
-
col_fin = ui_cols - 5
|
141
|
-
ui_text_aligned(2, :right, "#{(count_done * 100/count_total).to_i}%") if count_total.positive?
|
142
|
-
|
143
|
-
ui_progressbar(2, col_ini, col_fin, count_done, count_total)
|
144
|
-
|
145
|
-
max = 2
|
146
|
-
list = task_list.sort_by(&:run_score)
|
147
|
-
list.each_with_index do |task, index|
|
148
|
-
next if 3 + index >= ui_rows
|
149
|
-
|
150
|
-
x = ui_task_string(task, 3 + index, 2, icon: true)
|
151
|
-
max = x if x > max
|
152
|
-
end
|
153
|
-
|
154
|
-
list.each_with_index do |task, index|
|
155
|
-
next if 3 + index >= ui_rows || task.depends.nil? || task.depends.empty?
|
156
|
-
|
157
|
-
setpos 3 + index, max
|
158
|
-
x = max + 2
|
159
|
-
addstr ':'
|
160
|
-
task.depends.each do |dependant_task|
|
161
|
-
x = ui_task_string(dependant_task, 3 + index, x) + 1
|
162
|
-
end
|
163
|
-
end
|
15
|
+
ui.cleanup
|
164
16
|
end
|
165
17
|
end
|
166
18
|
end
|
data/lib/fasten/version.rb
CHANGED
data/lib/fasten/worker.rb
CHANGED
@@ -1,13 +1,38 @@
|
|
1
1
|
module Fasten
|
2
|
+
class WorkerError < StandardError
|
3
|
+
attr_reader :backtrace
|
4
|
+
|
5
|
+
def initialize(origin)
|
6
|
+
super "#{origin.class} #{origin.message}"
|
7
|
+
@backtrace = origin.backtrace
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
2
11
|
class Worker < Task
|
3
|
-
include Fasten::
|
12
|
+
include Fasten::Logger
|
4
13
|
|
5
14
|
def initialize(executor:, name: nil)
|
6
15
|
super executor: executor, name: name, spinner: 0
|
7
16
|
end
|
8
17
|
|
9
18
|
def perform(task)
|
10
|
-
|
19
|
+
perform_shell(task) if task.shell
|
20
|
+
perform_ruby(task) if task.ruby
|
21
|
+
perform_block(task) if block
|
22
|
+
end
|
23
|
+
|
24
|
+
def perform_ruby(task)
|
25
|
+
task.response = eval task.ruby # rubocop:disable Security/Eval we trust our users ;-)
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform_shell(task)
|
29
|
+
result = system task.shell
|
30
|
+
|
31
|
+
raise "Command failed with exit code: #{$CHILD_STATUS.exitstatus}" unless result
|
32
|
+
end
|
33
|
+
|
34
|
+
def perform_block(task)
|
35
|
+
task.response = block.call(task.request)
|
11
36
|
end
|
12
37
|
|
13
38
|
def fork
|
@@ -16,20 +41,20 @@ module Fasten
|
|
16
41
|
self.pid = Process.fork do
|
17
42
|
close_parent_pipes
|
18
43
|
|
19
|
-
|
44
|
+
process_incoming_requests
|
20
45
|
end
|
21
46
|
|
22
47
|
close_child_pipes
|
23
48
|
end
|
24
49
|
|
25
|
-
def
|
50
|
+
def send_request(task)
|
26
51
|
Marshal.dump(Task.new(task.to_h.merge(depends: nil, dependants: nil)), parent_write)
|
27
52
|
self.running_task = task
|
28
53
|
task.worker = self
|
29
54
|
task.state = :RUNNING
|
30
55
|
end
|
31
56
|
|
32
|
-
def
|
57
|
+
def receive_response
|
33
58
|
updated_task = Marshal.load(parent_read) # rubocop:disable Security/MarshalLoad because pipe is a secure channel
|
34
59
|
|
35
60
|
%i[ini fin response error].each { |key| running_task[key] = updated_task[key] }
|
@@ -74,14 +99,14 @@ module Fasten
|
|
74
99
|
child_write.close unless child_write.closed?
|
75
100
|
end
|
76
101
|
|
77
|
-
def
|
78
|
-
log_ini self, '
|
102
|
+
def process_incoming_requests
|
103
|
+
log_ini self, 'process_incoming_requests'
|
79
104
|
|
80
105
|
while (object = Marshal.load(child_read)) # rubocop:disable Security/MarshalLoad because pipe is a secure channel
|
81
106
|
run_task(object) if object.is_a? Fasten::Task
|
82
107
|
end
|
83
108
|
|
84
|
-
log_fin self, '
|
109
|
+
log_fin self, 'process_incoming_requests'
|
85
110
|
rescue EOFError
|
86
111
|
log_info 'Terminating on EOF'
|
87
112
|
end
|
@@ -102,12 +127,17 @@ module Fasten
|
|
102
127
|
log_ini task, 'perform'
|
103
128
|
|
104
129
|
perform(task)
|
105
|
-
|
106
|
-
log_fin task, 'perform'
|
107
|
-
Marshal.dump(task, child_write)
|
108
130
|
rescue StandardError => error
|
109
|
-
task.error = error
|
110
|
-
|
131
|
+
task.error = WorkerError.new(error)
|
132
|
+
ensure
|
133
|
+
log_fin task, 'perform'
|
134
|
+
send_response(task)
|
135
|
+
end
|
136
|
+
|
137
|
+
def send_response(task)
|
138
|
+
log_info "Sending task response back to executor #{task}"
|
139
|
+
data = Marshal.dump(task)
|
140
|
+
child_write.write(data)
|
111
141
|
end
|
112
142
|
end
|
113
143
|
end
|
data/lib/fasten.rb
CHANGED
@@ -8,8 +8,10 @@ require 'ostruct'
|
|
8
8
|
require 'curses'
|
9
9
|
require 'fileutils'
|
10
10
|
require 'csv'
|
11
|
+
require 'hirb'
|
12
|
+
require 'parallel'
|
11
13
|
|
12
|
-
require 'fasten/
|
14
|
+
require 'fasten/logger'
|
13
15
|
require 'fasten/stats'
|
14
16
|
require 'fasten/task'
|
15
17
|
require 'fasten/ui'
|
@@ -21,7 +23,7 @@ require 'fasten/version'
|
|
21
23
|
|
22
24
|
module Fasten
|
23
25
|
class << self
|
24
|
-
include Fasten::
|
26
|
+
include Fasten::Logger
|
25
27
|
|
26
28
|
def load(path, **options)
|
27
29
|
executor = Fasten::Executor.new(**options)
|
@@ -29,5 +31,19 @@ module Fasten
|
|
29
31
|
|
30
32
|
executor
|
31
33
|
end
|
34
|
+
|
35
|
+
def map(list, **options, &block)
|
36
|
+
executor = Fasten::Executor.new(**options)
|
37
|
+
executor.block = block
|
38
|
+
|
39
|
+
list.each do |item|
|
40
|
+
executor.add Fasten::Task.new name: item.to_s, request: item
|
41
|
+
end
|
42
|
+
|
43
|
+
executor.perform
|
44
|
+
executor.stats_table
|
45
|
+
|
46
|
+
executor.task_list.map(&:response)
|
47
|
+
end
|
32
48
|
end
|
33
49
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fasten
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aldrin Martoq
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-10-
|
11
|
+
date: 2018-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -108,6 +108,34 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: hirb
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: parallel
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
111
139
|
description: Fasten your seatbelts! Run jobs in parallel, intelligently.
|
112
140
|
email:
|
113
141
|
- contacto@a0.cl
|
@@ -134,10 +162,12 @@ files:
|
|
134
162
|
- lib/fasten/dag.rb
|
135
163
|
- lib/fasten/executor.rb
|
136
164
|
- lib/fasten/load_save.rb
|
137
|
-
- lib/fasten/
|
165
|
+
- lib/fasten/logger.rb
|
138
166
|
- lib/fasten/stats.rb
|
139
167
|
- lib/fasten/task.rb
|
140
168
|
- lib/fasten/ui.rb
|
169
|
+
- lib/fasten/ui/console.rb
|
170
|
+
- lib/fasten/ui/curses.rb
|
141
171
|
- lib/fasten/version.rb
|
142
172
|
- lib/fasten/worker.rb
|
143
173
|
homepage: https://github.com/a0/fasten/
|