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 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
@@ -0,0 +1,12 @@
1
+ .rspec
2
+ .rvmrc
3
+ /.bundle
4
+ /log/*
5
+ /tmp/*
6
+ /coverage/
7
+ /spec/tmp/*
8
+ .project
9
+ .DS_Store
10
+ Gemfile.lock
11
+
12
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+
4
+ gemspec
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,2 @@
1
+ require 'combi/version'
2
+ require 'combi/service_bus'
@@ -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