ruby_skynet 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/Gemfile.lock +1 -1
- data/dev.log +0 -0
- data/lib/ruby_skynet/client.rb +32 -244
- data/lib/ruby_skynet/connection.rb +269 -0
- data/lib/ruby_skynet/doozer/client.rb +27 -20
- data/lib/ruby_skynet/exceptions.rb +2 -1
- data/lib/ruby_skynet/registry.rb +252 -0
- data/lib/ruby_skynet/version.rb +1 -1
- data/lib/ruby_skynet.rb +3 -1
- data/test/ruby_skynet_client_test.rb +36 -21
- data/test.log +0 -0
- metadata +6 -5
- data/ruby_skynet-0.1.0.gem +0 -0
- data/ruby_skynet-0.1.1.gem +0 -0
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/dev.log
ADDED
Binary file
|
data/lib/ruby_skynet/client.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
require 'bson'
|
2
|
-
require 'sync_attr'
|
3
|
-
require 'multi_json'
|
4
|
-
|
5
2
|
#
|
6
3
|
# RubySkynet Client
|
7
4
|
#
|
@@ -13,124 +10,36 @@ module RubySkynet
|
|
13
10
|
class Client
|
14
11
|
include SyncAttr
|
15
12
|
|
16
|
-
# Default doozer configuration
|
17
|
-
# To replace this default, set the config as follows:
|
18
|
-
# RubySkynet::Client.doozer_config = { .... }
|
19
|
-
sync_attr_accessor :doozer_config do
|
20
|
-
{
|
21
|
-
:server => '127.0.0.1:8046',
|
22
|
-
:read_timeout => 5,
|
23
|
-
:connect_timeout => 3,
|
24
|
-
:connect_retry_interval => 0.1,
|
25
|
-
:connect_retry_count => 3
|
26
|
-
}
|
27
|
-
end
|
28
|
-
|
29
|
-
# Lazy initialize Doozer Client
|
30
|
-
sync_cattr_reader :doozer do
|
31
|
-
Doozer::Client.new
|
32
|
-
end
|
33
|
-
|
34
|
-
# Create a client connection, call the supplied block and close the connection on
|
35
|
-
# completion of the block
|
36
|
-
#
|
37
|
-
# Example
|
38
|
-
#
|
39
|
-
# require 'ruby_skynet'
|
40
|
-
# SemanticLogger.default_level = :trace
|
41
|
-
# SemanticLogger.appenders << SemanticLogger::Appender::File(STDOUT)
|
42
|
-
# RubySkynet::Client.connect('TutorialService') do |tutorial_service|
|
43
|
-
# p tutorial_service.call(:value => 5)
|
44
|
-
# end
|
45
|
-
def self.connect(service_name, params={})
|
46
|
-
begin
|
47
|
-
client = self.new(service_name, params)
|
48
|
-
yield(client)
|
49
|
-
ensure
|
50
|
-
client.close if client
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
13
|
# Returns a new RubySkynet Client for the named service
|
55
14
|
#
|
15
|
+
# Calls to an instance of the Client are thread-safe and can be called
|
16
|
+
# concurrently from multiple threads at the same time
|
17
|
+
#
|
56
18
|
# Parameters:
|
57
19
|
# :service_name
|
58
20
|
# Name of the service to look for and connect to on Skynet
|
59
21
|
#
|
60
|
-
# :
|
61
|
-
#
|
62
|
-
#
|
22
|
+
# :version
|
23
|
+
# Optional version number of the service in Skynet
|
24
|
+
# Default: '*' being the latest version of the service
|
63
25
|
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# server, only a connection failure or during an automatic reconnect
|
26
|
+
# :region
|
27
|
+
# Optional region for this service in Skynet
|
28
|
+
# Default: 'Development'
|
68
29
|
#
|
69
|
-
#
|
70
|
-
# Time in seconds to timeout on read
|
71
|
-
# Can be overridden by supplying a timeout in the read call
|
72
|
-
# Default: 60
|
73
|
-
#
|
74
|
-
# :connect_timeout [Float]
|
75
|
-
# Time in seconds to timeout when trying to connect to the server
|
76
|
-
# Default: Half of the :read_timeout ( 30 seconds )
|
30
|
+
# Example
|
77
31
|
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
32
|
+
# require 'ruby_skynet'
|
33
|
+
# SemanticLogger.default_level = :trace
|
34
|
+
# SemanticLogger.appenders << SemanticLogger::Appender::File(STDOUT)
|
81
35
|
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
|
85
|
-
def initialize(service_name, params = {})
|
36
|
+
# tutorial_service = RubySkynet::Client.new('TutorialService')
|
37
|
+
# p tutorial_service.call('Add', :value => 5)
|
38
|
+
def initialize(service_name, version='*', region='Development')
|
86
39
|
@service_name = service_name
|
87
|
-
@logger
|
88
|
-
|
89
|
-
|
90
|
-
params[:read_timeout] ||= 60
|
91
|
-
params[:connect_timeout] ||= 30
|
92
|
-
params[:connect_retry_interval] ||= 0.1
|
93
|
-
params[:connect_retry_count] ||= 5
|
94
|
-
|
95
|
-
# If Server name and port of where Skynet Service is running
|
96
|
-
# is not supplied look for it in Doozer
|
97
|
-
unless params[:server] || params[:servers]
|
98
|
-
params[:server] = self.class.server_for(service_name)
|
99
|
-
end
|
100
|
-
|
101
|
-
# Disable buffering the send since it is a RPC call
|
102
|
-
params[:buffered] = false
|
103
|
-
|
104
|
-
@logger.trace "Socket Connection parameters", params
|
105
|
-
|
106
|
-
# For each new connection perform the Skynet handshake
|
107
|
-
params[:on_connect] = Proc.new do |socket|
|
108
|
-
# Reset user_data on each connection
|
109
|
-
socket.user_data = 0
|
110
|
-
|
111
|
-
# Receive Service Handshake
|
112
|
-
# Registered bool
|
113
|
-
# Registered indicates the state of this service. If it is false, the connection will
|
114
|
-
# close immediately and the client should look elsewhere for this service.
|
115
|
-
#
|
116
|
-
# ClientID string
|
117
|
-
# ClientID is a UUID that is used by the client to identify itself in RPC requests.
|
118
|
-
@logger.debug "Waiting for Service Handshake"
|
119
|
-
service_handshake = self.class.read_bson_document(socket)
|
120
|
-
@logger.trace 'Service Handshake', service_handshake
|
121
|
-
|
122
|
-
# #TODO When a reconnect returns registered == false we need to go back to doozer
|
123
|
-
@registered = service_handshake['registered']
|
124
|
-
@client_id = service_handshake['clientid']
|
125
|
-
|
126
|
-
# Send blank ClientHandshake
|
127
|
-
client_handshake = { 'clientid' => @client_id }
|
128
|
-
@logger.debug "Sending Client Handshake"
|
129
|
-
@logger.trace 'Client Handshake', client_handshake
|
130
|
-
socket.write(BSON.serialize(client_handshake))
|
131
|
-
end
|
132
|
-
|
133
|
-
@socket = ResilientSocket::TCPClient.new(params)
|
40
|
+
@logger = SemanticLogger::Logger.new("#{self.class.name}: #{service_name}/#{version}/#{region}")
|
41
|
+
@version = version
|
42
|
+
@region = region
|
134
43
|
end
|
135
44
|
|
136
45
|
# Performs a synchronous call to the Skynet Service
|
@@ -145,149 +54,28 @@ module RubySkynet
|
|
145
54
|
#
|
146
55
|
# Raises RubySkynet::ProtocolError
|
147
56
|
# Raises RubySkynet::SkynetException
|
148
|
-
def call(method_name, parameters)
|
57
|
+
def call(method_name, parameters, connection_params={})
|
149
58
|
# Skynet requires BSON RPC Calls to have the following format:
|
150
59
|
# https://github.com/bketelsen/skynet/blob/protocol/protocol.md
|
151
60
|
request_id = BSON::ObjectId.new.to_s
|
152
61
|
@logger.tagged request_id do
|
153
62
|
@logger.benchmark_info "Called Skynet Service: #{@service_name}.#{method_name}" do
|
154
|
-
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
@logger.trace 'Header', header
|
167
|
-
socket.write(BSON.serialize(header))
|
168
|
-
|
169
|
-
@logger.trace 'Parameters:', parameters
|
170
|
-
|
171
|
-
# The parameters are placed in the request object in BSON serialized
|
172
|
-
# form
|
173
|
-
request = {
|
174
|
-
'clientid' => @client_id,
|
175
|
-
'in' => BSON.serialize(parameters).to_s,
|
176
|
-
'method' => method_name.to_s,
|
177
|
-
'requestinfo' => {
|
178
|
-
'requestid' => request_id,
|
179
|
-
# Increment retry count to indicate that the request may have been tried previously
|
180
|
-
# TODO: this should be incremented if request is retried,
|
181
|
-
'retrycount' => retry_count,
|
182
|
-
# TODO: this should be forwarded along in case of services also
|
183
|
-
# being a client and calling additional services. If empty it will
|
184
|
-
# be stuffed with connecting address
|
185
|
-
'originaddress' => ''
|
186
|
-
}
|
187
|
-
}
|
188
|
-
|
189
|
-
@logger.debug "Sending Request"
|
190
|
-
@logger.trace 'Request', request
|
191
|
-
socket.write(BSON.serialize(request))
|
192
|
-
end
|
193
|
-
|
194
|
-
# Once send is successful it could have been processed, so we can no
|
195
|
-
# longer retry now otherwise we could create a duplicate
|
196
|
-
# retry_count += 1
|
197
|
-
|
198
|
-
# Read header first as a separate BSON document
|
199
|
-
@logger.debug "Reading header from server"
|
200
|
-
header = self.class.read_bson_document(@socket)
|
201
|
-
@logger.debug 'Header', header
|
202
|
-
|
203
|
-
# Read the BSON response document
|
204
|
-
@logger.debug "Reading response from server"
|
205
|
-
response = self.class.read_bson_document(@socket)
|
206
|
-
@logger.trace 'Response', response
|
207
|
-
|
208
|
-
# Ensure the sequence number in the response header matches the
|
209
|
-
# sequence number sent in the request
|
210
|
-
if seq_no = header['seq']
|
211
|
-
raise ProtocolError.new("Incorrect Response received, expected seq=#{@socket.user_data}, received: #{header.inspect}") if seq_no != @socket.user_data
|
212
|
-
else
|
213
|
-
raise ProtocolError.new("Invalid Response header, missing 'seq': #{header.inspect}")
|
214
|
-
end
|
215
|
-
|
216
|
-
# Increment Sequence number only on successful response
|
217
|
-
@socket.user_data += 1
|
218
|
-
|
219
|
-
# If an error is returned from Skynet raise a Skynet exception
|
220
|
-
if error = header['error']
|
221
|
-
raise SkynetException.new(error) if error.to_s.length > 0
|
222
|
-
end
|
223
|
-
|
224
|
-
# If an error is returned from the service raise a Service exception
|
225
|
-
if error = response['error']
|
226
|
-
raise ServiceException.new(error) if error.to_s.length > 0
|
63
|
+
retries = 0
|
64
|
+
# If it cannot connect to a server, try a different server
|
65
|
+
begin
|
66
|
+
Connection.with_connection(Registry.server_for(@service_name, @version, @region), connection_params) do |connection|
|
67
|
+
connection.rpc_call(request_id, @service_name, method_name, parameters)
|
68
|
+
end
|
69
|
+
rescue ResilientSocket::ConnectionFailure => exc
|
70
|
+
if (retries < 3) && exc.cause.is_a?(Errno::ECONNREFUSED)
|
71
|
+
retries += 1
|
72
|
+
retry
|
73
|
+
end
|
74
|
+
# TODO rescue ServiceUnavailable retry x times until the service becomes available
|
227
75
|
end
|
228
|
-
|
229
|
-
# Return Value
|
230
|
-
# The return value is inside the response object, it's a byte array of it's own and needs to be deserialized
|
231
|
-
result = BSON.deserialize(response['out'])
|
232
|
-
@logger.trace 'Return Value', result
|
233
|
-
result
|
234
76
|
end
|
235
77
|
end
|
236
78
|
end
|
237
79
|
|
238
|
-
# Returns a BSON document read from the socket.
|
239
|
-
# Returns nil if the operation times out or if a network
|
240
|
-
# connection failure occurs
|
241
|
-
def self.read_bson_document(socket)
|
242
|
-
bytebuf = BSON::ByteBuffer.new
|
243
|
-
# Read 4 byte size of following BSON document
|
244
|
-
bytes = ''
|
245
|
-
socket.read(4, bytes)
|
246
|
-
|
247
|
-
# Read BSON document
|
248
|
-
sz = bytes.unpack("V")[0]
|
249
|
-
raise "Invalid Data received from server:#{bytes.inspect}" unless sz
|
250
|
-
|
251
|
-
bytebuf.append!(bytes)
|
252
|
-
bytes = ''
|
253
|
-
sz -= 4
|
254
|
-
until bytes.size >= sz
|
255
|
-
buf = ''
|
256
|
-
socket.read(sz, buf)
|
257
|
-
bytes << buf
|
258
|
-
end
|
259
|
-
bytebuf.append!(bytes)
|
260
|
-
return BSON.deserialize(bytebuf)
|
261
|
-
end
|
262
|
-
|
263
|
-
def close()
|
264
|
-
@socket.close
|
265
|
-
end
|
266
|
-
|
267
|
-
##############################
|
268
|
-
#protected
|
269
|
-
|
270
|
-
# Returns [Array] of the hostname and port pair [String] that implements a particular service
|
271
|
-
# Performs a doozer lookup to find the servers
|
272
|
-
#
|
273
|
-
# service_name:
|
274
|
-
# version: Version of service to locate
|
275
|
-
# Default: Find latest version
|
276
|
-
def self.registered_implementers(service_name, version = '*', region = 'Development')
|
277
|
-
hosts = []
|
278
|
-
doozer.walk("/services/#{service_name}/#{version}/#{region}/*/*").each do |node|
|
279
|
-
entry = MultiJson.load(node.value)
|
280
|
-
hosts << entry if entry['Registered']
|
281
|
-
end
|
282
|
-
hosts
|
283
|
-
end
|
284
|
-
|
285
|
-
# Randomly returns a server that implements the requested service
|
286
|
-
def self.server_for(service_name, version = '*', region = 'Development')
|
287
|
-
hosts = registered_implementers(service_name, version, region)
|
288
|
-
service = hosts[rand(hosts.size)]['Config']['ServiceAddr']
|
289
|
-
"#{service['IPAddress']}:#{service['Port']}"
|
290
|
-
end
|
291
|
-
|
292
80
|
end
|
293
81
|
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'bson'
|
2
|
+
require 'gene_pool'
|
3
|
+
require 'thread_safe'
|
4
|
+
|
5
|
+
#
|
6
|
+
# RubySkynet Connection
|
7
|
+
#
|
8
|
+
# Handles connecting to Skynet Servers as a host:port pair
|
9
|
+
#
|
10
|
+
module RubySkynet
|
11
|
+
class Connection
|
12
|
+
include SyncAttr
|
13
|
+
|
14
|
+
# Returns the underlying socket being used by a Connection instance
|
15
|
+
attr_reader :socket
|
16
|
+
|
17
|
+
# Default Pool configuration
|
18
|
+
sync_cattr_accessor :pool_config do
|
19
|
+
{
|
20
|
+
:pool_size => 30, # Maximum number of connections to any one server
|
21
|
+
:warn_timeout => 2, # Log a warning if no connections are available after the :warn_timeout seconds
|
22
|
+
:timeout => 10, # Raise a Timeout exception if no connections are available after the :timeout seconds
|
23
|
+
:idle_timeout => 600, # Renew a connection if it has been idle for this period of time
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Logging instance for the connection pool
|
28
|
+
sync_cattr_reader :logger do
|
29
|
+
SemanticLogger::Logger.new(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# For each server there is a connection pool keyed on the
|
33
|
+
# server address: 'host:port'
|
34
|
+
@@connection_pools = ThreadSafe::Hash.new
|
35
|
+
|
36
|
+
# Returns a new RubySkynet connection to the server
|
37
|
+
#
|
38
|
+
# Parameters:
|
39
|
+
# :read_timeout [Float]
|
40
|
+
# Time in seconds to timeout on read
|
41
|
+
# Can be overridden by supplying a timeout in the read call
|
42
|
+
# Default: 60
|
43
|
+
#
|
44
|
+
# :connect_timeout [Float]
|
45
|
+
# Time in seconds to timeout when trying to connect to the server
|
46
|
+
# Default: Half of the :read_timeout ( 30 seconds )
|
47
|
+
#
|
48
|
+
# :connect_retry_count [Fixnum]
|
49
|
+
# Number of times to retry connecting when a connection fails
|
50
|
+
# Default: 10
|
51
|
+
#
|
52
|
+
# :connect_retry_interval [Float]
|
53
|
+
# Number of seconds between connection retry attempts after the first failed attempt
|
54
|
+
# Default: 0.5
|
55
|
+
def initialize(server, params = {})
|
56
|
+
@logger = SemanticLogger::Logger.new("#{self.class.name}: #{server}")
|
57
|
+
|
58
|
+
# User configurable options
|
59
|
+
params[:read_timeout] ||= 60
|
60
|
+
params[:connect_timeout] ||= 30
|
61
|
+
params[:connect_retry_interval] ||= 0.1
|
62
|
+
params[:connect_retry_count] ||= 5
|
63
|
+
|
64
|
+
# Disable send buffering since it is a RPC call
|
65
|
+
params[:buffered] = false
|
66
|
+
|
67
|
+
# For each new connection perform the Skynet handshake
|
68
|
+
params[:on_connect] = Proc.new do |socket|
|
69
|
+
# Reset user_data on each connection
|
70
|
+
socket.user_data = {
|
71
|
+
:seq => 0,
|
72
|
+
:logger => @logger
|
73
|
+
}
|
74
|
+
|
75
|
+
# Receive Service Handshake
|
76
|
+
# Registered bool
|
77
|
+
# Registered indicates the state of this service. If it is false, the connection will
|
78
|
+
# close immediately and the client should look elsewhere for this service.
|
79
|
+
#
|
80
|
+
# ClientID string
|
81
|
+
# ClientID is a UUID that is used by the client to identify itself in RPC requests.
|
82
|
+
@logger.debug "Waiting for Service Handshake"
|
83
|
+
service_handshake = self.class.read_bson_document(socket)
|
84
|
+
@logger.trace 'Service Handshake', service_handshake
|
85
|
+
|
86
|
+
# #TODO When a reconnect returns registered == false need to throw an exception
|
87
|
+
# so that this host connection is not used
|
88
|
+
registered = service_handshake['registered']
|
89
|
+
client_id = service_handshake['clientid']
|
90
|
+
socket.user_data[:client_id] = client_id
|
91
|
+
|
92
|
+
# Send blank ClientHandshake
|
93
|
+
client_handshake = { 'clientid' => client_id }
|
94
|
+
@logger.debug "Sending Client Handshake"
|
95
|
+
@logger.trace 'Client Handshake', client_handshake
|
96
|
+
socket.write(BSON.serialize(client_handshake))
|
97
|
+
end
|
98
|
+
|
99
|
+
# To prevent strange issues if user incorrectly supplies server names
|
100
|
+
params.delete(:servers)
|
101
|
+
params[:server] = server
|
102
|
+
|
103
|
+
@socket = ResilientSocket::TCPClient.new(params)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Performs a synchronous call to a Skynet server
|
107
|
+
#
|
108
|
+
# Parameters:
|
109
|
+
# service_name [String|Symbol]:
|
110
|
+
# Name of the method to pass in the request
|
111
|
+
# method_name [String|Symbol]:
|
112
|
+
# Name of the method to pass in the request
|
113
|
+
# parameters [Hash]:
|
114
|
+
# Parameters to pass in the request
|
115
|
+
# idempotent [True|False]:
|
116
|
+
# If the request can be applied again to the server without changing its state
|
117
|
+
# Set to true to retry the entire request after the send is successful
|
118
|
+
#
|
119
|
+
# Returns the Hash result returned from the Skynet Service
|
120
|
+
#
|
121
|
+
# Raises RubySkynet::ProtocolError
|
122
|
+
# Raises RubySkynet::SkynetException
|
123
|
+
def rpc_call(request_id, service_name, method_name, parameters, idempotent=false)
|
124
|
+
retry_count = 0
|
125
|
+
header = nil
|
126
|
+
response = nil
|
127
|
+
|
128
|
+
socket.retry_on_connection_failure do |socket|
|
129
|
+
header = {
|
130
|
+
'servicemethod' => "#{service_name}.Forward",
|
131
|
+
'seq' => socket.user_data[:seq]
|
132
|
+
}
|
133
|
+
|
134
|
+
@logger.debug "Sending Header"
|
135
|
+
@logger.trace 'Header', header
|
136
|
+
socket.write(BSON.serialize(header))
|
137
|
+
|
138
|
+
# The parameters are placed in the request object in BSON serialized form
|
139
|
+
request = {
|
140
|
+
'clientid' => socket.user_data[:client_id],
|
141
|
+
'in' => BSON.serialize(parameters).to_s,
|
142
|
+
'method' => method_name.to_s,
|
143
|
+
'requestinfo' => {
|
144
|
+
'requestid' => request_id,
|
145
|
+
# Increment retry count to indicate that the request may have been tried previously
|
146
|
+
'retrycount' => retry_count,
|
147
|
+
# TODO: this should be forwarded along in case of services also
|
148
|
+
# being a client and calling additional services. If empty it will
|
149
|
+
# be stuffed with connecting address
|
150
|
+
'originaddress' => ''
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
@logger.debug "Sending Request"
|
155
|
+
@logger.trace 'Request', request
|
156
|
+
@logger.trace 'Parameters:', parameters
|
157
|
+
socket.write(BSON.serialize(request))
|
158
|
+
|
159
|
+
# Since Send does not affect state on the server we can also retry reads
|
160
|
+
if idempotent
|
161
|
+
@logger.debug "Reading header from server"
|
162
|
+
header = self.class.read_bson_document(socket)
|
163
|
+
@logger.debug 'Response Header', header
|
164
|
+
|
165
|
+
# Read the BSON response document
|
166
|
+
@logger.debug "Reading response from server"
|
167
|
+
response = self.class.read_bson_document(socket)
|
168
|
+
@logger.trace 'Response', response
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Perform the read outside the retry block since a successful write
|
173
|
+
# means that the servers state may have been changed
|
174
|
+
unless idempotent
|
175
|
+
# Read header first as a separate BSON document
|
176
|
+
@logger.debug "Reading header from server"
|
177
|
+
header = self.class.read_bson_document(socket)
|
178
|
+
@logger.debug 'Response Header', header
|
179
|
+
|
180
|
+
# Read the BSON response document
|
181
|
+
@logger.debug "Reading response from server"
|
182
|
+
response = self.class.read_bson_document(socket)
|
183
|
+
@logger.trace 'Response', response
|
184
|
+
end
|
185
|
+
|
186
|
+
# Ensure the sequence number in the response header matches the
|
187
|
+
# sequence number sent in the request
|
188
|
+
seq_no = header['seq']
|
189
|
+
if seq_no != socket.user_data[:seq]
|
190
|
+
raise ProtocolError.new("Incorrect Response received, expected seq=#{socket.user_data[:seq]}, received: #{header.inspect}")
|
191
|
+
end
|
192
|
+
|
193
|
+
# Increment Sequence number only on successful response
|
194
|
+
socket.user_data[:seq] += 1
|
195
|
+
|
196
|
+
# If an error is returned from Skynet raise a Skynet exception
|
197
|
+
error = header['error']
|
198
|
+
raise SkynetException.new(error) if error.to_s.length > 0
|
199
|
+
|
200
|
+
# If an error is returned from the service raise a Service exception
|
201
|
+
error = response['error']
|
202
|
+
raise ServiceException.new(error) if error.to_s.length > 0
|
203
|
+
|
204
|
+
# Return Value
|
205
|
+
# The return value is inside the response object, it's a byte array of it's own and needs to be deserialized
|
206
|
+
result = BSON.deserialize(response['out'])
|
207
|
+
@logger.trace 'Return Value', result
|
208
|
+
result
|
209
|
+
end
|
210
|
+
|
211
|
+
# Execute the supplied block with a connection from the pool
|
212
|
+
def self.with_connection(server, params={}, &block)
|
213
|
+
(@@connection_pools[server] ||= new_connection_pool(server, params)).with_connection(&block)
|
214
|
+
end
|
215
|
+
|
216
|
+
def close
|
217
|
+
@socket.close if @socket
|
218
|
+
end
|
219
|
+
|
220
|
+
########################
|
221
|
+
protected
|
222
|
+
|
223
|
+
# Returns a BSON document read from the socket.
|
224
|
+
# Returns nil if the operation times out or if a network
|
225
|
+
# connection failure occurs
|
226
|
+
def self.read_bson_document(socket)
|
227
|
+
bytebuf = BSON::ByteBuffer.new
|
228
|
+
# Read 4 byte size of following BSON document
|
229
|
+
bytes = socket.read(4)
|
230
|
+
|
231
|
+
# Read BSON document
|
232
|
+
sz = bytes.unpack("V")[0]
|
233
|
+
raise "Invalid Data received from server:#{bytes.inspect}" unless sz
|
234
|
+
|
235
|
+
bytebuf.append!(bytes)
|
236
|
+
bytebuf.append!(socket.read(sz - 4))
|
237
|
+
return BSON.deserialize(bytebuf)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns a new connection pool for the specified server
|
241
|
+
def self.new_connection_pool(server, params={})
|
242
|
+
# Connection pool configuration options
|
243
|
+
config = pool_config.dup
|
244
|
+
|
245
|
+
# Method to call to close idle connections
|
246
|
+
config[:close_proc] = :close
|
247
|
+
config[:logger] = logger
|
248
|
+
config[:name] = "Connection pool for #{server}"
|
249
|
+
|
250
|
+
pool = GenePool.new(pool_config) do
|
251
|
+
new(server, params)
|
252
|
+
end
|
253
|
+
|
254
|
+
# Cleanup corresponding connection pool when a server terminates
|
255
|
+
Registry.on_server_removed(server) do
|
256
|
+
pool = @@connection_pools.delete(server)
|
257
|
+
# Cannot close all the connections since they could still be in use
|
258
|
+
pool.remove_idle(0) if pool
|
259
|
+
#pool.close if pool
|
260
|
+
logger.debug "Connection pool for server:#{server} has been released"
|
261
|
+
end
|
262
|
+
|
263
|
+
pool
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
|
@@ -142,14 +142,29 @@ module RubySkynet
|
|
142
142
|
end
|
143
143
|
end
|
144
144
|
|
145
|
-
#
|
146
|
-
#
|
147
|
-
|
148
|
-
|
149
|
-
|
145
|
+
# Wait for changes to the supplied path
|
146
|
+
# Returns the next change to the supplied path
|
147
|
+
def wait(path, rev=current_revision, timeout=-1)
|
148
|
+
invoke(Request.new(:path => path, :rev => rev, :verb => Request::Verb::WAIT), true, timeout)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Watch for any changes to the supplied path, calling the supplied block
|
152
|
+
# for every change
|
153
|
+
# Runs until an exception is thrown
|
154
|
+
#
|
155
|
+
# If a connection error occurs it will create a new connection to doozer
|
156
|
+
# and resubmit the wait. I.e. Will continue from where it left off
|
157
|
+
# without any noticeable effect to the supplied block
|
158
|
+
def watch(path, rev=current_revision)
|
159
|
+
loop do
|
160
|
+
result = wait(path, rev, -1)
|
161
|
+
yield result
|
162
|
+
rev = result.rev + 1
|
163
|
+
end
|
164
|
+
end
|
150
165
|
|
151
166
|
#####################
|
152
|
-
#
|
167
|
+
#protected
|
153
168
|
|
154
169
|
# Call the Doozer server
|
155
170
|
#
|
@@ -158,16 +173,16 @@ module RubySkynet
|
|
158
173
|
# _only_ if a rev has been supplied
|
159
174
|
#
|
160
175
|
# When modifier is true
|
161
|
-
def invoke(request, readonly=true)
|
176
|
+
def invoke(request, readonly=true, timeout=nil)
|
162
177
|
retry_read = readonly || !request.rev.nil?
|
163
178
|
response = nil
|
164
179
|
@socket.retry_on_connection_failure do
|
165
180
|
send(request)
|
166
|
-
response = read if retry_read
|
181
|
+
response = read(timeout) if retry_read
|
167
182
|
end
|
168
183
|
# Network error on read must be sent back to caller since we do not
|
169
184
|
# know if the modification was made
|
170
|
-
response = read unless retry_read
|
185
|
+
response = read(timeout) unless retry_read
|
171
186
|
raise ResponseError.new("#{Response::Err.name_by_value(response.err_code)}: #{response.err_detail}") if response.err_code != 0
|
172
187
|
response
|
173
188
|
end
|
@@ -182,19 +197,11 @@ module RubySkynet
|
|
182
197
|
end
|
183
198
|
|
184
199
|
# Read the protobuf Response from Doozer
|
185
|
-
def read
|
200
|
+
def read(timeout=nil)
|
186
201
|
# First strip the additional header indicating the size of the subsequent response
|
187
|
-
head = @socket.read(4)
|
202
|
+
head = @socket.read(4,nil,timeout)
|
188
203
|
length = head.unpack("N")[0]
|
189
|
-
|
190
|
-
# Since can returns upto 'length' bytes we need to make sure it returns
|
191
|
-
# at least 'length' bytes
|
192
|
-
# TODO: Make this a binary buffer
|
193
|
-
data = ''
|
194
|
-
until data.size >= length
|
195
|
-
data << @socket.read(length)
|
196
|
-
end
|
197
|
-
Response.new.parse_from_string(data)
|
204
|
+
Response.new.parse_from_string(@socket.read(length))
|
198
205
|
end
|
199
206
|
|
200
207
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module RubySkynet
|
2
2
|
class Exception < ::RuntimeError; end
|
3
3
|
class ProtocolError < Exception; end
|
4
|
-
class SkynetException < Exception; end
|
5
4
|
class ServiceException < Exception; end
|
5
|
+
class SkynetException < Exception; end
|
6
|
+
class ServiceUnavailable < SkynetException; end
|
6
7
|
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'sync_attr'
|
2
|
+
require 'multi_json'
|
3
|
+
require 'thread_safe'
|
4
|
+
require 'gene_pool'
|
5
|
+
|
6
|
+
#
|
7
|
+
# RubySkynet Registry Client
|
8
|
+
#
|
9
|
+
# Keeps a local copy of the Skynet Registry
|
10
|
+
#
|
11
|
+
# Subscribes to Registry changes and the internal copy up to date
|
12
|
+
#
|
13
|
+
module RubySkynet
|
14
|
+
class Registry
|
15
|
+
include SyncAttr
|
16
|
+
|
17
|
+
# Service Registry has the following format
|
18
|
+
# Key: [String] 'service_name/version/region'
|
19
|
+
# Value: [Array<String>] 'host:port', 'host:port'
|
20
|
+
sync_cattr_accessor :service_registry do
|
21
|
+
start_monitoring
|
22
|
+
end
|
23
|
+
|
24
|
+
@@on_server_removed_callbacks = ThreadSafe::Hash.new
|
25
|
+
@@monitor_thread = nil
|
26
|
+
|
27
|
+
DOOZER_SERVICES_PATH = "/services/*/*/*/*/*"
|
28
|
+
|
29
|
+
# Default doozer configuration
|
30
|
+
# To replace this default, set the config as follows:
|
31
|
+
# RubySkynet::Client.doozer_config = { .... }
|
32
|
+
#
|
33
|
+
# :servers [Array of String]
|
34
|
+
# Array of URL's of doozer servers to connect to with port numbers
|
35
|
+
# ['server1:2000', 'server2:2000']
|
36
|
+
#
|
37
|
+
# The second server will only be attempted once the first server
|
38
|
+
# cannot be connected to or has timed out on connect
|
39
|
+
# A read failure or timeout will not result in switching to the second
|
40
|
+
# server, only a connection failure or during an automatic reconnect
|
41
|
+
#
|
42
|
+
# :read_timeout [Float]
|
43
|
+
# Time in seconds to timeout on read
|
44
|
+
# Can be overridden by supplying a timeout in the read call
|
45
|
+
#
|
46
|
+
# :connect_timeout [Float]
|
47
|
+
# Time in seconds to timeout when trying to connect to the server
|
48
|
+
#
|
49
|
+
# :connect_retry_count [Fixnum]
|
50
|
+
# Number of times to retry connecting when a connection fails
|
51
|
+
#
|
52
|
+
# :connect_retry_interval [Float]
|
53
|
+
# Number of seconds between connection retry attempts after the first failed attempt
|
54
|
+
sync_cattr_accessor :doozer_config do
|
55
|
+
{
|
56
|
+
:servers => ['127.0.0.1:8046'],
|
57
|
+
:read_timeout => 5,
|
58
|
+
:connect_timeout => 3,
|
59
|
+
:connect_retry_interval => 1,
|
60
|
+
:connect_retry_count => 300
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Lazy initialize Doozer Client Connection pool
|
65
|
+
sync_cattr_reader :doozer_pool do
|
66
|
+
GenePool.new(
|
67
|
+
:name =>"Doozer Connection Pool",
|
68
|
+
:pool_size => 5,
|
69
|
+
:timeout => 30,
|
70
|
+
:warn_timeout => 5,
|
71
|
+
:idle_timeout => 600,
|
72
|
+
:logger => logger,
|
73
|
+
:close_proc => :close
|
74
|
+
) do
|
75
|
+
Doozer::Client.new(doozer_config)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Logging instance for this class
|
80
|
+
sync_cattr_reader :logger do
|
81
|
+
SemanticLogger::Logger.new(self, :debug)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return a server that implements the specified service
|
85
|
+
def self.server_for(service_name, version='*', region='Development')
|
86
|
+
if servers = servers_for(service_name, version, region)
|
87
|
+
# Randomly select one of the servers offering the service
|
88
|
+
servers[rand(servers.size)]
|
89
|
+
else
|
90
|
+
msg = "No servers available for service: #{service_name} with version: #{version} in region: #{region}"
|
91
|
+
logger.warn msg
|
92
|
+
raise ServiceUnavailable.new(msg)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns [Array] of the hostname and port pair [String] that implements a particular service
|
97
|
+
# Performs a doozer lookup to find the servers
|
98
|
+
#
|
99
|
+
# service_name:
|
100
|
+
# Name of the service to lookup
|
101
|
+
# version:
|
102
|
+
# Version of service to locate
|
103
|
+
# Default: All versions
|
104
|
+
# region:
|
105
|
+
# Region to look for the service in
|
106
|
+
def self.registered_implementers(service_name='*', version='*', region='Development')
|
107
|
+
hosts = []
|
108
|
+
doozer_pool.with_connection do |doozer|
|
109
|
+
doozer.walk("/services/#{service_name}/#{version}/#{region}/*/*").each do |node|
|
110
|
+
entry = MultiJson.load(node.value)
|
111
|
+
hosts << entry if entry['Registered']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
hosts
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns [Array<String>] a list of servers implementing the requested service
|
118
|
+
def self.servers_for(service_name, version='*', region='Development', remote = false)
|
119
|
+
if remote
|
120
|
+
if version != '*'
|
121
|
+
registered_implementers(service_name, version, region).map do |host|
|
122
|
+
service = host['Config']['ServiceAddr']
|
123
|
+
"#{service['IPAddress']}:#{service['Port']}"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
# Find the highest version of any particular service
|
127
|
+
versions = {}
|
128
|
+
registered_implementers(service_name, version, region).each do |host|
|
129
|
+
service = host['Config']['ServiceAddr']
|
130
|
+
(versions[version.to_i] ||= []) << "#{service['IPAddress']}:#{service['Port']}"
|
131
|
+
end
|
132
|
+
# Return the servers implementing the highest version number
|
133
|
+
versions.sort.last.last
|
134
|
+
end
|
135
|
+
else
|
136
|
+
if version == '*'
|
137
|
+
# Find the highest version for the named service in this region
|
138
|
+
version = -1
|
139
|
+
service_registry.keys.each do |key|
|
140
|
+
if match = key.match(/#{service_name}\/(\d+)\/#{region}/)
|
141
|
+
ver = match[1].to_i
|
142
|
+
version = ver if ver > version
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
service_registry["#{service_name}/#{version}/#{region}"]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Invokes registered callbacks when a specific server is shutdown or terminates
|
151
|
+
# Not when a server de-registers itself
|
152
|
+
# The callback will only be called once and will need to be re-registered
|
153
|
+
# after being called if future callbacks are required for that server
|
154
|
+
def self.on_server_removed(server, &block)
|
155
|
+
(@@on_server_removed_callbacks[server] ||= ThreadSafe::Array.new) << block
|
156
|
+
end
|
157
|
+
|
158
|
+
############################
|
159
|
+
#protected
|
160
|
+
|
161
|
+
# Fetch the all registry information from Doozer and set the internal registry
|
162
|
+
# Also starts the monitoring thread to keep the registry up to date
|
163
|
+
def self.start_monitoring
|
164
|
+
registry = ThreadSafe::Hash.new
|
165
|
+
revision = nil
|
166
|
+
doozer_pool.with_connection do |doozer|
|
167
|
+
revision = doozer.current_revision
|
168
|
+
doozer.walk(DOOZER_SERVICES_PATH, revision).each do |node|
|
169
|
+
# path: "/services/TutorialService/1/Development/127.0.0.1/9000"
|
170
|
+
e = node.path.split('/')
|
171
|
+
|
172
|
+
# Key: [String] 'service_name/version/region'
|
173
|
+
key = "#{e[2]}/#{e[3]}/#{e[4]}"
|
174
|
+
server = "#{e[5]}:#{e[6]}"
|
175
|
+
|
176
|
+
if node.value.strip.size > 0
|
177
|
+
entry = MultiJson.load(node.value)
|
178
|
+
if entry['Registered']
|
179
|
+
# Value: [Array<String>] 'host:port', 'host:port'
|
180
|
+
servers = (registry[key] ||= ThreadSafe::Array.new)
|
181
|
+
servers << server unless servers.include?(server)
|
182
|
+
logger.debug "#start_monitoring Add Service: #{key} => #{server}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
# Start monitoring thread to keep the registry up to date
|
188
|
+
@@monitor_thread = Thread.new { self.watch(revision + 1) }
|
189
|
+
registry
|
190
|
+
end
|
191
|
+
|
192
|
+
# Waits for any updates from Doozer and updates the internal service registry
|
193
|
+
def self.watch(revision)
|
194
|
+
logger.info "Start monitoring #{DOOZER_SERVICES_PATH}"
|
195
|
+
# This thread must use its own dedicated doozer connection
|
196
|
+
doozer = Doozer::Client.new(doozer_config)
|
197
|
+
doozer.watch(DOOZER_SERVICES_PATH, revision) do |node|
|
198
|
+
# path: "/services/TutorialService/1/Development/127.0.0.1/9000"
|
199
|
+
e = node.path.split('/')
|
200
|
+
|
201
|
+
# Key: [String] 'service_name/version/region'
|
202
|
+
key = "#{e[2]}/#{e[3]}/#{e[4]}"
|
203
|
+
server = "#{e[5]}:#{e[6]}"
|
204
|
+
|
205
|
+
if node.value.strip.size > 0
|
206
|
+
entry = MultiJson.load(node.value)
|
207
|
+
if entry['Registered']
|
208
|
+
# Value: [Array<String>] 'host:port', 'host:port'
|
209
|
+
servers = (@@service_registry[key] ||= ThreadSafe::Array.new)
|
210
|
+
servers << server unless servers.include?(server)
|
211
|
+
logger.debug "#monitor Add/Update Service: #{key} => #{server}"
|
212
|
+
else
|
213
|
+
logger.debug "#monitor Service deregistered, remove: #{key} => #{server}"
|
214
|
+
if @@service_registry[key]
|
215
|
+
@@service_registry[key].delete(server)
|
216
|
+
@@service_registry.delete(key) if @@service_registry[key].size == 0
|
217
|
+
end
|
218
|
+
end
|
219
|
+
else
|
220
|
+
# Service has stopped and needs to be removed
|
221
|
+
logger.debug "#monitor Service stopped, remove: #{key} => #{server}"
|
222
|
+
if @@service_registry[key]
|
223
|
+
@@service_registry[key].delete(server)
|
224
|
+
@@service_registry.delete(key) if @@service_registry[key].size == 0
|
225
|
+
server_removed(server)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
logger.debug "Updated registry", @@service_registry
|
229
|
+
end
|
230
|
+
logger.info "Stopping monitoring thread normally"
|
231
|
+
rescue Exception => exc
|
232
|
+
logger.error "Exception in monitoring thread", exc
|
233
|
+
ensure
|
234
|
+
logger.info "Stopped monitoring"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Invoke any registered callbacks for the specific server
|
238
|
+
def self.server_removed(server)
|
239
|
+
if callbacks = @@on_server_removed_callbacks.delete(server)
|
240
|
+
callbacks.each do |block|
|
241
|
+
begin
|
242
|
+
logger.info "Calling callback for server: #{server}"
|
243
|
+
block.call(server)
|
244
|
+
rescue Exception => exc
|
245
|
+
logger.error("Exception during a callback for server: #{server}", exc)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
end
|
data/lib/ruby_skynet/version.rb
CHANGED
data/lib/ruby_skynet.rb
CHANGED
@@ -7,5 +7,7 @@ module RubySkynet
|
|
7
7
|
module Doozer
|
8
8
|
autoload :Client, 'ruby_skynet/doozer/client'
|
9
9
|
end
|
10
|
-
autoload :
|
10
|
+
autoload :Registry, 'ruby_skynet/registry'
|
11
|
+
autoload :Connection, 'ruby_skynet/connection'
|
12
|
+
autoload :Client, 'ruby_skynet/client'
|
11
13
|
end
|
@@ -7,6 +7,7 @@ require 'test/unit'
|
|
7
7
|
require 'shoulda'
|
8
8
|
require 'ruby_skynet'
|
9
9
|
require 'simple_server'
|
10
|
+
require 'multi_json'
|
10
11
|
|
11
12
|
SemanticLogger::Logger.default_level = :trace
|
12
13
|
SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new('test.log')
|
@@ -17,43 +18,57 @@ class RubySkynetClientTest < Test::Unit::TestCase
|
|
17
18
|
|
18
19
|
context "without server" do
|
19
20
|
should "raise exception when cannot reach server after 5 retries" do
|
20
|
-
exception = assert_raise
|
21
|
-
RubySkynet::Client.new('SomeService'
|
22
|
-
|
23
|
-
:connect_retry_interval => 0.1,
|
24
|
-
:connect_retry_count => 5)
|
21
|
+
exception = assert_raise RubySkynet::ServiceUnavailable do
|
22
|
+
client = RubySkynet::Client.new('SomeService')
|
23
|
+
client.call(:test, :hello => 'there')
|
25
24
|
end
|
26
|
-
assert_match /
|
25
|
+
assert_match /No servers available for service: SomeService with version: \* in region: Development/, exception.message
|
27
26
|
end
|
28
27
|
|
29
28
|
end
|
30
29
|
|
31
30
|
context "with server" do
|
32
31
|
setup do
|
32
|
+
@port = 2000
|
33
33
|
@read_timeout = 3.0
|
34
|
-
@server = SimpleServer.new(
|
35
|
-
@server_name =
|
34
|
+
@server = SimpleServer.new(@port)
|
35
|
+
@server_name = "localhost:#{@port}"
|
36
|
+
|
37
|
+
# Register service in doozer
|
38
|
+
@service_name = "TestService"
|
39
|
+
@version = 1
|
40
|
+
@region = 'Test'
|
41
|
+
@ip_address = "127.0.0.1"
|
42
|
+
config = {
|
43
|
+
"Config" => {
|
44
|
+
"UUID" => "3978b371-15e9-40f8-9b7b-59ae88d8c7ec",
|
45
|
+
"Name" => @service_name,
|
46
|
+
"Version" => @version.to_s,
|
47
|
+
"Region" => @region,
|
48
|
+
"ServiceAddr" => {
|
49
|
+
"IPAddress" => @ip_address,
|
50
|
+
"Port" => @port,
|
51
|
+
"MaxPort" => @port + 999
|
52
|
+
},
|
53
|
+
},
|
54
|
+
"Registered" => true
|
55
|
+
}
|
56
|
+
RubySkynet::Registry.doozer_pool.with_connection do |doozer|
|
57
|
+
doozer["/services/#{@service_name}/#{@version}/#{@region}/#{@ip_address}/#{@port}"] = MultiJson.encode(config)
|
58
|
+
end
|
36
59
|
end
|
37
60
|
|
38
61
|
teardown do
|
39
62
|
@server.stop if @server
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
should "call server" do
|
44
|
-
RubySkynet::Client.connect('TutorialService', :read_timeout => @read_timeout, :server => @server_name) do |tutorial_service|
|
45
|
-
assert_equal 'test1', tutorial_service.call(:test1, 'some' => 'parameters')['result']
|
46
|
-
end
|
63
|
+
# De-register server in doozer
|
64
|
+
RubySkynet::Registry.doozer_pool.with_connection do |doozer|
|
65
|
+
doozer.delete("/services/#{@service_name}/#{@version}/#{@region}/#{@ip_address}/#{@port}") rescue nil
|
47
66
|
end
|
48
67
|
end
|
49
68
|
|
50
69
|
context "with client connection" do
|
51
70
|
setup do
|
52
|
-
@client = RubySkynet::Client.new(
|
53
|
-
end
|
54
|
-
|
55
|
-
def teardown
|
56
|
-
@client.close if @client
|
71
|
+
@client = RubySkynet::Client.new(@service_name, @version, @region)
|
57
72
|
end
|
58
73
|
|
59
74
|
should "successfully send and receive data" do
|
@@ -66,7 +81,7 @@ class RubySkynetClientTest < Test::Unit::TestCase
|
|
66
81
|
|
67
82
|
exception = assert_raise ResilientSocket::ReadTimeout do
|
68
83
|
# Read 4 bytes from server
|
69
|
-
@client.call('sleep', request)
|
84
|
+
@client.call('sleep', request, :read_timeout => @read_timeout)
|
70
85
|
end
|
71
86
|
assert_match /Timedout after #{@read_timeout} seconds trying to read/, exception.message
|
72
87
|
end
|
data/test.log
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_skynet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: semantic_logger
|
@@ -98,13 +98,16 @@ executables: []
|
|
98
98
|
extensions: []
|
99
99
|
extra_rdoc_files: []
|
100
100
|
files:
|
101
|
+
- dev.log
|
101
102
|
- Gemfile
|
102
103
|
- Gemfile.lock
|
103
104
|
- lib/ruby_skynet/client.rb
|
105
|
+
- lib/ruby_skynet/connection.rb
|
104
106
|
- lib/ruby_skynet/doozer/client.rb
|
105
107
|
- lib/ruby_skynet/doozer/exceptions.rb
|
106
108
|
- lib/ruby_skynet/doozer/msg.pb.rb
|
107
109
|
- lib/ruby_skynet/exceptions.rb
|
110
|
+
- lib/ruby_skynet/registry.rb
|
108
111
|
- lib/ruby_skynet/version.rb
|
109
112
|
- lib/ruby_skynet.rb
|
110
113
|
- LICENSE.txt
|
@@ -116,8 +119,6 @@ files:
|
|
116
119
|
- nbproject/project.xml
|
117
120
|
- Rakefile
|
118
121
|
- README.md
|
119
|
-
- ruby_skynet-0.1.0.gem
|
120
|
-
- ruby_skynet-0.1.1.gem
|
121
122
|
- test/doozer_client_test.rb
|
122
123
|
- test/ruby_skynet_client_test.rb
|
123
124
|
- test/simple_server.rb
|
@@ -136,7 +137,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
137
|
version: '0'
|
137
138
|
segments:
|
138
139
|
- 0
|
139
|
-
hash:
|
140
|
+
hash: -4330021218658134009
|
140
141
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
142
|
none: false
|
142
143
|
requirements:
|
data/ruby_skynet-0.1.0.gem
DELETED
Binary file
|
data/ruby_skynet-0.1.1.gem
DELETED
Binary file
|