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
@@ -1,4 +1,4 @@
1
- # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,6 @@ require_relative 'forked'
22
22
  require_relative 'threaded'
23
23
 
24
24
  module Async
25
- # Manages a reactor within one or more threads.
26
25
  module Container
27
26
  class Hybrid < Forked
28
27
  def run(count: nil, forks: nil, threads: nil, **options, &block)
@@ -33,7 +32,7 @@ module Async
33
32
 
34
33
  forks.times do
35
34
  self.spawn(**options) do
36
- container = Threaded.new
35
+ container = Threaded::Container.new
37
36
 
38
37
  container.run(count: threads, **options, &block)
39
38
 
@@ -0,0 +1,53 @@
1
+ # Copyright, 2019, 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
+ module Async
22
+ module Container
23
+ class Keyed
24
+ def initialize(key, value)
25
+ @key = key
26
+ @value = value
27
+ @marked = true
28
+ end
29
+
30
+ attr :key
31
+ attr :value
32
+
33
+ def marked?
34
+ @marked
35
+ end
36
+
37
+ def mark!
38
+ @marked = true
39
+ end
40
+
41
+ def clear!
42
+ @marked = false
43
+ end
44
+
45
+ def stop?
46
+ unless @marked
47
+ @value.stop
48
+ return true
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require_relative 'notify/pipe'
24
+ require_relative 'notify/socket'
25
+
26
+ module Async
27
+ module Container
28
+ module Notify
29
+ # We cache the client on a per-process basis. Because that's the relevant scope for process readiness protocols.
30
+ @@client = nil
31
+
32
+ def self.open!
33
+ # Select the best available client:
34
+ @@client ||= (
35
+ Pipe.open! ||
36
+ Socket.open!
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ module Async
24
+ module Container
25
+ module Notify
26
+ class Client
27
+ def ready!(**message)
28
+ send(ready: true, **message)
29
+ end
30
+
31
+ def reloading!(**message)
32
+ message[:ready] = false
33
+ message[:reloading] = true
34
+ message[:status] ||= "Reloading..."
35
+
36
+ send(**message)
37
+ end
38
+
39
+ def restarting!(**message)
40
+ message[:ready] = false
41
+ message[:reloading] = true
42
+ message[:status] ||= "Restarting..."
43
+
44
+ send(**message)
45
+ end
46
+
47
+ def stopping!(**message)
48
+ message[:stopping] = true
49
+ end
50
+
51
+ def status!(text)
52
+ send(status: text)
53
+ end
54
+
55
+ def error!(text, **message)
56
+ send(status: text, **message)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require_relative 'client'
24
+
25
+ require 'json'
26
+
27
+ module Async
28
+ module Container
29
+ module Notify
30
+ class Pipe < Client
31
+ NOTIFY_PIPE = 'NOTIFY_PIPE'
32
+
33
+ def self.open!(environment = ENV)
34
+ if descriptor = environment.delete(NOTIFY_PIPE)
35
+ self.new(::IO.for_fd(descriptor.to_i))
36
+ end
37
+ rescue Errno::EBADF => error
38
+ Async.logger.error(self) {error}
39
+
40
+ return nil
41
+ end
42
+
43
+ def initialize(io)
44
+ @io = io
45
+ end
46
+
47
+ # Inserts or duplicates the environment given an argument array.
48
+ # Sets or clears it in a way that is suitable for {::Process.spawn}.
49
+ def before_spawn(arguments, options)
50
+ environment = environment_for(arguments)
51
+
52
+ # Use `notify_pipe` option if specified:
53
+ if notify_pipe = options.delete(:notify_pipe)
54
+ options[notify_pipe] = @io
55
+ environment[NOTIFY_PIPE] = notify_pipe.to_s
56
+
57
+ # Use stdout if it's not redirected:
58
+ elsif !options.key?(:out)
59
+ options[:out] = @io
60
+ environment[NOTIFY_PIPE] = "1"
61
+
62
+ # Use fileno 3 if it's available:
63
+ elsif !options.key?(3)
64
+ options[3] = @io
65
+ environment[NOTIFY_PIPE] = "3"
66
+
67
+ # Otherwise, give up!
68
+ else
69
+ raise ArgumentError, "Please specify valid file descriptor for notify_pipe!"
70
+ end
71
+ end
72
+
73
+ def send(**message)
74
+ data = ::JSON.dump(message)
75
+
76
+ @io.puts(data)
77
+ @io.flush
78
+ end
79
+
80
+ def ready!(**message)
81
+ send(ready: true, **message)
82
+ end
83
+
84
+ def reloading!(**message)
85
+ message[:ready] = false
86
+ message[:reloading] = true
87
+ message[:status] ||= "Reloading..."
88
+
89
+ send(**message)
90
+ end
91
+
92
+ def reloading!(**message)
93
+ message[:ready] = false
94
+ message[:reloading] = true
95
+ message[:status] ||= "Reloading..."
96
+
97
+ send(**message)
98
+ end
99
+
100
+ private
101
+
102
+ def environment_for(arguments)
103
+ # Insert or duplicate the environment hash which is the first argument:
104
+ if arguments.first.is_a?(Hash)
105
+ environment = arguments[0] = arguments.first.dup
106
+ else
107
+ arguments.unshift(environment = Hash.new)
108
+ end
109
+
110
+ return environment
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require 'async/io'
24
+ require 'async/io/unix_endpoint'
25
+ require 'kernel/sync'
26
+
27
+ require 'tmpdir'
28
+ require 'securerandom'
29
+
30
+ module Async
31
+ module Container
32
+ module Notify
33
+ class Server
34
+ NOTIFY_SOCKET = 'NOTIFY_SOCKET'
35
+ MAXIMUM_MESSAGE_SIZE = 4096
36
+
37
+ def self.load(message)
38
+ lines = message.split("\n")
39
+
40
+ lines.pop if lines.last == ""
41
+
42
+ pairs = lines.map do |line|
43
+ key, value = line.split("=", 2)
44
+
45
+ if value == '0'
46
+ value = false
47
+ elsif value == '1'
48
+ value = true
49
+ end
50
+
51
+ next [key.downcase.to_sym, value]
52
+ end
53
+
54
+ return Hash[pairs]
55
+ end
56
+
57
+ def self.generate_path
58
+ File.expand_path(
59
+ "async-container-#{::Process.pid}-#{SecureRandom.hex(8)}.ipc",
60
+ Dir.tmpdir
61
+ )
62
+ end
63
+
64
+ def self.open(path = self.generate_path)
65
+ self.new(path)
66
+ end
67
+
68
+ def initialize(path)
69
+ @path = path
70
+ end
71
+
72
+ attr :path
73
+
74
+ def bind
75
+ Context.new(@path)
76
+ end
77
+
78
+ class Context
79
+ def initialize(path)
80
+ @path = path
81
+ @endpoint = IO::Endpoint.unix(@path, ::Socket::SOCK_DGRAM)
82
+
83
+ Sync do
84
+ @bound = @endpoint.bind
85
+ end
86
+
87
+ @state = {}
88
+ end
89
+
90
+ def close
91
+ Sync do
92
+ @bound.close
93
+ end
94
+
95
+ File.unlink(@path)
96
+ end
97
+
98
+ def receive
99
+ while true
100
+ data, address, flags, *controls = @bound.recvmsg(MAXIMUM_MESSAGE_SIZE)
101
+
102
+ message = Server.load(data)
103
+
104
+ yield message
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require_relative 'client'
24
+
25
+ require 'async/io'
26
+ require 'async/io/unix_endpoint'
27
+ require 'kernel/sync'
28
+
29
+ module Async
30
+ module Container
31
+ module Notify
32
+ class Socket < Client
33
+ NOTIFY_SOCKET = 'NOTIFY_SOCKET'
34
+ MAXIMUM_MESSAGE_SIZE = 4096
35
+
36
+ def self.open!(environment = ENV)
37
+ if path = environment.delete(NOTIFY_SOCKET)
38
+ self.new(path)
39
+ end
40
+ end
41
+
42
+ def initialize(path)
43
+ @path = path
44
+ @endpoint = IO::Endpoint.unix(path, ::Socket::SOCK_DGRAM)
45
+ end
46
+
47
+ def dump(message)
48
+ buffer = String.new
49
+
50
+ message.each do |key, value|
51
+ # Conversions required by NOTIFY_SOCKET specifications:
52
+ if value == true
53
+ value = 1
54
+ elsif value == false
55
+ value = 0
56
+ end
57
+
58
+ buffer << "#{key.to_s.upcase}=#{value}\n"
59
+ end
60
+
61
+ return buffer
62
+ end
63
+
64
+ def send(**message)
65
+ data = dump(message)
66
+
67
+ if data.bytesize > MAXIMUM_MESSAGE_SIZE
68
+ raise ArgumentError, "Message length #{message.bytesize} exceeds #{MAXIMUM_MESSAGE_SIZE}: #{message.inspect}"
69
+ end
70
+
71
+ Sync do
72
+ @endpoint.connect do |peer|
73
+ peer.send(data)
74
+ end
75
+ end
76
+ end
77
+
78
+ def error!(text, **message)
79
+ message[:errno] ||= -1
80
+
81
+ send(status: text, **message)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end