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 +4 -4
- data/Gemfile.lock +6 -0
- data/lib/ruby_skynet/doozer/service_registry.rb +240 -0
- data/lib/ruby_skynet/ruby_skynet.rb +12 -17
- data/lib/ruby_skynet/service_registry.rb +21 -236
- data/lib/ruby_skynet/version.rb +1 -1
- data/lib/ruby_skynet/zookeeper/json/serializer.rb +1 -1
- data/lib/ruby_skynet/zookeeper/registry.rb +12 -6
- data/lib/ruby_skynet/zookeeper/service_registry.rb +271 -0
- data/lib/ruby_skynet/zookeeper.rb +1 -0
- metadata +4 -3
- data/lib/ruby_skynet/registry.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 088702cd6c216e6b48ec0124fb0ea95438be5a17
|
4
|
+
data.tar.gz: 6500952161438b2dcd00f2862164260ddc1788ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
:
|
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
|
-
#
|
102
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
data/lib/ruby_skynet/version.rb
CHANGED
@@ -34,7 +34,10 @@ module RubySkynet
|
|
34
34
|
# significant traffic since it will also monitor ZooKeeper Admin changes
|
35
35
|
# Mandatory
|
36
36
|
#
|
37
|
-
# :
|
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
|
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
|
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
|
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
|
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.
|
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-
|
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
|
data/lib/ruby_skynet/registry.rb
DELETED
@@ -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
|