protobuf 1.1.3 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile.lock +44 -25
- data/README.md +2 -2
- data/Rakefile +15 -0
- data/bin/rpc_server +52 -11
- data/lib/protobuf.rb +22 -10
- data/lib/protobuf/common/logger.rb +26 -25
- data/lib/protobuf/descriptor/file_descriptor.rb +1 -1
- data/lib/protobuf/message/field.rb +2 -2
- data/lib/protobuf/rpc/buffer.rb +30 -25
- data/lib/protobuf/rpc/client.rb +8 -8
- data/lib/protobuf/rpc/connector.rb +2 -0
- data/lib/protobuf/rpc/connectors/base.rb +0 -1
- data/lib/protobuf/rpc/connectors/common.rb +48 -48
- data/lib/protobuf/rpc/connectors/em_client.rb +53 -27
- data/lib/protobuf/rpc/connectors/eventmachine.rb +14 -17
- data/lib/protobuf/rpc/connectors/socket.rb +23 -16
- data/lib/protobuf/rpc/connectors/zmq.rb +73 -0
- data/lib/protobuf/rpc/error.rb +1 -2
- data/lib/protobuf/rpc/error/client_error.rb +4 -4
- data/lib/protobuf/rpc/server.rb +31 -43
- data/lib/protobuf/rpc/servers/evented/server.rb +43 -0
- data/lib/protobuf/rpc/servers/evented_runner.rb +1 -1
- data/lib/protobuf/rpc/servers/socket/server.rb +108 -0
- data/lib/protobuf/rpc/servers/socket/worker.rb +59 -0
- data/lib/protobuf/rpc/servers/socket_runner.rb +3 -3
- data/lib/protobuf/rpc/servers/zmq/broker.rb +85 -0
- data/lib/protobuf/rpc/servers/zmq/server.rb +50 -0
- data/lib/protobuf/rpc/servers/zmq/util.rb +27 -0
- data/lib/protobuf/rpc/servers/zmq/worker.rb +72 -0
- data/lib/protobuf/rpc/servers/zmq_runner.rb +26 -0
- data/lib/protobuf/rpc/service.rb +5 -5
- data/lib/protobuf/version.rb +1 -1
- data/protobuf.gemspec +12 -10
- data/spec/benchmark/tasks.rb +37 -5
- data/spec/functional/evented_server_spec.rb +64 -0
- data/spec/functional/socket_server_spec.rb +63 -0
- data/spec/functional/zmq_server_spec.rb +63 -0
- data/spec/helper/server.rb +32 -12
- data/spec/lib/protobuf/message/encoder_spec.rb +19 -0
- data/spec/proto/test.pb.rb +3 -3
- data/spec/proto/test.proto +3 -3
- data/spec/proto/test_service.rb +1 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/message_spec.rb +1 -1
- data/spec/unit/rpc/client_spec.rb +11 -3
- data/spec/unit/rpc/connectors/common_spec.rb +0 -1
- data/spec/unit/rpc/connectors/eventmachine_client_spec.rb +32 -0
- data/spec/unit/rpc/connectors/socket_spec.rb +2 -4
- data/spec/unit/rpc/connectors/zmq_spec.rb +27 -0
- data/spec/unit/rpc/servers/evented_server_spec.rb +3 -3
- data/spec/unit/rpc/servers/socket_server_spec.rb +14 -13
- data/spec/unit/rpc/servers/zmq/broker_spec.rb +27 -0
- data/spec/unit/rpc/servers/zmq/server_spec.rb +37 -0
- data/spec/unit/rpc/servers/zmq/util_spec.rb +41 -0
- data/spec/unit/rpc/servers/zmq/worker_spec.rb +36 -0
- data/spec/unit/rpc/service_spec.rb +22 -18
- metadata +87 -40
- data/lib/protobuf/rpc/servers/evented_server.rb +0 -28
- data/lib/protobuf/rpc/servers/socket_server.rb +0 -146
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'protobuf/rpc/connectors/base'
|
2
|
+
|
3
|
+
module Protobuf
|
4
|
+
module Rpc
|
5
|
+
module Connectors
|
6
|
+
class Zmq < Base
|
7
|
+
include Protobuf::Rpc::Connectors::Common
|
8
|
+
include Protobuf::Logger::LogMethods
|
9
|
+
|
10
|
+
def send_request
|
11
|
+
check_async
|
12
|
+
setup_connection
|
13
|
+
connect_to_rpc_server
|
14
|
+
post_init
|
15
|
+
read_response
|
16
|
+
ensure
|
17
|
+
@socket.close if @socket
|
18
|
+
@zmq_context.terminate if @zmq_context
|
19
|
+
@zmq_context = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def check_async
|
25
|
+
if async?
|
26
|
+
log_error { "[client-#{self.class}] Cannot run in async mode" }
|
27
|
+
raise "Cannot use Zmq client in async mode"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def close_connection
|
32
|
+
return if(@error)
|
33
|
+
zmq_error_check(@socket.close)
|
34
|
+
zmq_error_check(@zmq_context.terminate)
|
35
|
+
log_debug { "[client-#{self.class}] Connector closed" }
|
36
|
+
end
|
37
|
+
|
38
|
+
def connect_to_rpc_server
|
39
|
+
return if(@error)
|
40
|
+
log_debug { "[client-#{self.class} Establishing connection: #{options[:host]}:#{options[:port]}" }
|
41
|
+
@zmq_context = ::ZMQ::Context.new
|
42
|
+
@socket = @zmq_context.socket(::ZMQ::REQ)
|
43
|
+
zmq_error_check(@socket.connect("tcp://#{options[:host]}:#{options[:port]}"))
|
44
|
+
log_debug { "[client-#{self.class}] Connection established #{options[:host]}:#{options[:port]}" }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Method to determine error state, must be used with Connector api
|
48
|
+
def error?
|
49
|
+
!!@error
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_response
|
53
|
+
return if(@error)
|
54
|
+
@response_data = ''
|
55
|
+
zmq_error_check(@socket.recv_string(@response_data))
|
56
|
+
parse_response
|
57
|
+
end
|
58
|
+
|
59
|
+
def send_data
|
60
|
+
return if(@error)
|
61
|
+
log_debug { "[#{log_signature}] Sending Request: %s" % @request_data }
|
62
|
+
@stats.request_size = @request_data.size
|
63
|
+
zmq_error_check(@socket.send_string(@request_data))
|
64
|
+
log_debug { "[client-#{self.class}] write closed" }
|
65
|
+
end
|
66
|
+
|
67
|
+
def zmq_error_check(return_code)
|
68
|
+
raise "Last API call failed at #{caller(1)}" unless return_code >= 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/protobuf/rpc/error.rb
CHANGED
@@ -9,7 +9,7 @@ module Protobuf
|
|
9
9
|
attr_reader :error_type
|
10
10
|
|
11
11
|
def initialize message='An unknown RpcError occurred', error_type='RPC_ERROR'
|
12
|
-
@error_type = error_type.is_a?(String) ? Protobuf::Socketrpc::ErrorReason.const_get(error_type) : error_type
|
12
|
+
@error_type = error_type.is_a?(String) ? ::Protobuf::Socketrpc::ErrorReason.const_get(error_type) : error_type
|
13
13
|
super message
|
14
14
|
end
|
15
15
|
|
@@ -18,7 +18,6 @@ module Protobuf
|
|
18
18
|
response.error_reason = @error_type
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -4,25 +4,25 @@ module Protobuf
|
|
4
4
|
module Rpc
|
5
5
|
|
6
6
|
class InvalidRequestProto < PbError
|
7
|
-
def initialize
|
7
|
+
def initialize(message='Invalid request type given')
|
8
8
|
super message, 'INVALID_REQUEST_PROTO'
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
class BadResponseProto < PbError
|
13
|
-
def initialize
|
13
|
+
def initialize(message='Bad response type from server')
|
14
14
|
super message, 'BAD_RESPONSE_PROTO'
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
class UnkownHost < PbError
|
19
|
-
def initialize
|
19
|
+
def initialize(message='Unknown host or port')
|
20
20
|
super message, 'UNKNOWN_HOST'
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
class IOError < PbError
|
25
|
-
def initialize
|
25
|
+
def initialize(message='IO Error occurred')
|
26
26
|
super message, 'IO_ERROR'
|
27
27
|
end
|
28
28
|
end
|
data/lib/protobuf/rpc/server.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# require 'protobuf'
|
2
1
|
require 'protobuf/common/logger'
|
3
2
|
require 'protobuf/rpc/rpc.pb'
|
4
3
|
require 'protobuf/rpc/buffer'
|
@@ -7,52 +6,46 @@ require 'protobuf/rpc/stat'
|
|
7
6
|
|
8
7
|
module Protobuf
|
9
8
|
module Rpc
|
10
|
-
module Server
|
11
|
-
|
9
|
+
module Server
|
10
|
+
|
12
11
|
# Invoke the service method dictated by the proto wrapper request object
|
13
12
|
def handle_client
|
14
|
-
@stats.request_size = @buffer.size
|
15
|
-
|
16
|
-
# Setup the initial request and response
|
17
|
-
@request = Protobuf::Socketrpc::Request.new
|
18
|
-
@response = Protobuf::Socketrpc::Response.new
|
19
|
-
|
20
13
|
# Parse the protobuf request from the socket
|
21
|
-
log_debug "[#{log_signature}] Parsing request from client"
|
14
|
+
log_debug { "[#{log_signature}] Parsing request from client" }
|
22
15
|
parse_request_from_buffer
|
23
|
-
|
16
|
+
|
24
17
|
# Determine the service class and method name from the request
|
25
|
-
log_debug "[#{log_signature}] Extracting procedure call info from request"
|
18
|
+
log_debug { "[#{log_signature}] Extracting procedure call info from request" }
|
26
19
|
parse_service_info
|
27
|
-
|
20
|
+
|
28
21
|
# Call the service method
|
29
|
-
log_debug "[#{log_signature}] Dispatching client request to service"
|
22
|
+
log_debug { "[#{log_signature}] Dispatching client request to service" }
|
30
23
|
invoke_rpc_method
|
31
24
|
rescue => error
|
32
25
|
# Ensure we're handling any errors that try to slip out the back door
|
33
|
-
log_error
|
34
|
-
log_error
|
26
|
+
log_error(error.message)
|
27
|
+
log_error(error.backtrace.join("\n"))
|
35
28
|
handle_error(error)
|
36
29
|
send_response
|
37
30
|
end
|
38
31
|
|
39
32
|
# Client error handler. Receives an exception object and writes it into the @response
|
40
33
|
def handle_error(error)
|
41
|
-
log_debug "[#{log_signature}] handle_error: %s" % error.inspect
|
34
|
+
log_debug { "[#{log_signature}] handle_error: %s" % error.inspect }
|
42
35
|
if error.respond_to?(:to_response)
|
43
36
|
error.to_response(@response)
|
44
37
|
else
|
45
38
|
message = error.respond_to?(:message) ? error.message : error.to_s
|
46
39
|
code = error.respond_to?(:code) ? error.code.to_s : "RPC_ERROR"
|
47
|
-
PbError.new(message, code).to_response(@response)
|
40
|
+
::Protobuf::Rpc::PbError.new(message, code).to_response(@response)
|
48
41
|
end
|
49
42
|
end
|
50
|
-
|
43
|
+
|
51
44
|
# Assuming all things check out, we can call the service method
|
52
45
|
def invoke_rpc_method
|
53
46
|
# Get a new instance of the service
|
54
47
|
@service = @klass.new
|
55
|
-
|
48
|
+
|
56
49
|
# Define our response callback to perform the "successful" response to our client
|
57
50
|
# This decouples the service's rpc method from our response to the client,
|
58
51
|
# allowing the service to be the dictator for when the response should be sent back.
|
@@ -65,30 +58,30 @@ module Protobuf
|
|
65
58
|
send_response
|
66
59
|
end
|
67
60
|
end
|
68
|
-
|
61
|
+
|
69
62
|
@service.on_rpc_failed do |error|
|
70
63
|
unless @did_respond
|
71
64
|
handle_error(error)
|
72
65
|
send_response
|
73
66
|
end
|
74
67
|
end
|
75
|
-
|
68
|
+
|
76
69
|
# Call the service method
|
77
|
-
log_debug "[#{log_signature}] Invoking %s#%s with request %s" % [@klass.name, @method, @request.inspect]
|
70
|
+
log_debug { "[#{log_signature}] Invoking %s#%s with request %s" % [@klass.name, @method, @request.inspect] }
|
78
71
|
@service.__send__(@method, @request)
|
79
72
|
end
|
80
73
|
|
81
74
|
def log_signature
|
82
75
|
@log_signature ||= "server-#{self.class}"
|
83
76
|
end
|
84
|
-
|
77
|
+
|
85
78
|
# Parse the incoming request object into our expected request object
|
86
79
|
def parse_request_from_buffer
|
87
|
-
log_debug "[#{log_signature}] parsing request from buffer: %s" % @
|
88
|
-
@request.parse_from_string(@
|
80
|
+
log_debug { "[#{log_signature}] parsing request from buffer: %s" % @request_data }
|
81
|
+
@request.parse_from_string(@request_data)
|
89
82
|
rescue => error
|
90
|
-
exc = BadRequestData.new 'Unable to parse request: %s' % error.message
|
91
|
-
log_error exc.message
|
83
|
+
exc = ::Protobuf::Rpc::BadRequestData.new 'Unable to parse request: %s' % error.message
|
84
|
+
log_error { exc.message }
|
92
85
|
raise exc
|
93
86
|
end
|
94
87
|
|
@@ -97,18 +90,18 @@ module Protobuf
|
|
97
90
|
# response to the protobuf response wrapper
|
98
91
|
def parse_response_from_service(response)
|
99
92
|
expected = @klass.rpcs[@klass][@method].response_type
|
100
|
-
|
93
|
+
|
101
94
|
# Cannibalize the response if it's a Hash
|
102
95
|
response = expected.new(response) if response.is_a?(Hash)
|
103
96
|
actual = response.class
|
104
|
-
log_debug "[#{log_signature}] response (should/actual): %s/%s" % [expected.name, actual.name]
|
105
|
-
|
97
|
+
log_debug { "[#{log_signature}] response (should/actual): %s/%s" % [expected.name, actual.name] }
|
98
|
+
|
106
99
|
# Determine if the service tried to change response types on us
|
107
100
|
if expected == actual
|
108
101
|
serialize_response(response)
|
109
102
|
else
|
110
103
|
# response types do not match, throw the appropriate error
|
111
|
-
raise BadResponseProto, 'Response proto changed from %s to %s' % [expected.name, actual.name]
|
104
|
+
raise ::Protobuf::Rpc::BadResponseProto, 'Response proto changed from %s to %s' % [expected.name, actual.name]
|
112
105
|
end
|
113
106
|
rescue => error
|
114
107
|
log_error error.message
|
@@ -118,13 +111,13 @@ module Protobuf
|
|
118
111
|
|
119
112
|
# Parses and returns the service and method name from the request wrapper proto
|
120
113
|
def parse_service_info
|
121
|
-
@klass = Util.constantize(@request.service_name)
|
122
|
-
@method = Util.underscore(@request.method_name).to_sym
|
114
|
+
@klass = ::Protobuf::Util.constantize(@request.service_name)
|
115
|
+
@method = ::Protobuf::Util.underscore(@request.method_name).to_sym
|
123
116
|
|
124
117
|
unless @klass.instance_methods.include?(@method)
|
125
118
|
raise MethodNotFound, "Service method #{@request.method_name} is not defined by the service"
|
126
119
|
end
|
127
|
-
|
120
|
+
|
128
121
|
@stats.service = @klass.name
|
129
122
|
@stats.method = @method
|
130
123
|
rescue NameError
|
@@ -134,24 +127,19 @@ module Protobuf
|
|
134
127
|
# Write the response wrapper to the client
|
135
128
|
def send_response
|
136
129
|
raise 'Response already sent to client' if @did_respond
|
137
|
-
log_debug "[#{log_signature}] Sending response to client: %s" % @response.inspect
|
138
|
-
|
139
|
-
send_data(response_buffer.write)
|
140
|
-
@stats.response_size = response_buffer.size
|
130
|
+
log_debug { "[#{log_signature}] Sending response to client: %s" % @response.inspect }
|
131
|
+
send_data
|
141
132
|
@stats.end
|
142
133
|
@stats.log_stats
|
143
134
|
@did_respond = true
|
144
135
|
end
|
145
136
|
|
146
137
|
def serialize_response(response)
|
147
|
-
log_debug "[#{log_signature}] serializing response: %s" % response.inspect
|
138
|
+
log_debug { "[#{log_signature}] serializing response: %s" % response.inspect }
|
148
139
|
@response.response_proto = response.serialize_to_string
|
149
140
|
rescue
|
150
141
|
raise BadResponseProto, $!.message
|
151
142
|
end
|
152
|
-
|
153
143
|
end
|
154
|
-
|
155
144
|
end
|
156
|
-
|
157
145
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'protobuf/rpc/server'
|
2
|
+
|
3
|
+
module Protobuf
|
4
|
+
module Rpc
|
5
|
+
module Evented
|
6
|
+
class Server < ::EventMachine::Connection
|
7
|
+
include ::Protobuf::Rpc::Server
|
8
|
+
include ::Protobuf::Logger::LogMethods
|
9
|
+
|
10
|
+
# Initialize a new read buffer for storing client request info
|
11
|
+
def post_init
|
12
|
+
log_debug { '[server] Post init, new read buffer created' }
|
13
|
+
@stats = Protobuf::Rpc::Stat.new(:SERVER, true)
|
14
|
+
@stats.client = ::Socket.unpack_sockaddr_in(get_peername)
|
15
|
+
@request_buffer = Protobuf::Rpc::Buffer.new(:read)
|
16
|
+
@request = ::Protobuf::Socketrpc::Request.new
|
17
|
+
@response = ::Protobuf::Socketrpc::Response.new
|
18
|
+
|
19
|
+
@did_respond = false
|
20
|
+
end
|
21
|
+
|
22
|
+
# Receive a chunk of data, potentially flushed to handle_client
|
23
|
+
def receive_data(data)
|
24
|
+
log_debug { '[server] receive_data: %s' % data }
|
25
|
+
|
26
|
+
@request_buffer << data
|
27
|
+
@request_data = @request_buffer.data
|
28
|
+
@stats.request_size = @request_buffer.size
|
29
|
+
|
30
|
+
handle_client if @request_buffer.flushed?
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_data
|
34
|
+
response_buffer = Protobuf::Rpc::Buffer.new(:write)
|
35
|
+
response_buffer.set_data(@response)
|
36
|
+
@stats.response_size = response_buffer.size
|
37
|
+
log_debug { "[#{log_signature}] sending data: #{response_buffer.inspect}" }
|
38
|
+
super(response_buffer.write)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -20,7 +20,7 @@ module Protobuf
|
|
20
20
|
|
21
21
|
# Startup and run the rpc server
|
22
22
|
EM.schedule do
|
23
|
-
EventMachine.start_server(server.host, server.port, Protobuf::Rpc::
|
23
|
+
EventMachine.start_server(server.host, server.port, Protobuf::Rpc::Evented::Server) && \
|
24
24
|
Protobuf::Logger.info('RPC Server listening at %s:%d in %s' % [server.host, server.port, server.env])
|
25
25
|
end
|
26
26
|
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'protobuf/rpc/server'
|
2
|
+
require 'protobuf/rpc/servers/socket/worker'
|
3
|
+
module Protobuf
|
4
|
+
module Rpc
|
5
|
+
module Socket
|
6
|
+
|
7
|
+
class Server
|
8
|
+
include ::Protobuf::Rpc::Server
|
9
|
+
include ::Protobuf::Logger::LogMethods
|
10
|
+
|
11
|
+
def self.cleanup?
|
12
|
+
# every 10 connections run a cleanup routine after closing the response
|
13
|
+
@threads.size > (@thread_threshold - 1) && (@threads.size % @thread_threshold) == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.cleanup_threads
|
17
|
+
log_debug { "[#{log_signature}] Thread cleanup - #{@threads.size} - start" }
|
18
|
+
|
19
|
+
@threads = @threads.select do |t|
|
20
|
+
if t[:thread].alive?
|
21
|
+
true
|
22
|
+
else
|
23
|
+
t[:thread].join
|
24
|
+
@working.delete(t[:socket])
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
log_debug { "[#{log_signature}] Thread cleanup - #{@threads.size} - complete" }
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log_signature
|
33
|
+
@log_signature ||= "server-#{self}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.new_worker(socket)
|
37
|
+
Thread.new(socket) do |sock|
|
38
|
+
::Protobuf::Rpc::Socket::Worker.new(sock) do |s|
|
39
|
+
s.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.run(opts = {})
|
45
|
+
log_debug { "[#{log_signature}] Run" }
|
46
|
+
host = opts.fetch(:host, "127.0.0.1")
|
47
|
+
port = opts.fetch(:port, 9399)
|
48
|
+
backlog = opts.fetch(:backlog, 100)
|
49
|
+
thread_threshold = opts.fetch(:thread_threshold, 100)
|
50
|
+
auto_collect_timeout = opts.fetch(:auto_collect_timeout, 20)
|
51
|
+
|
52
|
+
@threads = []
|
53
|
+
@thread_threshold = thread_threshold
|
54
|
+
@server = ::TCPServer.new(host, port)
|
55
|
+
@server.listen(backlog)
|
56
|
+
@working = []
|
57
|
+
@listen_fds = [@server]
|
58
|
+
@running = true
|
59
|
+
|
60
|
+
while running?
|
61
|
+
log_debug { "[#{log_signature}] Waiting for connections" }
|
62
|
+
|
63
|
+
if ready_cnxns = IO.select(@listen_fds, [], [], auto_collect_timeout)
|
64
|
+
cnxns = ready_cnxns.first
|
65
|
+
cnxns.each do |client|
|
66
|
+
case
|
67
|
+
when !running? then
|
68
|
+
# no-op
|
69
|
+
when client == @server then
|
70
|
+
log_debug { "[#{log_signature}] Accepted new connection" }
|
71
|
+
client, sockaddr = @server.accept
|
72
|
+
@listen_fds << client
|
73
|
+
else
|
74
|
+
if !@working.include?(client)
|
75
|
+
@working << @listen_fds.delete(client)
|
76
|
+
log_debug { "[#{log_signature}] Working" }
|
77
|
+
@threads << { :thread => new_worker(client), :socket => client }
|
78
|
+
|
79
|
+
cleanup_threads if cleanup?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
else
|
84
|
+
# Run a cleanup if select times out while waiting
|
85
|
+
cleanup_threads if @threads.size > 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
rescue Errno::EADDRINUSE
|
90
|
+
raise
|
91
|
+
rescue
|
92
|
+
# Closing the server causes the loop to raise an exception here
|
93
|
+
raise if running?
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.running?
|
97
|
+
@running
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.stop
|
101
|
+
@running = false
|
102
|
+
@server.close if @server
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|