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
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'combi/buses/bus'
|
2
|
+
require 'combi/response_store'
|
3
|
+
|
4
|
+
module Combi
|
5
|
+
class WebSocket < Bus
|
6
|
+
|
7
|
+
class Server
|
8
|
+
|
9
|
+
def initialize(bus)
|
10
|
+
@bus = bus
|
11
|
+
@bus.ready.succeed
|
12
|
+
end
|
13
|
+
|
14
|
+
def start!
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop!
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_open(ws, handler)
|
21
|
+
@bus.log "ON OPEN #{handler.inspect}"
|
22
|
+
handler.new_session(ws)
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_message(ws, session, raw_message)
|
26
|
+
@bus.log "ON MESSAGE #{raw_message}"
|
27
|
+
message = JSON.parse(raw_message)
|
28
|
+
@bus.on_message(ws, message, session)
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_close(session)
|
32
|
+
session && session.close
|
33
|
+
end
|
34
|
+
|
35
|
+
def ws
|
36
|
+
@ws
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class Client
|
42
|
+
require 'faye/websocket'
|
43
|
+
|
44
|
+
def initialize(remote_api, handler, bus)
|
45
|
+
@handler = handler
|
46
|
+
@remote_api = remote_api
|
47
|
+
@bus = bus
|
48
|
+
end
|
49
|
+
|
50
|
+
def start!
|
51
|
+
open_websocket
|
52
|
+
end
|
53
|
+
|
54
|
+
def stop!
|
55
|
+
@ws && @ws.close
|
56
|
+
@bus.log "stop requested"
|
57
|
+
end
|
58
|
+
|
59
|
+
def restart!
|
60
|
+
stop!
|
61
|
+
start!
|
62
|
+
end
|
63
|
+
|
64
|
+
def open_websocket
|
65
|
+
@bus.log @remote_api
|
66
|
+
@ws = ws = Faye::WebSocket::Client.new(@remote_api)
|
67
|
+
ws.on :open do |event|
|
68
|
+
@bus.log "OPEN"
|
69
|
+
@bus.log "HANDLER #{@handler.inspect}"
|
70
|
+
@handler.on_open
|
71
|
+
@bus.ready.succeed
|
72
|
+
end
|
73
|
+
|
74
|
+
ws.on :message do |event|
|
75
|
+
@bus.log "ON MESSAGE: #{event.data}"
|
76
|
+
message = JSON.parse(event.data)
|
77
|
+
@bus.on_message(ws, message)
|
78
|
+
end
|
79
|
+
|
80
|
+
ws.on :close do |event|
|
81
|
+
@bus.log "close #{event.code}: #{event.reason}"
|
82
|
+
@ws = ws = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
ws.on :error do |event|
|
86
|
+
@bus.log "received error: #{event.inspect}"
|
87
|
+
stop!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def ws
|
92
|
+
@bus.log "ws present: #{@ws != nil}"
|
93
|
+
@ws
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_reader :handlers, :ready
|
99
|
+
|
100
|
+
def initialize(options)
|
101
|
+
super
|
102
|
+
@handlers = {}
|
103
|
+
end
|
104
|
+
|
105
|
+
def post_initialize
|
106
|
+
@ready = EventMachine::DefaultDeferrable.new
|
107
|
+
@response_store = Combi::ResponseStore.new
|
108
|
+
if @options[:remote_api]
|
109
|
+
require 'eventmachine'
|
110
|
+
@machine = Client.new(@options[:remote_api], @options[:handler], self)
|
111
|
+
else
|
112
|
+
@machine = Server.new(self)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def start!
|
117
|
+
@machine.start!
|
118
|
+
end
|
119
|
+
|
120
|
+
def stop!
|
121
|
+
@machine.stop!
|
122
|
+
end
|
123
|
+
|
124
|
+
def manage_request(env, handler)
|
125
|
+
require 'faye/websocket'
|
126
|
+
|
127
|
+
return unless Faye::WebSocket.websocket?(env)
|
128
|
+
@ws = ws = Faye::WebSocket.new(env)
|
129
|
+
session = nil
|
130
|
+
|
131
|
+
ws.on :message do |event|
|
132
|
+
@machine.on_message(ws, session, event.data)
|
133
|
+
end
|
134
|
+
|
135
|
+
ws.on :open do |event|
|
136
|
+
session = @machine.on_open(ws, handler)
|
137
|
+
end
|
138
|
+
|
139
|
+
ws.on :close do |event|
|
140
|
+
@machine.on_close(session)
|
141
|
+
end
|
142
|
+
# Return async Rack response
|
143
|
+
ws.rack_response
|
144
|
+
end
|
145
|
+
|
146
|
+
def manage_ws_event(ws, handler)
|
147
|
+
session = nil
|
148
|
+
|
149
|
+
ws.onmessage do |raw_message|
|
150
|
+
@machine.on_message(ws, session, raw_message)
|
151
|
+
end
|
152
|
+
|
153
|
+
ws.onopen do |handshake|
|
154
|
+
session = @machine.on_open(ws, handler)
|
155
|
+
end
|
156
|
+
|
157
|
+
ws.onclose do
|
158
|
+
@machine.on_close(session)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def on_message(ws, message, session = nil)
|
163
|
+
if message['correlation_id'] && message.has_key?('result')
|
164
|
+
@response_store.handle_rpc_response(message)
|
165
|
+
log "Stored message with correlation_id #{message['correlation_id']} - #{message.inspect}"
|
166
|
+
return
|
167
|
+
end
|
168
|
+
service_name = message['service']
|
169
|
+
kind = message['kind']
|
170
|
+
payload = message['payload'] || {}
|
171
|
+
payload['session'] = session
|
172
|
+
response = invoke_service(service_name, kind, payload)
|
173
|
+
|
174
|
+
msg = {result: 'ok', correlation_id: message['correlation_id']}
|
175
|
+
|
176
|
+
if response.respond_to? :succeed
|
177
|
+
response.callback do |service_response|
|
178
|
+
msg[:response] = service_response
|
179
|
+
ws.send(msg.to_json)
|
180
|
+
end
|
181
|
+
else
|
182
|
+
msg[:response] = response
|
183
|
+
ws.send(msg.to_json)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def invoke_service(service_name, kind, payload)
|
188
|
+
handler = handlers[service_name.to_s]
|
189
|
+
if handler
|
190
|
+
service_instance = handler[:service_instance]
|
191
|
+
if service_instance.respond_to? kind
|
192
|
+
response = service_instance.send(kind, payload)
|
193
|
+
else
|
194
|
+
log "[WARNING] Service #{service_name}(#{service_instance.class.name}) does not respond to message #{kind}"
|
195
|
+
end
|
196
|
+
else
|
197
|
+
log "[WARNING] Service #{service_name} not found"
|
198
|
+
log "[WARNING] handlers: #{handlers.keys.inspect}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def respond_to(service_instance, handler, options = {})
|
203
|
+
log "registering #{handler}"
|
204
|
+
handlers[handler.to_s] = {service_instance: service_instance, options: options}
|
205
|
+
log "handlers: #{handlers.keys.inspect}"
|
206
|
+
end
|
207
|
+
|
208
|
+
def request(name, kind, message, options = {})
|
209
|
+
options[:timeout] ||= RPC_DEFAULT_TIMEOUT
|
210
|
+
msg = {
|
211
|
+
service: name,
|
212
|
+
kind: kind,
|
213
|
+
payload: message
|
214
|
+
}
|
215
|
+
correlation_id = rand(10_000_000).to_s
|
216
|
+
msg[:correlation_id] = correlation_id
|
217
|
+
waiter = EventedWaiter.wait_for(correlation_id, @response_store, options[:timeout])
|
218
|
+
@ready.callback do |r|
|
219
|
+
web_socket = @machine.ws || options[:ws]
|
220
|
+
log "sending request #{msg.inspect}"
|
221
|
+
web_socket.send msg.to_json
|
222
|
+
end
|
223
|
+
waiter
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Combi
|
4
|
+
|
5
|
+
def self.wait_for(defer, options = {}, &block)
|
6
|
+
options[:timeout] ||= 2
|
7
|
+
poll_time = options[:timeout] / 10
|
8
|
+
resolved = false
|
9
|
+
defer.callback { |response|
|
10
|
+
resolved = true
|
11
|
+
block.call response
|
12
|
+
}
|
13
|
+
Timeout::timeout(options[:timeout]) do
|
14
|
+
sleep poll_time while !resolved
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
require 'amqp/utilities/event_loop_helper'
|
3
|
+
|
4
|
+
module Combi
|
5
|
+
class QueueService
|
6
|
+
|
7
|
+
RPC_DEFAULT_TIMEOUT = 1
|
8
|
+
RPC_WAIT_PERIOD = 0.01
|
9
|
+
|
10
|
+
attr_accessor :rpc_callback
|
11
|
+
|
12
|
+
def initialize(config, options)
|
13
|
+
@config = config
|
14
|
+
@options = options
|
15
|
+
@rpc_queue = nil
|
16
|
+
@ready_defer = EventMachine::DefaultDeferrable.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def ready(&block)
|
20
|
+
@ready_defer.callback &block
|
21
|
+
end
|
22
|
+
|
23
|
+
def log(message)
|
24
|
+
return unless @debug_mode ||= ENV['DEBUG'] == 'true'
|
25
|
+
puts "#{object_id} #{self.class.name} #{message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
connect @config do
|
30
|
+
if @options[:rpc] == :enabled
|
31
|
+
create_rpc_queue
|
32
|
+
else
|
33
|
+
puts "ready"
|
34
|
+
@ready_defer.succeed
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect(config, &after_connect)
|
40
|
+
@amqp_conn = AMQP.connect(config) do |connection, open_ok|
|
41
|
+
@channel = AMQP::Channel.new @amqp_conn
|
42
|
+
@channel.auto_recovery = true
|
43
|
+
@exchange = @channel.direct ''
|
44
|
+
after_connect.call
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def disconnect
|
49
|
+
@amqp_conn.close do
|
50
|
+
puts "disconnected from RABBIT"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def publish(*args, &block)
|
55
|
+
args[0] = args[0].to_json unless args[0].is_a? String
|
56
|
+
@exchange.publish *args, &block
|
57
|
+
end
|
58
|
+
|
59
|
+
def queue(name, options = {}, &block)
|
60
|
+
@channel.queue(name, options, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def acknowledge(delivery_info)
|
64
|
+
@channel.acknowledge(delivery_info.delivery_tag, false)
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond(response, delivery_info)
|
68
|
+
response = response.call if response.respond_to? :call
|
69
|
+
publish response, routing_key: delivery_info.reply_to, correlation_id: delivery_info.correlation_id
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_rpc_queue
|
73
|
+
@rpc_queue.unsubscribe unless @rpc_queue.nil?
|
74
|
+
@rpc_queue = queue('', exclusive: true, auto_delete: true) do |rpc_queue|
|
75
|
+
log "\tRPC QUEUE: #{@rpc_queue.name}"
|
76
|
+
rpc_queue.subscribe do |metadata, response|
|
77
|
+
message = {
|
78
|
+
'correlation_id' => metadata.correlation_id,
|
79
|
+
'response' => response
|
80
|
+
}
|
81
|
+
rpc_callback.call(message) unless rpc_callback.nil?
|
82
|
+
end
|
83
|
+
@ready_defer.succeed
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def call(kind, message, options = {})
|
88
|
+
log "sending request #{kind} #{message.inspect} with options #{options.inspect}"
|
89
|
+
raise "RPC is not enabled or reply_to is not included" if (@rpc_queue.nil? || @rpc_queue.name.nil?) && options[:reply_to].nil?
|
90
|
+
options[:timeout] ||= RPC_DEFAULT_TIMEOUT
|
91
|
+
options[:routing_key] ||= 'rcalls_queue'
|
92
|
+
options[:reply_to] ||= @rpc_queue.name
|
93
|
+
request = {
|
94
|
+
kind: kind,
|
95
|
+
payload: message,
|
96
|
+
options: {}
|
97
|
+
}
|
98
|
+
publish(request, options)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Combi
|
4
|
+
class Reactor
|
5
|
+
def self.start(&block)
|
6
|
+
EM::error_handler do |error|
|
7
|
+
STDERR << "ERROR IN EM\n"
|
8
|
+
STDERR << "\t#{error.inspect}"
|
9
|
+
STDERR << "\t#{error.backtrace.join("\t\n")}" << "\n"
|
10
|
+
end
|
11
|
+
log "-EM.start- the reactor is running: #{EM::reactor_running?}"
|
12
|
+
raise "EM did not shut down" if EM::reactor_running?
|
13
|
+
@@reactor_thread = Thread.new do
|
14
|
+
log "------- starting EM reactor"
|
15
|
+
EM::run do
|
16
|
+
log "------- reactor started"
|
17
|
+
Signal.trap("INT") { EM::stop_event_loop }
|
18
|
+
Signal.trap("TERM") { EM::stop_event_loop }
|
19
|
+
block.call unless block.nil?
|
20
|
+
end
|
21
|
+
log "------- reactor stopped"
|
22
|
+
end
|
23
|
+
30.times do
|
24
|
+
sleep 0.1 unless EM::reactor_running?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.stop
|
29
|
+
log "-EM.stop- the reactor is running: #{EM::reactor_running?}"
|
30
|
+
EM::stop_event_loop if EM::reactor_running?
|
31
|
+
50.times do
|
32
|
+
sleep 0.3 if EM::reactor_running?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.join_thread
|
37
|
+
@@reactor_thread.join if @@reactor_thread
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.log(message)
|
41
|
+
return unless @debug_mode ||= ENV['DEBUG'] == 'true'
|
42
|
+
puts "#{object_id} #{self.class.name} #{message}"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Combi
|
4
|
+
class ResponseStore
|
5
|
+
def initialize()
|
6
|
+
@waiters = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_waiter(key, waiter)
|
10
|
+
@waiters[key] = waiter
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_rpc_response(response)
|
14
|
+
correlation_id = response['correlation_id']
|
15
|
+
waiter = @waiters[correlation_id]
|
16
|
+
response = response['response']
|
17
|
+
waiter.succeed(response)
|
18
|
+
@waiters.delete correlation_id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class EventedWaiter
|
23
|
+
include EM::Deferrable
|
24
|
+
|
25
|
+
def self.wait_for(key, response_store, timeout)
|
26
|
+
waiter = new(key, response_store, timeout, Combi::Bus::RPC_MAX_POLLS)
|
27
|
+
response_store.add_waiter(key, waiter)
|
28
|
+
waiter
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(key, response_store, timeout, max_polls)
|
32
|
+
self.timeout(timeout, RuntimeError.new(Timeout::Error))
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Combi
|
2
|
+
module Service
|
3
|
+
|
4
|
+
def setup(service_bus, context)
|
5
|
+
context ||= {}
|
6
|
+
context[:service_bus] = service_bus
|
7
|
+
setup_context(context)
|
8
|
+
setup_services
|
9
|
+
register_actions
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup_context(context)
|
13
|
+
@context = context
|
14
|
+
@context.keys.each do |context_var|
|
15
|
+
define_singleton_method context_var do
|
16
|
+
@context[context_var]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup_services
|
22
|
+
end
|
23
|
+
|
24
|
+
def register_actions
|
25
|
+
actions.each do |handler|
|
26
|
+
service_bus.respond_to(self, handler)
|
27
|
+
end
|
28
|
+
fast_actions.each do |handler|
|
29
|
+
service_bus.respond_to(self, handler, fast: true)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def no_response
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def async_response(&block)
|
38
|
+
lambda &block
|
39
|
+
end
|
40
|
+
|
41
|
+
def actions
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
|
45
|
+
def fast_actions
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
|
49
|
+
def service_bus
|
50
|
+
@service_bus
|
51
|
+
end
|
52
|
+
|
53
|
+
def enable(*services, &block)
|
54
|
+
service_bus.enable(services)
|
55
|
+
yield block if block_given?
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|