ruby_skynet 0.5.0 → 0.6.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.
@@ -1,375 +0,0 @@
1
- require 'sync_attr'
2
- require 'multi_json'
3
- require 'thread_safe'
4
- require 'gene_pool'
5
- require 'resolv'
6
-
7
- #
8
- # RubySkynet Registry Client
9
- #
10
- # Keeps a local copy of the Skynet Registry
11
- #
12
- # Subscribes to Registry changes and the internal copy up to date
13
- #
14
- module RubySkynet
15
- class Registry
16
- include SyncAttr
17
-
18
- # Service Registry has the following format
19
- # Key: [String] 'name/version/region'
20
- # Value: [Array<String>] 'host:port', 'host:port'
21
- sync_cattr_reader :service_registry do
22
- logger.benchmark_info "Connected to Doozer" do
23
- start
24
- end
25
- end
26
-
27
- @@on_server_removed_callbacks = ThreadSafe::Hash.new
28
- @@monitor_thread = nil
29
-
30
- DOOZER_SERVICES_PATH = "/services/*/*/*/*/*"
31
-
32
- # Default doozer configuration
33
- # To replace this default, set the config as follows:
34
- # RubySkynet::Client.doozer_config = { .... }
35
- #
36
- # :servers [Array of String]
37
- # Array of URL's of doozer servers to connect to with port numbers
38
- # ['server1:2000', 'server2:2000']
39
- #
40
- # The second server will only be attempted once the first server
41
- # cannot be connected to or has timed out on connect
42
- # A read failure or timeout will not result in switching to the second
43
- # server, only a connection failure or during an automatic reconnect
44
- #
45
- # :read_timeout [Float]
46
- # Time in seconds to timeout on read
47
- # Can be overridden by supplying a timeout in the read call
48
- #
49
- # :connect_timeout [Float]
50
- # Time in seconds to timeout when trying to connect to the server
51
- #
52
- # :connect_retry_count [Fixnum]
53
- # Number of times to retry connecting when a connection fails
54
- #
55
- # :connect_retry_interval [Float]
56
- # Number of seconds between connection retry attempts after the first failed attempt
57
- sync_cattr_accessor :doozer_config do
58
- {
59
- :servers => ['127.0.0.1:8046'],
60
- :read_timeout => 5,
61
- :connect_timeout => 3,
62
- :connect_retry_interval => 1,
63
- :connect_retry_count => 30
64
- }
65
- end
66
-
67
- # Register the supplied service at this Skynet Server host and Port
68
- def self.register_service(name, version, region, hostname, port)
69
- config = {
70
- "Config" => {
71
- "UUID" => "#{hostname}:#{port}-#{$$}-#{name}-#{version}",
72
- "Name" => name,
73
- "Version" => version.to_s,
74
- "Region" => region,
75
- "ServiceAddr" => {
76
- "IPAddress" => hostname,
77
- "Port" => port,
78
- "MaxPort" => port + 999
79
- },
80
- },
81
- "Registered" => true
82
- }
83
- doozer_pool.with_connection do |doozer|
84
- doozer["/services/#{name}/#{version}/#{region}/#{hostname}/#{port}"] = MultiJson.encode(config)
85
- end
86
- end
87
-
88
- # Deregister the supplied service from the Registry
89
- def self.deregister_service(name, version, region, hostname, port)
90
- doozer_pool.with_connection do |doozer|
91
- doozer.delete("/services/#{name}/#{version}/#{region}/#{hostname}/#{port}") rescue nil
92
- end
93
- end
94
-
95
- # Return a server that implements the specified service
96
- def self.server_for(name, version='*', region='Development')
97
- if servers = servers_for(name, version, region)
98
- # Randomly select one of the servers offering the service
99
- servers[rand(servers.size)]
100
- else
101
- msg = "No servers available for service: #{name} with version: #{version} in region: #{region}"
102
- logger.warn msg
103
- raise ServiceUnavailable.new(msg)
104
- end
105
- end
106
-
107
- # Returns [Array] of the hostname and port pair [String] that implements a particular service
108
- # Performs a doozer lookup to find the servers
109
- #
110
- # name:
111
- # Name of the service to lookup
112
- # version:
113
- # Version of service to locate
114
- # Default: All versions
115
- # region:
116
- # Region to look for the service in
117
- def self.registered_implementers(name='*', version='*', region='Development')
118
- hosts = []
119
- doozer_pool.with_connection do |doozer|
120
- doozer.walk("/services/#{name}/#{version}/#{region}/*/*").each do |node|
121
- entry = MultiJson.load(node.value)
122
- hosts << entry if entry['Registered']
123
- end
124
- end
125
- hosts
126
- end
127
-
128
- # Returns [Array<String>] a list of servers implementing the requested service
129
- def self.servers_for(name, version='*', region='Development')
130
- if version == '*'
131
- # Find the highest version for the named service in this region
132
- version = -1
133
- service_registry.keys.each do |key|
134
- if match = key.match(/#{name}\/(\d+)\/#{region}/)
135
- ver = match[1].to_i
136
- version = ver if ver > version
137
- end
138
- end
139
- end
140
- if server_infos = service_registry["#{name}/#{version}/#{region}"]
141
- server_infos.first.servers
142
- end
143
- end
144
-
145
- # Invokes registered callbacks when a specific server is shutdown or terminates
146
- # Not when a server de-registers itself
147
- # The callback will only be called once and will need to be re-registered
148
- # after being called if future callbacks are required for that server
149
- def self.on_server_removed(server, &block)
150
- (@@on_server_removed_callbacks[server] ||= ThreadSafe::Array.new) << block
151
- end
152
-
153
- IPV4_REG_EXP = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
154
-
155
- # Returns [Integer] the score for the supplied ip_address
156
- # Score currently ranges from 0 to 4 with 4 being the best score
157
- # If the IP address does not match an IP v4 address a DNS lookup will
158
- # be performed
159
- def self.score_for_server(ip_address)
160
- score = 0
161
- # Each matching element adds 1 to the score
162
- # 192.168. 0. 0
163
- # 1
164
- # 1
165
- # 1
166
- # 1
167
- server_match = IPV4_REG_EXP.match(ip_address) || IPV4_REG_EXP.match(Resolv::DNS.new.getaddress(ip_address).to_s)
168
- if server_match
169
- @@local_match ||= IPV4_REG_EXP.match(RubySkynet.local_ip_address)
170
- score = 0
171
- (1..4).each do |i|
172
- break if @@local_match[i].to_i != server_match[i].to_i
173
- score += 1
174
- end
175
- end
176
- score
177
- end
178
-
179
- ############################
180
- protected
181
-
182
- # Logging instance for this class
183
- include SemanticLogger::Loggable
184
-
185
- # Lazy initialize Doozer Client Connection pool
186
- sync_cattr_reader :doozer_pool do
187
- GenePool.new(
188
- :name =>"Doozer Connection Pool",
189
- :pool_size => 5,
190
- :timeout => 30,
191
- :warn_timeout => 5,
192
- :idle_timeout => 600,
193
- :logger => logger,
194
- :close_proc => :close
195
- ) do
196
- Doozer::Client.new(doozer_config)
197
- end
198
- end
199
-
200
- # Fetch the all registry information from Doozer and sets the internal registry
201
- # Also starts the monitoring thread to keep the registry up to date
202
- def self.start
203
- # Populate internal registry from doozer server
204
- # Holds a lock in this process on the service_registry so that only
205
- # this thread will pre-populate the local copy of the registry
206
- registry = ThreadSafe::Hash.new
207
- revision = nil
208
- doozer_pool.with_connection do |doozer|
209
- revision = doozer.current_revision
210
- doozer.walk(DOOZER_SERVICES_PATH, revision).each do |node|
211
- service_info_change(registry, node.path, node.value)
212
- end
213
- end
214
- # Start monitoring thread to keep the registry up to date
215
- @@monitor_thread = Thread.new { watch_registry(revision + 1) }
216
-
217
- # Cleanup when process exits
218
- at_exit do
219
- if @@monitor_thread
220
- @@monitor_thread.kill
221
- @@monitor_thread.join
222
- @@monitor_thread = nil
223
- end
224
- doozer_pool.close
225
- end
226
- registry
227
- end
228
-
229
- # Waits for any updates from Doozer and updates the internal service registry
230
- def self.watch_registry(revision)
231
- logger.info "Start monitoring #{DOOZER_SERVICES_PATH}"
232
- # This thread must use its own dedicated doozer connection
233
- doozer = Doozer::Client.new(doozer_config)
234
-
235
- # Watch for any changes
236
- doozer.watch(DOOZER_SERVICES_PATH, revision) do |node|
237
- service_info_change(service_registry, node.path, node.value)
238
- logger.trace "Updated registry", service_registry
239
- end
240
- logger.info "Stopping monitoring thread normally"
241
- rescue Exception => exc
242
- logger.error "Exception in monitoring thread", exc
243
- ensure
244
- doozer.close if doozer
245
- logger.info "Stopped monitoring for changes in the doozer registry"
246
- end
247
-
248
- # Service information changed in doozer, so update internal registry
249
- def self.service_info_change(registry, path, value)
250
- # path from doozer: "/services/TutorialService/1/Development/127.0.0.1/9000"
251
- e = path.split('/')
252
-
253
- # Key: [String] 'name/version/region'
254
- key = "#{e[2]}/#{e[3]}/#{e[4]}"
255
- hostname, port = e[5], e[6]
256
-
257
- if value.strip.size > 0
258
- entry = MultiJson.load(value)
259
- if entry['Registered']
260
- add_server(registry, key, hostname, port)
261
- else
262
- # Service just de-registered
263
- remove_server(registry, key, hostname, port, false)
264
- end
265
- else
266
- # Service has stopped and needs to be removed
267
- remove_server(registry, key, hostname, port, true)
268
- end
269
- end
270
-
271
- # Invoke any registered callbacks for the specific server
272
- def self.server_removed(server)
273
- if callbacks = @@on_server_removed_callbacks.delete(server)
274
- callbacks.each do |block|
275
- begin
276
- logger.info "Calling callback for server: #{server}"
277
- block.call(server)
278
- rescue Exception => exc
279
- logger.error("Exception during a callback for server: #{server}", exc)
280
- end
281
- end
282
- end
283
- end
284
-
285
- # :score: [Integer] Score
286
- # :servers: [Array<String>] 'host:port', 'host:port'
287
- ServerInfo = Struct.new(:score, :servers )
288
-
289
- # Format of the internal services registry
290
- # key: [String] "<name>/<version>/<region>"
291
- # value: [ServiceInfo, ServiceInfo]
292
- # Sorted by highest score first
293
-
294
- # Add the host to the registry based on it's score
295
- def self.add_server(registry, key, hostname, port)
296
- server = "#{hostname}:#{port}"
297
- logger.debug "#monitor Add/Update Service: #{key} => #{server.inspect}"
298
-
299
- server_infos = (registry[key] ||= ThreadSafe::Array.new)
300
-
301
- # If already present, then nothing to do
302
- server_info = server_infos.find{|si| si.server == server}
303
- return server_info if server_info
304
-
305
- # Look for the same score with a different server
306
- score = score_for_server(hostname)
307
- if server_info = server_infos.find{|si| si.score == score}
308
- server_info.servers << server
309
- return server_info
310
- end
311
-
312
- # New score
313
- servers = ThreadSafe::Array.new
314
- servers << server
315
- server_info = ServerInfo.new(score, servers)
316
-
317
- # Insert into Array in order of score
318
- if index = server_infos.find_index {|si| si.score <= score}
319
- server_infos.insert(index, server_info)
320
- else
321
- server_infos << server_info
322
- end
323
- server_info
324
- end
325
-
326
- # Remove the host from the registry based
327
- # Returns the server instance if it was removed
328
- def self.remove_server(registry, key, hostname, port, notify)
329
- server = "#{hostname}:#{port}"
330
- logger.debug "Remove Service: #{key} => #{server.inspect}"
331
- server_info = nil
332
- if server_infos = registry[key]
333
- server_infos.each do |si|
334
- if si.servers.delete(server)
335
- server_info = si
336
- break
337
- end
338
- end
339
-
340
- # Found server
341
- if server_info
342
- # Cleanup if no more servers in server list
343
- server_infos.delete(server_info) if server_info.servers.size == 0
344
-
345
- # Cleanup if no more server infos
346
- registry.delete(key) if server_infos.size == 0
347
-
348
- server_removed(server) if notify
349
- end
350
- end
351
- server_info
352
- end
353
-
354
- # Check doozer for servers matching supplied criteria
355
- # Code unused, consider deleting
356
- def self.remote_servers_for(name, version='*', region='Development')
357
- if version != '*'
358
- registered_implementers(name, version, region).map do |host|
359
- service = host['Config']['ServiceAddr']
360
- "#{service['IPAddress']}:#{service['Port']}"
361
- end
362
- else
363
- # Find the highest version of any particular service
364
- versions = {}
365
- registered_implementers(name, version, region).each do |host|
366
- service = host['Config']['ServiceAddr']
367
- (versions[version.to_i] ||= []) << "#{service['IPAddress']}:#{service['Port']}"
368
- end
369
- # Return the servers implementing the highest version number
370
- versions.sort.last.last
371
- end
372
- end
373
-
374
- end
375
- end
@@ -1,70 +0,0 @@
1
- # Allow test to be run in-place without requiring a gem install
2
- $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
-
4
- require 'rubygems'
5
- require 'test/unit'
6
- require 'shoulda'
7
- require 'ruby_skynet/doozer/client'
8
-
9
- # NOTE:
10
- # This test assumes that doozerd is running locally on the default port of 8046
11
-
12
- # Unit Test for RubySkynet::Doozer::Client
13
- class DoozerClientTest < Test::Unit::TestCase
14
- context RubySkynet::Doozer::Client do
15
-
16
- context "without server" do
17
- should "raise exception when cannot reach doozer server after 5 retries" do
18
- exception = assert_raise ResilientSocket::ConnectionFailure do
19
- RubySkynet::Doozer::Client.new(
20
- # Bad server address to test exception is raised
21
- :server => 'localhost:9999',
22
- :connect_retry_interval => 0.1,
23
- :connect_retry_count => 5)
24
- end
25
- assert_match /After 5 connection attempts to host 'localhost:9999': Errno::ECONNREFUSED/, exception.message
26
- end
27
-
28
- end
29
-
30
- context "with client connection" do
31
- setup do
32
- @client = RubySkynet::Doozer::Client.new(:server => 'localhost:8046')
33
- end
34
-
35
- def teardown
36
- if @client
37
- @client.close
38
- @client.delete('/test/foo')
39
- end
40
- end
41
-
42
- should "return current revision" do
43
- assert @client.current_revision >= 0
44
- end
45
-
46
- should "successfully set and get data" do
47
- new_revision = @client.set('/test/foo', 'value')
48
- result = @client.get('/test/foo')
49
- assert_equal 'value', result.value
50
- assert_equal new_revision, result.rev
51
- end
52
-
53
- should "successfully set and get data using array operators" do
54
- @client['/test/foo'] = 'value2'
55
- result = @client['/test/foo']
56
- assert_equal 'value2', result
57
- end
58
-
59
- should "fetch directories in a path" do
60
- @path = '/'
61
- count = 0
62
- until @client.directory(@path, count).nil?
63
- count += 1
64
- end
65
- assert count > 0
66
- end
67
-
68
- end
69
- end
70
- end
@@ -1,99 +0,0 @@
1
- # Allow test to be run in-place without requiring a gem install
2
- $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
-
4
- require 'rubygems'
5
- require 'test/unit'
6
- require 'shoulda'
7
- require 'mocha/setup'
8
- require 'ruby_skynet'
9
-
10
- # Register an appender if one is not already registered
11
- if SemanticLogger::Logger.appenders.size == 0
12
- SemanticLogger::Logger.default_level = :trace
13
- SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new('test.log')
14
- end
15
-
16
- # Unit Test
17
- class RegistryTest < Test::Unit::TestCase
18
- context 'RubySkynet::Service' do
19
-
20
- setup do
21
- @service_name = 'MyRegistryService'
22
- @version = 5
23
- @region = 'RegistryTest'
24
- @hostname = '127.0.0.1'
25
- @port = 2100
26
- @service_key = "/services/#{@service_name}/#{@version}/#{@region}/#{@hostname}/#{@port}"
27
- end
28
-
29
- context "without a registered service" do
30
- should "not be in doozer" do
31
- RubySkynet::Registry.send(:doozer_pool).with_connection do |doozer|
32
- assert_equal '', doozer[@service_key]
33
- end
34
- end
35
- end
36
-
37
- context "with a registered service" do
38
- setup do
39
- RubySkynet::Registry.register_service(@service_name, @version, @region, @hostname, @port)
40
- # Allow time for doozer callback that service was registered
41
- sleep 0.1
42
- end
43
-
44
- teardown do
45
- RubySkynet::Registry.deregister_service(@service_name, @version, @region, @hostname, @port)
46
- # Allow time for doozer callback that service was deregistered
47
- sleep 0.1
48
- # No servers should be in the local registry
49
- assert_equal nil, RubySkynet::Registry.servers_for(@service_name, @version, @region)
50
- end
51
-
52
- should "find server using exact match" do
53
- assert servers = RubySkynet::Registry.servers_for(@service_name, @version, @region)
54
- assert_equal 1, servers.size
55
- assert_equal "#{@hostname}:#{@port}", servers.first
56
- end
57
-
58
- should "find server using * version match" do
59
- assert servers = RubySkynet::Registry.servers_for(@service_name, '*', @region)
60
- assert_equal 1, servers.size
61
- assert_equal "#{@hostname}:#{@port}", servers.first
62
- end
63
-
64
- should "return nil when service not found" do
65
- assert_equal nil, RubySkynet::Registry.servers_for('MissingService', @version, @region)
66
- end
67
-
68
- should "return nil when version not found" do
69
- assert_equal nil, RubySkynet::Registry.servers_for(@service_name, @version+1, @region)
70
- end
71
-
72
- should "return nil when region not found" do
73
- assert_equal nil, RubySkynet::Registry.servers_for(@service_name, @version, 'OtherRegion')
74
- end
75
-
76
- should "be in doozer" do
77
- RubySkynet::Registry.send(:doozer_pool).with_connection do |doozer|
78
- assert_equal true, doozer[@service_key].length > 20
79
- end
80
- end
81
- end
82
-
83
- context "scoring" do
84
- [
85
- ['192.168.11.0', 4 ],
86
- ['192.168.11.10', 3 ],
87
- ['192.168.10.0', 2 ],
88
- ['192.5.10.0', 1 ],
89
- ['10.0.11.0', 0 ],
90
- ].each do |test|
91
- should "handle score #{test[1]}" do
92
- RubySkynet.stubs(:local_ip_address).returns("192.168.11.0")
93
- assert_equal test[1], RubySkynet::Registry.score_for_server(test[0]), "Local: #{RubySkynet::Common.local_ip_address} Server: #{test[0]} Score: #{test[1]}"
94
- end
95
- end
96
- end
97
-
98
- end
99
- end