combi 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.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
|
+

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