async-container 0.15.0 → 0.16.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/.github/workflows/development.yml +36 -0
- data/.travis.yml +3 -3
- data/Gemfile +0 -3
- data/README.md +76 -9
- data/examples/async.rb +21 -0
- data/examples/channel.rb +44 -0
- data/examples/channels/client.rb +103 -0
- data/examples/container.rb +1 -1
- data/examples/isolate.rb +35 -0
- data/examples/minimal.rb +93 -0
- data/examples/test.rb +50 -0
- data/{title.rb → examples/title.rb} +0 -0
- data/examples/udppipe.rb +34 -0
- data/lib/async/container/best.rb +1 -1
- data/lib/async/container/channel.rb +57 -0
- data/lib/async/container/controller.rb +112 -21
- data/lib/async/container/error.rb +10 -0
- data/lib/async/container/forked.rb +3 -65
- data/lib/async/container/generic.rb +179 -8
- data/lib/async/container/group.rb +98 -93
- data/lib/async/container/hybrid.rb +2 -3
- data/lib/async/container/keyed.rb +53 -0
- data/lib/async/container/notify.rb +41 -0
- data/lib/async/container/notify/client.rb +61 -0
- data/lib/async/container/notify/pipe.rb +115 -0
- data/lib/async/container/notify/server.rb +111 -0
- data/lib/async/container/notify/socket.rb +86 -0
- data/lib/async/container/process.rb +167 -0
- data/lib/async/container/thread.rb +182 -0
- data/lib/async/container/threaded.rb +4 -90
- data/lib/async/container/version.rb +1 -1
- data/spec/async/container/controller_spec.rb +40 -0
- data/spec/async/container/forked_spec.rb +3 -1
- data/spec/async/container/hybrid_spec.rb +4 -1
- data/spec/async/container/notify/notify.rb +18 -0
- data/spec/async/container/notify/pipe_spec.rb +46 -0
- data/spec/async/container/notify_spec.rb +54 -0
- data/spec/async/container/shared_examples.rb +18 -6
- data/spec/async/container/threaded_spec.rb +2 -0
- 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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
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
|
40
|
-
|
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
|
-
|
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.
|
65
|
-
self.
|
64
|
+
while self.running?
|
65
|
+
self.wait_for_children
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
-
def
|
70
|
-
|
69
|
+
def interrupt
|
70
|
+
@running.each_value do |fiber|
|
71
|
+
fiber.resume(Interrupt)
|
72
|
+
end
|
71
73
|
end
|
72
74
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
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
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
90
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|