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 +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
|