combi 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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