servicy 0.0.3
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 +7 -0
- data/README.md +257 -0
- data/Servicy.gemspec +47 -0
- data/VERSION +1 -0
- data/bin/servicy +305 -0
- data/lib/api.rb +126 -0
- data/lib/client.rb +114 -0
- data/lib/hash.rb +14 -0
- data/lib/load_balancer/random.rb +12 -0
- data/lib/load_balancer/round_robin.rb +18 -0
- data/lib/load_balancer.rb +25 -0
- data/lib/server/server.rb +222 -0
- data/lib/server/service_searcher.rb +115 -0
- data/lib/server.rb +8 -0
- data/lib/service.rb +200 -0
- data/lib/servicy.rb +9 -0
- data/lib/transport/in_memory_transport.rb +44 -0
- data/lib/transport/messages.rb +141 -0
- data/lib/transport/tcp_transport.rb +50 -0
- data/lib/transports.rb +54 -0
- data/test.rb +74 -0
- metadata +65 -0
data/lib/api.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# TODO: Investigate this kind of thing using Delegator to see if I can get
|
2
|
+
# a speed boost. Each exported method would have to be dynamically defined
|
3
|
+
# in the API class to do contract checking and then pass that on to the
|
4
|
+
# delegated object. I don't, however, know if that's actually faster than
|
5
|
+
# using method_missing. Investigate
|
6
|
+
module Servicy
|
7
|
+
class API
|
8
|
+
def self.create(klass, method_descriptions)
|
9
|
+
@klass = klass
|
10
|
+
method_descriptions = { class: method_descriptions[:class].inject({}) { |h, info| h[info[:method].to_sym] = info; h },
|
11
|
+
instance: method_descriptions[:instance].inject({}) { |h, info| h[info[:method].to_sym] = info; h }
|
12
|
+
}
|
13
|
+
|
14
|
+
# We inherit from API here so that in the future we can add functionality
|
15
|
+
# for setting up servers, service discovery, etc.
|
16
|
+
# This may change to Client at some point... Who
|
17
|
+
# knows.
|
18
|
+
magic_class = Class.new(Servicy::API) do
|
19
|
+
@@base_class = klass
|
20
|
+
@@method_descriptions = method_descriptions
|
21
|
+
|
22
|
+
def initialize(*args)
|
23
|
+
@instance = @@base_class.new(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_descriptions
|
27
|
+
@@method_descriptions
|
28
|
+
end
|
29
|
+
|
30
|
+
# The magic happens in the two method_missing methods.
|
31
|
+
# Meta-programming-averse; gaze upon my works, ye haughty, and despair!
|
32
|
+
def method_missing(method, *args, &block)
|
33
|
+
# Verify the contract
|
34
|
+
data = method_descriptions[:instance][method]
|
35
|
+
raise NoMethodError.new("#{@klass.to_s} does not expose an API endpoint called, #{method.to_s}") unless data
|
36
|
+
data[:contract].valid_args?(*args)
|
37
|
+
|
38
|
+
# Dispatch the result
|
39
|
+
result = @instance.send(method, *args, &block)
|
40
|
+
|
41
|
+
# Check the result
|
42
|
+
data[:contract].valid_return?(*args, result)
|
43
|
+
|
44
|
+
# And we are good to go
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
# Make sure that we can respond to the things we actually do respond to.
|
49
|
+
def respond_to?(method)
|
50
|
+
method_descriptions[:instance].include?(method) || super
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.method_missing(method, *args, &block)
|
54
|
+
# Verify the contract
|
55
|
+
data = @@method_descriptions[:class][method]
|
56
|
+
raise NoMethodError.new("#{@klass.to_s} does not expose an API endpoint called, #{method.to_s}") unless data
|
57
|
+
data[:contract].valid_args?(*args)
|
58
|
+
|
59
|
+
# Dispatch the result
|
60
|
+
result = @@base_class.send(method, *args, &block)
|
61
|
+
|
62
|
+
# Check the result
|
63
|
+
data[:contract].valid_return?(*args, result)
|
64
|
+
|
65
|
+
# And we are good to go
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.respond_to?(method)
|
70
|
+
@@method_descriptions[:class].include?(method) || super
|
71
|
+
end
|
72
|
+
|
73
|
+
# Send back either all the documentation, or just for a particular
|
74
|
+
# method.
|
75
|
+
def self.docs(method=nil)
|
76
|
+
if method.nil?
|
77
|
+
return @@method_descriptions.map do |(type, methods)|
|
78
|
+
methods.values.map { |v| v[:docs] }
|
79
|
+
end.flatten
|
80
|
+
else
|
81
|
+
search_in = [:class, :instance]
|
82
|
+
if method.is_a?(String)
|
83
|
+
search_in = method[0] == '.' ? [:class] : (method[0] == '#' ? [:instance] : search_in)
|
84
|
+
end
|
85
|
+
search_in.each do |type|
|
86
|
+
@@method_descriptions[type].each do |(name,stuff)|
|
87
|
+
return stuff[:docs] if method.to_s =~ /(\.|#)?#{name.to_s}$/
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
magic_class.extend(ExtraMethods)
|
95
|
+
klass.constants(false).each do |const|
|
96
|
+
magic_class.const_set(const, klass.const_get(const))
|
97
|
+
end
|
98
|
+
|
99
|
+
# Give it a good name
|
100
|
+
class_name = "#{klass.to_s}API"
|
101
|
+
Object.const_set(class_name, magic_class)
|
102
|
+
end
|
103
|
+
|
104
|
+
# TODO: API wrappers that create a server of some kind to serve things up
|
105
|
+
# TODO: A gem/library generator based on the API
|
106
|
+
end
|
107
|
+
|
108
|
+
module ExtraMethods
|
109
|
+
# This method is used by the client discovery to build the query that can be
|
110
|
+
# used to find remote instances of this service
|
111
|
+
def search_query
|
112
|
+
domain = self.const_get(:DOMAIN) rescue `hostname`.strip
|
113
|
+
name = "#{domain}.#{self.to_s.downcase}"
|
114
|
+
version = self.const_get(:VERSION) rescue '1.0.0'
|
115
|
+
port = self.const_get(:PORT) rescue 1234
|
116
|
+
heartbeat_port = self.const_get(:HEARTBEAT_PORT) rescue port
|
117
|
+
|
118
|
+
{ name: name, host: 'localhost', port: port, version: version, heartbeat_port: heartbeat_port }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Sets the remote configuration options
|
122
|
+
def set_remote_configuration(config)
|
123
|
+
@remote_config = config
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/client.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'contraction'
|
2
|
+
require 'api'
|
3
|
+
|
4
|
+
# The client will be responsible for connecting to servers to do service
|
5
|
+
# registration and discovery. I would like the registration to happen
|
6
|
+
# auto-magically via reflection and system inspection, but we'll see how that
|
7
|
+
# goes.
|
8
|
+
module Servicy
|
9
|
+
class Client
|
10
|
+
class ServiceNotFoundError < StandardError; end;
|
11
|
+
attr_reader :transport
|
12
|
+
|
13
|
+
# Create a new Client instance
|
14
|
+
def initialize(transport=nil)
|
15
|
+
@transport = transport || Transport.InMemory.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Register a service with the service discovery portal found at the other
|
19
|
+
# end of @transport
|
20
|
+
def register_service(args)
|
21
|
+
if args.is_a?(Hash)
|
22
|
+
@transport.send(Servicy::Transport::Message.registration(Servicy::Service.new(args)))
|
23
|
+
else
|
24
|
+
# TODO: Check if it's an API, and if so register with that.
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Find a single service matching a query. This will cause the server to use
|
29
|
+
# whatever configured load-balancing mechanism is configured for it.
|
30
|
+
def find_service(args)
|
31
|
+
service = @transport.send(Servicy::Transport::Message.query_one(args)).services.first
|
32
|
+
service && Servicy::Service.new(service)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Find all services matching a query
|
36
|
+
def find_all_services(args)
|
37
|
+
@transport.send(Servicy::Transport::Message.query(args)).services.map { |s| Servicy::Service.new s }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Send a message requesting server statistics
|
41
|
+
def server_stats
|
42
|
+
@transport.send(Servicy::Transport::Message.statistics)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creats an api from the provided class' public methods and returns it. You
|
46
|
+
# can then use said API to build a service that exports it over the wire
|
47
|
+
# how you would like.
|
48
|
+
def self.create_api_for(klass)
|
49
|
+
# Get all the public methods, and for each one get their arguments. Parse
|
50
|
+
# around the method to find documentation if any is available.
|
51
|
+
file_contents = {}
|
52
|
+
methods = klass.public_instance_methods - Object.instance_methods - Module.instance_methods
|
53
|
+
all_methods = Contraction.methods_for klass
|
54
|
+
data = all_methods.inject({}) do |h, (type, methods)|
|
55
|
+
h[type] = methods.map do |method|
|
56
|
+
file, line = read_file_for_method(klass, method, type)
|
57
|
+
# If `file` is nil, there is no source file. Either it's an IRB
|
58
|
+
# session, or a standard library class.
|
59
|
+
next file if file.nil?
|
60
|
+
|
61
|
+
# Walking backwards through the text, we look for RDoc comments
|
62
|
+
lines = file[0...line-1]
|
63
|
+
doc = []
|
64
|
+
lines.reverse.each do |line|
|
65
|
+
line.strip!
|
66
|
+
break unless line.start_with? '#'
|
67
|
+
doc << line
|
68
|
+
end
|
69
|
+
doc.reverse!
|
70
|
+
next if doc.empty?
|
71
|
+
|
72
|
+
contract = Contraction::Parser.parse(doc, klass, method, type)
|
73
|
+
|
74
|
+
{ method: method.to_s, docs: doc.join("\n"), contract: contract }
|
75
|
+
end.compact
|
76
|
+
h
|
77
|
+
end
|
78
|
+
|
79
|
+
API.create klass, data
|
80
|
+
end
|
81
|
+
|
82
|
+
# Ask the server at the other end of transport for a provider for the
|
83
|
+
# provided API object, and configure the API object to use the remote
|
84
|
+
# service.
|
85
|
+
def self.find_service_provider_for(api, transport)
|
86
|
+
client = Client.new(transport)
|
87
|
+
service = client.find_service(api.search_query)
|
88
|
+
raise ServiceNotFoundError.new(api.search_query) unless service
|
89
|
+
|
90
|
+
api.set_remote_configuration(service.to_h)
|
91
|
+
api
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Get the contents of a file that contains a method, along with the line
|
97
|
+
# that the method appears on.
|
98
|
+
def self.read_file_for_method(klass, method_name, type)
|
99
|
+
file, line = nil, nil
|
100
|
+
if type == :class
|
101
|
+
file, line = klass.method(method_name).source_location
|
102
|
+
elsif type == :instance
|
103
|
+
file, line = klass.instance_method(method_name).source_location
|
104
|
+
else
|
105
|
+
raise ArgumentError.new("Unknown method type, #{type.inspect}")
|
106
|
+
end
|
107
|
+
return nil, nil unless file # For c extensions and built-ins, source_location returns nil for file.
|
108
|
+
filename = File.expand_path(file)
|
109
|
+
file_contents = File.read(filename).split("\n")
|
110
|
+
return [file_contents, line]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
data/lib/hash.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class Hash
|
2
|
+
def contains_only?(*args)
|
3
|
+
(keys - args).empty?
|
4
|
+
end
|
5
|
+
|
6
|
+
alias_method :orig_acc, :[]
|
7
|
+
def [](k)
|
8
|
+
return self.orig_acc(k) if self.orig_acc(k)
|
9
|
+
if k.respond_to?(:to_sym)
|
10
|
+
return self.orig_acc(k.to_sym) if self.orig_acc(k.to_sym)
|
11
|
+
end
|
12
|
+
return self.orig_acc(k.to_s) if self.orig_acc(k.to_s)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Servicy
|
2
|
+
class RoundRobinLoadBalancer < LoadBalancer
|
3
|
+
def initialize
|
4
|
+
super
|
5
|
+
@last_host_index = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def next_for_service(service_name)
|
9
|
+
@last_host_index[service_name] ||= 0
|
10
|
+
service = hosts[service_name][@last_host_index[service_name]]
|
11
|
+
@last_host_index[service_name] += 1
|
12
|
+
if @last_host_index[service_name] >= hosts[service_name].length
|
13
|
+
@last_host_index[service_name] = 0
|
14
|
+
end
|
15
|
+
service
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Servicy
|
2
|
+
class LoadBalancer
|
3
|
+
attr_accessor :hosts
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@hosts = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def next(set)
|
10
|
+
return nil if set.empty?
|
11
|
+
hosts[set.first.name] ||= []
|
12
|
+
hosts[set.first.name] += set
|
13
|
+
hosts[set.first.name].uniq!
|
14
|
+
next_for_service(set.first.name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def next_for_service(service_name)
|
18
|
+
raise 'Not implemented'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'load_balancer')) + "/*.rb"].each do |f|
|
24
|
+
require f.gsub(/.rb$/, '')
|
25
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module Servicy
|
2
|
+
class Server
|
3
|
+
attr_reader :services, :deactivated_services
|
4
|
+
attr_accessor :load_balancer, :transport
|
5
|
+
|
6
|
+
# Create a new server that is used to register and find Services.
|
7
|
+
# @param [{Servicy::Transport}] transport The transport used to send and
|
8
|
+
# receive messages.
|
9
|
+
# TODO: I should really make this take an options hash, but that will
|
10
|
+
# require changing a lot of shit...
|
11
|
+
def initialize(transport=nil, load_balancer=nil, logger_stream=nil, config_file=nil)
|
12
|
+
@transport = transport || InMemoryTransport.new
|
13
|
+
@services = [] # All services
|
14
|
+
@deactivated_services = [] # Dead services
|
15
|
+
@load_balancer = load_balancer || RoundRobinLoadBalancer.new
|
16
|
+
logger_stream ||= File.open(File.join('/var', 'log', 'servicy_server.log'), 'a')
|
17
|
+
@logger = Logger.new(logger_stream)
|
18
|
+
@config_file = config_file || File.expand_path("~/.servicy.conf")
|
19
|
+
start_heartbeat_process
|
20
|
+
end
|
21
|
+
|
22
|
+
# Starts the {Servicy::Transport} listening (whatever that means for the
|
23
|
+
# implementation), gets messages, handles them, and sends replies.
|
24
|
+
# TODO: I think I might need to invert this so that it tells the transport
|
25
|
+
# to start, and passes a block to the start method that handles the
|
26
|
+
# messages... That way I can have the transport fork when a connection
|
27
|
+
# comes in, handle it, and clean up the fork when it's done, allowing
|
28
|
+
# everything to keep chugging along.
|
29
|
+
def go!
|
30
|
+
transport.start do |message|
|
31
|
+
@logger.info "Received message, #{message.inspect}"
|
32
|
+
last_service_count = @services.length
|
33
|
+
|
34
|
+
result = nil
|
35
|
+
if message.message == 'registration'
|
36
|
+
self.register message.service
|
37
|
+
result = Transport::Message.success
|
38
|
+
elsif message.message == 'query'
|
39
|
+
if message.only_one
|
40
|
+
temp = [self.find_one(message.query)].compact
|
41
|
+
else
|
42
|
+
temp = self.find(message.query)
|
43
|
+
end
|
44
|
+
result = Transport::Message.services temp
|
45
|
+
elsif message.message == 'stats'
|
46
|
+
# Gather stats about the running system, and return it
|
47
|
+
# TODO: Other stats?
|
48
|
+
result = Transport::Message.statistics(num_services: last_service_count,
|
49
|
+
uptimes: uptimes,
|
50
|
+
latencies: latencies
|
51
|
+
)
|
52
|
+
else
|
53
|
+
result = Transport::Message.error("Bad message type, #{message.inspect}")
|
54
|
+
end
|
55
|
+
|
56
|
+
if @services.length > last_service_count
|
57
|
+
self.save!(@config_file)
|
58
|
+
last_service_count = @services.length
|
59
|
+
end
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
transport.stop
|
64
|
+
end
|
65
|
+
|
66
|
+
# Shut the server down
|
67
|
+
def stop!
|
68
|
+
transport.stop
|
69
|
+
@heartbeat_thread.kill
|
70
|
+
end
|
71
|
+
|
72
|
+
# Loads a server configuration off disk
|
73
|
+
# @param [String] filename The path of the file to load
|
74
|
+
# @return [Servicy::Server] configured server
|
75
|
+
def self.load(filename)
|
76
|
+
s = Servicy::Server.new
|
77
|
+
s.load!(filename)
|
78
|
+
s
|
79
|
+
end
|
80
|
+
|
81
|
+
# Register a new service
|
82
|
+
# @param [Hash{Symbol => String,Fixnum,Array<Hash>}] args The arguments for the
|
83
|
+
# service to register. This must include:
|
84
|
+
# :name
|
85
|
+
# :host
|
86
|
+
# :port
|
87
|
+
# You can also provide optional data,
|
88
|
+
# :version
|
89
|
+
# :heartbeat_port
|
90
|
+
# :protocol
|
91
|
+
# :api
|
92
|
+
# :name is the name of the service you are registering, in
|
93
|
+
# inverted-domain format (ie, "com.foo.api").
|
94
|
+
# :host must be either an IP address or hostname/domain name that is
|
95
|
+
# reachable by the Server instance (and any API consumers, of course.)
|
96
|
+
# :port must be an integer between 1 and 65535, as with :heartbeat_port,
|
97
|
+
# and is the port used to connect to the service provider.
|
98
|
+
# :heartbeat_port is the port to connect to to make sure that things are
|
99
|
+
# still running, and will default to the same as :port.
|
100
|
+
# :version is the version of the API/service that you are registering,
|
101
|
+
# and must be in the "major.minor.revision-patch" format, where the patch
|
102
|
+
# section is optional. For example, "1.0.3-p124". It defaults to,
|
103
|
+
# "1.0.0".
|
104
|
+
# :protocol can be anything you like, but is the protocol used to connect
|
105
|
+
# to the api. It defaults to "HTTP/S", meaning either HTTP or HTTP/TLS
|
106
|
+
# :api can be an array of hashes that describe the API provided. See the
|
107
|
+
# tests and README.md for examples.
|
108
|
+
def register(args)
|
109
|
+
service = Service.new(args)
|
110
|
+
@logger.info "Registering service, #{service.inspect}..."
|
111
|
+
if service.up?
|
112
|
+
@logger.info "Service, #{service}, up; registered."
|
113
|
+
@services << service
|
114
|
+
return service
|
115
|
+
else
|
116
|
+
@logger.info "Service, #{service}, down; not registered."
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Un-register a previously registered service. This removes it from the
|
122
|
+
# active list of services that are available to searches, etc.
|
123
|
+
# @param [{Service}] service the service to de-register
|
124
|
+
def unregister(service)
|
125
|
+
@logger.info "De-registering service, #{service}"
|
126
|
+
@deactivated_services << service
|
127
|
+
end
|
128
|
+
|
129
|
+
# Removes a service from the deactivated list if it's there. No
|
130
|
+
# side-effects otherwise.
|
131
|
+
# @param [{Service}] service The service to remove from deactivated list
|
132
|
+
def reregister(service)
|
133
|
+
if @deactivated_services.delete(service)
|
134
|
+
@logger.info "Re-registering service, #{service}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Save the service configuration to disk
|
139
|
+
# @param [String] filename The path to where the config should be saved
|
140
|
+
def save!(filename)
|
141
|
+
File.open(filename, 'w') do |f|
|
142
|
+
f.write services.map(&:as_json).to_json
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Load a configuration from disk, and override the current one with it.
|
147
|
+
# @param [String] filename The path to the configuration file.
|
148
|
+
def load!(filename)
|
149
|
+
json = ''
|
150
|
+
File.open(filename, 'r') do |f|
|
151
|
+
json = f.read
|
152
|
+
end
|
153
|
+
|
154
|
+
@config_file = filename
|
155
|
+
|
156
|
+
@services = JSON.parse(json).map do |s|
|
157
|
+
s = s.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
|
158
|
+
Servicy::Service.new s
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Find one or more services based on some search criteria.
|
163
|
+
# TODO: Give more documentation about this here.
|
164
|
+
def find(args={})
|
165
|
+
@logger.info "Finding #{args.inspect}..."
|
166
|
+
|
167
|
+
searcher = Servicy::ServiceSearcher.new(services - deactivated_services)
|
168
|
+
searcher.filter_by_name args[:name]
|
169
|
+
searcher.filter_by_versions args[:min_version], args[:max_version], args[:version]
|
170
|
+
searcher.filter_by_api args[:api]
|
171
|
+
|
172
|
+
@logger.info "Found #{searcher.services.inspect}"
|
173
|
+
searcher.services
|
174
|
+
end
|
175
|
+
|
176
|
+
# (see #find)
|
177
|
+
# Return only the first load-balanced instance. If you want load-balancing,
|
178
|
+
# use this.
|
179
|
+
def find_one(args={})
|
180
|
+
load_balancer.next(find(args))
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get a hash of uptimes for each service, where the key is the service name
|
184
|
+
# and hostname, joined with a '#', and the value is the uptime for that
|
185
|
+
# service on that host.
|
186
|
+
# @return [Hash{String => Float}] the uptimes of all services per host.
|
187
|
+
def uptimes
|
188
|
+
services.inject({}) { |h, s| h[s.to_s] = s.uptime; h }
|
189
|
+
end
|
190
|
+
|
191
|
+
# (see #uptimes)
|
192
|
+
# Hash is in the same format, but the values are avg latencies.
|
193
|
+
def latencies
|
194
|
+
services.inject({}) { |h, s| h[s.to_s] = s.avg_latency; h }
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def start_heartbeat_process
|
200
|
+
@heartbeat_thread = Thread.new(self) do |server|
|
201
|
+
timer = 0
|
202
|
+
while true
|
203
|
+
server.services.each do |service|
|
204
|
+
time_to_beat = service.heartbeat_check_rate
|
205
|
+
time_to_beat *= 2 if server.deactivated_services.include?(service)
|
206
|
+
if timer - service.heartbeat_last_check >= time_to_beat
|
207
|
+
service.heartbeat_last_check = timer
|
208
|
+
if service.up?
|
209
|
+
server.reregister(service)
|
210
|
+
else
|
211
|
+
server.unregister(service)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
timer += 1
|
216
|
+
sleep 1
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# The searcher is a class that handles all the search functionality for the server.
|
2
|
+
module Servicy
|
3
|
+
class ServiceSearcher
|
4
|
+
attr_reader :services
|
5
|
+
|
6
|
+
# Create a new searcher instance with the services that you want to filter
|
7
|
+
# down. To start with, we will just start it with all the server services,
|
8
|
+
# but as things get whiddled down, we will create more of these interfaces
|
9
|
+
# with new sets. It's a sort-of recursive class.
|
10
|
+
def initialize(services)
|
11
|
+
@services = services
|
12
|
+
end
|
13
|
+
|
14
|
+
# Filter based on the name of given, with some simple wild-carding.
|
15
|
+
def filter_by_name(name)
|
16
|
+
return unless name
|
17
|
+
@services = services.select { |s| name_matches_query?(s.name, name) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Filter down the services based on the version options. You can do min,
|
21
|
+
# max, by extension ranges, and exact matching.
|
22
|
+
def filter_by_versions(min=nil, max=nil, exact=nil)
|
23
|
+
if min
|
24
|
+
mv = Servicy::Service.version_as_number(min)
|
25
|
+
@services = services.select do |s|
|
26
|
+
s.version_as_number >= mv
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if max
|
31
|
+
mv = Servicy::Service.version_as_number(max)
|
32
|
+
@services = services.select do |s|
|
33
|
+
s.version_as_number <= mv
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if exact
|
38
|
+
mv = Servicy::Service.version_as_number(exact)
|
39
|
+
@services = services.select do |s|
|
40
|
+
s.version_as_number == mv
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# To search by api you can define either class or instance methods by name
|
46
|
+
# with optional arity, arguments by name, type, or both, and return types.
|
47
|
+
# TODO: Support a simple query language ("#foo/-1[String]", etc)
|
48
|
+
def filter_by_api(api)
|
49
|
+
return unless api
|
50
|
+
|
51
|
+
new_set = []
|
52
|
+
api.each do |(type, methods)|
|
53
|
+
methods.each do |method|
|
54
|
+
services.each do |service|
|
55
|
+
if service.api && service.api[type]
|
56
|
+
service_api = service.api_for(type, method[:name])
|
57
|
+
next unless service_api
|
58
|
+
next unless check_arity(service_api, method)
|
59
|
+
next unless check_arguments(service_api, method)
|
60
|
+
next unless check_return_type(service_api, method)
|
61
|
+
|
62
|
+
new_set << service
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
@services = new_set
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Check simple wild-cards. If you end a query with a single dot, then you
|
74
|
+
# are looking for something that starts with the query, instead of a
|
75
|
+
# complete match.
|
76
|
+
# TODO: Support *, %, others (?)
|
77
|
+
def name_matches_query?(name, query)
|
78
|
+
if query[-1] == "."
|
79
|
+
name.start_with? query
|
80
|
+
else
|
81
|
+
name == query
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check the return type against a method query
|
86
|
+
def check_return_type(service_api, method)
|
87
|
+
return true unless method[:return]
|
88
|
+
return true unless service_api[:return]
|
89
|
+
return false if method[:return][:type] != service_api[:return][:type]
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check if the arguments given in an api query match the argument
|
94
|
+
# definition for a given method.
|
95
|
+
def check_arguments(service_api, method)
|
96
|
+
return true unless method[:args]
|
97
|
+
return true unless service_api[:args]
|
98
|
+
|
99
|
+
return false if method[:args].length != service_api[:args].length
|
100
|
+
names = service_api[:args].map { |a| a[:name] }
|
101
|
+
return false if method[:args].any? { |a| !names.include? a[:name] }
|
102
|
+
return false if method[:args].any? do |a|
|
103
|
+
named_arg = service_api[:args].select { |b| b[:name] == a[:name] }.first
|
104
|
+
a.include?(:required) && named_arg.include?(:required) && a[:required] != named_arg[:required]
|
105
|
+
end
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
# True if matching arity, false otherwise.
|
110
|
+
def check_arity(service_api, method)
|
111
|
+
return true unless method[:arity]
|
112
|
+
method[:arity] == (service_api[:args] || []).length
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|