gilmour 0.3.4 → 0.4.1
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 +4 -4
- data/.travis.yml +1 -0
- data/README.md +59 -4
- data/examples/composition.rb +98 -0
- data/examples/container.rb +19 -0
- data/examples/echoclient.rb +12 -8
- data/examples/fibonacci.rb +32 -0
- data/examples/forkechoclient.rb +23 -0
- data/examples/signal_slot.rb +35 -0
- data/examples/subscribers/echo.rb +22 -0
- data/lib/gilmour/backends/backend.rb +154 -29
- data/lib/gilmour/backends/redis.rb +45 -31
- data/lib/gilmour/base.rb +61 -30
- data/lib/gilmour/composers.rb +190 -0
- data/lib/gilmour/responder.rb +184 -138
- data/lib/gilmour/waiter.rb +6 -25
- data/test/spec/helpers/common.rb +2 -22
- data/test/spec/helpers/connection.rb +2 -3
- data/test/spec/test_pipelines.rb +207 -0
- data/test/spec/test_subscriber_redis.rb +1 -1
- data/test/spec/test_subscriber_redis_forked.rb +1 -2
- data/test/spec/test_subscriber_redis_reply_slot.rb +303 -0
- data/test/spec/test_subscriber_redis_reply_slot_fork.rb +274 -0
- data/test/spec/test_waiter.rb +114 -0
- data/test/testservice/subscribers/test_reply.rb +69 -0
- data/test/testservice/subscribers/test_subscriber.rb +1 -0
- data/version.rb +1 -1
- metadata +19 -5
- data/examples/fork_log_server.rb +0 -97
- data/examples/server.rb +0 -47
- data/examples/thread_example.rb +0 -40
data/lib/gilmour/responder.rb
CHANGED
@@ -4,13 +4,12 @@ require 'json'
|
|
4
4
|
require 'logger'
|
5
5
|
require_relative './waiter'
|
6
6
|
|
7
|
-
# Top level module
|
8
7
|
module Gilmour
|
8
|
+
|
9
9
|
# The Responder module that provides the request and respond
|
10
10
|
# DSL
|
11
11
|
# The public methods in this class are available to be called
|
12
12
|
# from the body of the handlers directly
|
13
|
-
|
14
13
|
class Request
|
15
14
|
attr_reader :topic, :body
|
16
15
|
|
@@ -20,6 +19,10 @@ module Gilmour
|
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
22
|
+
# Every request handler is executed in the context of a Responder
|
23
|
+
# object.
|
24
|
+
# This class contains methods to respond to requests as well as
|
25
|
+
# proxy methods for carrying out gilmour actions inside the handlers.
|
23
26
|
class Responder
|
24
27
|
LOG_SEPERATOR = '%%'
|
25
28
|
LOG_PREFIX = "#{LOG_SEPERATOR}gilmour#{LOG_SEPERATOR}"
|
@@ -27,20 +30,21 @@ module Gilmour
|
|
27
30
|
attr_reader :logger
|
28
31
|
attr_reader :request
|
29
32
|
|
30
|
-
def
|
33
|
+
def child_logger(writer) #:nodoc:
|
31
34
|
logger = Logger.new(STDERR)
|
32
35
|
loglevel = ENV["LOG_LEVEL"] ? ENV["LOG_LEVEL"].to_sym : :warn
|
33
36
|
logger.level = Gilmour::LoggerLevels[loglevel] || Logger::WARN
|
34
37
|
logger.formatter = proc do |severity, datetime, progname, msg|
|
35
|
-
data =
|
36
|
-
|
37
|
-
|
38
|
+
data = JSON.generate(severity: severity, msg: msg)
|
39
|
+
# data = "#{LOG_PREFIX}#{severity}#{LOG_SEPERATOR}#{msg}"
|
40
|
+
writer.write(data+"\n")
|
41
|
+
writer.flush
|
38
42
|
nil
|
39
43
|
end
|
40
44
|
logger
|
41
45
|
end
|
42
46
|
|
43
|
-
def make_logger
|
47
|
+
def make_logger #:nodoc:
|
44
48
|
logger = Logger.new(STDERR)
|
45
49
|
loglevel = ENV["LOG_LEVEL"] ? ENV["LOG_LEVEL"].to_sym : :warn
|
46
50
|
logger.level = Gilmour::LoggerLevels[loglevel] || Logger::WARN
|
@@ -51,34 +55,38 @@ module Gilmour
|
|
51
55
|
logger
|
52
56
|
end
|
53
57
|
|
54
|
-
def initialize(sender, topic, data, backend,
|
58
|
+
def initialize(sender, topic, data, backend, opts={}) #:nodoc:
|
55
59
|
@sender = sender
|
56
60
|
@request = Request.new(topic, data)
|
57
61
|
@response = { data: nil, code: nil }
|
58
62
|
@backend = backend
|
59
|
-
@timeout = timeout || 600
|
60
|
-
@multi_process =
|
61
|
-
@
|
62
|
-
@
|
63
|
+
@timeout = opts[:timeout] || 600
|
64
|
+
@multi_process = opts[:fork] || false
|
65
|
+
@respond = opts[:respond]
|
66
|
+
@response_pipe = IO.pipe
|
67
|
+
@logger_pipe = IO.pipe
|
68
|
+
@command_pipe = IO.pipe
|
63
69
|
@logger = make_logger()
|
70
|
+
@delayed_response = false
|
64
71
|
end
|
65
72
|
|
66
|
-
def receive_data(data)
|
67
|
-
sender, res_data, res_code = JSON.parse(data)
|
73
|
+
def receive_data(data) #:nodoc:
|
74
|
+
sender, res_data, res_code, opts = JSON.parse(data)
|
75
|
+
res_code ||= 200 if @respond
|
68
76
|
write_response(sender, res_data, res_code) if sender && res_code
|
69
77
|
end
|
70
78
|
|
71
79
|
# Called by parent
|
72
|
-
def write_response(sender, data, code)
|
80
|
+
def write_response(sender, data, code) #:nodoc:
|
81
|
+
return unless @respond
|
73
82
|
if code >= 300 && @backend.report_errors?
|
74
83
|
emit_error data, code
|
75
84
|
end
|
76
|
-
|
77
85
|
@backend.send_response(sender, data, code)
|
78
86
|
end
|
79
87
|
|
80
|
-
#
|
81
|
-
def add_listener(topic, &handler)
|
88
|
+
# proxy to base add_listener
|
89
|
+
def add_listener(topic, opts={}, &handler)
|
82
90
|
if @multi_process
|
83
91
|
GLogger.error "Dynamic listeners using add_listener not supported \
|
84
92
|
in forked responder. Ignoring!"
|
@@ -87,6 +95,27 @@ module Gilmour
|
|
87
95
|
@backend.add_listener(topic, &handler)
|
88
96
|
end
|
89
97
|
|
98
|
+
# Proxy to register slot (see Backend#slot for details)
|
99
|
+
def slot(topic, opts={}, &handler)
|
100
|
+
if @multi_process
|
101
|
+
GLogger.error "Dynamic listeners using add_listener not supported \
|
102
|
+
in forked responder. Ignoring!"
|
103
|
+
end
|
104
|
+
|
105
|
+
@backend.slot(topic, opts, &handler)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Proxy to register reply listener (see Backend#reply_to for details)
|
109
|
+
def reply_to(topic, opts={}, &handler)
|
110
|
+
if @multi_process
|
111
|
+
GLogger.error "Dynamic listeners using add_listener not supported \
|
112
|
+
in forked responder. Ignoring!"
|
113
|
+
end
|
114
|
+
|
115
|
+
@backend.reply_to(topic, opts, &handler)
|
116
|
+
end
|
117
|
+
|
118
|
+
|
90
119
|
# Sends a response with _body_ and _code_
|
91
120
|
# If +opts[:now]+ is true, the response is sent immediately,
|
92
121
|
# else it is defered until the handler finishes executing
|
@@ -99,149 +128,140 @@ module Gilmour
|
|
99
128
|
end
|
100
129
|
end
|
101
130
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
131
|
+
# This prohibits sending a response from a reply handler even after
|
132
|
+
# the request execution has finished. This is useful for sending
|
133
|
+
# a response from inside a closure if the handler has to make further
|
134
|
+
# gilmour requests. To send a response later, call respond with the
|
135
|
+
# option "now" as true.
|
136
|
+
def delay_response
|
137
|
+
@delayed_response = true
|
138
|
+
end
|
139
|
+
|
140
|
+
def delayed_response? #:nodoc:
|
141
|
+
@delayed_response
|
142
|
+
end
|
143
|
+
|
144
|
+
def command_relay(reader, waiter) #:nodoc:
|
145
|
+
waiter.add
|
146
|
+
pub_mutex = Mutex.new
|
147
|
+
|
148
|
+
Thread.new do
|
149
|
+
loop do
|
106
150
|
begin
|
107
|
-
data =
|
108
|
-
|
109
|
-
|
151
|
+
data = reader.readline
|
152
|
+
pub_mutex.synchronize do
|
153
|
+
method, args = JSON.parse(data)
|
154
|
+
@backend.send(method.to_sym, *args)
|
155
|
+
end
|
110
156
|
rescue EOFError
|
111
157
|
waiter.done
|
158
|
+
break
|
112
159
|
rescue Exception => e
|
113
160
|
GLogger.debug e.message
|
114
161
|
GLogger.debug e.backtrace
|
115
162
|
end
|
116
|
-
|
117
|
-
|
163
|
+
end
|
164
|
+
end
|
118
165
|
end
|
119
166
|
|
120
167
|
# All logs in forked mode are relayed chr
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
loop
|
168
|
+
def logger_relay(read_logger_pipe, waiter, parent_logger) #:nodoc:
|
169
|
+
waiter.add 1
|
170
|
+
Thread.new do
|
171
|
+
loop do
|
125
172
|
begin
|
126
|
-
data =
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
case msg_grp[0]
|
134
|
-
when 'INFO'
|
135
|
-
parent_logger.info data
|
136
|
-
when 'UNKNOWN'
|
137
|
-
parent_logger.unknown data
|
138
|
-
when 'WARN'
|
139
|
-
parent_logger.warn data
|
140
|
-
when 'ERROR'
|
141
|
-
parent_logger.error data
|
142
|
-
when 'FATAL'
|
143
|
-
parent_logger.fatal data
|
144
|
-
else
|
145
|
-
parent_logger.debug data
|
146
|
-
end
|
147
|
-
else
|
148
|
-
parent_logger.debug msg
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
next
|
153
|
-
end
|
154
|
-
|
155
|
-
parent_logger.debug data
|
173
|
+
data = read_logger_pipe.readline.chomp
|
174
|
+
logdata = JSON.parse(data)
|
175
|
+
meth = logdata['severity'].downcase.to_sym
|
176
|
+
parent_logger.send(meth, logdata['msg'])
|
177
|
+
rescue JSON::ParserError
|
178
|
+
parent_logger.info data
|
179
|
+
next
|
156
180
|
rescue EOFError
|
157
181
|
waiter.done
|
182
|
+
break
|
158
183
|
rescue Exception => e
|
159
184
|
GLogger.error e.message
|
160
185
|
GLogger.error e.backtrace
|
161
186
|
end
|
162
|
-
|
163
|
-
|
187
|
+
end #loop
|
188
|
+
end
|
164
189
|
end
|
165
190
|
|
166
191
|
# Called by parent
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
@read_publish_pipe, @write_publish_pipe = @publish_pipe
|
174
|
-
|
175
|
-
io_reader, io_writer = IO.pipe
|
176
|
-
|
177
|
-
wg = Gilmour::Waiter.new
|
178
|
-
io_threads = []
|
179
|
-
io_threads << child_io_relay(io_reader, wg, @logger)
|
180
|
-
io_threads << pub_relay(wg)
|
192
|
+
def execute(handler) #:nodoc:
|
193
|
+
if !@multi_process
|
194
|
+
_execute(handler)
|
195
|
+
return
|
196
|
+
end
|
197
|
+
GLogger.debug "Executing #{@sender} in forked moode"
|
181
198
|
|
182
|
-
|
183
|
-
|
184
|
-
|
199
|
+
# Create pipes for child communication
|
200
|
+
@read_pipe, @write_pipe = @response_pipe
|
201
|
+
@read_command_pipe, @write_command_pipe = @command_pipe
|
202
|
+
@read_logger_pipe, @write_logger_pipe = @logger_pipe = IO.pipe
|
185
203
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
204
|
+
# setup relay threads
|
205
|
+
wg = Gilmour::Waiter.new
|
206
|
+
relay_threads = []
|
207
|
+
relay_threads << logger_relay(@read_logger_pipe, wg, @logger)
|
208
|
+
relay_threads << command_relay(@read_command_pipe, wg)
|
190
209
|
|
191
|
-
|
210
|
+
pid = Process.fork do
|
211
|
+
@backend.stop
|
212
|
+
EventMachine.stop_event_loop
|
192
213
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
214
|
+
#Close the parent channels in forked process
|
215
|
+
@read_pipe.close
|
216
|
+
@read_command_pipe.close
|
217
|
+
@read_logger_pipe.close
|
197
218
|
|
198
|
-
|
199
|
-
io_writer.close
|
200
|
-
@write_pipe.close
|
201
|
-
@write_publish_pipe.close
|
219
|
+
@response_sent = false
|
202
220
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
221
|
+
# override the logger for the child
|
222
|
+
@logger = child_logger(@write_logger_pipe)
|
223
|
+
_execute(handler)
|
224
|
+
@write_logger_pipe.close
|
225
|
+
end
|
208
226
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
# Set the multi-process mode as false, the child has died anyway.
|
214
|
-
@multi_process = false
|
215
|
-
write_response(@sender, msg, 500)
|
216
|
-
elsif status.exitstatus > 0
|
217
|
-
msg = "Child Process #{pid} exited with status #{status.exitstatus}"
|
218
|
-
logger.error msg
|
219
|
-
# Set the multi-process mode as false, the child has died anyway.
|
220
|
-
@multi_process = false
|
221
|
-
write_response(@sender, msg, 500)
|
222
|
-
end
|
227
|
+
# Cleanup the writers in Parent process.
|
228
|
+
@write_logger_pipe.close
|
229
|
+
@write_pipe.close
|
230
|
+
@write_command_pipe.close
|
223
231
|
|
224
|
-
|
232
|
+
begin
|
233
|
+
receive_data(@read_pipe.readline)
|
234
|
+
rescue EOFError => e
|
235
|
+
logger.debug e.message
|
236
|
+
end
|
225
237
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
238
|
+
pid, status = Process.waitpid2(pid)
|
239
|
+
if !status || status.exitstatus > 0
|
240
|
+
msg = if !status
|
241
|
+
"Child Process #{pid} crashed without status."
|
242
|
+
else
|
243
|
+
"Child Process #{pid} exited with status #{status.exitstatus}"
|
244
|
+
end
|
245
|
+
logger.error msg
|
246
|
+
# Set the multi-process mode as false, the child has died anyway.
|
247
|
+
@multi_process = false
|
248
|
+
write_response(@sender, msg, 500)
|
249
|
+
end
|
231
250
|
|
232
|
-
|
233
|
-
@read_publish_pipe.close
|
234
|
-
io_reader.close unless io_reader.closed?
|
251
|
+
@read_pipe.close
|
235
252
|
|
236
|
-
|
237
|
-
|
253
|
+
# relay cleanup.
|
254
|
+
wg.wait do
|
255
|
+
relay_threads.each { |th| th.kill }
|
238
256
|
end
|
257
|
+
@read_command_pipe.close
|
258
|
+
@read_logger_pipe.close
|
239
259
|
end
|
240
260
|
|
241
261
|
# Publish all errors on gilmour.error
|
242
262
|
# This may or may not have a listener based on the configuration
|
243
263
|
# supplied at setup.
|
244
|
-
def emit_error(message, code = 500, extra = {})
|
264
|
+
def emit_error(message, code = 500, extra = {}) #:nodoc:
|
245
265
|
opts = {
|
246
266
|
topic: @request.topic,
|
247
267
|
request_data: @request.body,
|
@@ -257,11 +277,11 @@ module Gilmour
|
|
257
277
|
end
|
258
278
|
|
259
279
|
# Called by child
|
260
|
-
|
261
|
-
|
280
|
+
def _execute(handler) #:nodoc:
|
281
|
+
ret = nil
|
262
282
|
begin
|
263
283
|
Timeout.timeout(@timeout) do
|
264
|
-
instance_eval(&handler)
|
284
|
+
ret = instance_eval(&handler)
|
265
285
|
end
|
266
286
|
rescue Timeout::Error => e
|
267
287
|
logger.error e.message
|
@@ -274,32 +294,58 @@ module Gilmour
|
|
274
294
|
@response[:code] = 500
|
275
295
|
@response[:data] = e.message
|
276
296
|
end
|
277
|
-
|
297
|
+
@response[:code] ||= 200 if @respond && !delayed_response?
|
278
298
|
send_response if @response[:code]
|
279
299
|
end
|
280
300
|
|
281
|
-
|
282
|
-
|
301
|
+
def call_parent_backend_method(method, *args) #:nodoc:
|
302
|
+
msg = JSON.generate([method, args])
|
303
|
+
@write_command_pipe.write(msg+"\n")
|
304
|
+
@write_command_pipe.flush
|
305
|
+
end
|
306
|
+
|
307
|
+
# Proxy to publish method. See Backend#publish
|
308
|
+
def publish(message, destination, opts = {}, code=nil, &blk)
|
283
309
|
if @multi_process
|
284
310
|
if block_given?
|
285
311
|
GLogger.error "Publish callback not supported in forked responder. Ignoring!"
|
286
|
-
# raise Exception.new("Publish Callback is not supported in forked mode.")
|
287
312
|
end
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
@
|
313
|
+
call_parent_backend_method('publish', message, destination, opts, code)
|
314
|
+
# method = opts[:method] || 'publish'
|
315
|
+
# msg = JSON.generate([method, [message, destination, opts, code]])
|
316
|
+
# @write_command_pipe.write(msg+"\n")
|
317
|
+
# @write_command_pipe.flush
|
292
318
|
elsif block_given?
|
293
|
-
blk = Proc.new
|
294
319
|
@backend.publish(message, destination, opts, &blk)
|
295
320
|
else
|
296
321
|
@backend.publish(message, destination, opts)
|
297
322
|
end
|
298
323
|
end
|
299
324
|
|
325
|
+
# Proxy to request! method. See Backend#request!
|
326
|
+
def request!(message, destination, opts={}, &blk)
|
327
|
+
if @multi_process
|
328
|
+
if block_given?
|
329
|
+
GLogger.error "Publish callback not supported in forked responder. Ignoring!"
|
330
|
+
end
|
331
|
+
call_parent_backend_method('request!', message, destination, opts)
|
332
|
+
else
|
333
|
+
@backend.request!(message, destination, opts, &blk)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Proxy to signal! method. See Backend#signal!
|
338
|
+
def signal!(message, destination, opts={})
|
339
|
+
if @multi_process
|
340
|
+
call_parent_backend_method('signal!', message, destination, opts)
|
341
|
+
else
|
342
|
+
@backend.signal!(message, destination, opts)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
|
300
347
|
# Called by child
|
301
|
-
|
302
|
-
def send_response
|
348
|
+
def send_response #:nodoc:
|
303
349
|
return if @response_sent
|
304
350
|
@response_sent = true
|
305
351
|
|
data/lib/gilmour/waiter.rb
CHANGED
@@ -14,7 +14,7 @@ module Gilmour
|
|
14
14
|
def done
|
15
15
|
synchronize do
|
16
16
|
@count -= 1
|
17
|
-
if @count
|
17
|
+
if @count <= 0
|
18
18
|
@done = true
|
19
19
|
@waiter_c.broadcast
|
20
20
|
end
|
@@ -26,6 +26,10 @@ module Gilmour
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def signal
|
29
|
+
if @count != 0
|
30
|
+
raise 'Cannot use signal alongside add/done'
|
31
|
+
end
|
32
|
+
|
29
33
|
synchronize do
|
30
34
|
@done = true
|
31
35
|
@count = 0
|
@@ -34,31 +38,8 @@ module Gilmour
|
|
34
38
|
end
|
35
39
|
|
36
40
|
def wait(timeout=nil)
|
37
|
-
synchronize
|
38
|
-
while !@done
|
39
|
-
@waiter_c.wait(@waiter_m, timeout)
|
40
|
-
end
|
41
|
-
end
|
41
|
+
synchronize { @waiter_c.wait(@waiter_m, timeout) unless @done }
|
42
42
|
yield if block_given?
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
46
|
-
|
47
|
-
def test
|
48
|
-
wg = Gilmour::Waiter.new
|
49
|
-
wg.add 3
|
50
|
-
|
51
|
-
3.times do
|
52
|
-
Thread.new {
|
53
|
-
t = rand(10000) / 10000.0
|
54
|
-
sleep(t)
|
55
|
-
puts "done\n"
|
56
|
-
wg.done
|
57
|
-
}
|
58
|
-
end
|
59
|
-
|
60
|
-
wg.wait do
|
61
|
-
puts "All jobs done"
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
data/test/spec/helpers/common.rb
CHANGED
@@ -1,26 +1,6 @@
|
|
1
|
+
require '../lib/gilmour/waiter'
|
1
2
|
|
2
|
-
|
3
|
-
def initialize
|
4
|
-
@done = false
|
5
|
-
@waiter_m = Mutex.new
|
6
|
-
@waiter_c = ConditionVariable.new
|
7
|
-
end
|
8
|
-
|
9
|
-
def synchronize(&blk)
|
10
|
-
@waiter_m.synchronize(&blk)
|
11
|
-
end
|
12
|
-
|
13
|
-
def signal
|
14
|
-
synchronize do
|
15
|
-
@done = true
|
16
|
-
@waiter_c.signal
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def wait(timeout=nil)
|
21
|
-
synchronize { @waiter_c.wait(@waiter_m, timeout) unless @done }
|
22
|
-
end
|
23
|
-
end
|
3
|
+
Waiter = Gilmour::Waiter
|
24
4
|
|
25
5
|
RSpec.configure do |config|
|
26
6
|
config.expect_with :rspec do |c|
|
@@ -25,12 +25,11 @@ def redis_wildcard_options
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def redis_publish_async(options, message, key)
|
28
|
-
|
29
|
-
|
28
|
+
redis = EM::Hiredis.connect
|
29
|
+
EM.defer do
|
30
30
|
payload, _ = Gilmour::Protocol.create_request(message)
|
31
31
|
redis.publish(key, payload)
|
32
32
|
end
|
33
|
-
EM.defer(operation)
|
34
33
|
end
|
35
34
|
|
36
35
|
def redis_send_and_recv(options, message, key)
|