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.
- 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
data/examples/test.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
require_relative 'group'
|
3
|
+
require_relative 'thread'
|
4
|
+
require_relative 'process'
|
5
|
+
|
6
|
+
group = Async::Container::Group.new
|
7
|
+
|
8
|
+
thread_monitor = Fiber.new do
|
9
|
+
while true
|
10
|
+
thread = Async::Container::Thread.fork do |instance|
|
11
|
+
if rand < 0.2
|
12
|
+
raise "Random Failure!"
|
13
|
+
end
|
14
|
+
|
15
|
+
instance.send(ready: true, status: "Started Thread")
|
16
|
+
|
17
|
+
sleep(1)
|
18
|
+
end
|
19
|
+
|
20
|
+
status = group.wait_for(thread) do |message|
|
21
|
+
puts "Thread message: #{message}"
|
22
|
+
end
|
23
|
+
|
24
|
+
puts "Thread status: #{status}"
|
25
|
+
end
|
26
|
+
end.resume
|
27
|
+
|
28
|
+
process_monitor = Fiber.new do
|
29
|
+
while true
|
30
|
+
# process = Async::Container::Process.fork do |instance|
|
31
|
+
# if rand < 0.2
|
32
|
+
# raise "Random Failure!"
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# instance.send(ready: true, status: "Started Process")
|
36
|
+
#
|
37
|
+
# sleep(1)
|
38
|
+
# end
|
39
|
+
|
40
|
+
process = Async::Container::Process.spawn('bash -c "sleep 1; echo foobar; sleep 1; exit -1"')
|
41
|
+
|
42
|
+
status = group.wait_for(process) do |message|
|
43
|
+
puts "Process message: #{message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "Process status: #{status}"
|
47
|
+
end
|
48
|
+
end.resume
|
49
|
+
|
50
|
+
group.wait
|
File without changes
|
data/examples/udppipe.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'async/io'
|
4
|
+
require 'async/io/endpoint'
|
5
|
+
require 'async/io/unix_endpoint'
|
6
|
+
|
7
|
+
@endpoint = Async::IO::Endpoint.unix("/tmp/notify-test.sock", Socket::SOCK_DGRAM)
|
8
|
+
# address = Async::IO::Address.udp("127.0.0.1", 6778)
|
9
|
+
# @endpoint = Async::IO::AddressEndpoint.new(address)
|
10
|
+
|
11
|
+
def server
|
12
|
+
@endpoint.bind do |server|
|
13
|
+
puts "Receiving..."
|
14
|
+
packet, address = server.recvfrom(512)
|
15
|
+
|
16
|
+
puts "Received: #{packet} from #{address}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def client(data = "Hello World!")
|
21
|
+
@endpoint.connect do |peer|
|
22
|
+
puts "Sending: #{data}"
|
23
|
+
peer.send(data)
|
24
|
+
puts "Sent!"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Async do |task|
|
29
|
+
server_task = task.async do
|
30
|
+
server
|
31
|
+
end
|
32
|
+
|
33
|
+
client
|
34
|
+
end
|
data/lib/async/container/best.rb
CHANGED
@@ -26,7 +26,7 @@ module Async
|
|
26
26
|
# Containers execute one or more "instances" which typically contain a reactor. A container spawns "instances" using threads and/or processes. Because these are resources that must be cleaned up some how (either by `join` or `waitpid`), their creation is deferred until the user invokes `Container#wait`. When executed this way, the container guarantees that all "instances" will be complete once `Container#wait` returns. Containers are constructs for achieving parallelism, and are not designed to be used directly for concurrency. Typically, you'd create one or more container, add some tasks to it, and then wait for it to complete.
|
27
27
|
module Container
|
28
28
|
def self.fork?
|
29
|
-
Process.respond_to?(:fork)
|
29
|
+
::Process.respond_to?(:fork) && ::Process.respond_to?(:setpgid)
|
30
30
|
end
|
31
31
|
|
32
32
|
def self.best_container_class
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'json'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
module Container
|
25
|
+
class Channel
|
26
|
+
def initialize
|
27
|
+
@in, @out = ::IO.pipe
|
28
|
+
end
|
29
|
+
|
30
|
+
attr :in
|
31
|
+
attr :out
|
32
|
+
|
33
|
+
def close_read
|
34
|
+
@in.close
|
35
|
+
end
|
36
|
+
|
37
|
+
def close_write
|
38
|
+
@out.close
|
39
|
+
end
|
40
|
+
|
41
|
+
def close
|
42
|
+
close_read
|
43
|
+
close_write
|
44
|
+
end
|
45
|
+
|
46
|
+
def receive
|
47
|
+
if data = @in.gets
|
48
|
+
begin
|
49
|
+
return JSON.parse(data, symbolize_names: true)
|
50
|
+
rescue
|
51
|
+
return {line: data}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -22,10 +22,11 @@ require_relative 'error'
|
|
22
22
|
require_relative 'best'
|
23
23
|
|
24
24
|
require_relative 'statistics'
|
25
|
+
require_relative 'notify'
|
25
26
|
|
26
27
|
module Async
|
27
28
|
module Container
|
28
|
-
class
|
29
|
+
class ContainerError < Error
|
29
30
|
def initialize(container)
|
30
31
|
super("Could not create container!")
|
31
32
|
@container = container
|
@@ -37,12 +38,37 @@ module Async
|
|
37
38
|
# Manages the life-cycle of a container.
|
38
39
|
class Controller
|
39
40
|
SIGHUP = Signal.list["HUP"]
|
40
|
-
|
41
|
+
SIGINT = Signal.list["INT"]
|
42
|
+
SIGTERM = Signal.list["TERM"]
|
43
|
+
SIGUSR1 = Signal.list["USR1"]
|
44
|
+
SIGUSR2 = Signal.list["USR2"]
|
41
45
|
|
42
|
-
def initialize(
|
46
|
+
def initialize(notify: Notify.open!)
|
43
47
|
@container = nil
|
44
48
|
|
45
|
-
@
|
49
|
+
if @notify = notify
|
50
|
+
@notify.status!("Initializing...")
|
51
|
+
end
|
52
|
+
|
53
|
+
@signals = {}
|
54
|
+
|
55
|
+
trap(SIGHUP, &self.method(:restart))
|
56
|
+
end
|
57
|
+
|
58
|
+
def state_string
|
59
|
+
if running?
|
60
|
+
"running"
|
61
|
+
else
|
62
|
+
"stopped"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
"#{self.class} #{state_string}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def trap(signal, &block)
|
71
|
+
@signals[signal] = block
|
46
72
|
end
|
47
73
|
|
48
74
|
attr :container
|
@@ -51,11 +77,21 @@ module Async
|
|
51
77
|
Container.new
|
52
78
|
end
|
53
79
|
|
80
|
+
def running?
|
81
|
+
!!@container
|
82
|
+
end
|
83
|
+
|
84
|
+
def wait
|
85
|
+
@container&.wait
|
86
|
+
end
|
87
|
+
|
54
88
|
def setup(container)
|
89
|
+
# Don't do this, otherwise calling super is risky for sub-classes:
|
90
|
+
# raise NotImplementedError, "Container setup is must be implemented in derived class!"
|
55
91
|
end
|
56
92
|
|
57
93
|
def start
|
58
|
-
self.restart
|
94
|
+
self.restart unless @container
|
59
95
|
end
|
60
96
|
|
61
97
|
def stop(graceful = true)
|
@@ -63,47 +99,94 @@ module Async
|
|
63
99
|
@container = nil
|
64
100
|
end
|
65
101
|
|
66
|
-
def restart
|
67
|
-
|
102
|
+
def restart
|
103
|
+
if @container
|
104
|
+
@notify&.restarting!
|
105
|
+
|
106
|
+
Async.logger.debug(self) {"Restarting container..."}
|
107
|
+
else
|
108
|
+
Async.logger.debug(self) {"Starting container..."}
|
109
|
+
end
|
110
|
+
|
68
111
|
container = self.create_container
|
69
112
|
|
70
113
|
begin
|
71
114
|
self.setup(container)
|
72
115
|
rescue
|
73
|
-
|
116
|
+
@notify&.error!($!.to_s)
|
117
|
+
|
118
|
+
raise ContainerError, container
|
74
119
|
end
|
75
120
|
|
121
|
+
# Wait for all child processes to enter the ready state.
|
76
122
|
Async.logger.debug(self, "Waiting for startup...")
|
77
|
-
container.
|
123
|
+
container.wait_until_ready
|
78
124
|
Async.logger.debug(self, "Finished startup.")
|
79
125
|
|
80
126
|
if container.failed?
|
127
|
+
@notify&.error!($!.to_s)
|
128
|
+
|
81
129
|
container.stop
|
82
130
|
|
83
|
-
raise
|
131
|
+
raise ContainerError, container
|
84
132
|
end
|
85
133
|
|
86
|
-
|
134
|
+
# Make this swap as atomic as possible:
|
135
|
+
old_container = @container
|
87
136
|
@container = container
|
88
|
-
|
89
|
-
|
137
|
+
|
138
|
+
old_container&.stop
|
139
|
+
@notify&.ready!
|
140
|
+
rescue
|
141
|
+
# If we are leaving this function with an exception, try to kill the container:
|
142
|
+
container&.stop(false)
|
143
|
+
end
|
144
|
+
|
145
|
+
def reload
|
146
|
+
@notify&.reloading!
|
147
|
+
|
148
|
+
Async.logger.info(self) {"Reloading container: #{@container}..."}
|
149
|
+
|
150
|
+
begin
|
151
|
+
self.setup(@container)
|
152
|
+
rescue
|
153
|
+
raise ContainerError, container
|
154
|
+
end
|
155
|
+
|
156
|
+
# Wait for all child processes to enter the ready state.
|
157
|
+
Async.logger.debug(self, "Waiting for startup...")
|
158
|
+
@container.wait_until_ready
|
159
|
+
Async.logger.debug(self, "Finished startup.")
|
160
|
+
|
161
|
+
if @container.failed?
|
162
|
+
@notify.error!("Container failed!")
|
163
|
+
|
164
|
+
raise ContainerError, @container
|
165
|
+
else
|
166
|
+
@notify&.ready!
|
167
|
+
end
|
90
168
|
end
|
91
169
|
|
92
170
|
def run
|
93
|
-
|
171
|
+
# I thought this was the default... but it doesn't always raise an exception unless you do this explicitly.
|
172
|
+
interrupt_action = Signal.trap(:INT) do
|
173
|
+
raise Interrupt
|
174
|
+
end
|
175
|
+
|
176
|
+
terminate_action = Signal.trap(:TERM) do
|
177
|
+
raise Terminate
|
178
|
+
end
|
94
179
|
|
95
180
|
self.start
|
96
181
|
|
97
|
-
while
|
182
|
+
while @container&.running?
|
98
183
|
begin
|
99
184
|
@container.wait
|
100
185
|
rescue SignalException => exception
|
101
|
-
if exception.signo
|
102
|
-
Async.logger.info(self) {"Reloading container..."}
|
103
|
-
|
186
|
+
if handler = @signals[exception.signo]
|
104
187
|
begin
|
105
|
-
|
106
|
-
rescue
|
188
|
+
handler.call
|
189
|
+
rescue ContainerError => failure
|
107
190
|
Async.logger.error(self) {failure}
|
108
191
|
end
|
109
192
|
else
|
@@ -111,8 +194,16 @@ module Async
|
|
111
194
|
end
|
112
195
|
end
|
113
196
|
end
|
197
|
+
rescue Interrupt
|
198
|
+
self.stop(true)
|
199
|
+
rescue Terminate
|
200
|
+
self.stop(false)
|
201
|
+
else
|
202
|
+
self.stop(true)
|
114
203
|
ensure
|
115
|
-
|
204
|
+
# Restore the interrupt handler:
|
205
|
+
Signal.trap(:INT, interrupt_action)
|
206
|
+
Signal.trap(:TERM, terminate_action)
|
116
207
|
end
|
117
208
|
end
|
118
209
|
end
|
@@ -18,81 +18,19 @@
|
|
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'
|
22
|
-
|
23
|
-
require_relative 'group'
|
24
21
|
require_relative 'generic'
|
25
|
-
require_relative '
|
22
|
+
require_relative 'process'
|
26
23
|
|
27
24
|
module Async
|
28
25
|
# Manages a reactor within one or more threads.
|
29
26
|
module Container
|
30
27
|
class Forked < Generic
|
31
|
-
UNNAMED = "Unnamed"
|
32
|
-
|
33
|
-
class Instance
|
34
|
-
def name= value
|
35
|
-
::Process.setproctitle(value)
|
36
|
-
end
|
37
|
-
|
38
|
-
def exec(*arguments)
|
39
|
-
::Process.exec(*arguments)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.run(*args, &block)
|
44
|
-
self.new.run(*args, &block)
|
45
|
-
end
|
46
|
-
|
47
28
|
def self.multiprocess?
|
48
29
|
true
|
49
30
|
end
|
50
31
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
@group = Group.new
|
55
|
-
end
|
56
|
-
|
57
|
-
def spawn(name: nil, restart: false)
|
58
|
-
Fiber.new do
|
59
|
-
while true
|
60
|
-
@statistics.spawn!
|
61
|
-
exit_status = @group.fork do
|
62
|
-
::Process.setproctitle(name) if name
|
63
|
-
|
64
|
-
yield Instance.new
|
65
|
-
end
|
66
|
-
|
67
|
-
if exit_status.success?
|
68
|
-
Async.logger.info(self) {"#{name || UNNAMED} #{exit_status}"}
|
69
|
-
else
|
70
|
-
@statistics.failure!
|
71
|
-
Async.logger.error(self) {exit_status}
|
72
|
-
end
|
73
|
-
|
74
|
-
if restart
|
75
|
-
@statistics.restart!
|
76
|
-
else
|
77
|
-
break
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end.resume
|
81
|
-
|
82
|
-
return self
|
83
|
-
end
|
84
|
-
|
85
|
-
def sleep(duration)
|
86
|
-
@group.sleep(duration)
|
87
|
-
end
|
88
|
-
|
89
|
-
def wait
|
90
|
-
@group.wait
|
91
|
-
end
|
92
|
-
|
93
|
-
# Gracefully shut down all children processes.
|
94
|
-
def stop(graceful = true)
|
95
|
-
@group.stop(graceful)
|
32
|
+
def start(name, &block)
|
33
|
+
Process.fork(name: name, &block)
|
96
34
|
end
|
97
35
|
end
|
98
36
|
end
|