async-container 0.18.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/async/container/best.rb +4 -4
- data/lib/async/container/channel.rb +2 -2
- data/lib/async/container/controller.rb +57 -44
- data/lib/async/container/error.rb +4 -4
- data/lib/async/container/forked.rb +204 -4
- data/lib/async/container/generic.rb +42 -16
- data/lib/async/container/group.rb +61 -13
- data/lib/async/container/hybrid.rb +3 -3
- data/lib/async/container/notify/console.rb +4 -4
- data/lib/async/container/notify/pipe.rb +5 -5
- data/lib/async/container/notify/server.rb +14 -7
- data/lib/async/container/notify/socket.rb +8 -4
- data/lib/async/container/notify.rb +4 -4
- data/lib/async/container/statistics.rb +2 -2
- data/lib/async/container/threaded.rb +224 -4
- data/lib/async/container/version.rb +1 -1
- data/lib/async/container.rb +2 -2
- data/license.md +1 -1
- data/releases.md +6 -0
- data.tar.gz.sig +0 -0
- metadata +6 -12
- metadata.gz.sig +0 -0
- data/lib/async/container/process.rb +0 -172
- data/lib/async/container/thread.rb +0 -199
@@ -3,17 +3,22 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2018-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
6
|
+
require "fiber"
|
7
|
+
require "async/clock"
|
8
8
|
|
9
|
-
require_relative
|
9
|
+
require_relative "error"
|
10
10
|
|
11
11
|
module Async
|
12
12
|
module Container
|
13
13
|
# Manages a group of running processes.
|
14
14
|
class Group
|
15
15
|
# Initialize an empty group.
|
16
|
-
|
16
|
+
#
|
17
|
+
# @parameter health_check_interval [Numeric | Nil] The (biggest) interval at which health checks are performed.
|
18
|
+
def initialize(health_check_interval: 1.0)
|
19
|
+
@health_check_interval = health_check_interval
|
20
|
+
|
21
|
+
# The running fibers, indexed by IO:
|
17
22
|
@running = {}
|
18
23
|
|
19
24
|
# This queue allows us to wait for processes to complete, without spawning new processes as a result.
|
@@ -57,15 +62,43 @@ module Async
|
|
57
62
|
def wait
|
58
63
|
self.resume
|
59
64
|
|
60
|
-
|
61
|
-
self.wait_for_children
|
65
|
+
with_health_checks do |duration|
|
66
|
+
self.wait_for_children(duration)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private def with_health_checks
|
71
|
+
if @health_check_interval
|
72
|
+
health_check_clock = Clock.start
|
73
|
+
|
74
|
+
while self.running?
|
75
|
+
duration = [@health_check_interval - health_check_clock.total, 0].max
|
76
|
+
|
77
|
+
yield duration
|
78
|
+
|
79
|
+
if health_check_clock.total > @health_check_interval
|
80
|
+
self.health_check!
|
81
|
+
health_check_clock.reset!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
while self.running?
|
86
|
+
yield nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Perform a health check on all running processes.
|
92
|
+
def health_check!
|
93
|
+
@running.each_value do |fiber|
|
94
|
+
fiber.resume(:health_check!)
|
62
95
|
end
|
63
96
|
end
|
64
97
|
|
65
98
|
# Interrupt all running processes.
|
66
99
|
# This resumes the controlling fiber with an instance of {Interrupt}.
|
67
100
|
def interrupt
|
68
|
-
Console.
|
101
|
+
Console.info(self, "Sending interrupt to #{@running.size} running processes...")
|
69
102
|
@running.each_value do |fiber|
|
70
103
|
fiber.resume(Interrupt)
|
71
104
|
end
|
@@ -74,7 +107,7 @@ module Async
|
|
74
107
|
# Terminate all running processes.
|
75
108
|
# This resumes the controlling fiber with an instance of {Terminate}.
|
76
109
|
def terminate
|
77
|
-
Console.
|
110
|
+
Console.info(self, "Sending terminate to #{@running.size} running processes...")
|
78
111
|
@running.each_value do |fiber|
|
79
112
|
fiber.resume(Terminate)
|
80
113
|
end
|
@@ -83,6 +116,7 @@ module Async
|
|
83
116
|
# Stop all child processes using {#terminate}.
|
84
117
|
# @parameter timeout [Boolean | Numeric | Nil] If specified, invoke a graceful shutdown using {#interrupt} first.
|
85
118
|
def stop(timeout = 1)
|
119
|
+
Console.info(self, "Stopping all processes...", timeout: timeout)
|
86
120
|
# Use a default timeout if not specified:
|
87
121
|
timeout = 1 if timeout == true
|
88
122
|
|
@@ -105,7 +139,7 @@ module Async
|
|
105
139
|
end
|
106
140
|
|
107
141
|
# Terminate all children:
|
108
|
-
self.terminate
|
142
|
+
self.terminate if any?
|
109
143
|
|
110
144
|
# Wait for all children to exit:
|
111
145
|
self.wait
|
@@ -118,15 +152,19 @@ module Async
|
|
118
152
|
@running[io] = Fiber.current
|
119
153
|
|
120
154
|
while @running.key?(io)
|
155
|
+
# Wait for some event on the channel:
|
121
156
|
result = Fiber.yield
|
122
157
|
|
123
158
|
if result == Interrupt
|
124
159
|
channel.interrupt!
|
125
160
|
elsif result == Terminate
|
126
161
|
channel.terminate!
|
162
|
+
elsif result
|
163
|
+
yield result
|
127
164
|
elsif message = channel.receive
|
128
165
|
yield message
|
129
166
|
else
|
167
|
+
# Wait for the channel to exit:
|
130
168
|
return channel.wait
|
131
169
|
end
|
132
170
|
end
|
@@ -137,14 +175,24 @@ module Async
|
|
137
175
|
protected
|
138
176
|
|
139
177
|
def wait_for_children(duration = nil)
|
140
|
-
Console.debug(self, "Waiting for children...", duration: duration)
|
178
|
+
Console.debug(self, "Waiting for children...", duration: duration, running: @running)
|
179
|
+
|
141
180
|
if !@running.empty?
|
142
181
|
# Maybe consider using a proper event loop here:
|
182
|
+
if ready = self.select(duration)
|
183
|
+
ready.each do |io|
|
184
|
+
@running[io].resume
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Wait for a child process to exit OR a signal to be received.
|
191
|
+
def select(duration)
|
192
|
+
::Thread.handle_interrupt(SignalException => :immediate) do
|
143
193
|
readable, _, _ = ::IO.select(@running.keys, nil, nil, duration)
|
144
194
|
|
145
|
-
readable
|
146
|
-
@running[io].resume
|
147
|
-
end
|
195
|
+
return readable
|
148
196
|
end
|
149
197
|
end
|
150
198
|
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2022, by Anton Sozontov.
|
6
6
|
|
7
|
-
require_relative
|
8
|
-
require_relative
|
7
|
+
require_relative "forked"
|
8
|
+
require_relative "threaded"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
module Container
|
@@ -3,9 +3,9 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2020-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "client"
|
7
7
|
|
8
|
-
require
|
8
|
+
require "console"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
module Container
|
@@ -24,8 +24,8 @@ module Async
|
|
24
24
|
end
|
25
25
|
|
26
26
|
# Send a message to the console.
|
27
|
-
def send(level: :
|
28
|
-
@logger.
|
27
|
+
def send(level: :info, **message)
|
28
|
+
@logger.public_send(level, self) {message}
|
29
29
|
end
|
30
30
|
|
31
31
|
# Send an error message to the console.
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2020-
|
4
|
+
# Copyright, 2020-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Juan Antonio Martín Lucas.
|
6
6
|
|
7
|
-
require_relative
|
7
|
+
require_relative "client"
|
8
8
|
|
9
|
-
require
|
9
|
+
require "json"
|
10
10
|
|
11
11
|
module Async
|
12
12
|
module Container
|
@@ -14,7 +14,7 @@ module Async
|
|
14
14
|
# Implements a process readiness protocol using an inherited pipe file descriptor.
|
15
15
|
class Pipe < Client
|
16
16
|
# The environment variable key which contains the pipe file descriptor.
|
17
|
-
NOTIFY_PIPE =
|
17
|
+
NOTIFY_PIPE = "NOTIFY_PIPE"
|
18
18
|
|
19
19
|
# Open a notification client attached to the current {NOTIFY_PIPE} if possible.
|
20
20
|
def self.open!(environment = ENV)
|
@@ -22,7 +22,7 @@ module Async
|
|
22
22
|
self.new(::IO.for_fd(descriptor.to_i))
|
23
23
|
end
|
24
24
|
rescue Errno::EBADF => error
|
25
|
-
Console.
|
25
|
+
Console.error(self) {error}
|
26
26
|
|
27
27
|
return nil
|
28
28
|
end
|
@@ -4,14 +4,13 @@
|
|
4
4
|
# Copyright, 2020-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Olle Jonsson.
|
6
6
|
|
7
|
-
require
|
8
|
-
require
|
7
|
+
require "tmpdir"
|
8
|
+
require "securerandom"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
module Container
|
12
12
|
module Notify
|
13
13
|
class Server
|
14
|
-
NOTIFY_SOCKET = 'NOTIFY_SOCKET'
|
15
14
|
MAXIMUM_MESSAGE_SIZE = 4096
|
16
15
|
|
17
16
|
def self.load(message)
|
@@ -22,13 +21,17 @@ module Async
|
|
22
21
|
pairs = lines.map do |line|
|
23
22
|
key, value = line.split("=", 2)
|
24
23
|
|
25
|
-
|
24
|
+
key = key.downcase.to_sym
|
25
|
+
|
26
|
+
if value == "0"
|
26
27
|
value = false
|
27
|
-
elsif value ==
|
28
|
+
elsif value == "1"
|
28
29
|
value = true
|
30
|
+
elsif key == :errno and value =~ /\A\-?\d+\z/
|
31
|
+
value = Integer(value)
|
29
32
|
end
|
30
33
|
|
31
|
-
next [key
|
34
|
+
next [key, value]
|
32
35
|
end
|
33
36
|
|
34
37
|
return Hash[pairs]
|
@@ -75,7 +78,11 @@ module Async
|
|
75
78
|
|
76
79
|
message = Server.load(data)
|
77
80
|
|
78
|
-
|
81
|
+
if block_given?
|
82
|
+
yield message
|
83
|
+
else
|
84
|
+
return message
|
85
|
+
end
|
79
86
|
end
|
80
87
|
end
|
81
88
|
end
|
@@ -3,7 +3,8 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2020-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "client"
|
7
|
+
require "socket"
|
7
8
|
|
8
9
|
module Async
|
9
10
|
module Container
|
@@ -12,7 +13,7 @@ module Async
|
|
12
13
|
# See <https://www.freedesktop.org/software/systemd/man/sd_notify.html> for more details of the underlying protocol.
|
13
14
|
class Socket < Client
|
14
15
|
# The name of the environment variable which contains the path to the notification socket.
|
15
|
-
NOTIFY_SOCKET =
|
16
|
+
NOTIFY_SOCKET = "NOTIFY_SOCKET"
|
16
17
|
|
17
18
|
# The maximum allowed size of the UDP message.
|
18
19
|
MAXIMUM_MESSAGE_SIZE = 4096
|
@@ -31,6 +32,9 @@ module Async
|
|
31
32
|
@address = Addrinfo.unix(path, ::Socket::SOCK_DGRAM)
|
32
33
|
end
|
33
34
|
|
35
|
+
# @attribute [String] The path to the UNIX socket used for sending messages to the controller.
|
36
|
+
attr :path
|
37
|
+
|
34
38
|
# Dump a message in the format requied by `sd_notify`.
|
35
39
|
# @parameter message [Hash] Keys and values should be string convertible objects. Values which are `true`/`false` are converted to `1`/`0` respectively.
|
36
40
|
def dump(message)
|
@@ -56,7 +60,7 @@ module Async
|
|
56
60
|
data = dump(message)
|
57
61
|
|
58
62
|
if data.bytesize > MAXIMUM_MESSAGE_SIZE
|
59
|
-
raise ArgumentError, "Message length #{
|
63
|
+
raise ArgumentError, "Message length #{data.bytesize} exceeds #{MAXIMUM_MESSAGE_SIZE}: #{message.inspect}"
|
60
64
|
end
|
61
65
|
|
62
66
|
@address.connect do |peer|
|
@@ -69,7 +73,7 @@ module Async
|
|
69
73
|
def error!(text, **message)
|
70
74
|
message[:errno] ||= -1
|
71
75
|
|
72
|
-
|
76
|
+
super
|
73
77
|
end
|
74
78
|
end
|
75
79
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2020-
|
4
|
+
# Copyright, 2020-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
6
|
+
require_relative "notify/pipe"
|
7
|
+
require_relative "notify/socket"
|
8
|
+
require_relative "notify/console"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
module Container
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
6
|
+
require_relative "generic"
|
7
|
+
require_relative "channel"
|
8
|
+
require_relative "notify/pipe"
|
8
9
|
|
9
10
|
module Async
|
10
11
|
module Container
|
@@ -15,11 +16,230 @@ module Async
|
|
15
16
|
false
|
16
17
|
end
|
17
18
|
|
19
|
+
# Represents a running child thread from the point of view of the parent container.
|
20
|
+
class Child < Channel
|
21
|
+
# Used to propagate the exit status of a child process invoked by {Instance#exec}.
|
22
|
+
class Exit < Exception
|
23
|
+
# Initialize the exit status.
|
24
|
+
# @parameter status [::Process::Status] The process exit status.
|
25
|
+
def initialize(status)
|
26
|
+
@status = status
|
27
|
+
end
|
28
|
+
|
29
|
+
# The process exit status.
|
30
|
+
# @attribute [::Process::Status]
|
31
|
+
attr :status
|
32
|
+
|
33
|
+
# The process exit status if it was an error.
|
34
|
+
# @returns [::Process::Status | Nil]
|
35
|
+
def error
|
36
|
+
unless status.success?
|
37
|
+
status
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Represents a running child thread from the point of view of the child thread.
|
43
|
+
class Instance < Notify::Pipe
|
44
|
+
# Wrap an instance around the {Thread} instance from within the threaded child.
|
45
|
+
# @parameter thread [Thread] The thread intance to wrap.
|
46
|
+
def self.for(thread)
|
47
|
+
instance = self.new(thread.out)
|
48
|
+
|
49
|
+
return instance
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(io)
|
53
|
+
@name = nil
|
54
|
+
@thread = ::Thread.current
|
55
|
+
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the name of the thread.
|
60
|
+
# @parameter value [String] The name to set.
|
61
|
+
def name= value
|
62
|
+
@thread.name = value
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the name of the thread.
|
66
|
+
# @returns [String]
|
67
|
+
def name
|
68
|
+
@thread.name
|
69
|
+
end
|
70
|
+
|
71
|
+
# Execute a child process using {::Process.spawn}. In order to simulate {::Process.exec}, an {Exit} instance is raised to propagage exit status.
|
72
|
+
# This creates the illusion that this method does not return (normally).
|
73
|
+
def exec(*arguments, ready: true, **options)
|
74
|
+
if ready
|
75
|
+
self.ready!(status: "(spawn)")
|
76
|
+
else
|
77
|
+
self.before_spawn(arguments, options)
|
78
|
+
end
|
79
|
+
|
80
|
+
begin
|
81
|
+
pid = ::Process.spawn(*arguments, **options)
|
82
|
+
ensure
|
83
|
+
_, status = ::Process.wait2(pid)
|
84
|
+
|
85
|
+
raise Exit, status
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.fork(**options)
|
91
|
+
self.new(**options) do |thread|
|
92
|
+
::Thread.new do
|
93
|
+
# This could be a configuration option (see forked implementation too):
|
94
|
+
::Thread.handle_interrupt(SignalException => :immediate) do
|
95
|
+
yield Instance.for(thread)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Initialize the thread.
|
102
|
+
# @parameter name [String] The name to use for the child thread.
|
103
|
+
def initialize(name: nil)
|
104
|
+
super()
|
105
|
+
|
106
|
+
@status = nil
|
107
|
+
|
108
|
+
@thread = yield(self)
|
109
|
+
@thread.report_on_exception = false
|
110
|
+
@thread.name = name
|
111
|
+
|
112
|
+
@waiter = ::Thread.new do
|
113
|
+
begin
|
114
|
+
@thread.join
|
115
|
+
rescue Exit => exit
|
116
|
+
finished(exit.error)
|
117
|
+
rescue Interrupt
|
118
|
+
# Graceful shutdown.
|
119
|
+
finished
|
120
|
+
rescue Exception => error
|
121
|
+
finished(error)
|
122
|
+
else
|
123
|
+
finished
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Convert the child process to a hash, suitable for serialization.
|
129
|
+
#
|
130
|
+
# @returns [Hash] The request as a hash.
|
131
|
+
def as_json(...)
|
132
|
+
{
|
133
|
+
name: @thread.name,
|
134
|
+
status: @status&.as_json,
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
# Convert the request to JSON.
|
139
|
+
#
|
140
|
+
# @returns [String] The request as JSON.
|
141
|
+
def to_json(...)
|
142
|
+
as_json.to_json(...)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Set the name of the thread.
|
146
|
+
# @parameter value [String] The name to set.
|
147
|
+
def name= value
|
148
|
+
@thread.name = value
|
149
|
+
end
|
150
|
+
|
151
|
+
# Get the name of the thread.
|
152
|
+
# @returns [String]
|
153
|
+
def name
|
154
|
+
@thread.name
|
155
|
+
end
|
156
|
+
|
157
|
+
# A human readable representation of the thread.
|
158
|
+
# @returns [String]
|
159
|
+
def to_s
|
160
|
+
"\#<#{self.class} #{@thread.name}>"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Invoke {#terminate!} and then {#wait} for the child thread to exit.
|
164
|
+
def close
|
165
|
+
self.terminate!
|
166
|
+
self.wait
|
167
|
+
ensure
|
168
|
+
super
|
169
|
+
end
|
170
|
+
|
171
|
+
# Raise {Interrupt} in the child thread.
|
172
|
+
def interrupt!
|
173
|
+
@thread.raise(Interrupt)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Raise {Terminate} in the child thread.
|
177
|
+
def terminate!
|
178
|
+
@thread.raise(Terminate)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Raise {Restart} in the child thread.
|
182
|
+
def restart!
|
183
|
+
@thread.raise(Restart)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Wait for the thread to exit and return he exit status.
|
187
|
+
# @returns [Status]
|
188
|
+
def wait
|
189
|
+
if @waiter
|
190
|
+
@waiter.join
|
191
|
+
@waiter = nil
|
192
|
+
end
|
193
|
+
|
194
|
+
return @status
|
195
|
+
end
|
196
|
+
|
197
|
+
# A pseudo exit-status wrapper.
|
198
|
+
class Status
|
199
|
+
# Initialise the status.
|
200
|
+
# @parameter error [::Process::Status] The exit status of the child thread.
|
201
|
+
def initialize(error = nil)
|
202
|
+
@error = error
|
203
|
+
end
|
204
|
+
|
205
|
+
# Whether the status represents a successful outcome.
|
206
|
+
# @returns [Boolean]
|
207
|
+
def success?
|
208
|
+
@error.nil?
|
209
|
+
end
|
210
|
+
|
211
|
+
def as_json(...)
|
212
|
+
if @error
|
213
|
+
@error.inspect
|
214
|
+
else
|
215
|
+
true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# A human readable representation of the status.
|
220
|
+
def to_s
|
221
|
+
"\#<#{self.class} #{success? ? "success" : "failure"}>"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
protected
|
226
|
+
|
227
|
+
# Invoked by the @waiter thread to indicate the outcome of the child thread.
|
228
|
+
def finished(error = nil)
|
229
|
+
if error
|
230
|
+
Console.error(self) {error}
|
231
|
+
end
|
232
|
+
|
233
|
+
@status = Status.new(error)
|
234
|
+
self.close_write
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
18
238
|
# Start a named child thread and execute the provided block in it.
|
19
239
|
# @parameter name [String] The name (title) of the child process.
|
20
240
|
# @parameter block [Proc] The block to execute in the child process.
|
21
241
|
def start(name, &block)
|
22
|
-
|
242
|
+
Child.fork(name: name, &block)
|
23
243
|
end
|
24
244
|
end
|
25
245
|
end
|
data/lib/async/container.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "container/controller"
|
7
7
|
|
8
8
|
module Async
|
9
9
|
module Container
|
data/license.md
CHANGED
data/releases.md
ADDED
data.tar.gz.sig
CHANGED
Binary file
|