Skip to content

Commit ad1efba

Browse files
committed
Updated implementation and documentation.
1 parent 1b202ad commit ad1efba

File tree

21 files changed

+282
-72
lines changed

21 files changed

+282
-72
lines changed

lib/async/container.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
require_relative 'container/controller'
2424

2525
module Async
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.
2726
module Container
2827
end
2928
end

lib/async/container/best.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@
2525
require_relative 'hybrid'
2626

2727
module Async
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.
2928
module Container
29+
# Whether the underlying process supports fork.
30+
# @returns [Boolean]
3031
def self.fork?
3132
::Process.respond_to?(:fork) && ::Process.respond_to?(:setpgid)
3233
end
3334

35+
# Determins the best container class based on the underlying Ruby implementation.
36+
# Some platforms, including JRuby, don't support fork. Applications which just want a reasonable default can use this method.
37+
# @returns [Class]
3438
def self.best_container_class
3539
if fork?
3640
return Forked
@@ -39,8 +43,10 @@ def self.best_container_class
3943
end
4044
end
4145

42-
def self.new(*arguments)
43-
best_container_class.new(*arguments)
46+
# Create an instance of the best container class.
47+
# @returns [Generic] Typically an instance of either {Forked} or {Threaded} containers.
48+
def self.new(*arguments, **options)
49+
best_container_class.new(*arguments, **options)
4450
end
4551
end
4652
end

lib/async/container/channel.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,40 @@
2424

2525
module Async
2626
module Container
27+
# Provides a basic multi-thread/multi-process uni-directional communication channel.
2728
class Channel
29+
# Initialize the channel using a pipe.
2830
def initialize
2931
@in, @out = ::IO.pipe
3032
end
3133

34+
# The input end of the pipe.
35+
# @attribute [IO]
3236
attr :in
37+
38+
# The output end of the pipe.
39+
# @attribute [IO]
3340
attr :out
3441

42+
# Close the input end of the pipe.
3543
def close_read
3644
@in.close
3745
end
3846

47+
# Close the output end of the pipe.
3948
def close_write
4049
@out.close
4150
end
4251

52+
# Close both ends of the pipe.
4353
def close
4454
close_read
4555
close_write
4656
end
4757

58+
# Receive an object from the pipe.
59+
# Internally, prefers to receive newline formatted JSON, otherwise returns a hash table with a single key `:line` which contains the line of data that could not be parsed as JSON.
60+
# @returns [Hash]
4861
def receive
4962
if data = @in.gets
5063
begin

lib/async/container/controller.rb

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,17 @@
2828

2929
module Async
3030
module Container
31-
class InitializationError < Error
32-
def initialize(container)
33-
super("Could not create container!")
34-
35-
@container = container
36-
end
37-
38-
attr :container
39-
end
40-
41-
# Manages the life-cycle of a container.
31+
# Manages the life-cycle of one or more containers in order to support a persistent system.
32+
# e.g. a web server, job server or some other long running system.
4233
class Controller
4334
SIGHUP = Signal.list["HUP"]
4435
SIGINT = Signal.list["INT"]
4536
SIGTERM = Signal.list["TERM"]
4637
SIGUSR1 = Signal.list["USR1"]
4738
SIGUSR2 = Signal.list["USR2"]
4839

40+
# Initialize the controller.
41+
# @parameter notify [Notify::Client] A client used for process readiness notifications.
4942
def initialize(notify: Notify.open!)
5043
@container = nil
5144

@@ -60,6 +53,8 @@ def initialize(notify: Notify.open!)
6053
end
6154
end
6255

56+
# The state of the controller.
57+
# @returns [String]
6358
def state_string
6459
if running?
6560
"running"
@@ -68,42 +63,61 @@ def state_string
6863
end
6964
end
7065

66+
# A human readable representation of the controller.
67+
# @returns [String]
7168
def to_s
7269
"#{self.class} #{state_string}"
7370
end
7471

72+
# Trap the specified signal.
73+
# @parameters signal [Symbol] The signal to trap, e.g. `:INT`.
74+
# @parameters block [Proc] The signal handler to invoke.
7575
def trap(signal, &block)
7676
@signals[signal] = block
7777
end
7878

79+
# The current container being managed by the controller.
7980
attr :container
8081

82+
# Create a container for the controller.
83+
# Can be overridden by a sub-class.
84+
# @returns [Generic] A specific container instance to use.
8185
def create_container
8286
Container.new
8387
end
8488

89+
# Whether the controller has a running container.
90+
# @returns [Boolean]
8591
def running?
8692
!!@container
8793
end
8894

95+
# Wait for the underlying container to start.
8996
def wait
9097
@container&.wait
9198
end
9299

100+
# Spawn container instances into the given container.
101+
# Should be overridden by a sub-class.
102+
# @parameter container [Generic] The container, generally from {#create_container}.
93103
def setup(container)
94104
# Don't do this, otherwise calling super is risky for sub-classes:
95105
# raise NotImplementedError, "Container setup is must be implemented in derived class!"
96106
end
97107

108+
# Start the container unless it's already running.
98109
def start
99110
self.restart unless @container
100111
end
101112

113+
# Stop the container if it's running.
114+
# @parameter graceful [Boolean] Whether to give the children instances time to shut down or to kill them immediately.
102115
def stop(graceful = true)
103116
@container&.stop(graceful)
104117
@container = nil
105118
end
106119

120+
# Restart the container. A new container is created, and if successful, any old container is terminated gracefully.
107121
def restart
108122
if @container
109123
@notify&.restarting!
@@ -120,7 +134,7 @@ def restart
120134
rescue
121135
@notify&.error!($!.to_s)
122136

123-
raise InitializationError, container
137+
raise SetupError, container
124138
end
125139

126140
# Wait for all child processes to enter the ready state.
@@ -133,7 +147,7 @@ def restart
133147

134148
container.stop
135149

136-
raise InitializationError, container
150+
raise SetupError, container
137151
end
138152

139153
# Make this swap as atomic as possible:
@@ -150,6 +164,7 @@ def restart
150164
raise
151165
end
152166

167+
# Reload the existing container. Children instances will be reloaded using `SIGHUP`.
153168
def reload
154169
@notify&.reloading!
155170

@@ -158,7 +173,7 @@ def reload
158173
begin
159174
self.setup(@container)
160175
rescue
161-
raise InitializationError, container
176+
raise SetupError, container
162177
end
163178

164179
# Wait for all child processes to enter the ready state.
@@ -169,12 +184,13 @@ def reload
169184
if @container.failed?
170185
@notify.error!("Container failed!")
171186

172-
raise InitializationError, @container
187+
raise SetupError, @container
173188
else
174189
@notify&.ready!
175190
end
176191
end
177192

193+
# Enter the controller run loop, trapping `SIGINT` and `SIGTERM`.
178194
def run
179195
# I thought this was the default... but it doesn't always raise an exception unless you do this explicitly.
180196
interrupt_action = Signal.trap(:INT) do
@@ -194,7 +210,7 @@ def run
194210
if handler = @signals[exception.signo]
195211
begin
196212
handler.call
197-
rescue InitializationError => error
213+
rescue SetupError => error
198214
Async.logger.error(self) {error}
199215
end
200216
else

lib/async/container/error.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,25 @@ class Error < StandardError
2727

2828
Interrupt = ::Interrupt
2929

30+
# Similar to {Interrupt}, but represents `SIGTERM`.
3031
class Terminate < SignalException
3132
SIGTERM = Signal.list['TERM']
3233

3334
def initialize
3435
super(SIGTERM)
3536
end
3637
end
38+
39+
# Represents the error which occured when a container failed to start up correctly.
40+
class SetupError < Error
41+
def initialize(container)
42+
super("Could not create container!")
43+
44+
@container = container
45+
end
46+
47+
# The container that failed.
48+
attr :container
49+
end
3750
end
3851
end

lib/async/container/forked.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
require_relative 'process'
2525

2626
module Async
27-
# Manages a reactor within one or more threads.
2827
module Container
28+
# A multi-process container which uses {Process.fork}.
2929
class Forked < Generic
30+
# Indicates that this is a multi-process container.
3031
def self.multiprocess?
3132
true
3233
end
3334

35+
# Start a named child process and execute the provided block in it.
36+
# @parameter name [String] The name (title) of the child process.
37+
# @parameter block [Proc] The block to execute in the child process.
3438
def start(name, &block)
3539
Process.fork(name: name, &block)
3640
end

0 commit comments

Comments
 (0)