gilmour 0.2.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ed168f421b891494b1f49ad0a572009604d4fe1c
4
+ data.tar.gz: 7a9fad0b5dfc987d2fd08cb3f19b3d611fd3938a
5
+ SHA512:
6
+ metadata.gz: acf59c9b55719ae57545085728705a1d300485e6a7c9d539655807eb1055a7cb826a2255f4617da775cb1853b9d61a0185bd5cddbdcb272f3fd011b5d2e147f1
7
+ data.tar.gz: 0ca69b252a271f54da89da5fdd6749ab9f655262f65c9593e2c7253e29605cb98695c6233def5568e464a48ab823bd0832b7daac7553cf8e15752064dd03afba
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .ruby-version
2
+ *.rdb
3
+ *.swp
4
+ *.gem
5
+
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.1.5
5
+
6
+ services:
7
+ - redis-server
8
+
9
+ install:
10
+ - bundle install
11
+
12
+ script:
13
+ - cd test/
14
+ - bundle exec rspec spec/test_service_base.rb -b --format documentation
15
+ - bundle exec rspec spec/test_subscriber_redis.rb -b --format documentation
16
+ - bundle exec rspec spec/test_subscriber_redis_forked.rb -b --format documentation
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2013 Aditya Godbole
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Gilmour
2
+
3
+ Gilmour is a framework for writing micro-services that exchange data over
4
+ non-http transports. Currently the supported backend is Redis PubSub.
5
+ Redis pubsub channels are used like "routes".
6
+ The DSL provided is similar to Sinatra.
7
+
8
+ ## Protocol
9
+
10
+ Gilmour uses it's own simple protocol to send request and response "headers".
11
+ The structure of the payload is a simple JSON as shown below:
12
+
13
+ {
14
+ data: The actual payload,
15
+ sender: The origin of the request (unique for each request),
16
+ code: The respoonse code if this is a response
17
+ }
18
+
19
+ The `sender` field actually represents a unique sender key. Any reponse that
20
+ is to be sent to the request is sent on the "reservered" topic
21
+ `response.<sender>` on the same exchange.
22
+
23
+ ## Usage Examples
24
+
25
+ See the `examples` directory for examples of usage
26
+
27
+ ## Specs
28
+
29
+ To run the specs, set `REDIS_HOST` (default is localhost) and `REDIS_PORT` (default is 6379)
30
+ to point to your rabbitmq or redis server.
31
+ Then, from within the `test` directory, run `rspec spec/*`
32
+
@@ -0,0 +1,22 @@
1
+ require 'securerandom'
2
+ require 'gilmour/backends/redis'
3
+
4
+ def redis_send_and_recv(message, key)
5
+ redis = Gilmour::RedisBackend.new({})
6
+ redis.setup_subscribers({})
7
+ count = 0
8
+ loop do
9
+ waiter = Thread.new { loop { sleep 1 } }
10
+ newkey = "#{key}.#{SecureRandom.hex(2)}"
11
+ redis.publish(count, newkey) do |data, code|
12
+ puts "Client got response: #{code}: #{data}"
13
+ waiter.kill
14
+ end
15
+ count = count + 1
16
+ waiter.join
17
+ sleep 1
18
+ end
19
+ end
20
+
21
+ redis_send_and_recv('Ping', 'echo')
22
+
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ require 'gilmour'
3
+
4
+ class EventServer
5
+ include Gilmour::Base
6
+
7
+ def initialize
8
+ backend = 'redis'
9
+ enable_backend(backend, { })
10
+ registered_subscribers.each do |sub|
11
+ sub.backend = backend
12
+ end
13
+ $stderr.puts "Starting server. To see messaging in action run clients."
14
+ start(true)
15
+ end
16
+ end
17
+
18
+ class EchoSubscriber < EventServer
19
+ # Passing second parameter as true makes only one instance of this handler handle a request
20
+ listen_to 'echo.*', {"exclusive" => true} do
21
+ if request.body == 'Palmolive'
22
+ respond nil
23
+ else
24
+ $stderr.puts request.body
25
+ respond "#{request.topic}"
26
+ end
27
+ end
28
+ end
29
+
30
+ class FibonacciSubscriber < EventServer
31
+ class << self
32
+ attr_accessor :last
33
+ end
34
+
35
+ listen_to 'fib.next' do
36
+ old = FibonacciSubscriber.last
37
+ FibonacciSubscriber.last = new = request.body
38
+ respond(old + new)
39
+ end
40
+
41
+ listen_to 'fib.init' do
42
+ FibonacciSubscriber.last = request.body
43
+ end
44
+
45
+ end
46
+
47
+ EventServer.new
@@ -0,0 +1,40 @@
1
+ require 'securerandom'
2
+ require 'gilmour/backends/redis'
3
+
4
+ def redis_send_and_recv(message, key, ident)
5
+ redis = Gilmour::RedisBackend.new({})
6
+ redis.setup_subscribers({})
7
+ #loop do
8
+ waiter = Thread.new { loop { sleep 1 } }
9
+ newkey = "#{key}.#{SecureRandom.hex(2)}"
10
+ puts "Process: #{ident} Sending: #{newkey}"
11
+ redis.publish(ident, newkey) do |data, code|
12
+ puts "Process: #{ident} Received: #{data}"
13
+ waiter.kill
14
+ end
15
+ waiter.join
16
+ #end
17
+ end
18
+
19
+ def fork_and_run(num)
20
+ pid_array = []
21
+
22
+ num.times do |i|
23
+ pid = Process.fork do
24
+ puts "Process #{i}"
25
+ yield i
26
+ end
27
+
28
+ pid_array.push(pid)
29
+ end
30
+
31
+ pid_array.each do |pid|
32
+ Process.waitpid(pid)
33
+ end
34
+
35
+ end
36
+
37
+ fork_and_run(5) do |i|
38
+ # Start echo server first
39
+ redis_send_and_recv('Ping', 'echo', i)
40
+ end
data/gilmour.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "./version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "gilmour"
7
+ s.version = Gilmour::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Aditya Godbole"]
10
+ s.email = ["code.aa@gdbl.me"]
11
+ s.homepage = ""
12
+ s.summary = %q{A Sinatra like DSL for implementing AMQP services}
13
+ s.description = %q{This gem provides a Sinatra like DSL and a simple protocol to enable writing services that communicate over AMQP}
14
+
15
+ s.add_development_dependency "rspec"
16
+ s.add_development_dependency "rspec-given"
17
+ s.add_dependency "mash"
18
+ s.add_dependency "redis"
19
+ s.add_dependency "gilmour-em-hiredis"
20
+ s.add_dependency "amqp"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+ end
@@ -0,0 +1,148 @@
1
+ # encoding: utf-8
2
+ require 'socket'
3
+ require 'securerandom'
4
+
5
+ require_relative '../protocol'
6
+
7
+ module Gilmour
8
+ ErrorChannel = "gilmour.error"
9
+
10
+ # Base class for loading backends
11
+ class Backend
12
+ SUPPORTED_BACKENDS = %w(redis)
13
+ @@registry = {}
14
+
15
+ def ident
16
+ @ident
17
+ end
18
+
19
+ def generate_ident
20
+ "#{Socket.gethostname}-pid-#{Process.pid}-uuid-#{SecureRandom.uuid}"
21
+ end
22
+
23
+ def report_errors?
24
+ #Override this method to adjust if you want errors to be reported.
25
+ return true
26
+ end
27
+
28
+ def initialize(opts={})
29
+ @ident = generate_ident
30
+ end
31
+
32
+ def register_health_check
33
+ raise NotImplementedError.new
34
+ end
35
+
36
+ def unregister_health_check
37
+ raise NotImplementedError.new
38
+ end
39
+
40
+ def self.implements(backend_name)
41
+ @@registry[backend_name] = self
42
+ end
43
+
44
+ def self.get(backend_name)
45
+ @@registry[backend_name]
46
+ end
47
+
48
+ # :nodoc:
49
+ # This should be implemented by the derived class
50
+ # subscriptions is a hash in the format -
51
+ # { topic => [handler1, handler2, ...],
52
+ # topic2 => [handler3, handler4, ...],
53
+ # ...
54
+ # }
55
+ # where handler is a hash
56
+ # { :handler => handler_proc,
57
+ # :subscriber => subscriber_derived_class
58
+ # }
59
+ def setup_subscribers(subscriptions)
60
+ end
61
+
62
+ # Sends a message
63
+ # If optional block is given, it will be executed when a response is received
64
+ # or if timeout occurs
65
+ # +message+:: The body of the message (any object that is serialisable)
66
+ # +destination+:: The channel to post to
67
+ # +opts+::
68
+ # ++timeout+:: Sender side timeout
69
+ #
70
+ def publish(message, destination, opts = {}, code = 0, &blk)
71
+ payload, sender = Gilmour::Protocol.create_request(message, code)
72
+
73
+ EM.defer do # Because publish can be called from outside the event loop
74
+ begin
75
+ send(sender, destination, payload, opts, &blk)
76
+ rescue Exception => e
77
+ GLogger.debug e.message
78
+ GLogger.debug e.backtrace
79
+ end
80
+ end
81
+ sender
82
+ end
83
+
84
+ def emit_error(message)
85
+ raise NotImplementedError.new
86
+ end
87
+
88
+ # Adds a new handler for the given _topic_
89
+ def add_listener(topic, &handler)
90
+ raise "Not implemented by child class"
91
+ end
92
+
93
+ # Removes existing _handler_ for the _topic_
94
+ def remove_listener(topic, &handler)
95
+ raise "Not implemented by child class"
96
+ end
97
+
98
+ def acquire_ex_lock(sender)
99
+ raise "Not implemented by child class"
100
+ end
101
+
102
+ def send_response(sender, body, code)
103
+ raise "Not implemented by child class"
104
+ end
105
+
106
+ def execute_handler(topic, payload, sub)
107
+ data, sender = Gilmour::Protocol.parse_request(payload)
108
+ if sub[:exclusive]
109
+ lock_key = sender + sub[:subscriber].to_s
110
+ acquire_ex_lock(lock_key) { _execute_handler(topic, data, sender, sub) }
111
+ else
112
+ _execute_handler(topic, data, sender, sub)
113
+ end
114
+ rescue Exception => e
115
+ GLogger.debug e.message
116
+ GLogger.debug e.backtrace
117
+ end
118
+
119
+ def _execute_handler(topic, data, sender, sub)
120
+ Gilmour::Responder.new(
121
+ sender, topic, data, self, sub[:timeout], sub[:fork]
122
+ ).execute(sub[:handler])
123
+ rescue Exception => e
124
+ GLogger.debug e.message
125
+ GLogger.debug e.backtrace
126
+ end
127
+
128
+ def send
129
+ raise "Not implemented by child class"
130
+ end
131
+
132
+ def self.load_backend(name)
133
+ require_relative name
134
+ end
135
+
136
+ def self.load_all_backends
137
+ SUPPORTED_BACKENDS.each do |f|
138
+ load_backend f
139
+ end
140
+ end
141
+
142
+ def stop(sender, body, code)
143
+ raise "Not implemented by child class"
144
+ end
145
+
146
+ end
147
+ end
148
+
@@ -0,0 +1,254 @@
1
+ require 'em-hiredis'
2
+ require_relative 'backend'
3
+ require_relative '../waiter'
4
+
5
+ module Gilmour
6
+ # Redis backend implementation
7
+ class RedisBackend < Backend
8
+ GilmourHealthKey = "gilmour.known_host.health"
9
+ GilmourErrorBufferLen = 9999
10
+
11
+ implements 'redis'
12
+
13
+ attr_writer :report_errors
14
+ attr_reader :subscriber
15
+ attr_reader :publisher
16
+
17
+ def redis_host(opts)
18
+ host = opts[:host] || '127.0.0.1'
19
+ port = opts[:port] || 6379
20
+ db = opts[:db] || 0
21
+ "redis://#{host}:#{port}/#{db}"
22
+ end
23
+
24
+ def initialize(opts)
25
+ @response_handlers = {}
26
+ @subscriptions = {}
27
+
28
+ waiter = Waiter.new
29
+
30
+ Thread.new do
31
+ EM.run do
32
+ setup_pubsub(opts)
33
+ waiter.signal
34
+ end
35
+ end
36
+
37
+ waiter.wait
38
+
39
+ @report_health = opts["health_check"] || opts[:health_check]
40
+ @report_health = false if @report_health != true
41
+
42
+ @report_errors = opts["broadcast_errors"] || opts[:broadcast_errors]
43
+ @report_errors = true if @report_errors != false
44
+ end
45
+
46
+ def report_health?
47
+ @report_health
48
+ end
49
+
50
+ def report_errors?
51
+ @report_errors
52
+ end
53
+
54
+ def emit_error(message)
55
+ report = self.report_errors?
56
+
57
+ if report == false
58
+ Glogger.debug "Skipping because report_errors is false"
59
+ elsif report == true
60
+ publish_error message
61
+ elsif report.is_a? String and !report.empty?
62
+ queue_error report, message
63
+ end
64
+ end
65
+
66
+ def setup_pubsub(opts)
67
+ @publisher = EM::Hiredis.connect(redis_host(opts))
68
+ @subscriber = @publisher.pubsub_client
69
+ register_handlers
70
+ rescue Exception => e
71
+ GLogger.debug e.message
72
+ GLogger.debug e.backtrace
73
+ end
74
+
75
+ def register_handlers
76
+ @subscriber.on(:pmessage) do |key, topic, payload|
77
+ pmessage_handler(key, topic, payload)
78
+ end
79
+ @subscriber.on(:message) do |topic, payload|
80
+ begin
81
+ if topic.start_with? 'gilmour.response.'
82
+ response_handler(topic, payload)
83
+ else
84
+ pmessage_handler(topic, topic, payload)
85
+ end
86
+ rescue Exception => e
87
+ GLogger.debug e.message
88
+ GLogger.debug e.backtrace
89
+ end
90
+ end
91
+ end
92
+
93
+ def subscribe_topic(topic)
94
+ method = topic.index('*') ? :psubscribe : :subscribe
95
+ @subscriber.method(method).call(topic)
96
+ end
97
+
98
+ def pmessage_handler(key, matched_topic, payload)
99
+ @subscriptions[key].each do |subscription|
100
+ EM.defer(->{execute_handler(matched_topic, payload, subscription)})
101
+ end
102
+ end
103
+
104
+ def register_response(sender, handler, timeout = 600)
105
+ topic = "gilmour.response.#{sender}"
106
+ timer = EM::Timer.new(timeout) do # Simulate error response
107
+ GLogger.info "Timeout: Killing handler for #{sender}"
108
+ payload, _ = Gilmour::Protocol.create_request({}, 499)
109
+ response_handler(topic, payload)
110
+ end
111
+ @response_handlers[topic] = {handler: handler, timer: timer}
112
+ subscribe_topic(topic)
113
+ rescue Exception => e
114
+ GLogger.debug e.message
115
+ GLogger.debug e.backtrace
116
+ end
117
+
118
+ def publish_error(messsage)
119
+ @publisher.publish(Gilmour::ErrorChannel, messsage)
120
+ end
121
+
122
+ def queue_error(key, message)
123
+ @publisher.lpush(key, message) do
124
+ @publisher.ltrim(key, 0, GilmourErrorBufferLen) do
125
+ Glogger.debug "Error queued"
126
+ end
127
+ end
128
+ end
129
+
130
+ def acquire_ex_lock(sender)
131
+ @publisher.set(sender, sender, 'EX', 600, 'NX') do |val|
132
+ EM.defer do
133
+ yield val if val && block_given?
134
+ end
135
+ end
136
+ end
137
+
138
+ def response_handler(sender, payload)
139
+ data, code, _ = Gilmour::Protocol.parse_response(payload)
140
+ handler = @response_handlers.delete(sender)
141
+ @subscriber.unsubscribe(sender)
142
+ if handler
143
+ handler[:timer].cancel
144
+ handler[:handler].call(data, code)
145
+ end
146
+ rescue Exception => e
147
+ GLogger.debug e.message
148
+ GLogger.debug e.backtrace
149
+ end
150
+
151
+ def send_response(sender, body, code)
152
+ publish(body, "gilmour.response.#{sender}", {}, code)
153
+ end
154
+
155
+ def setup_subscribers(subs = {})
156
+ @subscriptions.merge!(subs)
157
+ EM.defer do
158
+ subs.keys.each { |topic| subscribe_topic(topic) }
159
+ end
160
+ end
161
+
162
+ def add_listener(topic, &handler)
163
+ @subscriptions[topic] ||= []
164
+ @subscriptions[topic] << { handler: handler }
165
+ subscribe_topic(topic)
166
+ end
167
+
168
+ def remove_listener(topic, handler = nil)
169
+ if handler
170
+ subs = @subscriptions[topic]
171
+ subs.delete_if { |e| e[:handler] == handler }
172
+ else
173
+ @subscriptions[topic] = []
174
+ end
175
+ @subscriber.unsubscribe(topic) if @subscriptions[topic].empty?
176
+ end
177
+
178
+ def send(sender, destination, payload, opts = {}, &blk)
179
+ timeout = opts[:timeout] || 600
180
+ if opts[:confirm_subscriber]
181
+ confirm_subscriber(destination) do |present|
182
+ if !present
183
+ blk.call(nil, 404) if blk
184
+ else
185
+ _send(sender, destination, payload, timeout, &blk)
186
+ end
187
+ end
188
+ else
189
+ _send(sender, destination, payload, timeout, &blk)
190
+ end
191
+ rescue Exception => e
192
+ GLogger.debug e.message
193
+ GLogger.debug e.backtrace
194
+ end
195
+
196
+ def _send(sender, destination, payload, timeout, &blk)
197
+ register_response(sender, blk, timeout) if block_given?
198
+ @publisher.publish(destination, payload)
199
+ sender
200
+ end
201
+
202
+ def confirm_subscriber(dest, &blk)
203
+ @publisher.pubsub('numsub', dest) do |_, num|
204
+ blk.call(num.to_i > 0)
205
+ end
206
+ rescue Exception => e
207
+ GLogger.debug e.message
208
+ GLogger.debug e.backtrace
209
+ end
210
+
211
+ def stop
212
+ @subscriber.close_connection
213
+ end
214
+
215
+ # TODO: Health checks currently use Redis to keep keys in a data structure.
216
+ # An alternate approach would be that monitor subscribes to a topic
217
+ # and records nodenames that request to be monitored. The publish method
218
+ # should fail if there is no definite health monitor listening. However,
219
+ # that would require the health node to be running at all points of time
220
+ # before a Gilmour server starts up. To circumvent this dependency, till
221
+ # monitor is stable enough, use Redis to save/share these data structures.
222
+ #
223
+ def register_health_check
224
+ @publisher.hset GilmourHealthKey, self.ident, 'active'
225
+
226
+ # - Start listening on a dyanmic topic that Health Monitor can publish
227
+ # on.
228
+ #
229
+ # NOTE: Health checks are not run as forks, to ensure that event-machine's
230
+ # ThreadPool has sufficient resources to handle new requests.
231
+ #
232
+ topic = "gilmour.health.#{self.ident}"
233
+ add_listener(topic) do
234
+ respond @subscriptions.keys
235
+ end
236
+
237
+ # TODO: Need to do these manually. Alternate is to return the handler
238
+ # hash from add_listener.
239
+ @subscriptions[topic][0][:exclusive] = true
240
+
241
+ end
242
+
243
+ def unregister_health_check
244
+ waiter = Waiter.new
245
+
246
+ @publisher.hdel(GilmourHealthKey, self.ident) do
247
+ waiter.signal
248
+ end
249
+
250
+ waiter.wait(5)
251
+ end
252
+
253
+ end
254
+ end