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 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/