fasten 0.4.0 → 0.5.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 +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/
|