autorespawn 0.3.0 → 0.4.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
  SHA1:
3
- metadata.gz: 24740ded8fd2be27ffb5f7bd0d6e33f4b71f11f6
4
- data.tar.gz: 32036b70931a245bc4e1c12b70446160e439123b
3
+ metadata.gz: 74818e88a4f45fa8d63abe962bfa92381816f8d9
4
+ data.tar.gz: a2df936e42159d3e7156f305710a4976ac75bf68
5
5
  SHA512:
6
- metadata.gz: bf53297c07ebeb7edc339f60e961e9159da08b46275aa51cc8636002f08b37ec1a4b7410e4cd964b9be7c7fb6eb4540458427701739f464af7edc78f3f5cbee4
7
- data.tar.gz: eba4eff1d2c3d1c3ee07e80c541584ceede4231e6b20952a4c324fcd9d1e0b3a19141328906a5a10299b15cffbca961c53435bcd76a435774a3505f5ce6e4019
6
+ metadata.gz: a999ef955f12ef443ca425db10394ae4996d46cf0c8bf129697a31404bb7f6c7164381c16a8ca11e802374db6249cb64b4c2cadb4b7becabadf2283c476ad247
7
+ data.tar.gz: 6a3f7b0ac6e2e160f6f4a8fc3db0f9aa866cc1e3a4c252005d92ba8f806c2734f7ca97c1f046c0856acbddac5e2f62acd4c9384e0c212bb8315bde60f3e7ae63
@@ -3,3 +3,6 @@ rvm:
3
3
  - 2.1.6
4
4
  - 2.2.2
5
5
  before_install: gem install bundler -v 1.10.5
6
+ script:
7
+ - bundler exec rake
8
+ - bundler exec rake test
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :default => :test
10
+ task :default
@@ -181,7 +181,7 @@ def watch_yield
181
181
  end
182
182
  error_paths.merge(backtrace)
183
183
  if e.kind_of?(LoadError)
184
- error_paths << e.path
184
+ error_paths << Pathname.new(e.path)
185
185
  end
186
186
  end
187
187
  required_paths.merge(currently_loaded_files - current)
@@ -17,12 +17,14 @@ class Manager
17
17
  # {#workers} and {#active_slaves}
18
18
  attr_reader :self_slave
19
19
  # @return [Integer] the number of processes allowed to work in parallel
20
- attr_reader :parallel_level
20
+ attr_accessor :parallel_level
21
21
  # @return [Array<Slave>] declared worker processes, as a hash from
22
22
  # the PID to a Slave object
23
23
  attr_reader :workers
24
24
  # @return [Hash<Slave>] list of active slaves
25
25
  attr_reader :active_slaves
26
+ # @return [Array<Slave>] list of slaves explicitely queued with {#queue}
27
+ attr_reader :queued_slaves
26
28
 
27
29
  # @!group Hooks
28
30
 
@@ -68,13 +70,14 @@ def on_slave_start(&block)
68
70
  # @!endgroup
69
71
 
70
72
  def initialize(name: nil, parallel_level: 1)
71
- @parallel_level = parallel_level + 1
73
+ @parallel_level = parallel_level
72
74
  @workers = Array.new
73
75
  @name = name
74
76
  @seed = ProgramID.for_self
75
77
 
76
78
  @self_slave = Self.new(name: name)
77
79
  @workers << self_slave
80
+ @queued_slaves = Array.new
78
81
  @active_slaves = Hash[self_slave.pid => self_slave]
79
82
  end
80
83
 
@@ -85,11 +88,27 @@ def register_seed_files(files, search_patch = seed.ruby_load_path, ignore_not_fo
85
88
  seed.register_files(files, search_path, ignore_not_found)
86
89
  end
87
90
 
91
+ # Check whether this manager has slaves
92
+ def has_slaves?
93
+ # There's always a worker for self
94
+ workers.size != 1
95
+ end
96
+
97
+ # The number of slaves registered
98
+ def slave_count
99
+ workers.size - 1
100
+ end
101
+
88
102
  # Tests whether this slave is registered as a worker on self
89
103
  def include?(slave)
90
104
  workers.include?(slave)
91
105
  end
92
106
 
107
+ # Tests whether this manager has some slaves that are active
108
+ def has_active_slaves?
109
+ active_slaves.size != 1
110
+ end
111
+
93
112
  # Tests whether this slave is currently active on self
94
113
  def active?(slave)
95
114
  active_slaves[slave.pid] == slave
@@ -126,43 +145,48 @@ def register_slave(slave)
126
145
  slave
127
146
  end
128
147
 
148
+ # Queue a slave for execution
149
+ def queue(slave)
150
+ queued_slaves << slave
151
+ end
152
+
129
153
  # @api private
130
154
  #
131
155
  # Collect information about the finished slaves
132
156
  #
133
157
  # @return [Array<Slave>] the slaves that finished
134
- def collect_finished_slaves(wait: false)
158
+ def collect_finished_slaves
135
159
  finished_slaves = Array.new
136
- waitpid_options =
137
- if wait then []
138
- else [Process::WNOHANG]
139
- end
140
-
141
- while finished_child = Process.waitpid2(-1, *waitpid_options)
142
- pid, status = *finished_child
143
- if slave = active_slaves.delete(pid)
144
- finished_slaves << slave
145
- slave.finished(status)
146
- slave.subcommands.each do |name, cmdline, spawn_options|
147
- add_slave(*cmdline, name: name, **spawn_options)
148
- end
149
- seed.merge!(slave.program_id)
150
- run_hook :on_slave_finished, slave
151
- end
160
+ while finished_child = Process.waitpid2(-1, Process::WNOHANG)
161
+ finished_slaves << process_finished_slave(*finished_child)
152
162
  end
153
163
  finished_slaves
154
164
  rescue Errno::ECHILD
155
- Array.new
165
+ finished_slaves
166
+ end
167
+
168
+ def process_finished_slave(pid, status)
169
+ return if !(slave = active_slaves.delete(pid))
170
+
171
+ slave.finished(status)
172
+ slave.subcommands.each do |name, cmdline, spawn_options|
173
+ add_slave(*cmdline, name: name, **spawn_options)
174
+ end
175
+ seed.merge!(slave.program_id)
176
+ run_hook :on_slave_finished, slave
177
+ slave
156
178
  end
157
179
 
158
180
  # Kill all active slaves
159
181
  #
160
182
  # @see clear
161
183
  def kill
162
- active_slaves.each { |s| s.kill(join: false) }
163
- while active_slaves != [self_slave]
164
- collect_finished_slaves(wait: true)
184
+ active_slaves.each_value { |s| s.kill(join: false) }
185
+ while has_active_slaves?
186
+ finished_child = Process.waitpid2(-1)
187
+ process_finished_slave(*finished_child)
165
188
  end
189
+ rescue Errno::ECHILD
166
190
  end
167
191
 
168
192
  # Kill and remove all workers from this manager
@@ -191,13 +215,18 @@ def run
191
215
  end
192
216
 
193
217
  # Wait for children to terminate and spawns them when needed
194
- def poll
218
+ def poll(autospawn: true)
195
219
  finished_slaves = collect_finished_slaves
196
220
  new_slaves = Array.new
197
- while active_slaves.size < parallel_level
198
- if slave_i = workers.index { |s| s.needs_spawn? }
221
+ while active_slaves.size < parallel_level + 1
222
+ if slave = queued_slaves.find { |s| !s.running? }
223
+ queued_slaves.delete(slave)
224
+ elsif autospawn && (slave_i = workers.index { |s| s.needed? })
199
225
  slave = workers.delete_at(slave_i)
200
226
  @workers = workers[slave_i..-1] + workers[0, slave_i] + [slave]
227
+ end
228
+
229
+ if slave
201
230
  slave.spawn
202
231
  run_hook :__on_slave_start, slave
203
232
  new_slaves << slave
@@ -8,7 +8,7 @@ class Autorespawn
8
8
  # It basically stores information about all the files that form this
9
9
  # program.
10
10
  class ProgramID
11
- FileInfo = Struct.new :require_path, :path, :mtime, :size, :id
11
+ FileInfo = Struct.new :require_path, :path, :mtime, :size
12
12
 
13
13
  # Information about the files that form this program
14
14
  #
@@ -137,22 +137,18 @@ def id
137
137
  return @id if @id
138
138
 
139
139
  complete_id = files.keys.sort.map do |p|
140
- files[p].id
140
+ "#{p}#{files[p].mtime}#{files[p].size}"
141
141
  end.join("")
142
142
  @id = Digest::SHA1.hexdigest(complete_id)
143
143
  end
144
144
 
145
145
  # Whether the state on disk is different than the state stored in self
146
146
  def changed?
147
- files.each_value do |info|
147
+ files.each_value.any? do |info|
148
148
  return true if !info.path.exist?
149
149
  stat = info.path.stat
150
- if stat.mtime != info.mtime || stat.size != info.size
151
- new_id = compute_file_id(info.path)
152
- return new_id != info.id
153
- end
150
+ stat.mtime != info.mtime || stat.size != info.size
154
151
  end
155
- false
156
152
  end
157
153
 
158
154
  def include?(path, search_path = ruby_load_path)
@@ -202,45 +198,7 @@ def ruby_load_path
202
198
  def file_info(path, search_path = ruby_load_path)
203
199
  resolved = resolve_file_path(path, search_path)
204
200
  stat = resolved.stat
205
- id = compute_file_id(resolved)
206
- return FileInfo.new(path, resolved, stat.mtime, stat.size, id)
207
- end
208
-
209
- # @api private
210
- #
211
- # Compute the content ID of a text (code) file
212
- def compute_text_file_id(file)
213
- sanitized = file.readlines.map do |line|
214
- # Remove unnecessary spaces
215
- line = line.strip
216
- line = line.gsub(/\s\s+/, ' ')
217
- if !line.empty?
218
- line
219
- end
220
- end.compact
221
- Digest::SHA1.hexdigest(sanitized.join("\n"))
222
- end
223
-
224
- # @api private
225
- #
226
- # Compute the content ID of a binary file
227
- def compute_binary_file_id(file)
228
- Digest::SHA1.hexdigest(file.read(enc: 'BINARY'))
229
- end
230
-
231
- # Compute a SHA1 that is representative of the file's contents
232
- #
233
- # It does some whitespace cleanup, but is not meant to be super-robust
234
- # to changes that are irrelevant to the end program
235
- #
236
- # @param [Pathname] file the path to the file
237
- # @return [String] an ID string
238
- def compute_file_id(file)
239
- if file.extname == ".rb"
240
- compute_text_file_id(file)
241
- else
242
- compute_binary_file_id(file)
243
- end
201
+ return FileInfo.new(path, resolved, stat.mtime, stat.size)
244
202
  end
245
203
  end
246
204
  end
@@ -7,11 +7,12 @@ def initialize(*args, **options)
7
7
  @pid = Process.pid
8
8
  end
9
9
 
10
- def needs_spawn?; false end
10
+ def needed?; false end
11
+ def needed!; end
11
12
  def spawn
12
13
  pid
13
14
  end
14
- def kill
15
+ def kill(*, **)
15
16
  end
16
17
  def join
17
18
  end
@@ -47,7 +47,7 @@ def initialize(*cmdline, name: nil, seed: ProgramID.new, env: Hash.new, **spawn_
47
47
  @name = name
48
48
  @program_id = seed.dup
49
49
  @cmdline = cmdline
50
- @needs_spawn = true
50
+ @needed = true
51
51
  @spawn_env = env
52
52
  @spawn_options = spawn_options
53
53
  @subcommands = Array.new
@@ -85,7 +85,7 @@ def spawn
85
85
  SLAVE_RESULT_ENV => result_w.fileno.to_s)
86
86
 
87
87
  program_id.refresh
88
- @needs_spawn = false
88
+ @needed = false
89
89
  pid = Kernel.spawn(env, *cmdline, initial_r => initial_r, result_w => result_w, **spawn_options)
90
90
  initial_r.close
91
91
  result_w.close
@@ -112,8 +112,24 @@ def spawn
112
112
 
113
113
  # Whether this slave would need to be spawned, either because it has
114
114
  # never be, or because the program ID changed
115
- def needs_spawn?
116
- @needs_spawn || !status || program_id.changed?
115
+ def needed?
116
+ !running? && (@needed || program_id.changed?)
117
+ end
118
+
119
+ # Marks this slave for execution
120
+ #
121
+ # Note that it will only be executed by the underlying {Manager} when
122
+ # (1) a slot is available and (2) it has stopped running
123
+ #
124
+ # This is usually not called directly, but through {Manager#queue} which
125
+ # also ensures that the slave gets in front of the execution queue
126
+ def needed!
127
+ @needed = true
128
+ end
129
+
130
+ # Resets {#needed?} to false
131
+ def not_needed!
132
+ @needed = false
117
133
  end
118
134
 
119
135
  # Whether the slave is running
@@ -175,7 +191,7 @@ def finished(status)
175
191
  modified = program_id.register_files(file_list)
176
192
  @program_id = program_id.slice(file_list)
177
193
  if !modified.empty?
178
- @needs_spawn = true
194
+ needed!
179
195
  end
180
196
  result_r.close
181
197
  modified
@@ -1,3 +1,3 @@
1
1
  class Autorespawn
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: autorespawn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvain Joyeux
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-09-23 00:00:00.000000000 Z
11
+ date: 2015-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hooks