gilmour 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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