servicy 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +26 -2
- data/Servicy.gemspec +19 -3
- data/VERSION +1 -1
- data/lib/api.rb +45 -30
- data/lib/client.rb +15 -15
- data/lib/configurable.rb +19 -0
- data/lib/formats/json.rb +18 -0
- data/lib/formats.rb +36 -0
- data/lib/server/server.rb +65 -28
- data/lib/server.rb +0 -1
- data/lib/service.rb +35 -3
- data/lib/servicy.rb +157 -3
- data/lib/transport/in_memory_transport.rb +1 -1
- data/lib/transport/messages.rb +21 -1
- data/lib/transport/null_transport.rb +22 -0
- data/lib/transport/tcp_transport.rb +35 -14
- data/lib/transports.rb +40 -2
- data/test.rb +89 -63
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9048f3bb1f1521a29ceeb800dd7b3eb5149c9b7
|
4
|
+
data.tar.gz: 1c474e01e90d51469893fe19be368004cc4654d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7eb75dfafdc14c501aeb551579d839991aa312b3d554cadcc1efb410f1c9636651f9d6a8516b88415fe0739b867f242c95fd5e0adec9c067941d95c094e54d9b
|
7
|
+
data.tar.gz: 4986b4d40c49b4bb5b78f427fb5ccb25da068cb22bb225c09ca4adc245a8805af3f6ce62acb1f5b44153599a5cb740064a01700a172e9c6796ac23a5a964b126
|
data/README.md
CHANGED
@@ -245,6 +245,27 @@ with the provided query. In the last two queries, additional error information
|
|
245
245
|
may be provided, but should be considered for human-use only; ie, showing an
|
246
246
|
error message to a user.
|
247
247
|
|
248
|
+
Notes on design of your objects
|
249
|
+
===============================
|
250
|
+
|
251
|
+
See [here](http://martinfowler.com/articles/distributed-objects-microservices.html).
|
252
|
+
It's a good article. The upshot is that, while hiding weather or not an object
|
253
|
+
is remote is good for programmer burden (ie, it reduces it), it's bad for API
|
254
|
+
design.
|
255
|
+
|
256
|
+
Servicy's goal is to make it simple for you to build microservices in Ruby that
|
257
|
+
can either run locally or remotely without the developer having to worry about
|
258
|
+
or care which. That said, as the developer of a service, it is _your_
|
259
|
+
responsibility to make sure that you don't make services that suck (tm).
|
260
|
+
|
261
|
+
Don't build your objects to have iterators that require multiple-traversals
|
262
|
+
over the wire. Don't build objects that lazy-evaluate things. Don't build
|
263
|
+
objects that themselves rely on other services that are poorly built. Etc.
|
264
|
+
|
265
|
+
Do eat your own dogfood, and use your own toilet paper. If it hurts for you to
|
266
|
+
use it, with all your insider knowledge and perspective, it is downright agony
|
267
|
+
for other people.
|
268
|
+
|
248
269
|
TODO
|
249
270
|
====
|
250
271
|
|
@@ -253,5 +274,8 @@ Things left to do:
|
|
253
274
|
1. In the command-line tool, there is some kind of bug where the registration
|
254
275
|
message never returns back to the client. This causes registration to hang
|
255
276
|
forever...
|
256
|
-
1.
|
257
|
-
|
277
|
+
1. I should sit down and really think out code structure and refactor where
|
278
|
+
needed. Things are getting a little difficult to follow if you are jumping
|
279
|
+
around between files a bunch.
|
280
|
+
1. I need to cut down on the number of latency and heartbeat entries that are
|
281
|
+
saved. It gets a bit out of hand.
|
data/Servicy.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: servicy 0.0.
|
5
|
+
# stub: servicy 0.0.5 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "servicy"
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.5"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Thomas Luce"]
|
14
|
-
s.date = "2014-08-
|
14
|
+
s.date = "2014-08-15"
|
15
15
|
s.description = "A service registration and discovery framework, as a server and client library."
|
16
16
|
s.email = "thomas.luce@gmail.com"
|
17
17
|
s.executables = ["servicy"]
|
@@ -25,6 +25,9 @@ Gem::Specification.new do |s|
|
|
25
25
|
"bin/servicy",
|
26
26
|
"lib/api.rb",
|
27
27
|
"lib/client.rb",
|
28
|
+
"lib/configurable.rb",
|
29
|
+
"lib/formats.rb",
|
30
|
+
"lib/formats/json.rb",
|
28
31
|
"lib/hash.rb",
|
29
32
|
"lib/load_balancer.rb",
|
30
33
|
"lib/load_balancer/random.rb",
|
@@ -36,6 +39,7 @@ Gem::Specification.new do |s|
|
|
36
39
|
"lib/servicy.rb",
|
37
40
|
"lib/transport/in_memory_transport.rb",
|
38
41
|
"lib/transport/messages.rb",
|
42
|
+
"lib/transport/null_transport.rb",
|
39
43
|
"lib/transport/tcp_transport.rb",
|
40
44
|
"lib/transports.rb",
|
41
45
|
"test.rb"
|
@@ -43,5 +47,17 @@ Gem::Specification.new do |s|
|
|
43
47
|
s.homepage = "https://github.com/thomasluce/servicy"
|
44
48
|
s.rubygems_version = "2.2.2"
|
45
49
|
s.summary = "A service registration and discovery framework"
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
s.specification_version = 4
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_runtime_dependency(%q<contraction>, [">= 0"])
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<contraction>, [">= 0"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<contraction>, [">= 0"])
|
61
|
+
end
|
46
62
|
end
|
47
63
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.5
|
data/lib/api.rb
CHANGED
@@ -1,14 +1,8 @@
|
|
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
1
|
module Servicy
|
7
2
|
class API
|
8
|
-
def self.create(klass,
|
9
|
-
|
10
|
-
|
11
|
-
instance: method_descriptions[:instance].inject({}) { |h, info| h[info[:method].to_sym] = info; h }
|
3
|
+
def self.create(klass, mds)
|
4
|
+
method_descriptions = { class: mds[:class].inject({}) { |h, info| h[info[:method].to_sym] = info; h },
|
5
|
+
instance: mds[:instance].inject({}) { |h, info| h[info[:method].to_sym] = info; h }
|
12
6
|
}
|
13
7
|
|
14
8
|
# We inherit from API here so that in the future we can add functionality
|
@@ -16,27 +10,42 @@ module Servicy
|
|
16
10
|
# This may change to Client at some point... Who
|
17
11
|
# knows.
|
18
12
|
magic_class = Class.new(Servicy::API) do
|
19
|
-
@@base_class = klass
|
20
|
-
@@method_descriptions = method_descriptions
|
21
|
-
|
22
13
|
def initialize(*args)
|
23
|
-
@instance =
|
14
|
+
@instance = self.class.const_get('BASE_CLASS').new(*args)
|
24
15
|
end
|
25
16
|
|
26
|
-
def
|
27
|
-
|
17
|
+
def self.set_remote(client)
|
18
|
+
@remote = client
|
19
|
+
end
|
20
|
+
def self.remote?
|
21
|
+
@remote
|
22
|
+
end
|
23
|
+
def self.client
|
24
|
+
@remote
|
28
25
|
end
|
29
26
|
|
27
|
+
# TODO: I'm thinking that I might be able to do this with
|
28
|
+
# #define_method's instead. That will spead things up aroud 2-5x.
|
29
|
+
|
30
30
|
# The magic happens in the two method_missing methods.
|
31
31
|
# Meta-programming-averse; gaze upon my works, ye haughty, and despair!
|
32
32
|
def method_missing(method, *args, &block)
|
33
33
|
# Verify the contract
|
34
|
-
data =
|
35
|
-
raise NoMethodError.new("#{
|
34
|
+
data = self.class.const_get('METHOD_DESCRIPTIONS')[:instance][method]
|
35
|
+
raise NoMethodError.new("#{self.class.const_get('BASE_CLASS')} does not expose an API endpoint called, #{method.to_s}") unless data
|
36
36
|
data[:contract].valid_args?(*args)
|
37
37
|
|
38
38
|
# Dispatch the result
|
39
|
-
result =
|
39
|
+
result = nil
|
40
|
+
if self.class.remote?
|
41
|
+
params = data[:contract].params.each_with_index.inject({}) do |h, (param, index)|
|
42
|
+
h[param.to_s] = args[index]
|
43
|
+
h
|
44
|
+
end
|
45
|
+
result = self.class.client.transport.remote_request(method, params)
|
46
|
+
else
|
47
|
+
result = @instance.send(method, *args, &block)
|
48
|
+
end
|
40
49
|
|
41
50
|
# Check the result
|
42
51
|
data[:contract].valid_return?(*args, result)
|
@@ -47,17 +56,26 @@ module Servicy
|
|
47
56
|
|
48
57
|
# Make sure that we can respond to the things we actually do respond to.
|
49
58
|
def respond_to?(method)
|
50
|
-
|
59
|
+
self.class.const_get('METHOD_DESCRIPTIONS')[:instance].include?(method) || super
|
51
60
|
end
|
52
61
|
|
53
62
|
def self.method_missing(method, *args, &block)
|
54
63
|
# Verify the contract
|
55
|
-
data =
|
56
|
-
raise NoMethodError.new("#{
|
64
|
+
data = self.const_get('METHOD_DESCRIPTIONS')[:class][method]
|
65
|
+
raise NoMethodError.new("#{self.const_get('BASE_CLASS')} does not expose an API endpoint called, #{method.to_s}") unless data
|
57
66
|
data[:contract].valid_args?(*args)
|
58
67
|
|
59
68
|
# Dispatch the result
|
60
|
-
result =
|
69
|
+
result = nil
|
70
|
+
if remote?
|
71
|
+
params = data[:contract].params.each_with_index.inject({}) do |h, (param, index)|
|
72
|
+
h[param.to_s] = args[index]
|
73
|
+
h
|
74
|
+
end
|
75
|
+
result = client.transport.remote_request(method, params)
|
76
|
+
else
|
77
|
+
result = self.const_get('BASE_CLASS').send(method, *args, &block)
|
78
|
+
end
|
61
79
|
|
62
80
|
# Check the result
|
63
81
|
data[:contract].valid_return?(*args, result)
|
@@ -67,14 +85,14 @@ module Servicy
|
|
67
85
|
end
|
68
86
|
|
69
87
|
def self.respond_to?(method)
|
70
|
-
|
88
|
+
const_get('METHOD_DESCRIPTIONS')[:class].include?(method) || super
|
71
89
|
end
|
72
90
|
|
73
91
|
# Send back either all the documentation, or just for a particular
|
74
92
|
# method.
|
75
93
|
def self.docs(method=nil)
|
76
94
|
if method.nil?
|
77
|
-
return
|
95
|
+
return const_get('METHOD_DESCRIPTIONS').map do |(type, methods)|
|
78
96
|
methods.values.map { |v| v[:docs] }
|
79
97
|
end.flatten
|
80
98
|
else
|
@@ -83,7 +101,7 @@ module Servicy
|
|
83
101
|
search_in = method[0] == '.' ? [:class] : (method[0] == '#' ? [:instance] : search_in)
|
84
102
|
end
|
85
103
|
search_in.each do |type|
|
86
|
-
|
104
|
+
const_get('METHOD_DESCRIPTIONS')[type].each do |(name,stuff)|
|
87
105
|
return stuff[:docs] if method.to_s =~ /(\.|#)?#{name.to_s}$/
|
88
106
|
end
|
89
107
|
end
|
@@ -95,6 +113,8 @@ module Servicy
|
|
95
113
|
klass.constants(false).each do |const|
|
96
114
|
magic_class.const_set(const, klass.const_get(const))
|
97
115
|
end
|
116
|
+
magic_class.const_set('BASE_CLASS', klass)
|
117
|
+
magic_class.const_set('METHOD_DESCRIPTIONS', method_descriptions)
|
98
118
|
|
99
119
|
# Give it a good name
|
100
120
|
class_name = "#{klass.to_s}API"
|
@@ -117,10 +137,5 @@ module Servicy
|
|
117
137
|
|
118
138
|
{ name: name, host: 'localhost', port: port, version: version, heartbeat_port: heartbeat_port }
|
119
139
|
end
|
120
|
-
|
121
|
-
# Sets the remote configuration options
|
122
|
-
def set_remote_configuration(config)
|
123
|
-
@remote_config = config
|
124
|
-
end
|
125
140
|
end
|
126
141
|
end
|
data/lib/client.rb
CHANGED
@@ -12,23 +12,35 @@ module Servicy
|
|
12
12
|
|
13
13
|
# Create a new Client instance
|
14
14
|
def initialize(transport=nil)
|
15
|
-
@transport = transport ||
|
15
|
+
@transport = transport || Servicy.client.transport
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return weather or not you are connected to a remote servicy server
|
19
|
+
def connected?
|
20
|
+
@transport.send(Servicy::Transport::Message.query(name: 'com.servicy.not.a.real.service')) rescue return false
|
21
|
+
true
|
16
22
|
end
|
17
23
|
|
18
24
|
# Register a service with the service discovery portal found at the other
|
19
25
|
# end of @transport
|
20
26
|
def register_service(args)
|
27
|
+
if !args.is_a?(Hash) && args.ancestors.include?(Servicy::API)
|
28
|
+
args = args.search_query
|
29
|
+
end
|
30
|
+
|
21
31
|
if args.is_a?(Hash)
|
22
32
|
@transport.send(Servicy::Transport::Message.registration(Servicy::Service.new(args)))
|
23
33
|
else
|
24
|
-
|
34
|
+
raise ArgumentError.new("Don't know how to register type, #{args.inspect}")
|
25
35
|
end
|
26
36
|
end
|
27
37
|
|
28
38
|
# Find a single service matching a query. This will cause the server to use
|
29
39
|
# whatever configured load-balancing mechanism is configured for it.
|
30
40
|
def find_service(args)
|
31
|
-
|
41
|
+
result = @transport.send(Servicy::Transport::Message.query_one(args))
|
42
|
+
services = result && result.services
|
43
|
+
service = services && services.first
|
32
44
|
service && Servicy::Service.new(service)
|
33
45
|
end
|
34
46
|
|
@@ -79,18 +91,6 @@ module Servicy
|
|
79
91
|
API.create klass, data
|
80
92
|
end
|
81
93
|
|
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
94
|
private
|
95
95
|
|
96
96
|
# Get the contents of a file that contains a method, along with the line
|
data/lib/configurable.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class Configurable
|
2
|
+
def initialize
|
3
|
+
@stuff = {}
|
4
|
+
end
|
5
|
+
|
6
|
+
def method_missing(name, *args, &block)
|
7
|
+
if name.to_s =~ /=$/
|
8
|
+
@stuff[name.to_s.sub(/=$/, '')] = args.first
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
return @stuff[name] if @stuff[name]
|
13
|
+
@stuff[name] = Configurable.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def nil?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
data/lib/formats/json.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Servicy
|
4
|
+
module Formats
|
5
|
+
class JSON < Servicy::Format
|
6
|
+
class << self
|
7
|
+
def format(things)
|
8
|
+
things.to_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def unformat(things)
|
12
|
+
return nil if things == "null"
|
13
|
+
return ::JSON.parse(things) rescue ::JSON.parse("[#{things.inspect}]").first
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/formats.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Formats decide how a thing is encoded over the wire, regardless of what
|
2
|
+
# transport protocol is used. So, if you want to send protobufs over HTTP go
|
3
|
+
# nuts, dude.
|
4
|
+
module Servicy
|
5
|
+
# This represents an abstract class that should be overwritten for individual
|
6
|
+
# formats.
|
7
|
+
class Format
|
8
|
+
class << self
|
9
|
+
# @param [Hash] things is a key-value pairing where the values are the
|
10
|
+
# things to be encoded. You can call this recursively for nested objects.
|
11
|
+
def format(things)
|
12
|
+
raise NotImplementedError.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Turn a formatted thing back into a native representation
|
16
|
+
def unformat(thing)
|
17
|
+
raise NotImplementedError.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Formats
|
23
|
+
def self.method_missing(name, *args, &block)
|
24
|
+
c = Servicy::Formats.const_get(name)
|
25
|
+
return c if c && c.ancestors.include?(Servicy::Format)
|
26
|
+
rescue
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Load all the formatters
|
33
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'formats', '*.rb'))].each do |file|
|
34
|
+
next if file.start_with?('.') || File.directory?(file)
|
35
|
+
require File.expand_path(file)
|
36
|
+
end
|
data/lib/server/server.rb
CHANGED
@@ -9,52 +9,47 @@ module Servicy
|
|
9
9
|
# TODO: I should really make this take an options hash, but that will
|
10
10
|
# require changing a lot of shit...
|
11
11
|
def initialize(transport=nil, load_balancer=nil, logger_stream=nil, config_file=nil)
|
12
|
-
@transport = transport ||
|
12
|
+
@transport = transport || Servicy.config.server.transport
|
13
13
|
@services = [] # All services
|
14
14
|
@deactivated_services = [] # Dead services
|
15
|
-
@load_balancer = load_balancer ||
|
16
|
-
|
17
|
-
@logger = Logger.new(logger_stream)
|
18
|
-
@config_file = config_file || File.expand_path("~/.servicy.conf")
|
15
|
+
@load_balancer = load_balancer || Servicy.config.server.load_balancer
|
16
|
+
@config_file = config_file || Servicy.config.server.config_file
|
19
17
|
start_heartbeat_process
|
20
18
|
end
|
21
19
|
|
22
20
|
# Starts the {Servicy::Transport} listening (whatever that means for the
|
23
21
|
# 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
22
|
def go!
|
30
23
|
transport.start do |message|
|
31
|
-
|
24
|
+
Servicy.logger.info "Received message, #{message.inspect}"
|
32
25
|
last_service_count = @services.length
|
33
26
|
|
34
|
-
result =
|
35
|
-
|
27
|
+
result = case message.message
|
28
|
+
when 'registration'
|
36
29
|
self.register message.service
|
37
|
-
|
38
|
-
|
30
|
+
Transport::Message.success
|
31
|
+
when 'query'
|
39
32
|
if message.only_one
|
40
33
|
temp = [self.find_one(message.query)].compact
|
41
34
|
else
|
42
35
|
temp = self.find(message.query)
|
43
36
|
end
|
44
|
-
|
45
|
-
|
37
|
+
Transport::Message.services temp
|
38
|
+
when 'stats'
|
46
39
|
# Gather stats about the running system, and return it
|
47
40
|
# TODO: Other stats?
|
48
|
-
|
41
|
+
Transport::Message.statistics(num_services: last_service_count,
|
49
42
|
uptimes: uptimes,
|
50
43
|
latencies: latencies
|
51
44
|
)
|
52
45
|
else
|
53
|
-
|
46
|
+
Transport::Message.error("Bad message type, #{message.inspect}")
|
54
47
|
end
|
55
48
|
|
56
49
|
if @services.length > last_service_count
|
50
|
+
Servicy.logger.debug("Saving to #{@config_file}")
|
57
51
|
self.save!(@config_file)
|
52
|
+
Servicy.logger.debug("Saved")
|
58
53
|
last_service_count = @services.length
|
59
54
|
end
|
60
55
|
result
|
@@ -107,13 +102,17 @@ module Servicy
|
|
107
102
|
# tests and README.md for examples.
|
108
103
|
def register(args)
|
109
104
|
service = Service.new(args)
|
110
|
-
|
105
|
+
Servicy.logger.info "Registering service, #{service}..."
|
111
106
|
if service.up?
|
112
|
-
@
|
113
|
-
|
107
|
+
if @services.include? service
|
108
|
+
Servicy.logger.info "Service, #{service}, already exists. Not duplicating"
|
109
|
+
else
|
110
|
+
@services << service
|
111
|
+
Servicy.logger.info "Service, #{service}, up; registered."
|
112
|
+
end
|
114
113
|
return service
|
115
114
|
else
|
116
|
-
|
115
|
+
Servicy.logger.info "Service, #{service}, down; not registered."
|
117
116
|
return false
|
118
117
|
end
|
119
118
|
end
|
@@ -122,7 +121,8 @@ module Servicy
|
|
122
121
|
# active list of services that are available to searches, etc.
|
123
122
|
# @param [{Service}] service the service to de-register
|
124
123
|
def unregister(service)
|
125
|
-
@
|
124
|
+
return if @deactivated_services.include?(service)
|
125
|
+
Servicy.logger.info "De-registering service, #{service}"
|
126
126
|
@deactivated_services << service
|
127
127
|
end
|
128
128
|
|
@@ -131,7 +131,7 @@ module Servicy
|
|
131
131
|
# @param [{Service}] service The service to remove from deactivated list
|
132
132
|
def reregister(service)
|
133
133
|
if @deactivated_services.delete(service)
|
134
|
-
|
134
|
+
Servicy.logger.info "Re-registering service, #{service}"
|
135
135
|
end
|
136
136
|
end
|
137
137
|
|
@@ -160,16 +160,53 @@ module Servicy
|
|
160
160
|
end
|
161
161
|
|
162
162
|
# Find one or more services based on some search criteria.
|
163
|
-
#
|
163
|
+
# When finding things, you can search by any single or combination of name,
|
164
|
+
# version, and api definition. When searching for name:
|
165
|
+
#
|
166
|
+
# find(name: 'com.foobar.baz')
|
167
|
+
#
|
168
|
+
# ... or for wild-cards if you don't care who provides a thing
|
169
|
+
#
|
170
|
+
# find(name: '.baz')
|
171
|
+
#
|
172
|
+
# For searching by version, you can search by absolute version number,
|
173
|
+
# minimum, maximum, or a range (by passing min and max together):
|
174
|
+
#
|
175
|
+
# # Absolute version
|
176
|
+
# find(version: '1.2.4-p123')
|
177
|
+
#
|
178
|
+
# # Min and max as range
|
179
|
+
# find(min_version: '1.2.0', max_version: '2.0.0')
|
180
|
+
#
|
181
|
+
# All version numbers must follow the format:
|
182
|
+
#
|
183
|
+
# (major).(minor).(revision)(-patch)?
|
184
|
+
#
|
185
|
+
# And are compared to one another by converting the version to numbers and
|
186
|
+
# comparing those numbers. The version happens by:
|
187
|
+
#
|
188
|
+
# (major * 1000) + (minor * 100) + (revision * 10) + (1/patch)
|
189
|
+
#
|
190
|
+
# Searching by api can be used to search for a service that provides a
|
191
|
+
# certain interface, even if you don't know what that interface is called.
|
192
|
+
# For example, if you don't know what your authorization service is called,
|
193
|
+
# but you know that it takes 2 strings (username and password) and returns
|
194
|
+
# either false or a User object, you can search by:
|
195
|
+
#
|
196
|
+
# find(api: '.login/2[String,String] -> User|nil')
|
197
|
+
#
|
198
|
+
# NOTE The query syntax listed here isn't working currently. See
|
199
|
+
# {Servicy::ServiceSearcher} for information on how to actually search by
|
200
|
+
# api in the current version.
|
164
201
|
def find(args={})
|
165
|
-
|
202
|
+
Servicy.logger.info "Finding #{args.inspect}..."
|
166
203
|
|
167
204
|
searcher = Servicy::ServiceSearcher.new(services - deactivated_services)
|
168
205
|
searcher.filter_by_name args[:name]
|
169
206
|
searcher.filter_by_versions args[:min_version], args[:max_version], args[:version]
|
170
207
|
searcher.filter_by_api args[:api]
|
171
208
|
|
172
|
-
|
209
|
+
Servicy.logger.info "Found #{searcher.services.length} providers for #{args.inspect}"
|
173
210
|
searcher.services
|
174
211
|
end
|
175
212
|
|
data/lib/server.rb
CHANGED
data/lib/service.rb
CHANGED
@@ -4,7 +4,7 @@ module Servicy
|
|
4
4
|
class Service
|
5
5
|
include Comparable
|
6
6
|
attr_reader :name, :version, :host, :port, :heartbeat_port, :protocol,
|
7
|
-
:api, :heartbeat_check_rate, :latencies, :heartbeats
|
7
|
+
:api, :heartbeat_check_rate, :latencies, :heartbeats, :no_heartbeat
|
8
8
|
attr_accessor :heartbeat_last_check
|
9
9
|
|
10
10
|
NAME_REGEX = /[^\.]+\.([^\.]+\.?)+/
|
@@ -50,9 +50,14 @@ module Servicy
|
|
50
50
|
@heartbeat_port = args[:heartbeat_port]
|
51
51
|
@api = args[:api] || {}
|
52
52
|
@heartbeat_check_rate = args[:heartbeat_check_rate] || 1
|
53
|
+
@transport = transport_from_class_or_string(args[:protocol] || Servicy.config.client.transport)
|
54
|
+
if @transport
|
55
|
+
args[:protocol] = @transport.protocol_string # This is to make sure that it loads correctly after saving to disk.
|
56
|
+
end
|
53
57
|
@heartbeat_last_check = 0
|
54
58
|
@latencies = []
|
55
59
|
@heartbeats = []
|
60
|
+
@no_heartbeat = !!args[:no_heartbeat]
|
56
61
|
end
|
57
62
|
|
58
63
|
# Get a configuration value
|
@@ -99,12 +104,13 @@ module Servicy
|
|
99
104
|
def self.version_as_number(version)
|
100
105
|
parts = version.split('.')
|
101
106
|
parts.last.gsub(/\D/, '')
|
102
|
-
parts[0].to_i * 1000 + parts[1].to_i * 100 + parts[2].to_i * 10 + (parts[3] ||
|
107
|
+
parts[0].to_i * 1000 + parts[1].to_i * 100 + parts[2].to_i * 10 + (1 / (parts[3] || 1).to_f)
|
103
108
|
end
|
104
109
|
|
105
110
|
# Check weather or not a service is up, based on connecting to the hearbeat
|
106
111
|
# port, and reading a single byte.
|
107
112
|
def up?
|
113
|
+
return true if skip_heartbeat?
|
108
114
|
t1 = Time.now
|
109
115
|
s = TCPSocket.new(host, heartbeat_port)
|
110
116
|
Timeout.timeout(5) do
|
@@ -161,11 +167,37 @@ module Servicy
|
|
161
167
|
api[method_type] && api[method_type].select { |a| a[:name] == method }.first
|
162
168
|
end
|
163
169
|
|
170
|
+
# Make a call to a remote end-point.
|
171
|
+
def remote_call(method, *args)
|
172
|
+
# TODO: think about handling errors in a sane way
|
173
|
+
|
174
|
+
# Encode things for the transports formatter
|
175
|
+
args = @transport.format(args)
|
176
|
+
|
177
|
+
# Make the call
|
178
|
+
@transport.remote_request(method, args)
|
179
|
+
|
180
|
+
# Decode things coming back
|
181
|
+
@transport.unformat(result)
|
182
|
+
end
|
183
|
+
|
164
184
|
private
|
165
185
|
|
186
|
+
def skip_heartbeat?
|
187
|
+
!!no_heartbeat
|
188
|
+
end
|
189
|
+
|
190
|
+
def transport_from_class_or_string(transport)
|
191
|
+
transport.is_a?(Servicy::Transport) ? transport : transport_from_string(transport)
|
192
|
+
end
|
193
|
+
|
194
|
+
def transport_from_string(transport)
|
195
|
+
Servicy::Transport.all.select { |t| t.protocol_string == transport }.first
|
196
|
+
end
|
197
|
+
|
166
198
|
# These are just to keep us from filling up memory.
|
167
199
|
def record_latency(t1, t2)
|
168
|
-
@latencies << t2 - t1
|
200
|
+
@latencies << t2.to_i - t1.to_i
|
169
201
|
@latencies = @latencies[0...10] if @latencies.length > 10
|
170
202
|
end
|
171
203
|
|
data/lib/servicy.rb
CHANGED
@@ -1,9 +1,163 @@
|
|
1
1
|
$: << File.expand_path(File.join(File.dirname(__FILE__)))
|
2
2
|
|
3
|
-
# NOTE: You will almost never need both of these things. In all likeliness, you
|
4
|
-
# will want to do something like `require 'servicy/client'` yourself. This is
|
5
|
-
# here mostly as a convinience to rspec.
|
6
3
|
require 'hash'
|
4
|
+
require 'formats'
|
7
5
|
require 'server'
|
8
6
|
require 'client'
|
9
7
|
require 'api'
|
8
|
+
require 'configurable'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
module Servicy
|
12
|
+
# Get a logger instance that we can do something useful with.
|
13
|
+
def self.logger
|
14
|
+
@logger ||= begin
|
15
|
+
stream = config.server.logger_stream
|
16
|
+
stream = stream || File.open(File.join('/var', 'log', 'servicy_server.log'), 'a')
|
17
|
+
Logger.new(stream)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# After this method gets done running, you either have the state of the world
|
22
|
+
# exactly the same as it was, or your object is replaced in the global Object
|
23
|
+
# scope with a new object that will proxy methods to a remote service
|
24
|
+
# provider.
|
25
|
+
#
|
26
|
+
# To define a service provider is a bit more work, and less auto-magic
|
27
|
+
# because the exact details of the infrastructure and architecture of your
|
28
|
+
# setup will be very different from others. So Servicy doesn't make
|
29
|
+
# assumptions about how you want to do things. That said, it's relatively
|
30
|
+
# simple:
|
31
|
+
#
|
32
|
+
# # In some server class
|
33
|
+
# Servicy::Server.new(...).go!
|
34
|
+
#
|
35
|
+
# # In some service provider, possibly on a different box
|
36
|
+
# api = Servicy::Client.create_api_for(MyServiceClass)
|
37
|
+
# Servicy::Client.register_service(api)
|
38
|
+
# FIXME: @__api, and it's ilk, get overridden in the even that there is more
|
39
|
+
# than one.
|
40
|
+
def self.included(mod)
|
41
|
+
make_service_listener(mod)
|
42
|
+
|
43
|
+
# Attempt to find the service
|
44
|
+
client = Servicy::Client.new(config.server.transport)
|
45
|
+
raise 'go to bottom' unless client.connected?
|
46
|
+
|
47
|
+
# Create a new api object
|
48
|
+
@__api = Servicy::Client.create_api_for(mod)
|
49
|
+
port = Servicy.config.send(mod.to_s.to_sym).port
|
50
|
+
Servicy.logger.debug("Chose port, #{port} for service, #{mod.to_s}")
|
51
|
+
@__api.const_set("PORT", port) unless port.nil?
|
52
|
+
# TODO: for the other things as well
|
53
|
+
|
54
|
+
@__service = client.find_service(@__api.search_query)
|
55
|
+
raise 'go to bottom' unless @__service
|
56
|
+
|
57
|
+
# If all went well, then we can wrap the object to call to the remote
|
58
|
+
# service.
|
59
|
+
client = Servicy::Client.new(config.client.transport.class.new(port: port))
|
60
|
+
@__api.set_remote(client)
|
61
|
+
|
62
|
+
# ... and delegate that ish. We do it by killing off the original object,
|
63
|
+
# and replacing it with our own.
|
64
|
+
Object.send(:remove_const, mod.name.to_sym)
|
65
|
+
Object.const_set(mod.name.to_sym, @__api)
|
66
|
+
rescue => e
|
67
|
+
# Something went wrong, just give up silently, but don't do anything
|
68
|
+
# else; use this object as a regular-ass object. This is to allow you to
|
69
|
+
# use your API objects as normal objects in an environment where the
|
70
|
+
# object is bundled as part of a gem (,say, ) and there is no remote
|
71
|
+
# handler for it: use it locally
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.make_service_listener(mod)
|
75
|
+
# If we didn't manage to connect to a remote client, then we are, by
|
76
|
+
# definition, the provider. We should start the transports api_server, and
|
77
|
+
# dispatch requests here.
|
78
|
+
mod.send(:define_singleton_method, :start_servicy_server) do
|
79
|
+
begin
|
80
|
+
Thread.new do
|
81
|
+
begin
|
82
|
+
port = Servicy.config.send(self.to_s.to_sym).port || Servicy.config.server.port
|
83
|
+
transport = Servicy.config.service.transport
|
84
|
+
transport = transport.nil? ? Servicy.config.server.transport : transport
|
85
|
+
client = Servicy::Client.new(transport.class.new(port: port))
|
86
|
+
|
87
|
+
client.transport.start_api do |request|
|
88
|
+
method = nil
|
89
|
+
begin
|
90
|
+
method = self.method(request.method_name.to_sym)
|
91
|
+
rescue
|
92
|
+
method = self.instance_method(request.method_name.to_sym)
|
93
|
+
method = method.bind(self.new)
|
94
|
+
end
|
95
|
+
things = transport.unformat(request.args)
|
96
|
+
args = method.parameters.map do |param|
|
97
|
+
things[param.last.to_s]
|
98
|
+
end
|
99
|
+
|
100
|
+
Servicy.logger.info "Calling #{method} with #{args.inspect}"
|
101
|
+
begin
|
102
|
+
result = method.call(*args)
|
103
|
+
Servicy::Transport::Message.api_response(result)
|
104
|
+
rescue => e
|
105
|
+
Servicy::Transport::Message.error(e.to_s + "\n" + e.backtrace.join("\n"))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
client.transport.stop
|
110
|
+
rescue => e
|
111
|
+
Servicy::Transport::Message.error(e.to_s + "\n" + e.backtrace.join("\n"))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
Servicy.logger.info "Creating #{self.to_s} api"
|
116
|
+
api = Servicy::Client.create_api_for(self)
|
117
|
+
port = Servicy.config.send(mod.to_s.to_sym).port
|
118
|
+
port = port.nil? ? Servicy.config.server.port : port
|
119
|
+
api.const_set 'PORT', port
|
120
|
+
# TODO: the other things, too
|
121
|
+
Servicy.logger.debug("Chose port, #{port} for service, #{mod.to_s}")
|
122
|
+
|
123
|
+
server_port = Servicy.config.server.port
|
124
|
+
transport = Servicy.config.server.transport
|
125
|
+
client = Servicy::Client.new(transport.class.new(port: server_port))
|
126
|
+
Servicy.logger.info "Registering api, #{api.to_s}"
|
127
|
+
client.register_service api
|
128
|
+
rescue => e
|
129
|
+
Servicy.logger.error "An error occurred with the service: #{e.to_s}\n#{e.backtrace.join("\n")}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
rescue => e
|
133
|
+
# Again, just let it die silently. There is no reason to make things not
|
134
|
+
# work locally.
|
135
|
+
Servicy.logger.error(e)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Use this method to configure Servicy for other services
|
139
|
+
def self.configure(&block)
|
140
|
+
conf = Configurable.new
|
141
|
+
|
142
|
+
# Give some reasonable defaults
|
143
|
+
conf.server.host = 'localhost'
|
144
|
+
conf.server.transport = Servicy::Transport.InMemory.new
|
145
|
+
conf.server.load_balancer = Servicy::RoundRobinLoadBalancer.new
|
146
|
+
conf.server.logger_stream = File.open(File.join('/var', 'log', 'servicy_server.log'), 'a')
|
147
|
+
conf.server.config_file = File.expand_path("~/.servicy.conf")
|
148
|
+
# To set something for a given object, you would do something like:
|
149
|
+
# conf.transport.port = 1234
|
150
|
+
|
151
|
+
conf.client.transport = Servicy::Transport.InMemory.new
|
152
|
+
conf.transport.format = Servicy::Formats.JSON
|
153
|
+
|
154
|
+
conf = yield conf if block_given?
|
155
|
+
|
156
|
+
# Set the options
|
157
|
+
@_config = conf
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.config
|
161
|
+
@_config ||= configure
|
162
|
+
end
|
163
|
+
end
|
@@ -33,7 +33,7 @@ module Servicy
|
|
33
33
|
|
34
34
|
def socket_path
|
35
35
|
@socket_path ||= begin
|
36
|
-
file = Tempfile.new(@config[:path] || 'servicy-in-memory-transport')
|
36
|
+
file = Tempfile.new(@config[:path] || Servicy.config.transport.path || 'servicy-in-memory-transport')
|
37
37
|
file.close
|
38
38
|
path = file.path
|
39
39
|
file.delete
|
data/lib/transport/messages.rb
CHANGED
@@ -15,7 +15,8 @@ module Servicy
|
|
15
15
|
if stuff.is_a?(Hash)
|
16
16
|
@struct = stuff
|
17
17
|
else
|
18
|
-
|
18
|
+
# TODO: should this be a formatter that I can swap out?
|
19
|
+
@struct = JSON.parse(stuff || stuff.to_s)
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
@@ -117,6 +118,25 @@ module Servicy
|
|
117
118
|
})
|
118
119
|
end
|
119
120
|
|
121
|
+
# Make a remote API call.
|
122
|
+
# @param [String] method The method to call on the remote implementor.
|
123
|
+
# @param [Hash{String => Object}] args The arguments to pass to the method.
|
124
|
+
# These are expected to be pre-formatted by an appropriate Formatter
|
125
|
+
def self.api(method, args)
|
126
|
+
new({
|
127
|
+
message: 'api',
|
128
|
+
method_name: method,
|
129
|
+
args: args
|
130
|
+
})
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.api_response(result)
|
134
|
+
new({
|
135
|
+
message: 'api_response',
|
136
|
+
result: result
|
137
|
+
})
|
138
|
+
end
|
139
|
+
|
120
140
|
# Allow comparing of messages. This doesn't make for useful sorting, at
|
121
141
|
# the moment, but does make it easy to know if two things are saying the
|
122
142
|
# same thing.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'transports'
|
2
|
+
|
3
|
+
# A transport which does nothing
|
4
|
+
module Servicy
|
5
|
+
# This is a simple transport implementation that is to be used primarily for
|
6
|
+
# testing, so that I don't have to have a bunch of servers running just to
|
7
|
+
# run rspec...
|
8
|
+
class NilTransport < Servicy::Transport
|
9
|
+
def send(message)
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
while true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def stop
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -7,44 +7,65 @@ module Servicy
|
|
7
7
|
# file" business works out well...
|
8
8
|
class TCPTransport < Servicy::Transport
|
9
9
|
HELLO = "Servicy says HI!"
|
10
|
+
|
10
11
|
def send(message)
|
12
|
+
Servicy.logger.debug("Connecting")
|
11
13
|
socket = TCPSocket.new host, port
|
12
14
|
hello = socket.gets
|
13
15
|
raise "Not a servicy server" unless hello.strip == HELLO
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
message = Message.new(
|
16
|
+
Servicy.logger.debug("Sending #{message.body}")
|
17
|
+
socket.puts message.body
|
18
|
+
response = socket.gets
|
19
|
+
Servicy.logger.debug("Received #{response}")
|
20
|
+
message = Message.new(response)
|
19
21
|
socket.close
|
20
22
|
message
|
21
23
|
end
|
22
24
|
|
23
25
|
def start(&block)
|
24
26
|
@server = TCPServer.open(port)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
|
28
|
+
loop do
|
29
|
+
Thread.start(@server.accept) do |client|
|
30
|
+
client.puts HELLO
|
31
|
+
message = Message.new client.gets
|
32
|
+
Servicy.logger.debug("Got #{message.body}")
|
33
|
+
result = block.call(message)
|
34
|
+
Servicy.logger.debug("Replying #{result.body}")
|
35
|
+
client.puts result.body
|
36
|
+
client.close
|
37
|
+
Servicy.logger.debug("Closed connection")
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
41
|
+
alias_method :start_api, :start
|
35
42
|
|
36
43
|
def stop
|
37
44
|
@server && @server.close
|
38
45
|
end
|
39
46
|
|
47
|
+
# This makes a request as an actual API consumer over the wire, and returns
|
48
|
+
# the results.
|
49
|
+
def remote_request(name, args)
|
50
|
+
args = format(args)
|
51
|
+
response = send(Message.api(name, args))
|
52
|
+
if response.struct['error']
|
53
|
+
raise response.error
|
54
|
+
else
|
55
|
+
results = unformat(response.result)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
40
59
|
private
|
41
60
|
|
42
61
|
def host
|
43
|
-
|
62
|
+
host = Servicy.config.client.host
|
63
|
+
host = host.nil? ? 'localhost' : host
|
64
|
+
@config[:host] || host
|
44
65
|
end
|
45
66
|
|
46
67
|
def port
|
47
|
-
@config[:port]
|
68
|
+
@config[:port] || Servicy.config.transport.port
|
48
69
|
end
|
49
70
|
end
|
50
71
|
end
|
data/lib/transports.rb
CHANGED
@@ -16,7 +16,7 @@ module Servicy
|
|
16
16
|
|
17
17
|
# Override this method to send a message from {Client} to {Server}
|
18
18
|
def send(messge)
|
19
|
-
raise
|
19
|
+
raise NotImplementedError.new
|
20
20
|
end
|
21
21
|
|
22
22
|
# Override this method to yield a message when received on the {Server}. It
|
@@ -24,7 +24,12 @@ module Servicy
|
|
24
24
|
# {Transport::Message} object, and send back the return value of the block
|
25
25
|
# to the client.
|
26
26
|
def start(&block)
|
27
|
-
raise
|
27
|
+
raise NotImplementedError.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Same as start, but used in creation of API handlers.
|
31
|
+
def start_api(&block)
|
32
|
+
raise NotImplementedError.new
|
28
33
|
end
|
29
34
|
|
30
35
|
# Called when a transport is stopped
|
@@ -44,6 +49,39 @@ module Servicy
|
|
44
49
|
rescue
|
45
50
|
super
|
46
51
|
end
|
52
|
+
|
53
|
+
def self.all
|
54
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a printable string representing what protocol this would be used
|
58
|
+
# as. Override if you like.
|
59
|
+
def self.protocol_string
|
60
|
+
self.name.split("Transport").first
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [Hash] values Key-value pairs for things to encode.
|
64
|
+
def format(values)
|
65
|
+
formatter.format(values);
|
66
|
+
end
|
67
|
+
|
68
|
+
def unformat(values)
|
69
|
+
formatter.unformat(values)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Method called to make a remote request of an API call. To be implemented
|
73
|
+
# per transport.
|
74
|
+
# @return [Object] the raw values coming back from the request, to be
|
75
|
+
# passed to the formatter for unformatting.
|
76
|
+
def remote_request(name, args)
|
77
|
+
raise NotImplementedError.new
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def formatter
|
83
|
+
@formatter ||= @config['format'] || Servicy.config.transport.format
|
84
|
+
end
|
47
85
|
end
|
48
86
|
end
|
49
87
|
|
data/test.rb
CHANGED
@@ -1,74 +1,100 @@
|
|
1
1
|
require './lib/servicy'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
end
|
7
|
-
|
8
|
-
# Do some foobar work
|
9
|
-
# @param [String] a
|
10
|
-
# @param [Integer] b
|
11
|
-
def foobar(a,b)
|
12
|
-
p a
|
13
|
-
p b
|
14
|
-
end
|
15
|
-
|
16
|
-
# Do something on the class
|
17
|
-
# @return [Hash{Integer => Array<String>}]
|
18
|
-
def self.barbaz
|
19
|
-
puts "OH MY GOD!"
|
20
|
-
{ 1 => ['foo', 'bar'] }
|
21
|
-
end
|
3
|
+
Servicy.configure do |config|
|
4
|
+
config.server.transport = Servicy::Transport.TCP.new(port: 1234)
|
5
|
+
config
|
22
6
|
end
|
23
7
|
|
24
|
-
|
25
|
-
|
26
|
-
# future we extend to behave more like an API
|
27
|
-
api = Servicy::Client.create_api_for Foo
|
28
|
-
# p api
|
29
|
-
|
30
|
-
# Instance-y-stuff
|
31
|
-
foo = api.new(:something)
|
32
|
-
foo.foobar('string',1)
|
8
|
+
server = Servicy::Server.new
|
9
|
+
Thread.new { server.go! }
|
33
10
|
|
34
|
-
|
35
|
-
api.barbaz
|
36
|
-
|
37
|
-
# Now we should get some explosions for failing to comply with the contracts
|
38
|
-
begin
|
39
|
-
foo.foobar(:not_a_string, 'or an integer')
|
40
|
-
rescue => e
|
41
|
-
puts e
|
11
|
+
class Foobar
|
42
12
|
end
|
13
|
+
api = Servicy::Client.create_api_for(Foobar)
|
43
14
|
|
44
|
-
|
45
|
-
# Making a working api with an api object. This is just some ideas to outline
|
46
|
-
# how I would like the interface to look, not really any working code just yet.
|
47
|
-
#
|
48
|
-
|
49
|
-
# Finding a registered service and using a remote service instead of a local
|
50
|
-
# object. This would take the API object, gather information about it, make the
|
51
|
-
# query to the service discovery portal, and return a new api model (that is
|
52
|
-
# the same specifications as the original) that talks with the remote service
|
53
|
-
# instead of the local object, raising an error if something goes wrong.
|
54
|
-
transport = Servicy::Transport.TCP.new(port: 1234)
|
55
|
-
api = Servicy::Client.find_service_provider_for(api, transport)
|
56
|
-
|
57
|
-
# This should also be able to work with non-API objects given to it. In the
|
58
|
-
# case of a library that you don't have installed locally.
|
59
|
-
service = Servicy::Client.find_service(name: 'foobar') # This alredy exists
|
60
|
-
api = service.api
|
15
|
+
server.register(api.search_query)
|
61
16
|
|
62
|
-
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
17
|
+
class Foobar
|
18
|
+
# @param [String] a
|
19
|
+
def foo(a)
|
20
|
+
puts "If you see this, something went wrong"
|
21
|
+
end
|
67
22
|
|
68
|
-
|
69
|
-
|
70
|
-
api.start_server t
|
23
|
+
include Servicy
|
24
|
+
end
|
71
25
|
|
72
|
-
|
73
|
-
|
26
|
+
f = Foobar.new
|
27
|
+
p f
|
74
28
|
|
29
|
+
# class Foo
|
30
|
+
# def initialize(thing)
|
31
|
+
# @thing = thing
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Do some foobar work
|
35
|
+
# # @param [String] a
|
36
|
+
# # @param [Integer] b
|
37
|
+
# def foobar(a,b)
|
38
|
+
# p a
|
39
|
+
# p b
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# # Do something on the class
|
43
|
+
# # @return [Hash{Integer => Array<String>}]
|
44
|
+
# def self.barbaz
|
45
|
+
# puts "OH MY GOD!"
|
46
|
+
# { 1 => ['foo', 'bar'] }
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # Create a new class that wraps foo. It will be called FooAPI, and will behave
|
51
|
+
# # just like foo, but with contract checking. It will be the place that in the
|
52
|
+
# # future we extend to behave more like an API
|
53
|
+
# api = Servicy::Client.create_api_for Foo
|
54
|
+
# # p api
|
55
|
+
#
|
56
|
+
# # Instance-y-stuff
|
57
|
+
# foo = api.new(:something)
|
58
|
+
# foo.foobar('string',1)
|
59
|
+
#
|
60
|
+
# # Class-y stuff
|
61
|
+
# api.barbaz
|
62
|
+
#
|
63
|
+
# # Now we should get some explosions for failing to comply with the contracts
|
64
|
+
# begin
|
65
|
+
# foo.foobar(:not_a_string, 'or an integer')
|
66
|
+
# rescue => e
|
67
|
+
# puts e
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# ###############################################################################
|
71
|
+
# # Making a working api with an api object. This is just some ideas to outline
|
72
|
+
# # how I would like the interface to look, not really any working code just yet.
|
73
|
+
# #
|
74
|
+
#
|
75
|
+
# # Finding a registered service and using a remote service instead of a local
|
76
|
+
# # object. This would take the API object, gather information about it, make the
|
77
|
+
# # query to the service discovery portal, and return a new api model (that is
|
78
|
+
# # the same specifications as the original) that talks with the remote service
|
79
|
+
# # instead of the local object, raising an error if something goes wrong.
|
80
|
+
# transport = Servicy::Transport.TCP.new(port: 1234)
|
81
|
+
# api = Servicy::Client.find_service_provider_for(api, transport)
|
82
|
+
#
|
83
|
+
# # This should also be able to work with non-API objects given to it. In the
|
84
|
+
# # case of a library that you don't have installed locally.
|
85
|
+
# service = Servicy::Client.find_service(name: 'foobar') # This alredy exists
|
86
|
+
# api = service.api
|
87
|
+
#
|
88
|
+
# # Starting a server with a given client/api object. Ideally, you can start
|
89
|
+
# # multipl servers.
|
90
|
+
# api.start_server('HTTP', 80)
|
91
|
+
# api.start_server('HTTPS', 443)
|
92
|
+
# api.start_server('TCP', 1235)
|
93
|
+
#
|
94
|
+
# # Maybe it would be better to do it with Transport objects...
|
95
|
+
# t = Transport.tcp.new(1235)
|
96
|
+
# api.start_server t
|
97
|
+
#
|
98
|
+
# # Combining registration and server starting
|
99
|
+
# api.register_with('127.0.0.1:1234', Transport.TCP.new(1235))
|
100
|
+
#
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: servicy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Luce
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-08-
|
12
|
-
dependencies:
|
11
|
+
date: 2014-08-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: contraction
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
description: A service registration and discovery framework, as a server and client
|
14
28
|
library.
|
15
29
|
email: thomas.luce@gmail.com
|
@@ -25,6 +39,9 @@ files:
|
|
25
39
|
- bin/servicy
|
26
40
|
- lib/api.rb
|
27
41
|
- lib/client.rb
|
42
|
+
- lib/configurable.rb
|
43
|
+
- lib/formats.rb
|
44
|
+
- lib/formats/json.rb
|
28
45
|
- lib/hash.rb
|
29
46
|
- lib/load_balancer.rb
|
30
47
|
- lib/load_balancer/random.rb
|
@@ -36,6 +53,7 @@ files:
|
|
36
53
|
- lib/servicy.rb
|
37
54
|
- lib/transport/in_memory_transport.rb
|
38
55
|
- lib/transport/messages.rb
|
56
|
+
- lib/transport/null_transport.rb
|
39
57
|
- lib/transport/tcp_transport.rb
|
40
58
|
- lib/transports.rb
|
41
59
|
- test.rb
|