rainbows 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +1 -0
- data/Documentation/GNUmakefile +4 -1
- data/Documentation/comparison.css +6 -0
- data/Documentation/comparison.haml +297 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +24 -17
- data/README +32 -28
- data/Summary +7 -0
- data/TODO +4 -6
- data/bin/rainbows +2 -2
- data/lib/rainbows.rb +33 -3
- data/lib/rainbows/actor_spawn.rb +29 -0
- data/lib/rainbows/app_pool.rb +17 -6
- data/lib/rainbows/base.rb +10 -13
- data/lib/rainbows/const.rb +1 -1
- data/lib/rainbows/dev_fd_response.rb +6 -0
- data/lib/rainbows/error.rb +34 -0
- data/lib/rainbows/ev_core.rb +3 -12
- data/lib/rainbows/event_machine.rb +7 -9
- data/lib/rainbows/fiber.rb +15 -0
- data/lib/rainbows/fiber/base.rb +112 -0
- data/lib/rainbows/fiber/io.rb +65 -0
- data/lib/rainbows/fiber/queue.rb +35 -0
- data/lib/rainbows/fiber_pool.rb +44 -0
- data/lib/rainbows/fiber_spawn.rb +34 -0
- data/lib/rainbows/http_server.rb +14 -1
- data/lib/rainbows/never_block.rb +69 -0
- data/lib/rainbows/rev.rb +7 -0
- data/lib/rainbows/rev/client.rb +9 -3
- data/lib/rainbows/rev/core.rb +2 -5
- data/lib/rainbows/rev/heartbeat.rb +5 -1
- data/lib/rainbows/rev_thread_spawn.rb +62 -60
- data/lib/rainbows/revactor.rb +22 -23
- data/lib/rainbows/thread_pool.rb +28 -26
- data/lib/rainbows/thread_spawn.rb +33 -33
- data/local.mk.sample +9 -7
- data/rainbows.gemspec +8 -2
- data/t/GNUmakefile +14 -7
- data/t/fork-sleep.ru +10 -0
- data/t/simple-http_FiberPool.ru +9 -0
- data/t/simple-http_FiberSpawn.ru +9 -0
- data/t/simple-http_NeverBlock.ru +11 -0
- data/t/sleep.ru +2 -0
- data/t/t0000-simple-http.sh +12 -1
- data/t/t0001-unix-http.sh +12 -1
- data/t/t0009-broken-app.sh +56 -0
- data/t/t0009.ru +13 -0
- data/t/t0010-keepalive-timeout-effective.sh +42 -0
- data/t/t0011-close-on-exec-set.sh +54 -0
- data/t/t0300-async_sinatra.sh +1 -1
- data/t/t9000-rack-app-pool.sh +1 -1
- data/t/t9000.ru +8 -5
- data/t/test-lib.sh +14 -4
- metadata +33 -5
- data/lib/rainbows/ev_thread_core.rb +0 -80
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Rainbows
|
3
|
+
module Fiber
|
4
|
+
|
5
|
+
# A partially complete IO wrapper, this exports an IO.select()-able
|
6
|
+
# #to_io method and gives users the illusion of a synchronous
|
7
|
+
# interface that yields away from the current Fiber whenever
|
8
|
+
# the underlying IO object cannot read or write
|
9
|
+
class IO < Struct.new(:to_io, :f)
|
10
|
+
|
11
|
+
# for wrapping output response bodies
|
12
|
+
def each(&block)
|
13
|
+
begin
|
14
|
+
yield readpartial(16384)
|
15
|
+
rescue EOFError
|
16
|
+
break
|
17
|
+
end while true
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def close
|
22
|
+
to_io.close
|
23
|
+
end
|
24
|
+
|
25
|
+
def write(buf)
|
26
|
+
begin
|
27
|
+
(w = to_io.write_nonblock(buf)) == buf.size and return
|
28
|
+
buf = buf[w..-1]
|
29
|
+
rescue Errno::EAGAIN
|
30
|
+
WR[self] = false
|
31
|
+
::Fiber.yield
|
32
|
+
WR.delete(self)
|
33
|
+
retry
|
34
|
+
end while true
|
35
|
+
end
|
36
|
+
|
37
|
+
# used for reading headers (respecting keepalive_timeout)
|
38
|
+
def read_timeout
|
39
|
+
expire = nil
|
40
|
+
begin
|
41
|
+
to_io.read_nonblock(16384)
|
42
|
+
rescue Errno::EAGAIN
|
43
|
+
return if expire && expire < Time.now
|
44
|
+
RD[self] = false
|
45
|
+
expire ||= Time.now + G.kato
|
46
|
+
::Fiber.yield
|
47
|
+
RD.delete(self)
|
48
|
+
retry
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def readpartial(length, buf = "")
|
53
|
+
begin
|
54
|
+
to_io.read_nonblock(length, buf)
|
55
|
+
rescue Errno::EAGAIN
|
56
|
+
RD[self] = false
|
57
|
+
::Fiber.yield
|
58
|
+
RD.delete(self)
|
59
|
+
retry
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Rainbows
|
3
|
+
module Fiber
|
4
|
+
|
5
|
+
# a self-sufficient Queue implementation for Fiber-based concurrency
|
6
|
+
# models. This requires no external scheduler, so it may be used with
|
7
|
+
# Revactor as well as FiberSpawn and FiberPool.
|
8
|
+
class Queue < Struct.new(:queue, :waiters)
|
9
|
+
|
10
|
+
def initialize(queue = [], waiters = [])
|
11
|
+
# move elements of the Queue into an Array
|
12
|
+
if queue.class.name == "Queue"
|
13
|
+
queue = queue.length.times.map { queue.pop }
|
14
|
+
end
|
15
|
+
super queue, waiters
|
16
|
+
end
|
17
|
+
|
18
|
+
def shift
|
19
|
+
# ah the joys of not having to deal with race conditions
|
20
|
+
if queue.empty?
|
21
|
+
waiters << ::Fiber.current
|
22
|
+
::Fiber.yield
|
23
|
+
end
|
24
|
+
queue.shift
|
25
|
+
end
|
26
|
+
|
27
|
+
def <<(obj)
|
28
|
+
queue << obj
|
29
|
+
blocked = waiters.shift and blocked.resume
|
30
|
+
queue # not quite 100% compatible but no-one's looking :>
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'rainbows/fiber'
|
3
|
+
|
4
|
+
module Rainbows
|
5
|
+
|
6
|
+
# A Fiber-based concurrency model for Ruby 1.9. This uses a pool of
|
7
|
+
# Fibers to handle client IO to run the application and the root Fiber
|
8
|
+
# for scheduling and connection acceptance. The pool size is equal to
|
9
|
+
# the number of +worker_connections+. Compared to the ThreadPool
|
10
|
+
# model, Fibers are very cheap in terms of memory usage so you can
|
11
|
+
# have more active connections. This model supports a streaming
|
12
|
+
# "rack.input" with lightweight concurrency. Applications are
|
13
|
+
# strongly advised to wrap all slow IO objects (sockets, pipes) using
|
14
|
+
# the Rainbows::Fiber::IO class whenever possible.
|
15
|
+
|
16
|
+
module FiberPool
|
17
|
+
include Fiber::Base
|
18
|
+
|
19
|
+
def worker_loop(worker)
|
20
|
+
init_worker_process(worker)
|
21
|
+
pool = []
|
22
|
+
worker_connections.times {
|
23
|
+
::Fiber.new {
|
24
|
+
process_client(::Fiber.yield) while pool << ::Fiber.current
|
25
|
+
}.resume # resume to hit ::Fiber.yield so it waits on a client
|
26
|
+
}
|
27
|
+
Fiber::Base.const_set(:APP, app)
|
28
|
+
|
29
|
+
begin
|
30
|
+
schedule do |l|
|
31
|
+
fib = pool.shift or break # let another worker process take it
|
32
|
+
if io = Rainbows.accept(l)
|
33
|
+
fib.resume(Fiber::IO.new(io, fib))
|
34
|
+
else
|
35
|
+
pool << fib
|
36
|
+
end
|
37
|
+
end
|
38
|
+
rescue => e
|
39
|
+
Error.listen_loop(e)
|
40
|
+
end while G.alive || G.cur > 0
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'rainbows/fiber'
|
3
|
+
|
4
|
+
module Rainbows
|
5
|
+
|
6
|
+
# Simple Fiber-based concurrency model for 1.9. This spawns a new
|
7
|
+
# Fiber for every incoming client connection and the root Fiber for
|
8
|
+
# scheduling and connection acceptance. This exports a streaming
|
9
|
+
# "rack.input" with lightweight concurrency. Applications are
|
10
|
+
# strongly advised to wrap all slow IO objects (sockets, pipes) using
|
11
|
+
# the Rainbows::Fiber::IO class whenever possible.
|
12
|
+
|
13
|
+
module FiberSpawn
|
14
|
+
include Fiber::Base
|
15
|
+
|
16
|
+
def worker_loop(worker)
|
17
|
+
init_worker_process(worker)
|
18
|
+
Fiber::Base.const_set(:APP, app)
|
19
|
+
limit = worker_connections
|
20
|
+
fio = Rainbows::Fiber::IO
|
21
|
+
|
22
|
+
begin
|
23
|
+
schedule do |l|
|
24
|
+
break if G.cur >= limit
|
25
|
+
io = Rainbows.accept(l) or next
|
26
|
+
::Fiber.new { process_client(fio.new(io, ::Fiber.current)) }.resume
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
Error.listen_loop(e)
|
30
|
+
end while G.alive || G.cur > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/rainbows/http_server.rb
CHANGED
@@ -52,10 +52,18 @@ module Rainbows
|
|
52
52
|
Module === mod or
|
53
53
|
raise ArgumentError, "concurrency model #{model.inspect} not supported"
|
54
54
|
extend(mod)
|
55
|
+
args.each do |opt|
|
56
|
+
case opt
|
57
|
+
when Hash; O.update(opt)
|
58
|
+
when Symbol; O[opt] = true
|
59
|
+
else; raise ArgumentError, "can't handle option: #{opt.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
mod.setup if mod.respond_to?(:setup)
|
55
63
|
Const::RACK_DEFAULTS['rainbows.model'] = @use = model.to_sym
|
56
64
|
Const::RACK_DEFAULTS['rack.multithread'] = !!(/Thread/ =~ model.to_s)
|
57
65
|
case @use
|
58
|
-
when :Rev, :EventMachine
|
66
|
+
when :Rev, :EventMachine, :NeverBlock
|
59
67
|
Const::RACK_DEFAULTS['rainbows.autochunk'] = true
|
60
68
|
end
|
61
69
|
end
|
@@ -68,6 +76,11 @@ module Rainbows
|
|
68
76
|
@worker_connections = nr
|
69
77
|
end
|
70
78
|
|
79
|
+
def keepalive_timeout(nr)
|
80
|
+
(Integer === nr && nr >= 0) or
|
81
|
+
raise ArgumentError, "keepalive must be a non-negative Integer"
|
82
|
+
G.kato = nr
|
83
|
+
end
|
71
84
|
end
|
72
85
|
|
73
86
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module Rainbows
|
4
|
+
|
5
|
+
# {NeverBlock}[www.espace.com.eg/neverblock/] library that combines
|
6
|
+
# the EventMachine library with Ruby Fibers. This includes use of
|
7
|
+
# Thread-based Fibers under Ruby 1.8. It currently does NOT support
|
8
|
+
# a streaming "rack.input" but is compatible with everything else
|
9
|
+
# EventMachine supports.
|
10
|
+
#
|
11
|
+
# In your Rainbows! config block, you may specify a Fiber pool size
|
12
|
+
# to limit your application concurrency (without using Rainbows::AppPool)
|
13
|
+
#
|
14
|
+
# Rainbows! do
|
15
|
+
# use :NeverBlock, :pool_size => 50
|
16
|
+
# worker_connections 100
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module NeverBlock
|
20
|
+
|
21
|
+
DEFAULTS = {
|
22
|
+
:pool_size => 20, # same default size used by NB
|
23
|
+
:backend => :EventMachine, # NeverBlock doesn't support Rev yet
|
24
|
+
}
|
25
|
+
|
26
|
+
# same pool size NB core itself uses
|
27
|
+
def self.setup
|
28
|
+
DEFAULTS.each { |k,v| O[k] ||= v }
|
29
|
+
Integer === O[:pool_size] && O[:pool_size] > 0 or
|
30
|
+
raise ArgumentError, "pool_size must a be an Integer > 0"
|
31
|
+
mod = Rainbows.const_get(O[:backend])
|
32
|
+
require "never_block" # require EM first since we need a higher version
|
33
|
+
G.server.extend(mod)
|
34
|
+
G.server.extend(Core)
|
35
|
+
end
|
36
|
+
|
37
|
+
module Client
|
38
|
+
|
39
|
+
def self.setup
|
40
|
+
const_set(:POOL, ::NB::Pool::FiberPool.new(O[:pool_size]))
|
41
|
+
Rainbows.const_get(O[:backend]).const_get(:Client).module_eval do
|
42
|
+
include Rainbows::NeverBlock::Client
|
43
|
+
alias _app_call app_call
|
44
|
+
undef_method :app_call
|
45
|
+
alias app_call nb_app_call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def nb_app_call
|
50
|
+
POOL.spawn do
|
51
|
+
begin
|
52
|
+
_app_call
|
53
|
+
rescue => e
|
54
|
+
handle_error(e)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module Core
|
61
|
+
def init_worker_process(worker)
|
62
|
+
super
|
63
|
+
Client.setup
|
64
|
+
logger.info "NeverBlock/#{O[:backend]} pool_size=#{O[:pool_size]}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/rainbows/rev.rb
CHANGED
data/lib/rainbows/rev/client.rb
CHANGED
@@ -8,7 +8,7 @@ module Rainbows
|
|
8
8
|
G = Rainbows::G
|
9
9
|
|
10
10
|
def initialize(io)
|
11
|
-
|
11
|
+
CONN[self] = false
|
12
12
|
super(io)
|
13
13
|
post_init
|
14
14
|
@deferred_bodies = [] # for (fast) regular files only
|
@@ -23,9 +23,14 @@ module Rainbows
|
|
23
23
|
schedule_write unless out_headers # triggers a write
|
24
24
|
end
|
25
25
|
|
26
|
+
def timeout?
|
27
|
+
@_write_buffer.empty? && @deferred_bodies.empty? and close.nil?
|
28
|
+
end
|
29
|
+
|
26
30
|
def app_call
|
27
31
|
begin
|
28
|
-
(
|
32
|
+
KATO.delete(self)
|
33
|
+
@env[RACK_INPUT] = @input
|
29
34
|
@env[REMOTE_ADDR] = @remote_addr
|
30
35
|
response = APP.call(@env.update(RACK_DEFAULTS))
|
31
36
|
alive = @hp.keepalive? && G.alive
|
@@ -38,6 +43,7 @@ module Rainbows
|
|
38
43
|
@state = :headers
|
39
44
|
# keepalive requests are always body-less, so @input is unchanged
|
40
45
|
@hp.headers(@env, @buf) and next
|
46
|
+
KATO[self] = Time.now
|
41
47
|
else
|
42
48
|
quit
|
43
49
|
end
|
@@ -65,7 +71,7 @@ module Rainbows
|
|
65
71
|
end
|
66
72
|
|
67
73
|
def on_close
|
68
|
-
|
74
|
+
CONN.delete(self)
|
69
75
|
end
|
70
76
|
|
71
77
|
end # module Client
|
data/lib/rainbows/rev/core.rb
CHANGED
@@ -11,11 +11,8 @@ module Rainbows
|
|
11
11
|
# CL and MAX will be defined in the corresponding worker loop
|
12
12
|
|
13
13
|
def on_readable
|
14
|
-
return if
|
15
|
-
|
16
|
-
CL.new(@_io.accept_nonblock).attach(LOOP)
|
17
|
-
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
18
|
-
end
|
14
|
+
return if CONN.size >= MAX
|
15
|
+
io = Rainbows.accept(@_io) and CL.new(io).attach(LOOP)
|
19
16
|
end
|
20
17
|
end # class Server
|
21
18
|
|
@@ -10,7 +10,11 @@ module Rainbows
|
|
10
10
|
class Heartbeat < ::Rev::TimerWatcher
|
11
11
|
|
12
12
|
def on_timer
|
13
|
-
|
13
|
+
if (ot = G.kato) > 0
|
14
|
+
ot = Time.now - ot
|
15
|
+
KATO.delete_if { |client, time| time < ot and client.timeout? }
|
16
|
+
end
|
17
|
+
exit if (! G.tick && CONN.size <= 0)
|
14
18
|
end
|
15
19
|
|
16
20
|
end
|
@@ -1,93 +1,95 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
require 'rainbows/rev'
|
3
|
-
require 'rainbows/ev_thread_core'
|
4
3
|
|
5
|
-
|
4
|
+
RUBY_VERSION =~ %r{\A1\.8} && ::Rev::VERSION < "0.3.2" and
|
5
|
+
warn "Rainbows::RevThreadSpawn + Rev (< 0.3.2)" \
|
6
|
+
" does not work well under Ruby 1.8"
|
6
7
|
|
7
8
|
module Rainbows
|
8
9
|
|
9
|
-
# This concurrency model is EXTREMELY experimental and does
|
10
|
-
# not perform very well.
|
11
|
-
#
|
12
10
|
# A combination of the Rev and ThreadSpawn models. This allows Ruby
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# applications running under this mode should be thread-safe.
|
20
|
-
# DevFdResponse should be used with this class to proxy asynchronous
|
21
|
-
# responses. All network I/O between the client and server are
|
22
|
-
# handled by the main thread (even when streaming "rack.input").
|
23
|
-
#
|
24
|
-
# Caveats:
|
11
|
+
# Thread-based concurrency for application processing. It DOES NOT
|
12
|
+
# expose a streamable "rack.input" for upload processing within the
|
13
|
+
# app. DevFdResponse should be used with this class to proxy
|
14
|
+
# asynchronous responses. All network I/O between the client and
|
15
|
+
# server are handled by the main thread and outside of the core
|
16
|
+
# application dispatch.
|
25
17
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
# processing should be alright as the current Rev::IO::INPUT_SIZE
|
31
|
-
# of 16384 bytes matches the read size used by
|
32
|
-
# Rack::Utils::Multipart::parse_multipart.
|
18
|
+
# WARNING: this model does not currently perform well under 1.8. See the
|
19
|
+
# {rev-talk mailing list}[http://rubyforge.org/mailman/listinfo/rev-talk]
|
20
|
+
# for ongoing performance work that will hopefully make it into the
|
21
|
+
# next release of {Rev}[http://rev.rubyforge.org/].
|
33
22
|
|
34
23
|
module RevThreadSpawn
|
35
|
-
class Client < Rainbows::Rev::Client
|
36
|
-
include EvThreadCore
|
37
|
-
LOOP = ::Rev::Loop.default
|
38
|
-
DR = Rainbows::Rev::DeferredResponse
|
39
|
-
TEE_RESUMER = ::Rev::AsyncWatcher.new
|
40
24
|
|
41
|
-
|
42
|
-
@lock.synchronize { disable if enabled? }
|
43
|
-
end
|
25
|
+
class Master < ::Rev::AsyncWatcher
|
44
26
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
27
|
+
def initialize
|
28
|
+
super
|
29
|
+
@queue = Queue.new
|
48
30
|
end
|
49
31
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
super
|
54
|
-
else
|
55
|
-
@lock.synchronize { super }
|
56
|
-
end
|
32
|
+
def <<(output)
|
33
|
+
@queue << output
|
34
|
+
signal
|
57
35
|
end
|
58
36
|
|
59
|
-
def
|
60
|
-
|
37
|
+
def on_signal
|
38
|
+
client, response = @queue.pop
|
39
|
+
client.response_write(response)
|
61
40
|
end
|
41
|
+
end
|
62
42
|
|
63
|
-
|
43
|
+
class Client < Rainbows::Rev::Client
|
44
|
+
DR = Rainbows::Rev::DeferredResponse
|
45
|
+
KATO = Rainbows::Rev::KATO
|
46
|
+
|
47
|
+
def response_write(response)
|
48
|
+
enable
|
49
|
+
alive = @hp.keepalive? && G.alive
|
50
|
+
out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
|
64
51
|
DR.write(self, response, out)
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
52
|
+
return quit unless alive && G.alive
|
53
|
+
|
54
|
+
@env.clear
|
55
|
+
@hp.reset
|
56
|
+
@state = :headers
|
57
|
+
# keepalive requests are always body-less, so @input is unchanged
|
58
|
+
if @hp.headers(@env, @buf)
|
59
|
+
@input = HttpRequest::NULL_IO
|
60
|
+
app_call
|
61
|
+
else
|
62
|
+
KATO[self] = Time.now
|
63
|
+
end
|
70
64
|
end
|
71
65
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
66
|
+
# fails-safe application dispatch, we absolutely cannot
|
67
|
+
# afford to fail or raise an exception (killing the thread)
|
68
|
+
# here because that could cause a deadlock and we'd leak FDs
|
69
|
+
def app_response
|
70
|
+
begin
|
71
|
+
@env[REMOTE_ADDR] = @remote_addr
|
72
|
+
APP.call(@env.update(RACK_DEFAULTS))
|
73
|
+
rescue => e
|
74
|
+
Error.app(e) # we guarantee this does not raise
|
75
|
+
[ 500, {}, [] ]
|
81
76
|
end
|
82
77
|
end
|
83
78
|
|
79
|
+
def app_call
|
80
|
+
KATO.delete(client = self)
|
81
|
+
disable
|
82
|
+
@env[RACK_INPUT] = @input
|
83
|
+
@input = nil # not sure why, @input seems to get closed otherwise...
|
84
|
+
Thread.new { MASTER << [ client, app_response ] }
|
85
|
+
end
|
84
86
|
end
|
85
87
|
|
86
88
|
include Rainbows::Rev::Core
|
87
89
|
|
88
90
|
def init_worker_process(worker)
|
89
91
|
super
|
90
|
-
Client
|
92
|
+
Client.const_set(:MASTER, Master.new.attach(::Rev::Loop.default))
|
91
93
|
end
|
92
94
|
|
93
95
|
end
|