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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b5fa7853f4b7c0e9a187bb5f76f59019e7db4defdbc68d059ca71a90ad38b8b
4
- data.tar.gz: 91db6845b40c890928b3f217e5d91ab88f127187b95257a4863533db92dc0840
3
+ metadata.gz: ba214b0518a9968fceffca5ba9ce8ed32e845ecbacb56f153e14cd63fded465b
4
+ data.tar.gz: 6fb2041f89edda667b3b35522ac50503e90e93e7cbe6ecb5e2e93de46b45171d
5
5
  SHA512:
6
- metadata.gz: 184e4868d0cc1b5e24ae365e636e7fb988c656665c05420abce2551024143b53857cf32ebb1e717cadedfba655cae42079d1044aa5ae8f82872478d762be8190
7
- data.tar.gz: c7f0c1c4d1a8a23ee97be8916de501eacda6a70b526b387fbabbac130743b6aadcfccba1d626581cf0ebe860006cecb58cb7729cd48a4d8aa9d86633ef9a61ab
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.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.4)
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
@@ -26,7 +26,7 @@ module Fasten
26
26
  update_error_task task
27
27
  end
28
28
 
29
- stats_add_entry(self, task.state, task)
29
+ stats_add_entry(task.state, task)
30
30
  end
31
31
 
32
32
  def update_done_task(task)
@@ -1,14 +1,14 @@
1
1
  module Fasten
2
2
  class Executor < Task
3
- include Fasten::LogSupport
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: 8, worker_class: Fasten::Worker, fasten_dir: '.fasten')
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(self, state, self)
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
- dispatch_pending_tasks
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 (no_waiting_tasks? && tasks_running?) || task_running_list.count >= workers || (tasks_running? && tasks_failed?)
60
- ui_update
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, [], [], 0.2)
81
+ reads, _writes, _errors = IO.select(reads, [], [], 1)
63
82
 
64
83
  receive_workers_tasks(reads)
65
84
  end
66
- ui_update
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
- task = worker.receive
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
- remove_all_workers
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
- count = task_error_list.count
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
- raise "Stopping because the following #{count} #{count == 1 ? 'task' : 'tasks'} failed: #{task_error_list.map(&:to_s).join(', ')}"
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.dispatch(task)
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
- while (worker = worker_list.pop)
129
- worker.kill
130
- end
169
+ worker_list.each(&:kill)
170
+ worker_list.clear
171
+
172
+ ui.force_clear
131
173
  end
132
174
  end
133
175
  end
@@ -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.each do |key, val|
8
- next unless val.is_a?(String) && (match = %r{^/(.+)/$}.match(val))
9
-
10
- params[key] = Regexp.new(match[1])
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 LogSupport
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
- diff = object.fin - object.ini
24
+ object.dif = object.fin - object.ini
25
25
 
26
- log_info "Done #{object.class} #{object} #{message} in #{diff}"
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::INFO, progname: $PROGRAM_NAME)
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(object, state, target)
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
- object.stats_data ||= []
19
- object.stats_data << entry
18
+ self.stats_data ||= []
19
+ self.stats_entries ||= []
20
+ stats_data << entry
21
+ stats_entries << entry
20
22
 
21
- history = stats_history(object, entry)
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
- def stats_history(object, entry)
28
- object.stats_data.select { |e| e['state'] == entry['state'] && e['kind'] == entry['kind'] && e['name'] == entry['name'] }
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
@@ -1,6 +1,6 @@
1
1
  module Fasten
2
2
  class Task < OpenStruct
3
- include Fasten::LogSupport
3
+ include Fasten::Logger
4
4
 
5
5
  def to_s
6
6
  name
@@ -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
- # frozen_string_literal: true
1
+ require 'fasten/ui/console'
2
+ require 'fasten/ui/curses'
2
3
 
3
4
  module Fasten
4
5
  module UI
5
- include Curses
6
-
7
- SPINNER_STR = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
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
- ui_setup
14
- ui_title
15
- ui_workers
11
+ ui.update
16
12
 
17
13
  yield
18
14
  ensure
19
- close_screen
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fasten
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
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::LogSupport
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
- system task.shell if task.shell
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
- process_incoming_tasks
44
+ process_incoming_requests
20
45
  end
21
46
 
22
47
  close_child_pipes
23
48
  end
24
49
 
25
- def dispatch(task)
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 receive
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 process_incoming_tasks
78
- log_ini self, 'process_incoming_tasks'
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, 'process_incoming_tasks'
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
- Marshal.dump(task, child_write)
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/log_support'
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::LogSupport
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.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-16 00:00:00.000000000 Z
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/log_support.rb
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/