ruby_skynet 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 71af05d7950d049061c39abe50a23dd1fe02a51d
4
- data.tar.gz: b312ef155c2aacea545f434d4d9e38348bc94ac5
3
+ metadata.gz: 088702cd6c216e6b48ec0124fb0ea95438be5a17
4
+ data.tar.gz: 6500952161438b2dcd00f2862164260ddc1788ca
5
5
  SHA512:
6
- metadata.gz: 240897f81ded9f668da49ed7280b378bbe92d17e819e9c795a47affd7e6996a972d61168f4b9c104b8a21b4a01937523d5104661590ab82005de8fa1f39e6c38
7
- data.tar.gz: 423409dbf2038a5723438004d451a28b4493edd85eadbf6cd239a0190c11b508ac56eb36be29872be28c2c6b057c31f7e2ba099912153b5cebd0384daf46a517
6
+ metadata.gz: 44c050a725b5fdf2ebda68a12be4a08117bb067f9df0bd19d4a28223e3f62bbdce5d5fe0dc715088a6857f462682dee61afc7b1e9c8843d52f3a8ee336f4165c
7
+ data.tar.gz: 8cf35886b4c988835b705d39777ac3d74e87ae26fe55b9038d713f6d9f87a06c143a6d5a05ccbfa46b8da0e5c0972148c92c4328406eaf60092b17262dc1353e
data/Gemfile.lock CHANGED
@@ -7,8 +7,12 @@ GEM
7
7
  multi_json (~> 1.3)
8
8
  thread_safe (~> 0.1)
9
9
  tzinfo (~> 0.3.37)
10
+ atomic (1.1.10)
10
11
  atomic (1.1.10-java)
12
+ bson (1.9.0)
11
13
  bson (1.9.0-java)
14
+ bson_ext (1.9.0)
15
+ bson (~> 1.9.0)
12
16
  gene_pool (1.3.0)
13
17
  i18n (0.6.4)
14
18
  minitest (4.7.5)
@@ -39,12 +43,14 @@ GEM
39
43
  thread_safe (0.1.0)
40
44
  atomic
41
45
  tzinfo (0.3.37)
46
+ zookeeper (1.4.4)
42
47
  zookeeper (1.4.4-java)
43
48
  slyphon-log4j (= 1.2.15)
44
49
  slyphon-zookeeper_jar (= 3.3.5)
45
50
 
46
51
  PLATFORMS
47
52
  java
53
+ ruby
48
54
 
49
55
  DEPENDENCIES
50
56
  bson
@@ -0,0 +1,240 @@
1
+ require 'semantic_logger'
2
+ require 'thread_safe'
3
+ require 'gene_pool'
4
+ require 'resolv'
5
+
6
+ #
7
+ # RubySkynet Sevices Registry
8
+ #
9
+ # Based on the Skynet Services Registry, obtains and keeps up to date a list of
10
+ # all services and which servers they are available on.
11
+ #
12
+ module RubySkynet
13
+ module Doozer
14
+ class ServiceRegistry
15
+ include SemanticLogger::Loggable
16
+
17
+ # Create a service registry
18
+ # See: RubyDoozer::Registry for the parameters
19
+ def initialize
20
+ # Registry has the following format
21
+ # Key: [String] 'name/version/region'
22
+ # Value: [Array<String>] 'host:port', 'host:port'
23
+ @cache = ThreadSafe::Hash.new
24
+
25
+ # Supply block to load the current keys from the Registry
26
+ @registry = Doozer::Registry.new(:root => '/services') do |key, value|
27
+ service_info_changed(key, value)
28
+ end
29
+ # Register Callbacks
30
+ @registry.on_update {|path, value| service_info_changed(path, value) }
31
+ @registry.on_delete {|path| service_info_changed(path) }
32
+
33
+ # Zookeeper Registry also supports on_create
34
+ @registry.on_create {|path, value| service_info_changed(path, value) } if @registry.respond_to?(:on_create)
35
+ end
36
+
37
+ # Returns the Service Registry as a Hash
38
+ def to_h
39
+ @cache.dup
40
+ end
41
+
42
+ # Register the supplied service at this Skynet Server host and Port
43
+ def register_service(name, version, region, hostname, port)
44
+ @registry["#{name}/#{version}/#{region}/#{hostname}/#{port}"] = {
45
+ "Config" => {
46
+ "UUID" => "#{hostname}:#{port}-#{$$}-#{name}-#{version}",
47
+ "Name" => name,
48
+ "Version" => version.to_s,
49
+ "Region" => region,
50
+ "ServiceAddr" => {
51
+ "IPAddress" => hostname,
52
+ "Port" => port,
53
+ "MaxPort" => port + 999
54
+ },
55
+ },
56
+ "Registered" => true
57
+ }
58
+ end
59
+
60
+ # Deregister the supplied service from the Registry
61
+ def deregister_service(name, version, region, hostname, port)
62
+ @registry.delete("#{name}/#{version}/#{region}/#{hostname}/#{port}")
63
+ end
64
+
65
+ # Return a server that implements the specified service
66
+ def server_for(name, version='*', region=RubySkynet.region)
67
+ if servers = servers_for(name, version, region)
68
+ # Randomly select one of the servers offering the service
69
+ servers[rand(servers.size)]
70
+ else
71
+ msg = "No servers available for service: #{name} with version: #{version} in region: #{region}"
72
+ logger.warn msg
73
+ raise ServiceUnavailable.new(msg)
74
+ end
75
+ end
76
+
77
+ # Returns [Array<String>] a list of servers implementing the requested service
78
+ def servers_for(name, version='*', region=RubySkynet.region)
79
+ if version == '*'
80
+ # Find the highest version for the named service in this region
81
+ version = -1
82
+ @cache.keys.each do |key|
83
+ if match = key.match(/#{name}\/(\d+)\/#{region}/)
84
+ ver = match[1].to_i
85
+ version = ver if ver > version
86
+ end
87
+ end
88
+ end
89
+ if server_infos = @cache["#{name}/#{version}/#{region}"]
90
+ server_infos.first.servers
91
+ end
92
+ end
93
+
94
+ # Invokes registered callbacks when a specific server is shutdown or terminates
95
+ # Not when a server de-registers itself
96
+ # The callback will only be called once and will need to be re-registered
97
+ # after being called if future callbacks are required for that server
98
+ def on_server_removed(server, &block)
99
+ ((@on_server_removed_callbacks ||= ThreadSafe::Hash.new)[server] ||= ThreadSafe::Array.new) << block
100
+ end
101
+
102
+ ############################
103
+ protected
104
+
105
+ # Service information changed in doozer, so update internal registry
106
+ def service_info_changed(path, value=nil)
107
+ logger.info("service_info_changed: #{path}", value)
108
+ # path: "TutorialService/1/Development/127.0.0.1/9000"
109
+ e = path.split('/')
110
+
111
+ # Key: [String] 'name/version/region'
112
+ key = "#{e[0]}/#{e[1]}/#{e[2]}"
113
+ hostname, port = e[3], e[4]
114
+
115
+ if value
116
+ if value['Registered']
117
+ add_server(key, hostname, port)
118
+ else
119
+ # Service just de-registered
120
+ remove_server(key, hostname, port, false)
121
+ end
122
+ else
123
+ # Service has stopped and needs to be removed
124
+ remove_server(key, hostname, port, true)
125
+ end
126
+ end
127
+
128
+ # :score: [Integer] Score
129
+ # :servers: [Array<String>] 'host:port', 'host:port'
130
+ ServerInfo = Struct.new(:score, :servers )
131
+
132
+ # Format of the internal services registry
133
+ # key: [String] "<name>/<version>/<region>"
134
+ # value: [ServiceInfo, ServiceInfo]
135
+ # Sorted by highest score first
136
+
137
+ # Add the host to the registry based on it's score
138
+ def add_server(key, hostname, port)
139
+ server = "#{hostname}:#{port}"
140
+
141
+ server_infos = (@cache[key] ||= ThreadSafe::Array.new)
142
+
143
+ # If already present, then nothing to do
144
+ server_info = server_infos.find{|si| si.servers.include?(server)}
145
+ return server_info if server_info
146
+
147
+ # Look for the same score with a different server
148
+ score = self.class.score_for_server(hostname, RubySkynet.local_ip_address)
149
+ logger.info "Service: #{key} now running at #{server} with score #{score}"
150
+ if server_info = server_infos.find{|si| si.score == score}
151
+ server_info.servers << server
152
+ return server_info
153
+ end
154
+
155
+ # New score
156
+ servers = ThreadSafe::Array.new
157
+ servers << server
158
+ server_info = ServerInfo.new(score, servers)
159
+
160
+ # Insert into Array in order of score
161
+ if index = server_infos.find_index {|si| si.score <= score}
162
+ server_infos.insert(index, server_info)
163
+ else
164
+ server_infos << server_info
165
+ end
166
+ server_info
167
+ end
168
+
169
+ # Remove the host from the registry based
170
+ # Returns the server instance if it was removed
171
+ def remove_server(key, hostname, port, notify)
172
+ server = "#{hostname}:#{port}"
173
+ logger.info "Service: #{key} stopped running at #{server}"
174
+ server_info = nil
175
+ if server_infos = @cache[key]
176
+ server_infos.each do |si|
177
+ if si.servers.delete(server)
178
+ server_info = si
179
+ break
180
+ end
181
+ end
182
+
183
+ # Found server
184
+ if server_info
185
+ # Cleanup if no more servers in server list
186
+ server_infos.delete(server_info) if server_info.servers.size == 0
187
+
188
+ # Cleanup if no more server infos
189
+ @cache.delete(key) if server_infos.size == 0
190
+
191
+ server_removed(server) if notify
192
+ end
193
+ end
194
+ server_info
195
+ end
196
+
197
+ # Invoke any registered callbacks for the specific server
198
+ def server_removed(server)
199
+ if @on_server_removed_callbacks && (callbacks = @on_server_removed_callbacks.delete(server))
200
+ callbacks.each do |block|
201
+ begin
202
+ logger.debug "Calling callback for server: #{server}"
203
+ block.call(server)
204
+ rescue Exception => exc
205
+ logger.error("Exception during a callback for server: #{server}", exc)
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ IPV4_REG_EXP = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
212
+
213
+ # Returns [Integer] the score for the supplied ip_address
214
+ # Score currently ranges from 0 to 4 with 4 being the best score
215
+ # If the IP address does not match an IP v4 address a DNS lookup will
216
+ # be performed
217
+ def self.score_for_server(ip_address, local_ip_address)
218
+ ip_address = '127.0.0.1' if ip_address == 'localhost'
219
+ score = 0
220
+ # Each matching element adds 1 to the score
221
+ # 192.168. 0. 0
222
+ # 1
223
+ # 1
224
+ # 1
225
+ # 1
226
+ server_match = IPV4_REG_EXP.match(ip_address) || IPV4_REG_EXP.match(Resolv::DNS.new.getaddress(ip_address).to_s)
227
+ if server_match
228
+ local_match = IPV4_REG_EXP.match(local_ip_address)
229
+ score = 0
230
+ (1..4).each do |i|
231
+ break if local_match[i].to_i != server_match[i].to_i
232
+ score += 1
233
+ end
234
+ end
235
+ score
236
+ end
237
+
238
+ end
239
+ end
240
+ end
@@ -51,7 +51,15 @@ module RubySkynet
51
51
  # By default it connects to a local ZooKeeper instance
52
52
  # Use .configure! to supply a configuration file with any other settings
53
53
  sync_cattr_reader :service_registry do
54
- ServiceRegistry.new(:root => '/services')
54
+ ServiceRegistry.new
55
+ end
56
+
57
+ # Returns the current Registry Config information
58
+ #
59
+ # By default it connects to a local ZooKeeper instance
60
+ # Use .configure! to supply a configuration file with any other settings
61
+ def self.registry_config
62
+ @@config.dup if @@config && defined?(@@config)
55
63
  end
56
64
 
57
65
  # Set the services registry
@@ -89,27 +97,14 @@ module RubySkynet
89
97
  RubySkynet.local_ip_address = config.delete(:local_ip_address) || Common::local_ip_address
90
98
 
91
99
  # Extract just the zookeeper or doozer configuration element
92
- key = config[:zookeeper] ? :zookeeper : :doozer
93
100
  RubySkynet.service_registry = ServiceRegistry.new(
94
- :root => '/services',
95
- key => config.delete(key)
101
+ :registry => config[:registry]
96
102
  )
97
103
 
98
104
  config.each_pair {|k,v| RubySkynet::Server.logger.warn "Ignoring unknown RubySkynet config option #{k} => #{v}"}
99
105
  end
100
106
 
101
- # Returns an instance of RubySkynet::Zookeeper::CachedRegistry or RubyDoozer::CachedRegistry
102
- # based on which was loaded in RubySkynet.configure!
103
- def self.new_cache_registry(root)
104
- # Load config
105
- service_registry
106
-
107
- if zookeeper = @@config[:zookeeper]
108
- RubySkynet::Zookeeper::CachedRegistry.new(:root => root, :zookeeper => zookeeper)
109
- else
110
- raise "How did we get here", @@config
111
- Doozer::CachedRegistry.new(:root => root, :doozer => @@config[:doozer])
112
- end
113
- end
107
+ # Initialize internal class variable
108
+ @@config = nil
114
109
 
115
110
  end
@@ -1,238 +1,23 @@
1
- require 'semantic_logger'
2
- require 'thread_safe'
3
- require 'gene_pool'
4
- require 'resolv'
5
-
6
- #
7
- # RubySkynet Sevices Registry
8
- #
9
- # Based on the Skynet Services Registry, obtains and keeps up to date a list of
10
- # all services and which servers they are available on.
11
- #
1
+ # Define RubySkynet::ServiceRegistry based on whether the ZooKeeper or Doozer gem is present
12
2
  module RubySkynet
13
- class ServiceRegistry
14
- include SemanticLogger::Loggable
15
-
16
- # Create a service registry
17
- # See: RubyDoozer::Registry for the parameters
18
- def initialize(params)
19
- # Registry has the following format
20
- # Key: [String] 'name/version/region'
21
- # Value: [Array<String>] 'host:port', 'host:port'
22
- @cache = ThreadSafe::Hash.new
23
-
24
- # Supply block to load the current keys from the Registry
25
- @registry = Registry.new(params) do |key, value|
26
- service_info_changed(key, value)
27
- end
28
- # Register Callbacks
29
- @registry.on_update {|path, value| service_info_changed(path, value) }
30
- @registry.on_delete {|path| service_info_changed(path) }
31
-
32
- # Zookeeper Registry also supports on_create
33
- @registry.on_create {|path, value| service_info_changed(path, value) } if @registry.respond_to?(:on_create)
34
- end
35
-
36
- # Returns the Service Registry as a Hash
37
- def to_h
38
- @cache.dup
39
- end
40
-
41
- # Register the supplied service at this Skynet Server host and Port
42
- def register_service(name, version, region, hostname, port)
43
- @registry["#{name}/#{version}/#{region}/#{hostname}/#{port}"] = {
44
- "Config" => {
45
- "UUID" => "#{hostname}:#{port}-#{$$}-#{name}-#{version}",
46
- "Name" => name,
47
- "Version" => version.to_s,
48
- "Region" => region,
49
- "ServiceAddr" => {
50
- "IPAddress" => hostname,
51
- "Port" => port,
52
- "MaxPort" => port + 999
53
- },
54
- },
55
- "Registered" => true
56
- }
57
- end
58
-
59
- # Deregister the supplied service from the Registry
60
- def deregister_service(name, version, region, hostname, port)
61
- @registry.delete("#{name}/#{version}/#{region}/#{hostname}/#{port}")
62
- end
63
-
64
- # Return a server that implements the specified service
65
- def server_for(name, version='*', region=RubySkynet.region)
66
- if servers = servers_for(name, version, region)
67
- # Randomly select one of the servers offering the service
68
- servers[rand(servers.size)]
69
- else
70
- msg = "No servers available for service: #{name} with version: #{version} in region: #{region}"
71
- logger.warn msg
72
- raise ServiceUnavailable.new(msg)
73
- end
74
- end
75
-
76
- # Returns [Array<String>] a list of servers implementing the requested service
77
- def servers_for(name, version='*', region=RubySkynet.region)
78
- if version == '*'
79
- # Find the highest version for the named service in this region
80
- version = -1
81
- @cache.keys.each do |key|
82
- if match = key.match(/#{name}\/(\d+)\/#{region}/)
83
- ver = match[1].to_i
84
- version = ver if ver > version
85
- end
86
- end
87
- end
88
- if server_infos = @cache["#{name}/#{version}/#{region}"]
89
- server_infos.first.servers
90
- end
91
- end
92
-
93
- # Invokes registered callbacks when a specific server is shutdown or terminates
94
- # Not when a server de-registers itself
95
- # The callback will only be called once and will need to be re-registered
96
- # after being called if future callbacks are required for that server
97
- def on_server_removed(server, &block)
98
- ((@on_server_removed_callbacks ||= ThreadSafe::Hash.new)[server] ||= ThreadSafe::Array.new) << block
99
- end
100
-
101
- ############################
102
- protected
103
-
104
- # Service information changed in doozer, so update internal registry
105
- def service_info_changed(path, value=nil)
106
- logger.info("service_info_changed: #{path}", value)
107
- # path: "TutorialService/1/Development/127.0.0.1/9000"
108
- e = path.split('/')
109
-
110
- # Key: [String] 'name/version/region'
111
- key = "#{e[0]}/#{e[1]}/#{e[2]}"
112
- hostname, port = e[3], e[4]
113
-
114
- if value
115
- if value['Registered']
116
- add_server(key, hostname, port)
117
- else
118
- # Service just de-registered
119
- remove_server(key, hostname, port, false)
120
- end
121
- else
122
- # Service has stopped and needs to be removed
123
- remove_server(key, hostname, port, true)
124
- end
125
- end
126
-
127
- # :score: [Integer] Score
128
- # :servers: [Array<String>] 'host:port', 'host:port'
129
- ServerInfo = Struct.new(:score, :servers )
130
-
131
- # Format of the internal services registry
132
- # key: [String] "<name>/<version>/<region>"
133
- # value: [ServiceInfo, ServiceInfo]
134
- # Sorted by highest score first
135
-
136
- # Add the host to the registry based on it's score
137
- def add_server(key, hostname, port)
138
- server = "#{hostname}:#{port}"
139
-
140
- server_infos = (@cache[key] ||= ThreadSafe::Array.new)
141
-
142
- # If already present, then nothing to do
143
- server_info = server_infos.find{|si| si.servers.include?(server)}
144
- return server_info if server_info
145
-
146
- # Look for the same score with a different server
147
- score = self.class.score_for_server(hostname, RubySkynet.local_ip_address)
148
- logger.info "Service: #{key} now running at #{server} with score #{score}"
149
- if server_info = server_infos.find{|si| si.score == score}
150
- server_info.servers << server
151
- return server_info
152
- end
153
-
154
- # New score
155
- servers = ThreadSafe::Array.new
156
- servers << server
157
- server_info = ServerInfo.new(score, servers)
158
-
159
- # Insert into Array in order of score
160
- if index = server_infos.find_index {|si| si.score <= score}
161
- server_infos.insert(index, server_info)
162
- else
163
- server_infos << server_info
164
- end
165
- server_info
166
- end
167
-
168
- # Remove the host from the registry based
169
- # Returns the server instance if it was removed
170
- def remove_server(key, hostname, port, notify)
171
- server = "#{hostname}:#{port}"
172
- logger.info "Service: #{key} stopped running at #{server}"
173
- server_info = nil
174
- if server_infos = @cache[key]
175
- server_infos.each do |si|
176
- if si.servers.delete(server)
177
- server_info = si
178
- break
179
- end
180
- end
181
-
182
- # Found server
183
- if server_info
184
- # Cleanup if no more servers in server list
185
- server_infos.delete(server_info) if server_info.servers.size == 0
186
-
187
- # Cleanup if no more server infos
188
- @cache.delete(key) if server_infos.size == 0
189
-
190
- server_removed(server) if notify
191
- end
192
- end
193
- server_info
194
- end
195
-
196
- # Invoke any registered callbacks for the specific server
197
- def server_removed(server)
198
- if @on_server_removed_callbacks && (callbacks = @on_server_removed_callbacks.delete(server))
199
- callbacks.each do |block|
200
- begin
201
- logger.debug "Calling callback for server: #{server}"
202
- block.call(server)
203
- rescue Exception => exc
204
- logger.error("Exception during a callback for server: #{server}", exc)
205
- end
206
- end
207
- end
208
- end
209
-
210
- IPV4_REG_EXP = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
211
-
212
- # Returns [Integer] the score for the supplied ip_address
213
- # Score currently ranges from 0 to 4 with 4 being the best score
214
- # If the IP address does not match an IP v4 address a DNS lookup will
215
- # be performed
216
- def self.score_for_server(ip_address, local_ip_address)
217
- ip_address = '127.0.0.1' if ip_address == 'localhost'
218
- score = 0
219
- # Each matching element adds 1 to the score
220
- # 192.168. 0. 0
221
- # 1
222
- # 1
223
- # 1
224
- # 1
225
- server_match = IPV4_REG_EXP.match(ip_address) || IPV4_REG_EXP.match(Resolv::DNS.new.getaddress(ip_address).to_s)
226
- if server_match
227
- local_match = IPV4_REG_EXP.match(local_ip_address)
228
- score = 0
229
- (1..4).each do |i|
230
- break if local_match[i].to_i != server_match[i].to_i
231
- score += 1
232
- end
233
- end
234
- score
235
- end
236
-
3
+ begin
4
+ require 'zookeeper'
5
+ require 'zookeeper/client'
6
+ require 'ruby_skynet/zookeeper/service_registry'
7
+ # Monkey-patch so that the Zookeeper JRuby code can handle nil values in Zookeeper
8
+ require 'ruby_skynet/zookeeper/extensions/java_base' if defined?(::JRUBY_VERSION)
9
+ ServiceRegistry = RubySkynet::Zookeeper::ServiceRegistry
10
+ CachedRegistry = RubySkynet::Zookeeper::CachedRegistry
11
+ Registry = RubySkynet::Zookeeper::Registry
12
+ rescue LoadError
13
+ begin
14
+ require 'ruby_doozer'
15
+ require 'ruby_skynet/doozer/service_registry'
16
+ rescue LoadError
17
+ raise LoadError, "Must gem install either 'zookeeper' or 'ruby_doozer'. 'zookeeper' is recommended"
18
+ end
19
+ ServiceRegistry = RubySkynet::Doozer::ServiceRegistry
20
+ CachedRegistry = Doozer::CachedRegistry
21
+ Registry = Doozer::Registry
237
22
  end
238
- end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module RubySkynet #:nodoc
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -3,7 +3,7 @@ module RubySkynet
3
3
  module Zookeeper
4
4
  module Json
5
5
 
6
- # Serialize to JSON for storing in Doozer
6
+ # Serialize to JSON for storing in Registry
7
7
  module Serializer
8
8
  def self.serialize(value)
9
9
  if value.is_a?(Hash) || value.is_a?(Array)
@@ -34,7 +34,10 @@ module RubySkynet
34
34
  # significant traffic since it will also monitor ZooKeeper Admin changes
35
35
  # Mandatory
36
36
  #
37
- # :zookeeper [Hash|ZooKeeper]
37
+ # :ephemeral [Boolean]
38
+ # All set operations of non-nil values will result in ephemeral nodes.
39
+ #
40
+ # :registry [Hash|ZooKeeper]
38
41
  # ZooKeeper configuration information, or an existing
39
42
  # ZooKeeper ( ZooKeeper client) instance
40
43
  #
@@ -87,6 +90,9 @@ module RubySkynet
87
90
  @serializer = params.delete(:serializer) || RubySkynet::Zookeeper::Json::Serializer
88
91
  @deserializer = params.delete(:deserializer) || RubySkynet::Zookeeper::Json::Deserializer
89
92
 
93
+ @ephemeral = params.delete(:ephemeral)
94
+ @ephemeral = false if @ephemeral.nil?
95
+
90
96
  # Generate warning log entries for any unknown configuration options
91
97
  params.each_pair {|k,v| logger.warn "Ignoring unknown configuration option: #{k}"}
92
98
 
@@ -206,7 +212,7 @@ module RubySkynet
206
212
  # Default: '*'
207
213
  #
208
214
  # value
209
- # New value from doozer
215
+ # New value from the registry
210
216
  #
211
217
  # version
212
218
  # The version number of this node
@@ -230,12 +236,12 @@ module RubySkynet
230
236
  #
231
237
  # Parameters passed to the block:
232
238
  # key
233
- # The key that was updated in doozer
239
+ # The key that was updated in the registry
234
240
  # Supplying a key of '*' means all paths
235
241
  # Default: '*'
236
242
  #
237
243
  # value
238
- # New value from doozer
244
+ # New value from the registry
239
245
  #
240
246
  # version
241
247
  # The version number of this node
@@ -259,7 +265,7 @@ module RubySkynet
259
265
  #
260
266
  # Parameters passed to the block:
261
267
  # key
262
- # The key that was deleted from doozer
268
+ # The key that was deleted from the registry
263
269
  # Supplying a key of '*' means all paths
264
270
  # Default: '*'
265
271
  #
@@ -314,7 +320,7 @@ module RubySkynet
314
320
  @zookeeper.create(:path => path)
315
321
  end
316
322
  if value
317
- @zookeeper.create(:path => full_path, :data => value)
323
+ @zookeeper.create(:path => full_path, :data => value, :ephemeral => @ephemeral)
318
324
  else
319
325
  @zookeeper.create(:path => full_path)
320
326
  end
@@ -0,0 +1,271 @@
1
+ require 'semantic_logger'
2
+ require 'thread_safe'
3
+ require 'gene_pool'
4
+ require 'resolv'
5
+
6
+ #
7
+ # RubySkynet Sevices Registry
8
+ #
9
+ # Based on the Skynet Services Registry, obtains and keeps up to date a list of
10
+ # all services and which servers they are available on.
11
+ #
12
+ module RubySkynet
13
+ module Zookeeper
14
+ class ServiceRegistry
15
+ include SemanticLogger::Loggable
16
+
17
+ # Create a service registry
18
+ # See: RubyDoozer::Registry for the parameters
19
+ def initialize(params = {})
20
+ # Registry has the following format
21
+ # Key: [String] 'name/version/region'
22
+ # Value: [Array<String>] 'host:port', 'host:port'
23
+ @cache = ThreadSafe::Hash.new
24
+ @notifications_cache = ThreadSafe::Hash.new
25
+
26
+ # Supply block to load the current keys from the Registry
27
+ params[:root] = '/instances'
28
+ params[:ephemeral] = true
29
+ @registry = Zookeeper::Registry.new(params) do |key, value|
30
+ service_info_created(key, value)
31
+ end
32
+ # Register Callbacks
33
+ @registry.on_create {|path, value| service_info_created(path, value) }
34
+ @registry.on_update {|path, value| service_info_updated(path, value) }
35
+ @registry.on_delete {|path| service_info_deleted(path) }
36
+ end
37
+
38
+ # Returns the Service Registry as a Hash
39
+ def to_h
40
+ @cache.dup
41
+ end
42
+
43
+ # Register the supplied service at this Skynet Server host and Port
44
+ # Returns the UUID for the service that was created
45
+ def register_service(name, version, region, hostname, port)
46
+ uuid = "#{hostname}:#{port}-#{$$}-#{name}-#{version}"
47
+ # TODO Make sets ephemeral
48
+ @registry[File.join(uuid,'addr')] = "#{hostname}:#{port}"
49
+ @registry[File.join(uuid,'name')] = name
50
+ @registry[File.join(uuid,'version')] = version
51
+ @registry[File.join(uuid,'region')] = region
52
+ @registry[File.join(uuid,'registered')] = true
53
+ uuid
54
+ end
55
+
56
+ # Deregister the supplied service from the Registry
57
+ def deregister_service(name, version, region, hostname, port)
58
+ uuid = "#{hostname}:#{port}-#{$$}-#{name}-#{version}"
59
+ @registry.delete(File.join(uuid,'addr'), false)
60
+ @registry.delete(File.join(uuid,'name'), false)
61
+ @registry.delete(File.join(uuid,'version'), false)
62
+ @registry.delete(File.join(uuid,'region'), false)
63
+ @registry.delete(File.join(uuid,'registered'), false)
64
+ @registry.delete(uuid, false)
65
+ end
66
+
67
+ # Return a server that implements the specified service
68
+ def server_for(name, version='*', region=RubySkynet.region)
69
+ if servers = servers_for(name, version, region)
70
+ # Randomly select one of the servers offering the service
71
+ servers[rand(servers.size)]
72
+ else
73
+ msg = "No servers available for service: #{name} with version: #{version} in region: #{region}"
74
+ logger.warn msg
75
+ raise ServiceUnavailable.new(msg)
76
+ end
77
+ end
78
+
79
+ # Returns [Array<String>] a list of servers implementing the requested service
80
+ def servers_for(name, version='*', region=RubySkynet.region)
81
+ if version == '*'
82
+ # Find the highest version for the named service in this region
83
+ version = -1
84
+ @cache.keys.each do |key|
85
+ if match = key.match(/#{name}\/(\d+)\/#{region}/)
86
+ ver = match[1].to_i
87
+ version = ver if ver > version
88
+ end
89
+ end
90
+ end
91
+ if server_infos = @cache["#{name}/#{version}/#{region}"]
92
+ server_infos.first.servers
93
+ end
94
+ end
95
+
96
+ # Invokes registered callbacks when a specific server is shutdown or terminates
97
+ # Not when a server de-registers itself
98
+ # The callback will only be called once and will need to be re-registered
99
+ # after being called if future callbacks are required for that server
100
+ def on_server_removed(server, &block)
101
+ ((@on_server_removed_callbacks ||= ThreadSafe::Hash.new)[server] ||= ThreadSafe::Array.new) << block
102
+ end
103
+
104
+ ############################
105
+ protected
106
+
107
+ def service_info_deleted(path)
108
+ logger.info("service_info_deleted: #{path}")
109
+ # path: "uuid/key"
110
+ uuid, key = path.split('/')
111
+ if (key == 'registered')
112
+ if server = @notifications_cache.delete(uuid)
113
+ hostname, port = server['addr'].split(':')
114
+ # Service has stopped and needs to be removed
115
+ remove_server(File.join(server['name'], server['version'].to_s, server['region']), hostname, port, true)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Service information changed, so update internal registry
121
+ def service_info_created(path, value=nil)
122
+ logger.info("service_info_created: #{path}", value)
123
+ # path: "uuid/key"
124
+ uuid, key = path.split('/')
125
+
126
+ # Big Assumption: 'registered' event will be received last as true
127
+ if (key == 'registered') && (value == true)
128
+ server = @notifications_cache[uuid]
129
+ if server && server['addr'] && server['name'] && server['version'] && server['region']
130
+ hostname, port = server['addr'].split(':')
131
+ add_server(File.join(server['name'], server['version'].to_s, server['region']), hostname, port)
132
+ else
133
+ logger.warn "Registered notification received for #{uuid} but is missing critical information. Received:", server
134
+ end
135
+ else
136
+ (@notifications_cache[uuid] ||= ThreadSafe::Hash.new)[key] = value
137
+ end
138
+ end
139
+
140
+ def service_info_updated(path, value=nil)
141
+ logger.info("service_info_updated: #{path}", value)
142
+ # path: "uuid/key"
143
+ uuid, key = path.split('/')
144
+
145
+ # Big Assumption: 'registered' event will be received last as true
146
+ if (key == 'registered') && (value == true)
147
+ server = @notifications_cache[uuid]
148
+ if server && server['addr'] && server['name'] && server['version'] && server['region']
149
+ hostname, port = server['addr'].split(':')
150
+ add_server(File.join(server['name'], server['version'].to_s, server['region']), hostname, port)
151
+ else
152
+ logger.warn "Registered notification received for #{uuid} but is missing critical information. Received:", server
153
+ end
154
+ else
155
+ (@notifications_cache[uuid] ||= ThreadSafe::Hash.new)[key] = value
156
+ end
157
+ end
158
+
159
+ # :score: [Integer] Score
160
+ # :servers: [Array<String>] 'host:port', 'host:port'
161
+ ServerInfo = Struct.new(:score, :servers )
162
+
163
+ # Format of the internal services registry
164
+ # key: [String] "<name>/<version>/<region>"
165
+ # value: [ServiceInfo, ServiceInfo]
166
+ # Sorted by highest score first
167
+
168
+ # Add the host to the registry based on it's score
169
+ def add_server(key, hostname, port)
170
+ server = "#{hostname}:#{port}"
171
+
172
+ server_infos = (@cache[key] ||= ThreadSafe::Array.new)
173
+
174
+ # If already present, then nothing to do
175
+ server_info = server_infos.find{|si| si.servers.include?(server)}
176
+ return server_info if server_info
177
+
178
+ # Look for the same score with a different server
179
+ score = self.class.score_for_server(hostname, RubySkynet.local_ip_address)
180
+ logger.info "Service: #{key} now running at #{server} with score #{score}"
181
+ if server_info = server_infos.find{|si| si.score == score}
182
+ server_info.servers << server
183
+ return server_info
184
+ end
185
+
186
+ # New score
187
+ servers = ThreadSafe::Array.new
188
+ servers << server
189
+ server_info = ServerInfo.new(score, servers)
190
+
191
+ # Insert into Array in order of score
192
+ if index = server_infos.find_index {|si| si.score <= score}
193
+ server_infos.insert(index, server_info)
194
+ else
195
+ server_infos << server_info
196
+ end
197
+ server_info
198
+ end
199
+
200
+ # Remove the host from the registry based
201
+ # Returns the server instance if it was removed
202
+ def remove_server(key, hostname, port, notify)
203
+ server = "#{hostname}:#{port}"
204
+ logger.info "Service: #{key} stopped running at #{server}"
205
+ server_info = nil
206
+ if server_infos = @cache[key]
207
+ server_infos.each do |si|
208
+ if si.servers.delete(server)
209
+ server_info = si
210
+ break
211
+ end
212
+ end
213
+
214
+ # Found server
215
+ if server_info
216
+ # Cleanup if no more servers in server list
217
+ server_infos.delete(server_info) if server_info.servers.size == 0
218
+
219
+ # Cleanup if no more server infos
220
+ @cache.delete(key) if server_infos.size == 0
221
+
222
+ server_removed(server) if notify
223
+ end
224
+ end
225
+ server_info
226
+ end
227
+
228
+ # Invoke any registered callbacks for the specific server
229
+ def server_removed(server)
230
+ if @on_server_removed_callbacks && (callbacks = @on_server_removed_callbacks.delete(server))
231
+ callbacks.each do |block|
232
+ begin
233
+ logger.debug "Calling callback for server: #{server}"
234
+ block.call(server)
235
+ rescue Exception => exc
236
+ logger.error("Exception during a callback for server: #{server}", exc)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ IPV4_REG_EXP = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
243
+
244
+ # Returns [Integer] the score for the supplied ip_address
245
+ # Score currently ranges from 0 to 4 with 4 being the best score
246
+ # If the IP address does not match an IP v4 address a DNS lookup will
247
+ # be performed
248
+ def self.score_for_server(ip_address, local_ip_address)
249
+ ip_address = '127.0.0.1' if ip_address == 'localhost'
250
+ score = 0
251
+ # Each matching element adds 1 to the score
252
+ # 192.168. 0. 0
253
+ # 1
254
+ # 1
255
+ # 1
256
+ # 1
257
+ server_match = IPV4_REG_EXP.match(ip_address) || IPV4_REG_EXP.match(Resolv::DNS.new.getaddress(ip_address).to_s)
258
+ if server_match
259
+ local_match = IPV4_REG_EXP.match(local_ip_address)
260
+ score = 0
261
+ (1..4).each do |i|
262
+ break if local_match[i].to_i != server_match[i].to_i
263
+ score += 1
264
+ end
265
+ end
266
+ score
267
+ end
268
+
269
+ end
270
+ end
271
+ end
@@ -2,6 +2,7 @@ require 'semantic_logger'
2
2
  module RubySkynet
3
3
  module Zookeeper
4
4
  autoload :Registry, 'ruby_skynet/zookeeper/registry'
5
+ autoload :ServiceRegistry, 'ruby_skynet/zookeeper/service_registry'
5
6
  autoload :CachedRegistry, 'ruby_skynet/zookeeper/cached_registry'
6
7
  module Json
7
8
  autoload :Deserializer, 'ruby_skynet/zookeeper/json/deserializer'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_skynet
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-06-27 00:00:00.000000000 Z
11
+ date: 2013-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: semantic_logger
@@ -115,10 +115,10 @@ files:
115
115
  - lib/ruby_skynet/client.rb
116
116
  - lib/ruby_skynet/common.rb
117
117
  - lib/ruby_skynet/connection.rb
118
+ - lib/ruby_skynet/doozer/service_registry.rb
118
119
  - lib/ruby_skynet/exceptions.rb
119
120
  - lib/ruby_skynet/railtie.rb
120
121
  - lib/ruby_skynet/railties/ruby_skynet.rake
121
- - lib/ruby_skynet/registry.rb
122
122
  - lib/ruby_skynet/ruby_skynet.rb
123
123
  - lib/ruby_skynet/server.rb
124
124
  - lib/ruby_skynet/service.rb
@@ -130,6 +130,7 @@ files:
130
130
  - lib/ruby_skynet/zookeeper/json/deserializer.rb
131
131
  - lib/ruby_skynet/zookeeper/json/serializer.rb
132
132
  - lib/ruby_skynet/zookeeper/registry.rb
133
+ - lib/ruby_skynet/zookeeper/service_registry.rb
133
134
  - test/client_test.rb
134
135
  - test/service_registry_test.rb
135
136
  - test/service_test.rb
@@ -1,17 +0,0 @@
1
- # Define RubySkynet::Registry based on whether the ZooKeeper or Doozer gem is present
2
- module RubySkynet
3
- begin
4
- require 'zookeeper'
5
- require 'zookeeper/client'
6
- # Monkey-patch so that the Zookeeper JRuby code can handle nil values in Zookeeper
7
- require 'ruby_skynet/zookeeper/extensions/java_base' if defined?(::JRUBY_VERSION)
8
- Registry = RubySkynet::Zookeeper::Registry
9
- rescue LoadError
10
- begin
11
- require 'ruby_doozer'
12
- rescue LoadError
13
- raise LoadError, "Must gem install either 'zookeeper' or 'ruby_doozer'. 'zookeeper' is recommended"
14
- end
15
- Registry = Doozer::Registry
16
- end
17
- end