ruby_skynet 1.0.0 → 1.1.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.
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