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,167 @@
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_relative 'channel'
22
+ require_relative 'error'
23
+
24
+ require_relative 'notify/pipe'
25
+
26
+ module Async
27
+ module Container
28
+ class Process < Channel
29
+ class Instance < Notify::Pipe
30
+ def self.for(process)
31
+ instance = self.new(process.out)
32
+
33
+ # The child process won't be reading from the channel:
34
+ process.close_read
35
+
36
+ instance.name = process.name
37
+
38
+ return instance
39
+ end
40
+
41
+ def initialize(io)
42
+ super
43
+
44
+ @name = nil
45
+ end
46
+
47
+ def name= value
48
+ if @name = value
49
+ ::Process.setproctitle(@name)
50
+ end
51
+ end
52
+
53
+ def name
54
+ @name
55
+ end
56
+
57
+ def exec(*arguments, ready: true, **options)
58
+ if ready
59
+ self.ready!(status: "(exec)") if ready
60
+ else
61
+ self.before_spawn(arguments, options)
62
+ end
63
+
64
+ ::Process.exec(*arguments, options)
65
+ end
66
+ end
67
+
68
+ def self.fork(**options)
69
+ self.new(**options) do |process|
70
+ ::Process.fork do
71
+ Signal.trap(:INT) {raise Interrupt}
72
+ Signal.trap(:TERM) {raise Terminate}
73
+
74
+ begin
75
+ yield Instance.for(process)
76
+ rescue Interrupt
77
+ # Graceful exit.
78
+ rescue Exception => error
79
+ Async.logger.error(self) {error}
80
+
81
+ exit!(1)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # def self.spawn(*arguments, name: nil, **options)
88
+ # self.new(name: name) do |process|
89
+ # unless options.key?(:out)
90
+ # options[:out] = process.out
91
+ # end
92
+ #
93
+ # ::Process.spawn(*arguments, **options)
94
+ # end
95
+ # end
96
+
97
+ def initialize(name: nil)
98
+ super()
99
+
100
+ @name = name
101
+ @status = nil
102
+ @pid = nil
103
+
104
+ @pid = yield(self)
105
+
106
+ # The parent process won't be writing to the channel:
107
+ self.close_write
108
+ end
109
+
110
+ def name= value
111
+ @name = value
112
+
113
+ # If we are the child process:
114
+ ::Process.setproctitle(@name) if @pid.nil?
115
+ end
116
+
117
+ attr :name
118
+
119
+ def to_s
120
+ if @status
121
+ "\#<#{self.class} #{@name} -> #{@status}>"
122
+ elsif @pid
123
+ "\#<#{self.class} #{@name} -> #{@pid}>"
124
+ else
125
+ "\#<#{self.class} #{@name}>"
126
+ end
127
+ end
128
+
129
+ def close
130
+ self.terminate!
131
+ self.wait
132
+ ensure
133
+ super
134
+ end
135
+
136
+ def interrupt!
137
+ unless @status
138
+ ::Process.kill(:INT, @pid)
139
+ end
140
+ end
141
+
142
+ def terminate!
143
+ unless @status
144
+ ::Process.kill(:TERM, @pid)
145
+ end
146
+ end
147
+
148
+ def wait
149
+ if @pid && @status.nil?
150
+ _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
151
+
152
+ if @status.nil?
153
+ sleep(0.01)
154
+ _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
155
+ end
156
+
157
+ if @status.nil?
158
+ Async.logger.warn(self) {"Process #{@pid} is blocking, has it exited?"}
159
+ _, @status = ::Process.wait2(@pid)
160
+ end
161
+ end
162
+
163
+ return @status
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,182 @@
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_relative 'channel'
22
+ require_relative 'notify/pipe'
23
+
24
+ require 'async/logger'
25
+
26
+ module Async
27
+ module Container
28
+ class Thread < Channel
29
+ class Exit < Exception
30
+ def initialize(status)
31
+ @status = status
32
+ end
33
+
34
+ attr :status
35
+
36
+ def error
37
+ unless status.success?
38
+ status
39
+ end
40
+ end
41
+ end
42
+
43
+ class Instance < Notify::Pipe
44
+ def self.for(thread)
45
+ instance = self.new(thread.out)
46
+
47
+ return instance
48
+ end
49
+
50
+ def initialize(io)
51
+ @name = nil
52
+ @thread = ::Thread.current
53
+
54
+ super
55
+ end
56
+
57
+ def name= value
58
+ @thread.name = value
59
+ end
60
+
61
+ def name
62
+ @thread.name
63
+ end
64
+
65
+ def exec(*arguments, ready: true, **options)
66
+ if ready
67
+ self.ready!(status: "(spawn)") if ready
68
+ else
69
+ self.before_spawn(arguments, options)
70
+ end
71
+
72
+ begin
73
+ # TODO prefer **options... but it doesn't support redirections on < 2.7
74
+ pid = ::Process.spawn(*arguments, options)
75
+ ensure
76
+ _, status = ::Process.wait2(pid)
77
+
78
+ raise Exit, status
79
+ end
80
+ end
81
+ end
82
+
83
+ def self.fork(**options)
84
+ self.new(**options) do |thread|
85
+ ::Thread.new do
86
+ yield Instance.for(thread)
87
+ end
88
+ end
89
+ end
90
+
91
+ def initialize(name: nil)
92
+ super()
93
+
94
+ @status = nil
95
+
96
+ @thread = yield(self)
97
+ @thread.report_on_exception = false
98
+ @thread.name = name
99
+
100
+ @waiter = ::Thread.new do
101
+ begin
102
+ @thread.join
103
+ rescue Exit => exit
104
+ finished(exit.error)
105
+ rescue Interrupt
106
+ # Graceful shutdown.
107
+ finished
108
+ rescue Exception => error
109
+ finished(error)
110
+ else
111
+ finished
112
+ end
113
+ end
114
+ end
115
+
116
+ def name= value
117
+ @thread.name = name
118
+ end
119
+
120
+ def name
121
+ @thread.name
122
+ end
123
+
124
+ def to_s
125
+ if @status
126
+ "\#<#{self.class} #{@thread.name} -> #{@status}>"
127
+ else
128
+ "\#<#{self.class} #{@thread.name}>"
129
+ end
130
+ end
131
+
132
+ def close
133
+ self.terminate!
134
+ self.wait
135
+ ensure
136
+ super
137
+ end
138
+
139
+ def interrupt!
140
+ @thread.raise(Interrupt)
141
+ end
142
+
143
+ def terminate!
144
+ @thread.raise(Terminate)
145
+ end
146
+
147
+ def wait
148
+ if @waiter
149
+ @waiter.join
150
+ @waiter = nil
151
+ end
152
+
153
+ return @status
154
+ end
155
+
156
+ class Status
157
+ def initialize(result = nil)
158
+ @result = result
159
+ end
160
+
161
+ def success?
162
+ @result.nil?
163
+ end
164
+
165
+ def to_s
166
+ "\#<#{self.class} #{success? ? "success" : "failure"}>"
167
+ end
168
+ end
169
+
170
+ protected
171
+
172
+ def finished(error = nil)
173
+ if error
174
+ Async.logger.error(self) {error}
175
+ end
176
+
177
+ @status = Status.new(error)
178
+ self.close_write
179
+ end
180
+ end
181
+ end
182
+ end
@@ -18,105 +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
- require 'thread'
23
-
24
21
  require_relative 'generic'
25
- require_relative 'statistics'
22
+ require_relative 'thread'
26
23
 
27
24
  module Async
25
+ # Manages a reactor within one or more threads.
28
26
  module Container
29
- # Manages a reactor within one or more threads.
30
27
  class Threaded < Generic
31
- class Instance
32
- def initialize(thread)
33
- @thread = thread
34
- end
35
-
36
- def name= value
37
- @thread.name = value
38
- end
39
-
40
- def exec(*arguments)
41
- pid = ::Process.spawn(*arguments)
42
-
43
- ::Process.waitpid(pid)
44
- ensure
45
- ::Process.kill(:TERM, pid)
46
- end
47
- end
48
-
49
- def self.run(*args, &block)
50
- self.new.run(*args, &block)
51
- end
52
-
53
28
  def self.multiprocess?
54
29
  false
55
30
  end
56
31
 
57
- def initialize
58
- super
59
-
60
- @threads = []
61
- @running = true
62
- end
63
-
64
- def spawn(name: nil, restart: false, &block)
65
- @statistics.spawn!
66
-
67
- thread = ::Thread.new do
68
- thread = ::Thread.current
69
-
70
- thread.name = name if name
71
-
72
- instance = Instance.new(thread)
73
-
74
- while @running
75
- begin
76
- yield instance
77
- rescue Exception => exception
78
- Async.logger.error(self) {exception}
79
-
80
- @statistics.failure!
81
- end
82
-
83
- if restart
84
- @statistics.restart!
85
- else
86
- break
87
- end
88
- end
89
- # rescue Interrupt
90
- # # Graceful exit.
91
- end
92
-
93
- @threads << thread
94
-
95
- return self
96
- end
97
-
98
- def sleep(duration)
99
- Kernel::sleep(duration)
100
- end
101
-
102
- def wait
103
- @threads.each(&:join)
104
- @threads.clear
105
- end
106
-
107
- # Gracefully shut down all reactors.
108
- def stop(graceful = true)
109
- @running = false
110
-
111
- if graceful
112
- @threads.each{|thread| thread.raise(Interrupt)}
113
- else
114
- @threads.each(&:kill)
115
- end
116
-
117
- self.wait
118
- ensure
119
- @running = true
32
+ def start(name, &block)
33
+ Thread.fork(name: name, &block)
120
34
  end
121
35
  end
122
36
  end