ruby_skynet 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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