async-container 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74fb6a60e18be3ebd4876fa168b27fbda82701c6fbd475b09f5a21ea24b19911
4
- data.tar.gz: 49d1a26bffd2e7a836ff46e954ccfb76a3041cc9babe9e856c1b5a089844e97f
3
+ metadata.gz: 38a0f42651154dff3a1d914b9112c847233dce1636f14c69b4314a254f27cbcf
4
+ data.tar.gz: 7634511012e538ce9564c0dcfb87915ae0d53d46cd222cd1ee813a8ed3418607
5
5
  SHA512:
6
- metadata.gz: a80baba7a0f2f148cbfcf72bfc3cda444f81a550f9c9583178e729af0241d181f6053a85a0f2aa74b14cbb8dc4e3ac42d89383aa11962720a97dd2e4bef97654
7
- data.tar.gz: a1d3d589b9bc3500d7ff0a53800f2bced7bc8cdf868d538b8ef0c5ea1e4014806c2c804d5c3fd9a8cee2a9a9e3a5958de8661774a43c1271d1668dd3cf270f1c
6
+ metadata.gz: bc085b10275b339f7dd680ace8d4462ff5d9f462b5e0cc1ab595200d009a743789c0b999a4bfa11df8d2bf9efe09659ebda702dde60398dc276183a244354616
7
+ data.tar.gz: 1e50231bca1d3aceb8f66f162f290edfd3ef1d00b9d0f3009c412b9bc594db78d96d3812ffd80d1517548333609a6a572a1672a773db81f35412a7d6308dd525
@@ -27,8 +27,12 @@ require 'etc'
27
27
  module Async
28
28
  # 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.
29
29
  module Container
30
- def self.run(concurrency_class: Threaded, **options, &block)
31
- concurrency_class.run(**options, &block)
30
+ def self.best_container_class
31
+ if Process.respond_to?(:fork) and Process.respond_to(:setpgid)
32
+ return Forked
33
+ else
34
+ return Threaded
35
+ end
32
36
  end
33
37
 
34
38
  def self.processor_count
@@ -36,5 +40,9 @@ module Async
36
40
  rescue
37
41
  2
38
42
  end
43
+
44
+ def self.new
45
+ best_container_class.new(*arguments)
46
+ end
39
47
  end
40
48
  end
@@ -23,20 +23,22 @@ require 'async/reactor'
23
23
  module Async
24
24
  module Container
25
25
  class Controller
26
- def initialize
27
- @containers = []
26
+ def async(**options, &block)
27
+ spawn(**options) do |instance|
28
+ begin
29
+ Async::Reactor.run(instance, &block)
30
+ rescue Interrupt
31
+ # Graceful exit.
32
+ end
33
+ end
28
34
  end
29
35
 
30
- def << container
31
- @containers << container
32
- end
33
-
34
- def wait
35
- @containers.each(&:wait)
36
- end
37
-
38
- def stop
39
- @containers.each(&:stop)
36
+ def run(count: Container.processor_count, **options, &block)
37
+ count.times do
38
+ async(**options, &block)
39
+ end
40
+
41
+ return self
40
42
  end
41
43
  end
42
44
  end
@@ -19,13 +19,15 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'async/reactor'
22
- require 'process/group'
22
+
23
+ require_relative 'group'
24
+ require_relative 'controller'
23
25
  require_relative 'statistics'
24
26
 
25
27
  module Async
26
28
  # Manages a reactor within one or more threads.
27
29
  module Container
28
- class Forked
30
+ class Forked < Controller
29
31
  UNNAMED = "Unnamed"
30
32
 
31
33
  class Instance
@@ -38,26 +40,20 @@ module Async
38
40
  self.new.run(*args, &block)
39
41
  end
40
42
 
43
+ def self.multiprocess?
44
+ true
45
+ end
46
+
41
47
  def initialize
42
- @group = ::Process::Group.new
48
+ @group = Group.new
43
49
  @statistics = Statistics.new
44
-
45
- @running = true
46
50
  end
47
51
 
48
52
  attr :statistics
49
53
 
50
- def run(count: Container.processor_count, **options, &block)
51
- count.times do
52
- async(**options, &block)
53
- end
54
-
55
- return self
56
- end
57
-
58
54
  def spawn(name: nil, restart: false)
59
55
  Fiber.new do
60
- while @running
56
+ while true
61
57
  @statistics.spawn!
62
58
  exit_status = @group.fork do
63
59
  ::Process.setproctitle(name) if name
@@ -83,35 +79,15 @@ module Async
83
79
  return self
84
80
  end
85
81
 
86
- def async(**options, &block)
87
- spawn(**options) do |instance|
88
- begin
89
- Async::Reactor.run(instance, &block)
90
- rescue Interrupt
91
- # Graceful exit.
92
- end
93
- end
94
- end
95
-
96
- def self.multiprocess?
97
- true
98
- end
99
-
100
- def wait(&block)
101
- @group.wait(&block)
82
+ def wait
83
+ @group.wait
102
84
  rescue Interrupt
103
85
  # Graceful exit.
104
86
  end
105
87
 
106
88
  # Gracefully shut down all children processes.
107
- def stop(graceful = true, &block)
108
- @running = false
109
-
110
- @group.kill(graceful ? :INT : :TERM)
111
-
112
- self.wait(&block)
113
- ensure
114
- @running = true
89
+ def stop(graceful = true)
90
+ @group.stop(graceful)
115
91
  end
116
92
  end
117
93
  end
@@ -0,0 +1,150 @@
1
+ # Copyright, 2017, 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 'async/reactor'
22
+
23
+ require_relative 'controller'
24
+ require_relative 'statistics'
25
+
26
+ module Async
27
+ # Manages a reactor within one or more threads.
28
+ module Container
29
+ class Group
30
+ def initialize
31
+ @pgid = nil
32
+ @running = {}
33
+ end
34
+
35
+ def spawn(*arguments)
36
+ if pid = ::Process.spawn(*arguments)
37
+ wait_for(pid)
38
+ end
39
+ end
40
+
41
+ def fork(&block)
42
+ if pid = ::Process.fork(&block)
43
+ wait_for(pid)
44
+ end
45
+ end
46
+
47
+ def any?
48
+ @running.any?
49
+ end
50
+
51
+ def wait
52
+ while self.any?
53
+ self.wait_one
54
+ end
55
+ rescue Interrupt
56
+ # If the user interrupts the wait, interrupt the process group and wait for them to finish:
57
+ self.kill(:INT)
58
+
59
+ # If user presses Ctrl-C again (or something else goes wrong), we will come out and kill(:TERM) in the ensure below:
60
+ wait_all
61
+
62
+ raise
63
+ ensure
64
+ self.close
65
+ end
66
+
67
+ def kill(signal = :INT)
68
+ ::Process.kill(signal, -@pgid) if @pgid
69
+ end
70
+
71
+ def stop(graceful = false)
72
+ if graceful
73
+ self.kill(:INT)
74
+ wait_all
75
+ end
76
+ ensure
77
+ self.close
78
+ end
79
+
80
+ def close
81
+ begin
82
+ self.kill(:TERM)
83
+ rescue Errno::EPERM
84
+ # 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.
85
+ end
86
+
87
+ # Clean up zombie processes - if user presses Ctrl-C or for some reason something else blows up, exception would propagate back to caller:
88
+ wait_all
89
+ end
90
+
91
+ protected
92
+
93
+ def wait_all
94
+ while self.any?
95
+ self.wait_one do |fiber, status|
96
+ begin
97
+ fiber.resume(nil)
98
+ rescue Interrupt
99
+ # Graceful exit.
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # Wait for one process, should only be called when a child process has finished, otherwise would block.
106
+ def wait_one(flags = 0)
107
+ return unless @pgid
108
+
109
+ # Wait for processes in this group:
110
+ pid, status = ::Process.wait2(-@pgid, flags)
111
+
112
+ return if flags & ::Process::WNOHANG and pid == nil
113
+
114
+ fiber = @running.delete(pid)
115
+
116
+ if @running.empty?
117
+ @pgid = nil
118
+ end
119
+
120
+ if block_given?
121
+ yield fiber, status
122
+ else
123
+ fiber.resume(status)
124
+ end
125
+ end
126
+
127
+ def wait_for(pid)
128
+ if @pgid
129
+ # Set this process as part of the existing process group:
130
+ ::Process.setpgid(pid, @pgid)
131
+ else
132
+ # Establishes the child process as a process group leader:
133
+ ::Process.setpgid(pid, 0)
134
+
135
+ # Save the process group id:
136
+ @pgid = pid
137
+ end
138
+
139
+ @running[pid] = Fiber.current
140
+
141
+ # Return process status:
142
+ if result = Fiber.yield
143
+ return result
144
+ else
145
+ raise Interrupt
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -20,12 +20,14 @@
20
20
 
21
21
  require 'async/reactor'
22
22
  require 'thread'
23
+
24
+ require_relative 'controller'
23
25
  require_relative 'statistics'
24
26
 
25
27
  module Async
26
28
  module Container
27
29
  # Manages a reactor within one or more threads.
28
- class Threaded
30
+ class Threaded < Controller
29
31
  class Instance
30
32
  def initialize(thread)
31
33
  @thread = thread
@@ -40,6 +42,10 @@ module Async
40
42
  self.new.run(*args, &block)
41
43
  end
42
44
 
45
+ def self.multiprocess?
46
+ false
47
+ end
48
+
43
49
  def initialize
44
50
  @threads = []
45
51
  @running = true
@@ -48,14 +54,6 @@ module Async
48
54
 
49
55
  attr :statistics
50
56
 
51
- def run(count: Container.processor_count, **options, &block)
52
- count.times do
53
- async(**options, &block)
54
- end
55
-
56
- return self
57
- end
58
-
59
57
  def spawn(name: nil, restart: false, &block)
60
58
  @statistics.spawn!
61
59
 
@@ -90,25 +88,7 @@ module Async
90
88
  return self
91
89
  end
92
90
 
93
- def async(name: nil, restart: false, &block)
94
- spawn do |instance|
95
- begin
96
- Async::Reactor.run(instance, &block)
97
- rescue Interrupt
98
- # Graceful exit.
99
- end
100
- end
101
-
102
- return self
103
- end
104
-
105
- def self.multiprocess?
106
- false
107
- end
108
-
109
91
  def wait
110
- yield if block_given?
111
-
112
92
  @threads.each(&:join)
113
93
  @threads.clear
114
94
 
@@ -118,7 +98,7 @@ module Async
118
98
  end
119
99
 
120
100
  # Gracefully shut down all reactors.
121
- def stop(graceful = true, &block)
101
+ def stop(graceful = true)
122
102
  @running = false
123
103
 
124
104
  if graceful
@@ -127,7 +107,7 @@ module Async
127
107
  @threads.each(&:kill)
128
108
  end
129
109
 
130
- self.wait(&block)
110
+ self.wait
131
111
  ensure
132
112
  @running = true
133
113
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module Container
23
- VERSION = "0.10.2"
23
+ VERSION = "0.11.0"
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-container
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-01 00:00:00.000000000 Z
11
+ date: 2019-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: process-group
@@ -144,11 +144,11 @@ files:
144
144
  - lib/async/container.rb
145
145
  - lib/async/container/controller.rb
146
146
  - lib/async/container/forked.rb
147
+ - lib/async/container/group.rb
147
148
  - lib/async/container/hybrid.rb
148
149
  - lib/async/container/statistics.rb
149
150
  - lib/async/container/threaded.rb
150
151
  - lib/async/container/version.rb
151
- - spec/async/container/controller_spec.rb
152
152
  - spec/async/container/forked_spec.rb
153
153
  - spec/async/container/hybrid_spec.rb
154
154
  - spec/async/container/shared_examples.rb
@@ -180,7 +180,6 @@ signing_key:
180
180
  specification_version: 4
181
181
  summary: Async is an asynchronous I/O framework based on nio4r.
182
182
  test_files:
183
- - spec/async/container/controller_spec.rb
184
183
  - spec/async/container/forked_spec.rb
185
184
  - spec/async/container/hybrid_spec.rb
186
185
  - spec/async/container/shared_examples.rb
@@ -1,43 +0,0 @@
1
- # Copyright, 2017, 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 "async/container/controller"
22
-
23
- RSpec.describe Async::Container::Controller do
24
- it 'can manage multiple containers' do
25
- mutex = Mutex.new
26
- count = 0
27
-
28
- subject << Async::Container::Threaded.run(count: 2) do
29
- mutex.synchronize do
30
- count += 1
31
- end
32
- end
33
-
34
- subject << Async::Container::Threaded.run(count: 2) do
35
- mutex.synchronize do
36
- count += 1
37
- end
38
- end
39
-
40
- subject.wait
41
- expect(count).to be == 4
42
- end
43
- end