servicy 0.0.3 → 0.0.5
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/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
|