gilmour 0.3.4 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e019128d85d1f18f07ee9c90a8444950e17e77e4
|
4
|
+
data.tar.gz: 424ecd2839a45bd7f89ed8e5abf9c256315200bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bd6a702576f5cc27f791a6b25f6520dd08ae3212fa1ab1adf056976e8086f44de057e37aef55bbac3ae9ad278efe690b23adadf3a285d1600c8c39f870fd012
|
7
|
+
data.tar.gz: 2540aabffac9e90f958d28170ce143fd5194efb01ed77ad5dfbdc2d0e17e6337c12191491f7700f04b8024c1e178d4950b1268f5e3e91949c3a18302d5d948b8
|
data/.travis.yml
CHANGED
@@ -14,3 +14,4 @@ script:
|
|
14
14
|
- bundle exec rspec spec/test_service_base.rb -b --format documentation
|
15
15
|
- bundle exec rspec spec/test_subscriber_redis.rb -b --format documentation
|
16
16
|
- bundle exec rspec spec/test_subscriber_redis_forked.rb -b --format documentation
|
17
|
+
- bundle exec rspec spec/test_waiter.rb -b --format documentation
|
data/README.md
CHANGED
@@ -3,7 +3,35 @@
|
|
3
3
|
Gilmour is a framework for writing micro-services that exchange data over
|
4
4
|
non-http transports. Currently the supported backend is Redis PubSub.
|
5
5
|
Redis pubsub channels are used like "routes".
|
6
|
-
|
6
|
+
Gilmour started off simply as a non-http alternative to Sinatra, but has grown into a feature rich microservices communication library and framework.
|
7
|
+
|
8
|
+
## Patterns
|
9
|
+
|
10
|
+
Gilmour enables two patterns of communication:
|
11
|
+
|
12
|
+
### The request-response pattern
|
13
|
+
This is the most common pattern found in microservice architectures. An entity
|
14
|
+
sends a `request`, and a subscriber processes it and sends a `reply`. See
|
15
|
+
`examples/fibonacci.rb` as an example. Gilmour also allows horizontal scaling
|
16
|
+
by using __exclusion groups__. When there are multiple instances of a subscriber running, only one subscriber from an exclusion group processes that request.
|
17
|
+
Eg, if you run `container.rb` from the examples multiple times simultaneously, running `echoclient.rb` or `forkechoclient.rb` will produce the same result, irrespective of how many instances of the container are running. (More of containers later).
|
18
|
+
|
19
|
+
### The signal-slot pattern
|
20
|
+
A less common, but very powerful pattern is event driven service design, or the signal-slot pattern (a term borrowed from the Qt library). One service emits or signals events, which are picked up by one or more subscribers and processed. None of the subscribers send a reply, because a reply really has no meaning in this context. See `examples/signal_slot.rb`.
|
21
|
+
|
22
|
+
Which pattern to chose, is primarily dependent on the ownership of failure. If the subscribers own the failures they encounter, then you should consider the signal-slot pattern.
|
23
|
+
|
24
|
+
It is also possible to have slots as well as reply subscribers for a given message. Gilmour provides a utility `broadcast` which internally does a signal and a request.
|
25
|
+
|
26
|
+
## Composition
|
27
|
+
|
28
|
+
Microservices increase the granularity of our services oriented architectures. Borrowing from unix philosohy, they should do one thing and do it well. However, for this to be really useful, there should be a facility such as is provided by unix shells. Unix shells allow the composition of small commands using the following methods
|
29
|
+
|
30
|
+
* Composition: `cmd1 | cmd2 | cmd2`
|
31
|
+
* AndAnd: `cmd1 && cmd2 && cmd3`
|
32
|
+
* Batch: `cmd1; cmd2; cmd3 > out` or `(cmd1; cmd2; cmd3) > out`
|
33
|
+
|
34
|
+
Also, you should be able to use these methods of composition in any combination and also in a nexted manner - `(cmd1 | cmd2) && cmd3`. Gilmour enables you to do just that. See `examples/composition.rb`.
|
7
35
|
|
8
36
|
## Protocol
|
9
37
|
|
@@ -13,17 +41,44 @@ The structure of the payload is a simple JSON as shown below:
|
|
13
41
|
{
|
14
42
|
data: The actual payload,
|
15
43
|
sender: The origin of the request (unique for each request),
|
16
|
-
code: The
|
44
|
+
code: The response code if this is a response. This uses HTTP error codes
|
17
45
|
}
|
18
46
|
|
47
|
+
Gilmour has builtin support for error publishing. The error channel is separate from the response channels and everything that is put on the error channel is prefixed by the per-request unique sender uuid, which makes debugging easier. The error packet protocol is
|
48
|
+
|
49
|
+
{
|
50
|
+
code: response code (non-200)
|
51
|
+
sender: the "sender" from the request
|
52
|
+
topic: the topic to which the request was sent
|
53
|
+
request_data: the request payload
|
54
|
+
userdata: implementation dependent debug infomation
|
55
|
+
backtrace: the backtrace of the error
|
56
|
+
timestamp: the timestamp of the error
|
57
|
+
}
|
58
|
+
|
59
|
+
## Redis backend conventions
|
60
|
+
|
19
61
|
The `sender` field actually represents a unique sender key. Any reponse that
|
20
62
|
is to be sent to the request is sent on the "reservered" topic
|
21
63
|
`response.<sender>` on the same exchange.
|
22
64
|
|
23
|
-
|
65
|
+
For the request-reply pattern, the topic is prefixed with `gilmour.request`.
|
66
|
+
For the signal-slot pettern, the topic is prefixed with `gilmour.slot`.
|
67
|
+
|
68
|
+
The topic on which errors are published is `gilmour.error`.
|
69
|
+
|
70
|
+
## Auto-loading
|
71
|
+
|
72
|
+
Gilmour code can be structured such that the subscribers are auto-loaded from the filesystem. This enables creation of containers which house multiple microservices, with a choice of which ones to enable. See `examples/container.rb`.
|
73
|
+
|
74
|
+
## Health monitoring
|
75
|
+
|
76
|
+
Gilmour supports health monitoring heartbeats. Every gilmour process, which has health monitoring enabled, listens for hearbeat pings and responds accordingly. External monitors can use this to monitor the health. [The health bulletin](https://github.com/gilmour-libs/health-bulletin) and [the health-tools]( https://github.com/gilmour-libs/health-tools) are examples of such tools.
|
24
77
|
|
25
|
-
|
78
|
+
## More Usage Examples
|
26
79
|
|
80
|
+
See the `examples` directory
|
81
|
+
|
27
82
|
## Specs
|
28
83
|
|
29
84
|
To run the specs, set `REDIS_HOST` (default is localhost) and `REDIS_PORT` (default is 6379)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'gilmour'
|
2
|
+
|
3
|
+
class Server
|
4
|
+
include Gilmour::Base
|
5
|
+
end
|
6
|
+
server = Server.new
|
7
|
+
gilmour = server.enable_backend('redis')
|
8
|
+
|
9
|
+
gilmour.reply_to 'one' do
|
10
|
+
respond request.body.merge({'one' => 'foo'})
|
11
|
+
end
|
12
|
+
|
13
|
+
gilmour.reply_to 'two' do
|
14
|
+
respond request.body.merge({'two' => 'foo'})
|
15
|
+
end
|
16
|
+
|
17
|
+
gilmour.reply_to 'badtwo' do
|
18
|
+
respond request.body.merge({'badtwo' => 'foo'}), 500
|
19
|
+
end
|
20
|
+
|
21
|
+
gilmour.reply_to 'three' do
|
22
|
+
respond request.body.merge({'three' => 'foo'})
|
23
|
+
end
|
24
|
+
|
25
|
+
composed = gilmour.compose([{topic: 'one'}, {topic: 'two'},
|
26
|
+
{topic: 'three', message: {'anotherthree' => 'anotherthree'}}])
|
27
|
+
compose_waiter = Gilmour::Waiter.new
|
28
|
+
composed.execute({zero: 'foo'}) do |data, code|
|
29
|
+
puts "composition:"
|
30
|
+
puts data # data will have the keys 'one', 'two', 'three' and 'anotherthree'
|
31
|
+
compose_waiter.signal
|
32
|
+
end
|
33
|
+
compose_waiter.wait
|
34
|
+
|
35
|
+
andand = gilmour.andand([{topic: 'one'}, {topic: 'two'},
|
36
|
+
{topic: 'three', message: {'anotherthree' => 'anotherthree'}}])
|
37
|
+
andand_waiter = Gilmour::Waiter.new
|
38
|
+
andand.execute do |data, code|
|
39
|
+
puts "\nandand:"
|
40
|
+
puts data # data will have the keys 'three' and 'anotherthree'
|
41
|
+
andand_waiter.signal
|
42
|
+
end
|
43
|
+
andand_waiter.wait
|
44
|
+
|
45
|
+
|
46
|
+
error_andand = gilmour.andand([{topic: 'one'}, {topic: 'badtwo'},
|
47
|
+
{topic: 'three', message: {'anotherthree' => 'anotherthree'}}])
|
48
|
+
error_andand_waiter = Gilmour::Waiter.new
|
49
|
+
continuation = nil
|
50
|
+
error_andand.execute do |data, code, c|
|
51
|
+
continuation = c # the part of the pipeline that was not executed
|
52
|
+
puts "\nerror_andand:\n code - #{code}" # should be 500 fron badtwo
|
53
|
+
puts "error_andand:\n data - #{data}" # this will have keys one and badtwo
|
54
|
+
error_andand_waiter.signal
|
55
|
+
end
|
56
|
+
error_andand_waiter.wait
|
57
|
+
|
58
|
+
continuation_waiter = Gilmour::Waiter.new
|
59
|
+
continuation.execute do |data, code|
|
60
|
+
puts "\ncontinuation:"
|
61
|
+
puts data # This will have keys three and anotherthree
|
62
|
+
continuation_waiter.signal
|
63
|
+
end
|
64
|
+
continuation_waiter.wait
|
65
|
+
|
66
|
+
|
67
|
+
batch = gilmour.batch([{topic: 'one'}, {topic: 'badtwo'},
|
68
|
+
{topic: 'three', message: {'anotherthree' => 'anotherthree'}}])
|
69
|
+
batch_waiter = Gilmour::Waiter.new
|
70
|
+
batch.execute do |data, code|
|
71
|
+
puts "\nbatch:"
|
72
|
+
puts data # this will have keys three and anotherthree (wont abort on badtwo)
|
73
|
+
batch_waiter.signal
|
74
|
+
end
|
75
|
+
batch_waiter.wait
|
76
|
+
|
77
|
+
batch_record = gilmour.batch([{topic: 'one'}, {topic: 'badtwo'},
|
78
|
+
{topic: 'three', message: {'anotherthree' => 'anotherthree'}}],
|
79
|
+
true) # true will record all responses in an array
|
80
|
+
batch_record_waiter = Gilmour::Waiter.new
|
81
|
+
batch_record.execute do |data, code|
|
82
|
+
puts "\nbatch with record:"
|
83
|
+
puts data # This is an array
|
84
|
+
batch_record_waiter.signal
|
85
|
+
end
|
86
|
+
batch_record_waiter.wait
|
87
|
+
|
88
|
+
t_andand = gilmour.andand([{topic: 'one'}, {topic: 'two'}])
|
89
|
+
# Use the above composition inside another
|
90
|
+
compose = gilmour.compose([t_andand, {topic: 'three'}])
|
91
|
+
combination_waiter = Gilmour::Waiter.new
|
92
|
+
compose.execute do |data, code|
|
93
|
+
puts "\nnested composition"
|
94
|
+
puts data # will have keys two and three
|
95
|
+
combination_waiter.signal
|
96
|
+
end
|
97
|
+
combination_waiter.wait
|
98
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Example of running as a container with subscribers
|
3
|
+
# Run ruby server.rb first, followed by echoclient.rb
|
4
|
+
require 'gilmour'
|
5
|
+
|
6
|
+
class EventServer
|
7
|
+
include Gilmour::Base
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
enable_backend('redis')
|
11
|
+
registered_subscribers.each do |sub|
|
12
|
+
sub.backend = 'redis'
|
13
|
+
end
|
14
|
+
start(true)
|
15
|
+
end
|
16
|
+
load_all './subscribers'
|
17
|
+
end
|
18
|
+
|
19
|
+
EventServer.new
|
data/examples/echoclient.rb
CHANGED
@@ -1,19 +1,23 @@
|
|
1
1
|
require 'securerandom'
|
2
|
-
require 'gilmour
|
2
|
+
require 'gilmour'
|
3
|
+
|
4
|
+
class Server
|
5
|
+
include Gilmour::Base
|
6
|
+
end
|
7
|
+
|
3
8
|
|
4
9
|
def redis_send_and_recv(message, key)
|
5
|
-
|
6
|
-
|
10
|
+
server = Server.new
|
11
|
+
gilmour = server.enable_backend('redis')
|
7
12
|
count = 0
|
8
13
|
loop do
|
9
|
-
waiter =
|
10
|
-
|
11
|
-
redis.publish(count, newkey) do |data, code|
|
14
|
+
waiter = Gilmour::Waiter.new
|
15
|
+
gilmour.request!(count, key) do |data, code|
|
12
16
|
puts "Client got response: #{code}: #{data}"
|
13
|
-
waiter.
|
17
|
+
waiter.signal
|
14
18
|
end
|
15
19
|
count = count + 1
|
16
|
-
waiter.
|
20
|
+
waiter.wait
|
17
21
|
sleep 1
|
18
22
|
end
|
19
23
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'gilmour'
|
2
|
+
|
3
|
+
class Server
|
4
|
+
include Gilmour::Base
|
5
|
+
end
|
6
|
+
server = Server.new
|
7
|
+
gilmour = server.enable_backend('redis')
|
8
|
+
|
9
|
+
gilmour.reply_to "fib", excl_group: 'fib', timeout: 1 do
|
10
|
+
first = request.body['first']
|
11
|
+
second = request.body['second']
|
12
|
+
respond('next' => first + second)
|
13
|
+
end
|
14
|
+
|
15
|
+
handler = proc do |first, second|
|
16
|
+
first ||= 1
|
17
|
+
second ||= 1
|
18
|
+
gilmour.request!({ 'first' => first, 'second' => second }, 'fib',
|
19
|
+
timeout: 5) do |resp, code|
|
20
|
+
if code != 200
|
21
|
+
$stderr.puts "Something went wrong in the response! Aborting"
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
next_val = resp['next']
|
25
|
+
puts next_val
|
26
|
+
EM.add_timer(1) { handler.call(second, resp['next']) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
EM.next_tick { handler.call }
|
31
|
+
server.start true
|
32
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'gilmour'
|
3
|
+
|
4
|
+
class Server
|
5
|
+
include Gilmour::Base
|
6
|
+
end
|
7
|
+
|
8
|
+
def redis_send_and_recv(message, key)
|
9
|
+
server = Server.new
|
10
|
+
gilmour = server.enable_backend('redis')
|
11
|
+
loop do
|
12
|
+
waiter = Gilmour::Waiter.new
|
13
|
+
puts "Process: Sending: #{key}"
|
14
|
+
gilmour.request!(message, key) do |data, code|
|
15
|
+
puts "Process: Received: #{data}"
|
16
|
+
waiter.signal
|
17
|
+
end
|
18
|
+
waiter.wait
|
19
|
+
sleep 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
redis_send_and_recv('Ping', 'forkecho')
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Run this as LOG_LEVEL=info ruby relay.rb
|
2
|
+
require 'gilmour'
|
3
|
+
|
4
|
+
class Server
|
5
|
+
include Gilmour::Base
|
6
|
+
end
|
7
|
+
|
8
|
+
server = Server.new
|
9
|
+
gilmour = server.enable_backend('redis')
|
10
|
+
|
11
|
+
waiter = Gilmour::Waiter.new
|
12
|
+
|
13
|
+
waiter.add
|
14
|
+
gilmour.slot "user.added" do
|
15
|
+
# This one will send out email notifications
|
16
|
+
logger.info "Sent email notification for -"
|
17
|
+
logger.info request.body
|
18
|
+
waiter.done
|
19
|
+
end
|
20
|
+
|
21
|
+
waiter.add
|
22
|
+
gilmour.slot "user.added" do
|
23
|
+
# This one will send out push notifications
|
24
|
+
logger.info "Sent push notification for -"
|
25
|
+
logger.info request.body
|
26
|
+
waiter.done
|
27
|
+
end
|
28
|
+
|
29
|
+
EM.next_tick {
|
30
|
+
gilmour.signal!({ username: 'foo', email: 'foo@bar.com' }, 'user.added')
|
31
|
+
}
|
32
|
+
|
33
|
+
waiter.wait
|
34
|
+
|
35
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class EchoSubscriber < EventServer
|
2
|
+
reply_to 'echo', excl_group: 'echo' do
|
3
|
+
if request.body == 'quit'
|
4
|
+
respond nil
|
5
|
+
else
|
6
|
+
$stderr.puts request.body
|
7
|
+
respond "#{request.topic}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
reply_to 'forkecho', fork:true, excl_group: 'forkecho' do
|
12
|
+
if request.body == 'quit'
|
13
|
+
respond nil
|
14
|
+
else
|
15
|
+
$stderr.puts "Forked response. pid: #{Process.pid}"
|
16
|
+
respond "#{request.topic}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
|
@@ -3,6 +3,7 @@ require 'socket'
|
|
3
3
|
require 'securerandom'
|
4
4
|
|
5
5
|
require_relative '../protocol'
|
6
|
+
require_relative '../composers'
|
6
7
|
|
7
8
|
module Gilmour
|
8
9
|
ErrorChannel = "gilmour.error"
|
@@ -12,55 +13,59 @@ module Gilmour
|
|
12
13
|
SUPPORTED_BACKENDS = %w(redis)
|
13
14
|
@@registry = {}
|
14
15
|
|
15
|
-
|
16
|
+
include Gilmour::Composers
|
17
|
+
attr_accessor :broadcast_errors
|
18
|
+
|
19
|
+
def report_errors? #:nodoc:
|
16
20
|
#Override this method to adjust if you want errors to be reported.
|
17
|
-
return
|
21
|
+
return false
|
18
22
|
end
|
19
23
|
|
20
|
-
def register_health_check
|
24
|
+
def register_health_check #:nodoc:
|
21
25
|
raise NotImplementedError.new
|
22
26
|
end
|
23
27
|
|
24
|
-
def unregister_health_check
|
28
|
+
def unregister_health_check #:nodoc:
|
25
29
|
raise NotImplementedError.new
|
26
30
|
end
|
27
31
|
|
28
|
-
def self.implements(backend_name)
|
32
|
+
def self.implements(backend_name) #:nodoc:
|
29
33
|
@@registry[backend_name] = self
|
30
34
|
end
|
31
35
|
|
32
|
-
def self.get(backend_name)
|
36
|
+
def self.get(backend_name) #:nodoc:
|
33
37
|
@@registry[backend_name]
|
34
38
|
end
|
35
39
|
|
36
|
-
# :nodoc:
|
37
40
|
# This should be implemented by the derived class
|
38
41
|
# subscriptions is a hash in the format -
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
42
|
+
# { topic => [handler1, handler2, ...],
|
43
|
+
# topic2 => [handler3, handler4, ...],
|
44
|
+
# ...
|
45
|
+
# }
|
43
46
|
# where handler is a hash
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
def setup_subscribers(subscriptions)
|
47
|
+
# { :handler => handler_proc,
|
48
|
+
# :subscriber => subscriber_derived_class
|
49
|
+
# }
|
50
|
+
def setup_subscribers(subscriptions) #:nodoc:
|
48
51
|
end
|
49
52
|
|
53
|
+
# This is the underlying method for all publishes. Use only if you know
|
54
|
+
# what you are doing. Use the request! and signal! methods instead.
|
50
55
|
# Sends a message
|
51
56
|
# If optional block is given, it will be executed when a response is received
|
52
57
|
# or if timeout occurs
|
53
58
|
# +message+:: The body of the message (any object that is serialisable)
|
54
59
|
# +destination+:: The channel to post to
|
55
|
-
# +opts+::
|
56
|
-
#
|
60
|
+
# +opts+:: Options
|
61
|
+
# timeout:: Sender side timeout
|
57
62
|
#
|
58
63
|
def publish(message, destination, opts = {}, code = 0, &blk)
|
59
64
|
payload, sender = Gilmour::Protocol.create_request(message, code)
|
60
65
|
|
61
66
|
EM.defer do # Because publish can be called from outside the event loop
|
62
67
|
begin
|
63
|
-
|
68
|
+
send_message(sender, destination, payload, opts, &blk)
|
64
69
|
rescue Exception => e
|
65
70
|
GLogger.debug e.message
|
66
71
|
GLogger.debug e.backtrace
|
@@ -69,32 +74,149 @@ module Gilmour
|
|
69
74
|
sender
|
70
75
|
end
|
71
76
|
|
77
|
+
def request_destination(dest) #:nodoc:
|
78
|
+
'gilmour.request.' + dest
|
79
|
+
end
|
80
|
+
|
81
|
+
def slot_destination(dest) #:nodoc:
|
82
|
+
'gilmour.slot.' + dest
|
83
|
+
end
|
84
|
+
|
85
|
+
# Sends a request
|
86
|
+
# If optional block is given, it will be executed when a response is received
|
87
|
+
# or if timeout occurs. You usually want to provide this (otherwise consider
|
88
|
+
# using slots)
|
89
|
+
# Params:
|
90
|
+
# +message+:: The body of the message (any object that is serialisable)
|
91
|
+
# +destination+:: The channel to post to
|
92
|
+
# +opts+:: Options
|
93
|
+
# timeout:: Sender side timeout
|
94
|
+
# confirm_subscriber:: Confirms that active subscriber exists,
|
95
|
+
# else calls blk with 404 error code.
|
96
|
+
def request!(message, dest, opts = {}, &blk)
|
97
|
+
opts[:confirm_subscriber] = true
|
98
|
+
publish(message, request_destination(dest), opts, 0, &blk)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Emits a signal
|
102
|
+
# Params:
|
103
|
+
# +message+:: The body of the message (any object that is serialisable)
|
104
|
+
# +destination+:: The channel to post to
|
105
|
+
def signal!(message, dest, opts = {})
|
106
|
+
GLogger.error('Signal cannot have a callback. Ignoring!') if block_given?
|
107
|
+
publish(message, slot_destination(dest), opts)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Emits a signal and sends a request
|
111
|
+
def broadcast(message, destination, opts = {}, code = 0, &blk)
|
112
|
+
request(message, destination, opts, code, &blk)
|
113
|
+
signal(message, destination, opts)
|
114
|
+
end
|
115
|
+
|
72
116
|
def emit_error(message)
|
73
117
|
raise NotImplementedError.new
|
74
118
|
end
|
75
119
|
|
76
120
|
# Adds a new handler for the given _topic_
|
77
|
-
def add_listener(topic, &
|
121
|
+
def add_listener(topic, opts={}, &blk)
|
78
122
|
raise "Not implemented by child class"
|
79
123
|
end
|
80
124
|
|
125
|
+
def listeners(topic) #:nodoc:
|
126
|
+
raise NotImplementedError.new
|
127
|
+
end
|
128
|
+
|
129
|
+
def excl_dups?(topic, opts) #:nodoc:
|
130
|
+
group = exclusive_group(opts)
|
131
|
+
existing = listeners(topic).select { |l| exclusive_group(l) == group }
|
132
|
+
!existing.empty?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Sets up a reply listener
|
136
|
+
# Params:
|
137
|
+
# +topic+:: The topic to listen on
|
138
|
+
# +options+:: Options Hash
|
139
|
+
# timeout:: a 504 error code is sent after this time and the
|
140
|
+
# execution for the request is terminated
|
141
|
+
# fork:: The request will be processed in a forked process.
|
142
|
+
# useful for long running executions
|
143
|
+
# excl_group:: The exclustion group for this handler (see README for explanation)
|
144
|
+
# +blk+:: The block to the executed for the request
|
145
|
+
def reply_to(topic, options={}, &blk)
|
146
|
+
opts = options.dup
|
147
|
+
group = exclusive_group(opts)
|
148
|
+
if group.empty?
|
149
|
+
group = "_default"
|
150
|
+
opts[:excl_group] = group
|
151
|
+
GLogger.warn("Using default exclusion group for #{topic}")
|
152
|
+
end
|
153
|
+
req_topic = request_destination(topic)
|
154
|
+
if excl_dups?(req_topic, opts)
|
155
|
+
raise RuntimeError.new("Duplicate reply handler for #{topic}:#{group}")
|
156
|
+
end
|
157
|
+
opts[:type] = :reply
|
158
|
+
opts[:excl] = true
|
159
|
+
add_listener(req_topic, opts, &blk)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Sets up a slot listener
|
163
|
+
# Params:
|
164
|
+
# +topic+:: The topic to listen on
|
165
|
+
# +options+:: Options hash
|
166
|
+
# timeout:: a 504 error code is sent after this time and the
|
167
|
+
# execution for the request is terminated
|
168
|
+
# fork:: The request will be processed in a forked process.
|
169
|
+
# useful for long running executions
|
170
|
+
# +blk+:: The block to the executed for the request
|
171
|
+
def slot(topic, options={}, &blk)
|
172
|
+
opts = options.dup
|
173
|
+
stopic = slot_destination(topic)
|
174
|
+
if opts[:excl] && excl_dups?(stopic, opts)
|
175
|
+
raise RuntimeError.new("Duplicate reply handler for #{topic}:#{group}")
|
176
|
+
end
|
177
|
+
opts[:type] = :slot
|
178
|
+
#TODO: Check whether topic has a registered subscriber class?
|
179
|
+
# or leave it to a linter??
|
180
|
+
add_listener(stopic, opts, &blk)
|
181
|
+
end
|
182
|
+
|
81
183
|
# Removes existing _handler_ for the _topic_
|
82
|
-
|
184
|
+
# Use only if you have registered the handler with add_listener
|
185
|
+
# or listen_to
|
186
|
+
def remove_listener(topic, handler)
|
83
187
|
raise "Not implemented by child class"
|
84
188
|
end
|
85
189
|
|
86
|
-
|
190
|
+
# Removes existing slot handler for the _topic_
|
191
|
+
def remove_slot(topic, handler)
|
192
|
+
remove_listener(slot_destination(topic), handler)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Removes existing reply handler for the _topic_
|
196
|
+
def remove_reply(topic, handler)
|
197
|
+
remove_listener(request_destination(topic), handler)
|
198
|
+
end
|
199
|
+
|
200
|
+
def acquire_ex_lock(sender) #:nodoc:
|
87
201
|
raise "Not implemented by child class"
|
88
202
|
end
|
89
203
|
|
90
|
-
def send_response(sender, body, code)
|
204
|
+
def send_response(sender, body, code) #:nodoc:
|
91
205
|
raise "Not implemented by child class"
|
92
206
|
end
|
93
207
|
|
94
|
-
def
|
208
|
+
def exclusive_group(sub) #:nodoc:
|
209
|
+
(sub[:excl_group] || sub[:subscriber]).to_s
|
210
|
+
end
|
211
|
+
|
212
|
+
def execute_handler(topic, payload, sub) #:nodoc:
|
95
213
|
data, sender = Gilmour::Protocol.parse_request(payload)
|
96
214
|
if sub[:exclusive]
|
97
|
-
|
215
|
+
group = exclusive_group(sub)
|
216
|
+
if group.empty?
|
217
|
+
raise RuntimeError.new("Exclusive flag without group encountered!")
|
218
|
+
end
|
219
|
+
lock_key = sender + group
|
98
220
|
acquire_ex_lock(lock_key) { _execute_handler(topic, data, sender, sub) }
|
99
221
|
else
|
100
222
|
_execute_handler(topic, data, sender, sub)
|
@@ -104,29 +226,32 @@ module Gilmour
|
|
104
226
|
GLogger.debug e.backtrace
|
105
227
|
end
|
106
228
|
|
107
|
-
def _execute_handler(topic, data, sender, sub)
|
229
|
+
def _execute_handler(topic, data, sender, sub) #:nodoc:
|
230
|
+
respond = (sub[:type] != :slot)
|
108
231
|
Gilmour::Responder.new(
|
109
|
-
sender, topic, data, self, sub[:timeout],
|
232
|
+
sender, topic, data, self, timeout: sub[:timeout],
|
233
|
+
fork: sub[:fork], respond: respond
|
110
234
|
).execute(sub[:handler])
|
111
235
|
rescue Exception => e
|
112
236
|
GLogger.debug e.message
|
113
237
|
GLogger.debug e.backtrace
|
114
238
|
end
|
115
239
|
|
116
|
-
def
|
240
|
+
def send_message #:nodoc:
|
117
241
|
raise "Not implemented by child class"
|
118
242
|
end
|
119
243
|
|
120
|
-
def self.load_backend(name)
|
244
|
+
def self.load_backend(name) #:nodoc:
|
121
245
|
require_relative name
|
122
246
|
end
|
123
247
|
|
124
|
-
def self.load_all_backends
|
248
|
+
def self.load_all_backends #:nodoc:
|
125
249
|
SUPPORTED_BACKENDS.each do |f|
|
126
250
|
load_backend f
|
127
251
|
end
|
128
252
|
end
|
129
253
|
|
254
|
+
# stop the backend
|
130
255
|
def stop(sender, body, code)
|
131
256
|
raise "Not implemented by child class"
|
132
257
|
end
|