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
@@ -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
@@ -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
@@ -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) and Process.respond_to?(:setpgid)
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 ContainerFailed < Error
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
- DEFAULT_TIMEOUT = 2
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(startup_duration: DEFAULT_TIMEOUT)
46
+ def initialize(notify: Notify.open!)
43
47
  @container = nil
44
48
 
45
- @startup_duration = startup_duration
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(duration = @startup_duration)
67
- hup_action = Signal.trap(:HUP, :IGNORE)
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
- raise ContainerFailed, container
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.sleep(duration)
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 ContainerFailed, container
131
+ raise ContainerError, container
84
132
  end
85
133
 
86
- @container&.stop
134
+ # Make this swap as atomic as possible:
135
+ old_container = @container
87
136
  @container = container
88
- ensure
89
- Signal.trap(:HUP, hup_action)
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
- Async.logger.debug(self) {"Starting container..."}
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 true
182
+ while @container&.running?
98
183
  begin
99
184
  @container.wait
100
185
  rescue SignalException => exception
101
- if exception.signo == SIGHUP
102
- Async.logger.info(self) {"Reloading container..."}
103
-
186
+ if handler = @signals[exception.signo]
104
187
  begin
105
- self.restart
106
- rescue ContainerFailed => failure
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
- self.stop
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
@@ -22,5 +22,15 @@ module Async
22
22
  module Container
23
23
  class Error < StandardError
24
24
  end
25
+
26
+ Interrupt = ::Interrupt
27
+
28
+ class Terminate < SignalException
29
+ SIGTERM = Signal.list['TERM']
30
+
31
+ def initialize
32
+ super(SIGTERM)
33
+ end
34
+ end
25
35
  end
26
36
  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 'statistics'
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 initialize
52
- super
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