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