combi 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/combi.gemspec +24 -0
- data/lib/combi.rb +2 -0
- data/lib/combi/buses/bus.rb +82 -0
- data/lib/combi/buses/http.rb +88 -0
- data/lib/combi/buses/in_process.rb +45 -0
- data/lib/combi/buses/queue.rb +86 -0
- data/lib/combi/buses/web_socket.rb +227 -0
- data/lib/combi/helpers.rb +18 -0
- data/lib/combi/queue_service.rb +102 -0
- data/lib/combi/reactor.rb +46 -0
- data/lib/combi/response_store.rb +36 -0
- data/lib/combi/service.rb +59 -0
- data/lib/combi/service_bus.rb +31 -0
- data/lib/combi/version.rb +3 -0
- data/spec/integration/multi_bus_spec.rb +140 -0
- data/spec/lib/combi/buses/bus_spec.rb +22 -0
- data/spec/lib/combi/buses/http_spec.rb +25 -0
- data/spec/lib/combi/buses/in_process_spec.rb +24 -0
- data/spec/lib/combi/buses/queue_spec.rb +41 -0
- data/spec/lib/combi/buses/web_socket_spec.rb +34 -0
- data/spec/lib/combi/service_spec.rb +40 -0
- data/spec/shared_examples/standard_bus.rb +62 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/rabbitmq_server.rb +81 -0
- data/spec/support/web_server.rb +50 -0
- data/spec/support/websocket_server.rb +13 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f32c087a7be58753b98c2453f2b30051eaf5abf5
|
4
|
+
data.tar.gz: 0204a9e540356da36488b94166bc7b479f49f29c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a7b26b16b375706f08da91f6e3fbdb3fa71423e2e3c9f1081a3f03ff65d1a1c1d9aa8ec1497311089f80fc2c57f61a1e72b741d8d59f0101b7755f5018276a13
|
7
|
+
data.tar.gz: 3e1e5784274ab360b7b165901244da15bdb7df3dfc2d3530e6bead4308c6c42250fe38e170f4710d6c1ccf0090177db2aa83d27f9dbbfce392b6f5c9fc18c193
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 1uptalent
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
combi: A mini bus for micro services
|
2
|
+
====================================
|
3
|
+
|
4
|
+
![A Volkswagen Combi](http://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Volkswagen-rapunzel.jpg/640px-Volkswagen-rapunzel.jpg)
|
5
|
+
|
6
|
+
###Implemented buses
|
7
|
+
|
8
|
+
- In Process: for testing or fast running implementations
|
9
|
+
- Web Sockets: for remote (may be behind proxy) clients
|
10
|
+
- Queue: for inter server communications (based on AMQP)
|
11
|
+
- HTTP: for consumer only services (not session, stateless). Probably will work in any TCP capable network
|
12
|
+
|
13
|
+
## Disclaimer
|
14
|
+
|
15
|
+
This is a work in progress. Expect serious refactors, breaking changes.
|
16
|
+
|
17
|
+
## How to use
|
18
|
+
|
19
|
+
###Server
|
20
|
+
|
21
|
+
Define a new service:
|
22
|
+
```
|
23
|
+
module Service
|
24
|
+
module Salutation
|
25
|
+
|
26
|
+
def actions
|
27
|
+
[:salute]
|
28
|
+
end
|
29
|
+
|
30
|
+
def say_hello(params)
|
31
|
+
"hello #{params['name']}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
Launch the server (web sockets):
|
38
|
+
```
|
39
|
+
require 'combi'
|
40
|
+
require 'combi/reactor'
|
41
|
+
require 'em-websocket'
|
42
|
+
|
43
|
+
Combi::Reactor.start
|
44
|
+
bus = Combi::ServiceBus.for(:web_socket)
|
45
|
+
bus.start!
|
46
|
+
bus.add_service(Service::Salutation)
|
47
|
+
|
48
|
+
ws_handler = Class.new do
|
49
|
+
def new_session(arg); end
|
50
|
+
end.new
|
51
|
+
|
52
|
+
port = 9292
|
53
|
+
EM::next_tick do
|
54
|
+
EM::WebSocket.start(host: '0.0.0.0', port: port) do |ws|
|
55
|
+
bus.manage_ws_event(ws, ws_handler)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
Combi::Reactor.join_thread
|
59
|
+
```
|
60
|
+
|
61
|
+
###Client
|
62
|
+
Launch the client (web sockets):
|
63
|
+
```
|
64
|
+
require 'combi'
|
65
|
+
require 'combi/reactor'
|
66
|
+
Combi::Reactor.start
|
67
|
+
|
68
|
+
ws_handler = Class.new do
|
69
|
+
def on_open; end
|
70
|
+
end.new
|
71
|
+
|
72
|
+
port = 9292
|
73
|
+
bus = Combi::ServiceBus.for(:web_socket, remote_api: "ws://localhost:#{port}/", handler: ws_handler)
|
74
|
+
bus.start!
|
75
|
+
|
76
|
+
request = bus.request(:salute, :say_hello, {name: 'world'}) # :salute is the name of action declared in service
|
77
|
+
request.callback do |response|
|
78
|
+
puts "Server says: #{response}"
|
79
|
+
end
|
80
|
+
|
81
|
+
Combi::Reactor.join_thread
|
82
|
+
```
|
83
|
+
|
84
|
+
## Testing
|
85
|
+
|
86
|
+
`rspec`, the integration suite, test services requesting other services through other buses (compisition).
|
87
|
+
|
88
|
+
For AMQP buses, a RabbitMQ server is required. We provide a setup/teardown based in docker.
|
89
|
+
|
90
|
+
OSX users, you will need docker and boot2docker installed.
|
91
|
+
Linux users, you will need docker installed, and make some adjustments to spec/support/rabbitmq_server.rb (the ssh tunnel is not needed)
|
92
|
+
|
93
|
+
##Contributors
|
94
|
+
|
95
|
+
[Abel Muino](https://twitter.com/amuino)
|
96
|
+
[German DZ](https://twitter.com/GermanDZ)
|
97
|
+
|
98
|
+
## License
|
99
|
+
|
100
|
+
MIT License.
|
data/combi.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'combi/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'combi'
|
6
|
+
s.version = Combi::VERSION
|
7
|
+
s.summary = 'Mini Bus for microservices'
|
8
|
+
s.description = 'Provides implementation for in process, amqp or web socket service bus'
|
9
|
+
s.authors = ['German Del Zotto', 'Abel Muiño']
|
10
|
+
s.email = ['germ@ndz.com.ar', 'amuino@1uptalent.com']
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_paths = ['lib']
|
13
|
+
s.homepage = 'https://github.com/1uptalent/combi'
|
14
|
+
s.license = 'MIT'
|
15
|
+
s.add_dependency 'yajl-ruby', '~> 1.2.0'
|
16
|
+
s.add_development_dependency 'rspec-given', '~> 3.5.4'
|
17
|
+
s.add_development_dependency 'amqp', '~> 1.3.0'
|
18
|
+
s.add_development_dependency 'faye-websocket', '~> 0.7.2'
|
19
|
+
s.add_development_dependency 'em-websocket', '~> 0.5.1'
|
20
|
+
s.add_development_dependency 'thin', '~> 1.6.2'
|
21
|
+
s.add_development_dependency 'em-synchrony', '~> 1.0.3'
|
22
|
+
s.add_development_dependency 'em-http-request', '~> 1.1.2'
|
23
|
+
s.add_development_dependency 'evented-spec', '~> 0.9.0'
|
24
|
+
end
|
data/lib/combi.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require "combi/service"
|
2
|
+
require 'yajl'
|
3
|
+
require 'yajl/json_gem' # for object.to_json, JSON.parse, etc...
|
4
|
+
|
5
|
+
module Combi
|
6
|
+
class Bus
|
7
|
+
attr_reader :services
|
8
|
+
|
9
|
+
RPC_DEFAULT_TIMEOUT = 1
|
10
|
+
RPC_MAX_POLLS = 10
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
@options = options
|
14
|
+
@services = []
|
15
|
+
post_initialize
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_initialize
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_service(service_definition, options = {})
|
22
|
+
service = make_service_instance(service_definition)
|
23
|
+
service.setup(self, options[:context])
|
24
|
+
@services << service
|
25
|
+
end
|
26
|
+
|
27
|
+
def start!
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop!
|
31
|
+
end
|
32
|
+
|
33
|
+
def restart!
|
34
|
+
stop!
|
35
|
+
start!
|
36
|
+
end
|
37
|
+
|
38
|
+
def enable(services)
|
39
|
+
services.each do |service|
|
40
|
+
case service
|
41
|
+
when :queue
|
42
|
+
require 'queue_service'
|
43
|
+
EventMachine.run do
|
44
|
+
Combi::QueueService.start ConfigProvider.for(:amqp)
|
45
|
+
end
|
46
|
+
when :redis
|
47
|
+
require 'redis'
|
48
|
+
$redis = Redis.new ConfigProvider.for(:redis)
|
49
|
+
when :active_record
|
50
|
+
require 'active_record'
|
51
|
+
ActiveRecord::Base.establish_connection ConfigProvider.for(:database)
|
52
|
+
when :bus
|
53
|
+
$service_bus = Combi::ServiceBus.for(:queue)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def log(message)
|
59
|
+
return unless @debug_mode ||= ENV['DEBUG'] == 'true'
|
60
|
+
puts "#{object_id} #{self.class.name} #{message}"
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def make_service_instance(service_definition)
|
66
|
+
if Combi::Service === service_definition
|
67
|
+
service = service_definition
|
68
|
+
else
|
69
|
+
service = create_service_from_module(service_definition)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_service_from_module(a_module)
|
74
|
+
service_class = Class.new do
|
75
|
+
include Combi::Service
|
76
|
+
include a_module
|
77
|
+
end
|
78
|
+
service = service_class.new
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'combi/buses/bus'
|
2
|
+
require 'combi/response_store'
|
3
|
+
require 'em-http-request'
|
4
|
+
|
5
|
+
module Combi
|
6
|
+
class Http < Bus
|
7
|
+
|
8
|
+
class Server
|
9
|
+
|
10
|
+
def initialize(bus)
|
11
|
+
@bus = bus
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_message(request)
|
15
|
+
path = request.path.split('/')
|
16
|
+
message = {
|
17
|
+
"service" => path[1],
|
18
|
+
"kind" => path[2],
|
19
|
+
"payload" => JSON.parse(request.body)
|
20
|
+
}
|
21
|
+
@bus.on_message(message)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class Client
|
27
|
+
|
28
|
+
def initialize(remote_api, handler, bus)
|
29
|
+
@handler = handler
|
30
|
+
@remote_api = remote_api
|
31
|
+
@bus = bus
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def post_initialize
|
37
|
+
@response_store = Combi::ResponseStore.new
|
38
|
+
if @options[:remote_api]
|
39
|
+
@machine = Client.new(@options[:remote_api], @options[:handler], self)
|
40
|
+
else
|
41
|
+
@machine = Server.new(self)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def manage_request(env)
|
46
|
+
@machine.on_message Rack::Request.new(env)
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_message(message)
|
50
|
+
service_name = message['service']
|
51
|
+
handler = handlers[service_name.to_s]
|
52
|
+
if handler
|
53
|
+
service_instance = handler[:service_instance]
|
54
|
+
kind = message['kind']
|
55
|
+
if service_instance.respond_to? kind
|
56
|
+
message['payload'] ||= {}
|
57
|
+
response = service_instance.send(kind, message['payload'])
|
58
|
+
{result: 'ok', response: response}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_to(service_instance, handler, options = {})
|
64
|
+
handlers[handler.to_s] = {service_instance: service_instance, options: options}
|
65
|
+
end
|
66
|
+
|
67
|
+
def handlers
|
68
|
+
@handlers ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def request(name, kind, message, options = {})
|
72
|
+
options[:timeout] ||= RPC_DEFAULT_TIMEOUT
|
73
|
+
|
74
|
+
correlation_id = rand(10_000_000).to_s
|
75
|
+
waiter = EventedWaiter.wait_for(correlation_id, @response_store, options[:timeout])
|
76
|
+
url = "#{@options[:remote_api]}#{name}/#{kind}"
|
77
|
+
request_async = EventMachine::HttpRequest.new(url, connection_timeout: options[:timeout]).post(body: message.to_json)
|
78
|
+
request_async.callback do |r|
|
79
|
+
waiter.succeed(JSON.parse(r.response)['response'])
|
80
|
+
end
|
81
|
+
request_async.errback do |x|
|
82
|
+
waiter.fail(RuntimeError.new(Timeout::Error))
|
83
|
+
end
|
84
|
+
waiter
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'combi/buses/bus'
|
2
|
+
|
3
|
+
module Combi
|
4
|
+
class InProcess < Bus
|
5
|
+
|
6
|
+
def request(handler_name, kind, message, options = {})
|
7
|
+
options[:timeout] ||= RPC_DEFAULT_TIMEOUT
|
8
|
+
handler = memory_handlers[handler_name.to_s]
|
9
|
+
return if handler.nil?
|
10
|
+
service_instance = handler[:service_instance]
|
11
|
+
message = JSON.parse(message.to_json)
|
12
|
+
return unless service_instance.respond_to?(kind)
|
13
|
+
waiter = EventMachine::DefaultDeferrable.new
|
14
|
+
waiter.timeout(options[:timeout], RuntimeError.new(Timeout::Error))
|
15
|
+
begin
|
16
|
+
Timeout.timeout(options[:timeout]) do
|
17
|
+
response = service_instance.send(kind, message)
|
18
|
+
if response.respond_to? :succeed
|
19
|
+
response.callback do |service_response|
|
20
|
+
waiter.succeed service_response
|
21
|
+
end
|
22
|
+
else
|
23
|
+
waiter.succeed response
|
24
|
+
end
|
25
|
+
end
|
26
|
+
rescue Timeout::Error => e
|
27
|
+
log "ERROR"
|
28
|
+
waiter.fail RuntimeError.new(Timeout::Error)
|
29
|
+
rescue e
|
30
|
+
log "other ERROR"
|
31
|
+
log e.inspect
|
32
|
+
end
|
33
|
+
waiter
|
34
|
+
end
|
35
|
+
|
36
|
+
def respond_to(service_instance, handler, options = {})
|
37
|
+
memory_handlers[handler.to_s] = {service_instance: service_instance, options: options}
|
38
|
+
end
|
39
|
+
|
40
|
+
def memory_handlers
|
41
|
+
@memory_handlers ||= {}
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'combi/buses/bus'
|
2
|
+
require 'combi/response_store'
|
3
|
+
require 'combi/queue_service'
|
4
|
+
|
5
|
+
module Combi
|
6
|
+
class Queue < Bus
|
7
|
+
attr_reader :queue_service
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
super
|
11
|
+
@response_store = Combi::ResponseStore.new
|
12
|
+
@queue_service = Combi::QueueService.new(options[:amqp_config], rpc: :enabled)
|
13
|
+
queue_service.rpc_callback = lambda do |message|
|
14
|
+
@response_store.handle_rpc_response(message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def start!
|
19
|
+
queue_service.start
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop!
|
23
|
+
queue_service.ready do
|
24
|
+
@queue_service.disconnect
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to(service_instance, handler, options = {})
|
29
|
+
log "registering #{handler}"
|
30
|
+
queue_options = {}
|
31
|
+
subscription_options = {}
|
32
|
+
if options[:fast] == true
|
33
|
+
queue_options[:auto_delete] = false
|
34
|
+
else
|
35
|
+
subscription_options[:ack] = true
|
36
|
+
end
|
37
|
+
queue_service.ready do
|
38
|
+
queue_service.queue(handler.to_s, queue_options) do |queue|
|
39
|
+
log "subscribing to queue #{handler.to_s} with options #{queue_options}"
|
40
|
+
queue.subscribe(subscription_options) do |delivery_info, payload|
|
41
|
+
respond service_instance, payload, delivery_info
|
42
|
+
queue_service.acknowledge delivery_info unless options[:fast] == true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def request(name, kind, message, options = {})
|
49
|
+
options[:timeout] ||= RPC_DEFAULT_TIMEOUT
|
50
|
+
options[:routing_key] = name.to_s
|
51
|
+
correlation_id = rand(10_000_000).to_s
|
52
|
+
options[:correlation_id] = correlation_id
|
53
|
+
waiter = EventedWaiter.wait_for(correlation_id, @response_store, options[:timeout])
|
54
|
+
queue_service.ready do
|
55
|
+
log "Making request: #{name}.#{kind} #{message.inspect}\t|| #{options.inspect}"
|
56
|
+
queue_service.call(kind, message, options)
|
57
|
+
end
|
58
|
+
waiter
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond(service_instance, request, delivery_info)
|
62
|
+
message = JSON.parse request
|
63
|
+
kind = message['kind']
|
64
|
+
payload = message['payload']
|
65
|
+
options = message['options']
|
66
|
+
unless service_instance.respond_to?(kind)
|
67
|
+
log "Service instance does not respond to #{kind}: #{service_instance.inspect}"
|
68
|
+
return
|
69
|
+
end
|
70
|
+
log "generating response for #{service_instance.class}#{service_instance.actions.inspect}.#{kind} #{payload.inspect}"
|
71
|
+
response = service_instance.send(kind, payload)
|
72
|
+
|
73
|
+
if response.respond_to? :succeed
|
74
|
+
log "response is deferred"
|
75
|
+
response.callback do |service_response|
|
76
|
+
log "responding with deferred answer: #{service_response.inspect}"
|
77
|
+
queue_service.respond(service_response, delivery_info)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
log "responding with inmediate answer: #{response.inspect}"
|
81
|
+
queue_service.respond(response, delivery_info) unless response.nil?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|