async-container 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/development.yml +36 -0
  3. data/.travis.yml +3 -3
  4. data/Gemfile +0 -3
  5. data/README.md +76 -9
  6. data/examples/async.rb +21 -0
  7. data/examples/channel.rb +44 -0
  8. data/examples/channels/client.rb +103 -0
  9. data/examples/container.rb +1 -1
  10. data/examples/isolate.rb +35 -0
  11. data/examples/minimal.rb +93 -0
  12. data/examples/test.rb +50 -0
  13. data/{title.rb → examples/title.rb} +0 -0
  14. data/examples/udppipe.rb +34 -0
  15. data/lib/async/container/best.rb +1 -1
  16. data/lib/async/container/channel.rb +57 -0
  17. data/lib/async/container/controller.rb +112 -21
  18. data/lib/async/container/error.rb +10 -0
  19. data/lib/async/container/forked.rb +3 -65
  20. data/lib/async/container/generic.rb +179 -8
  21. data/lib/async/container/group.rb +98 -93
  22. data/lib/async/container/hybrid.rb +2 -3
  23. data/lib/async/container/keyed.rb +53 -0
  24. data/lib/async/container/notify.rb +41 -0
  25. data/lib/async/container/notify/client.rb +61 -0
  26. data/lib/async/container/notify/pipe.rb +115 -0
  27. data/lib/async/container/notify/server.rb +111 -0
  28. data/lib/async/container/notify/socket.rb +86 -0
  29. data/lib/async/container/process.rb +167 -0
  30. data/lib/async/container/thread.rb +182 -0
  31. data/lib/async/container/threaded.rb +4 -90
  32. data/lib/async/container/version.rb +1 -1
  33. data/spec/async/container/controller_spec.rb +40 -0
  34. data/spec/async/container/forked_spec.rb +3 -1
  35. data/spec/async/container/hybrid_spec.rb +4 -1
  36. data/spec/async/container/notify/notify.rb +18 -0
  37. data/spec/async/container/notify/pipe_spec.rb +46 -0
  38. data/spec/async/container/notify_spec.rb +54 -0
  39. data/spec/async/container/shared_examples.rb +18 -6
  40. data/spec/async/container/threaded_spec.rb +2 -0
  41. metadata +27 -4
@@ -18,10 +18,14 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'async/reactor'
21
+ require 'async'
22
22
 
23
23
  require 'etc'
24
24
 
25
+ require_relative 'group'
26
+ require_relative 'keyed'
27
+ require_relative 'statistics'
28
+
25
29
  module Async
26
30
  module Container
27
31
  # @return [Integer] the number of hardware processors which can run threads/processes simultaneously.
@@ -32,8 +36,30 @@ module Async
32
36
  end
33
37
 
34
38
  class Generic
35
- def initialize
39
+ def self.run(*arguments, **options, &block)
40
+ self.new.run(*arguments, **options, &block)
41
+ end
42
+
43
+ UNNAMED = "Unnamed"
44
+
45
+ def initialize(**options)
46
+ @group = Group.new
47
+ @running = true
48
+
49
+ @state = {}
50
+
36
51
  @statistics = Statistics.new
52
+ @keyed = {}
53
+ end
54
+
55
+ attr :state
56
+
57
+ def to_s
58
+ "#{self.class} with #{@statistics.spawns} spawns and #{@statistics.failures} failures."
59
+ end
60
+
61
+ def [] key
62
+ @keyed[key]&.value
37
63
  end
38
64
 
39
65
  attr :statistics
@@ -42,23 +68,168 @@ module Async
42
68
  @statistics.failed?
43
69
  end
44
70
 
71
+ # Whether there are running tasks.
72
+ def running?
73
+ @group.running?
74
+ end
75
+
76
+ # Sleep until some state change occurs.
77
+ # @param duration [Integer] the maximum amount of time to sleep for.
78
+ def sleep(duration = nil)
79
+ @group.sleep(duration)
80
+ end
81
+
82
+ # Wait until all spawned tasks are completed.
83
+ def wait
84
+ @group.wait
85
+ end
86
+
87
+ def status?(flag)
88
+ # This also returns true if all processes have exited/failed:
89
+ @state.all?{|_, state| state[flag]}
90
+ end
91
+
92
+ def wait_until_ready
93
+ while true
94
+ Async.logger.debug(self) do |buffer|
95
+ buffer.puts "Waiting for ready:"
96
+ @state.each do |child, state|
97
+ buffer.puts "\t#{child.class}: #{state.inspect}"
98
+ end
99
+ end
100
+
101
+ self.sleep
102
+
103
+ if self.status?(:ready)
104
+ return true
105
+ end
106
+ end
107
+ end
108
+
109
+ def stop(timeout = true)
110
+ @running = false
111
+ @group.stop(timeout)
112
+
113
+ if @group.running?
114
+ Async.logger.warn(self) {"Group is still running after stopping it!"}
115
+ end
116
+ ensure
117
+ @running = true
118
+ end
119
+
120
+ def spawn(name: nil, restart: false, key: nil, &block)
121
+ name ||= UNNAMED
122
+
123
+ if mark?(key)
124
+ Async.logger.debug(self) {"Reusing existing child for #{key}: #{name}"}
125
+ return false
126
+ end
127
+
128
+ @statistics.spawn!
129
+
130
+ Fiber.new do
131
+ while @running
132
+ child = self.start(name, &block)
133
+
134
+ state = insert(key, child)
135
+
136
+ begin
137
+ status = @group.wait_for(child) do |message|
138
+ state.update(message)
139
+ end
140
+ ensure
141
+ delete(key, child)
142
+ end
143
+
144
+ if status.success?
145
+ Async.logger.info(self) {"#{child} #{status}"}
146
+ else
147
+ @statistics.failure!
148
+ Async.logger.error(self) {status}
149
+ end
150
+
151
+ if restart
152
+ @statistics.restart!
153
+ else
154
+ break
155
+ end
156
+ end
157
+ # ensure
158
+ # Async.logger.error(self) {$!} if $!
159
+ end.resume
160
+
161
+ return true
162
+ end
163
+
45
164
  def async(**options, &block)
46
165
  spawn(**options) do |instance|
47
- begin
48
- Async::Reactor.run(instance, &block)
49
- rescue Interrupt
50
- # Graceful exit.
51
- end
166
+ Async::Reactor.run(instance, &block)
52
167
  end
53
168
  end
54
169
 
55
170
  def run(count: Container.processor_count, **options, &block)
56
171
  count.times do
57
- async(**options, &block)
172
+ spawn(**options, &block)
58
173
  end
59
174
 
60
175
  return self
61
176
  end
177
+
178
+ def reload
179
+ @keyed.each_value(&:clear!)
180
+
181
+ yield
182
+
183
+ dirty = false
184
+
185
+ @keyed.delete_if do |key, value|
186
+ value.stop? && (dirty = true)
187
+ end
188
+
189
+ return dirty
190
+ end
191
+
192
+ def mark?(key)
193
+ if key
194
+ if value = @keyed[key]
195
+ value.mark!
196
+
197
+ return true
198
+ end
199
+ end
200
+
201
+ return false
202
+ end
203
+
204
+ def key?(key)
205
+ if key
206
+ @keyed.key?(key)
207
+ end
208
+ end
209
+
210
+ protected
211
+
212
+ # Register the child (value) as running.
213
+ def insert(key, child)
214
+ if key
215
+ @keyed[key] = Keyed.new(key, child)
216
+ end
217
+
218
+ state = {}
219
+
220
+ @state[child] = state
221
+
222
+ return state
223
+ end
224
+
225
+ # Clear the child (value) as running.
226
+ def delete(key, child)
227
+ if key
228
+ @keyed.delete(key)
229
+ end
230
+
231
+ @state.delete(child)
232
+ end
62
233
  end
63
234
  end
64
235
  end
@@ -18,83 +18,143 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
+ require 'fiber'
22
+
23
+ require 'async/clock'
24
+
25
+ require_relative 'error'
26
+
21
27
  module Async
22
28
  module Container
29
+ # Manages a group of running processes.
23
30
  class Group
24
31
  def initialize
25
- @pgid = nil
26
32
  @running = {}
27
33
 
34
+ # This queue allows us to wait for processes to complete, without spawning new processes as a result.
28
35
  @queue = nil
29
36
  end
30
37
 
31
- def spawn(*arguments)
32
- self.yield
33
-
34
- if pid = ::Process.spawn(*arguments)
35
- wait_for(pid)
36
- end
37
- end
38
+ # @attribute [Hash<IO, Fiber>] the running tasks, indexed by IO.
39
+ attr :running
38
40
 
39
- def fork(&block)
40
- self.yield
41
-
42
- if pid = ::Process.fork(&block)
43
- wait_for(pid)
44
- end
41
+ def running?
42
+ @running.any?
45
43
  end
46
44
 
47
45
  def any?
48
46
  @running.any?
49
47
  end
50
48
 
49
+ def empty?
50
+ @running.empty?
51
+ end
52
+
53
+ # This method sleeps for at most the specified duration.
51
54
  def sleep(duration)
52
55
  self.resume
53
56
  self.suspend
54
57
 
55
- Kernel::sleep(duration)
56
-
57
- while self.wait_one(::Process::WNOHANG)
58
- end
58
+ self.wait_for_children(duration)
59
59
  end
60
60
 
61
61
  def wait
62
62
  self.resume
63
63
 
64
- while self.any?
65
- self.wait_one
64
+ while self.running?
65
+ self.wait_for_children
66
66
  end
67
67
  end
68
68
 
69
- def kill(signal = :INT)
70
- ::Process.kill(signal, -@pgid) if @pgid
69
+ def interrupt
70
+ @running.each_value do |fiber|
71
+ fiber.resume(Interrupt)
72
+ end
71
73
  end
72
74
 
73
- def stop(graceful = false)
74
- if graceful
75
- self.kill(:INT)
76
- interrupt_all
75
+ def terminate
76
+ @running.each_value do |fiber|
77
+ fiber.resume(Terminate)
77
78
  end
78
- ensure
79
- self.close
80
79
  end
81
80
 
82
- def close
83
- begin
84
- self.kill(:TERM)
85
- rescue Errno::EPERM
86
- # Sometimes, `kill` code can give EPERM, if any signal couldn't be delivered to a child. This might occur if an exception is thrown in the user code (e.g. within the fiber), and there are other zombie processes which haven't been reaped yet. These should be dealt with below, so it shouldn't be an issue to ignore this condition.
81
+ def stop(timeout = 1)
82
+ # Handle legacy `graceful = true` argument:
83
+ if timeout
84
+ start_time = Async::Clock.now
85
+
86
+ # Use a default timeout if not specified:
87
+ timeout = 1 if timeout == true
88
+
89
+ self.interrupt
90
+
91
+ while self.any?
92
+ duration = Async::Clock.now - start_time
93
+ remaining = timeout - duration
94
+
95
+ if remaining >= 0
96
+ self.wait_for_children(duration)
97
+ else
98
+ self.wait_for_children(0)
99
+ break
100
+ end
101
+ end
102
+ end
103
+
104
+ # Timeout can also be `graceful = false`:
105
+ if timeout
106
+ self.interrupt
107
+ self.sleep(timeout)
87
108
  end
88
109
 
89
- # Clean up zombie processes - if user presses Ctrl-C or for some reason something else blows up, exception would propagate back to caller:
90
- interrupt_all
110
+ self.wait_for_children(duration)
111
+
112
+ # Terminate all children:
113
+ self.terminate
114
+
115
+ # Wait for all children to exit:
116
+ self.wait
117
+ end
118
+
119
+ def wait_for(channel)
120
+ io = channel.in
121
+
122
+ @running[io] = Fiber.current
123
+
124
+ while @running.key?(io)
125
+ result = Fiber.yield
126
+
127
+ if result == Interrupt
128
+ channel.interrupt!
129
+ elsif result == Terminate
130
+ channel.terminate!
131
+ elsif message = channel.receive
132
+ yield message
133
+ else
134
+ return channel.wait
135
+ end
136
+ end
137
+ ensure
138
+ @running.delete(io)
91
139
  end
92
140
 
93
141
  protected
94
142
 
143
+ def wait_for_children(duration = nil)
144
+ if !@running.empty?
145
+ readable, _, _ = ::IO.select(@running.keys, nil, nil, duration)
146
+
147
+ readable&.each do |io|
148
+ @running[io].resume
149
+ end
150
+ end
151
+ end
152
+
95
153
  def yield
96
154
  if @queue
97
- @queue << Fiber.current
155
+ fiber = Fiber.current
156
+
157
+ @queue << fiber
98
158
  Fiber.yield
99
159
  end
100
160
  end
@@ -105,65 +165,10 @@ module Async
105
165
 
106
166
  def resume
107
167
  if @queue
108
- @queue.each(&:resume)
168
+ queue = @queue
109
169
  @queue = nil
110
- end
111
- end
112
-
113
- def interrupt_all
114
- while self.any?
115
- self.wait_one do |fiber, status|
116
- begin
117
- # This causes the waiting fiber to `raise Interrupt`:
118
- fiber.resume(nil)
119
- rescue Interrupt
120
- # Graceful exit.
121
- end
122
- end
123
- end
124
- end
125
-
126
- # Wait for one process, should only be called when a child process has finished, otherwise would block.
127
- def wait_one(flags = 0)
128
- return unless @pgid
129
-
130
- # Wait for processes in this group:
131
- pid, status = ::Process.wait2(-@pgid, flags)
132
-
133
- return if flags & ::Process::WNOHANG and pid == nil
134
-
135
- fiber = @running.delete(pid)
136
-
137
- if @running.empty?
138
- @pgid = nil
139
- end
140
-
141
- if block_given?
142
- yield fiber, status
143
- else
144
- fiber.resume(status)
145
- end
146
- end
147
-
148
- def wait_for(pid)
149
- if @pgid
150
- # Set this process as part of the existing process group:
151
- ::Process.setpgid(pid, @pgid)
152
- else
153
- # Establishes the child process as a process group leader:
154
- ::Process.setpgid(pid, 0)
155
170
 
156
- # Save the process group id:
157
- @pgid = pid
158
- end
159
-
160
- @running[pid] = Fiber.current
161
-
162
- # Return process status:
163
- if result = Fiber.yield
164
- return result
165
- else
166
- raise Interrupt
171
+ queue.each(&:resume)
167
172
  end
168
173
  end
169
174
  end