em-postman 0.1.0

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,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in em-postman.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'em-synchrony', :git => 'git://github.com/igrigorik/em-synchrony.git'
data/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2011 by Vivien Schilis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all 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
19
+ THE SOFTWARE.
20
+
@@ -0,0 +1,66 @@
1
+ Getting started
2
+ ===============
3
+
4
+ Postman is an EventMachine Pub/Sub resilient to failure.
5
+ Postman uses Redis lists (brpop and lpush) to send messages to subscribers into a mailbox.
6
+
7
+ Postman has been designed to be resilient to failure on both side: publisher and subscriber.
8
+ If your postman process dies or need to be restarted, it will be able to collect again messages from its mailbox.
9
+
10
+
11
+ Example
12
+ =======
13
+
14
+ Server postbox: (server.rb)
15
+
16
+ ``` ruby
17
+ require 'em-postman'
18
+
19
+ EM.run {
20
+ postman = EM::Postman.new('server')
21
+ postman.onmessage(:greetings) {|data|
22
+ puts data.inspect
23
+ postman.send_message data['from'], :greetings, {:message => 'hello ' + data['from']}
24
+ }
25
+
26
+ postman.listen
27
+ }
28
+ ```
29
+
30
+ Client postbox: (client.rb)
31
+
32
+ ``` ruby
33
+ require 'em-postman'
34
+
35
+ EM.run {
36
+ postman = EM::Postman.new('client-' + Process.pid.to_s)
37
+ postman.onmessage(:greetings) {|data|
38
+ puts data.inspect
39
+ EM.stop
40
+ }
41
+
42
+ postman.send_message 'server', :greetings, {:message => 'hello server', :from => postman.mailbox}
43
+ postman.listen
44
+ }
45
+ ```
46
+
47
+ em-synchrony
48
+ ============
49
+
50
+ To use em-postman with em-synchrony
51
+
52
+ ``` ruby
53
+ require 'em-synchrony/em-hiredis'
54
+ require 'em-postman/synchrony'
55
+ ```
56
+
57
+ Credits
58
+ =======
59
+
60
+ - This gem has been extracted from http://pandastream.com.
61
+ - Thanks to Jonas Pfenniger(zimbatm) and Martyn Loughran(mloughran)
62
+
63
+ License
64
+ =======
65
+
66
+ The MIT License - Copyright (c) 2011 Vivien Schilis
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "em-postman/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "em-postman"
7
+ s.version = EventMachine::Postman::VERSION
8
+ s.authors = ["Vivien Schilis"]
9
+ s.email = ["vivien.schilis@gmail.com"]
10
+ s.homepage = "http://github.com/vivienschilis/em-postman"
11
+ s.summary = %q{EventMachine pub/sub using Redis list, resilient to failure}
12
+ s.description = %q{EventMachine pub/sub using Redis list, resilient to failure}
13
+
14
+ s.rubyforge_project = "em-postman"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "em-hiredis"
22
+ s.add_dependency "multi_json"
23
+ s.add_dependency "yajl-ruby"
24
+
25
+ s.add_development_dependency "em-spec"
26
+ s.add_development_dependency "rspec"
27
+
28
+ end
@@ -0,0 +1,16 @@
1
+ require 'em-postman'
2
+
3
+ EM.run {
4
+
5
+ postbox = EM::Postman.new('client-' + Process.pid.to_s )
6
+ postbox.onmessage(:greetings) {|data|
7
+ puts data.inspect
8
+ EM.stop
9
+ }
10
+
11
+ 100.times {|i|
12
+ postbox.send_message('server', :greetings, {:message => "hello server, message #{i} ", :from => postbox.mailbox})
13
+ }
14
+
15
+ postbox.listen
16
+ }
@@ -0,0 +1,12 @@
1
+ require 'em-postman'
2
+
3
+ EM.run {
4
+ postman = EM::Postman.new('server')
5
+
6
+ postman.onmessage(:greetings) {|data|
7
+ puts data.inspect
8
+ postman.send_message(data['from'], :greetings, {:message => 'hello ' + data['from']})
9
+ }
10
+
11
+ postman.listen
12
+ }
@@ -0,0 +1,10 @@
1
+ require 'logger'
2
+ require 'eventmachine'
3
+ require 'em-hiredis'
4
+
5
+ require 'multi_json'
6
+ require 'yajl'
7
+ MultiJson.engine = :yajl
8
+
9
+ require 'em-postman/version'
10
+ require 'em-postman/postman'
@@ -0,0 +1,118 @@
1
+ module EventMachine
2
+ class Postman
3
+ attr_accessor :mailbox
4
+ attr_writer :logger
5
+
6
+ def logger
7
+ @logger ||= Logger.new(STDOUT)
8
+ end
9
+
10
+ def handlers
11
+ @handlers ||= {}
12
+ end
13
+
14
+ def initialize(mailbox, options = {})
15
+ @mailbox = mailbox
16
+ @timeout = options[:timeout] || 5
17
+ @redis_options = {:db => 0, :host => 'localhost', :port => 6379}
18
+
19
+ @options = options
20
+ if options[:redis].is_a?(EventMachine::Hiredis::Client)
21
+ @redis = options[:redis]
22
+ @redis_options = extract_redis_options(@redis)
23
+ else
24
+ @redis_options.merge(options[:redis] || {})
25
+ end
26
+ end
27
+
28
+ def redis
29
+ @redis ||= new_redis_client
30
+ end
31
+
32
+ def clear
33
+ redis.del(inbox_name)
34
+ end
35
+
36
+ def onmessage(method, &callback)
37
+ debug "##{method} registered"
38
+
39
+ handlers[method.to_s] ||= []
40
+ handlers[method.to_s] << callback
41
+ end
42
+
43
+ def unlisten(method, callback)
44
+ if cbs = handlers[method.to_s]
45
+ cbs.delete(callback)
46
+ end
47
+ end
48
+
49
+ def send_message(recipient, method, body)
50
+ message = MultiJson.encode({
51
+ 'method' => method,
52
+ 'body' => body
53
+ })
54
+
55
+ debug "-> #{recipient}##{method}: #{body.inspect}"
56
+ redis.lpush("postman:inbox_#{recipient}", message)
57
+ end
58
+
59
+ def listen
60
+ listen_messages
61
+ end
62
+
63
+ private
64
+
65
+ def new_redis_client
66
+ new_redis_client = EventMachine::Hiredis::Client.connect(@redis_options[:host], @redis_options[:port])
67
+ new_redis_client.select(@redis_options[:db])
68
+ new_redis_client
69
+ end
70
+
71
+ def extract_redis_options(redis)
72
+ {
73
+ :host => redis.host,
74
+ :port => redis.port,
75
+ :db => redis.db
76
+ }
77
+ end
78
+
79
+ def inbox_name
80
+ "postman:inbox_#{@mailbox}"
81
+ end
82
+
83
+ def debug(message)
84
+ logger.debug "Postman[#{mailbox}]: #{message}"
85
+ end
86
+
87
+ def listen_messages(redis = new_redis_client)
88
+ EM.next_tick do
89
+ deferable = redis.brpop(inbox_name, @timeout)
90
+ deferable.callback do |channel, message|
91
+ handle_rpop(channel, message) unless channel.nil?
92
+ listen_messages(redis)
93
+ end
94
+
95
+ deferable.errback do |error|
96
+ logger.error "Postman[#{mailbox}]: #{error.inspect}"
97
+ listen_messages(redis)
98
+ end
99
+ end
100
+ end
101
+
102
+ def handle_rpop(channel, message)
103
+ begin
104
+ debug "<- #{message}"
105
+ data = MultiJson.decode(message)
106
+ if (callbacks = handlers[data['method'].to_s]) && !callbacks.empty?
107
+ callbacks.each {|cb| cb.call(data['body'])}
108
+ else
109
+ logger.warn "Postman[#{mailbox}]: no handler found for #{data['method']}"
110
+ end
111
+ rescue MultiJson::DecodeError => error
112
+ logger.error "Postman[#{mailbox}]: unable to parse message #{message}"
113
+ rescue => error
114
+ logger.error "Postman[#{mailbox}]: #{error}"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,15 @@
1
+ module EventMachine
2
+ class Postman
3
+ def listen_messages(redis = new_redis_client)
4
+ channel, message = redis.brpop(inbox_name, @timeout)
5
+
6
+ handle_rpop(channel, message) if channel && message
7
+
8
+ EM.next_tick do
9
+ Fiber.new {
10
+ listen_messages(redis)
11
+ }.resume
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ class Postman
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe EventMachine::Postman do
4
+ describe "when listening for messages" do
5
+ let(:redis) {
6
+ redis = EventMachine::Hiredis.connect("redis://localhost:6379/0")
7
+ redis.flushall
8
+ redis
9
+ }
10
+
11
+ let(:postman) {
12
+ p = EM::Postman.new('test', :redis => redis)
13
+ p.logger = Logger.new(nil)
14
+ p
15
+ }
16
+
17
+ it "should send and receive messages asynchronously" do
18
+ em(1) do
19
+ postman.send_message("test", 'cb', {:value => 'abc'})
20
+
21
+ responses = []
22
+ postman.onmessage(:cb) do |msg|
23
+ responses << msg
24
+ end
25
+ postman.onmessage(:cb) do |msg|
26
+ responses << msg
27
+ end
28
+ postman.listen
29
+
30
+ EM.add_timer(0.2) do
31
+ responses.should == [{"value"=>"abc"}, {"value"=>"abc"}]
32
+ done
33
+ end
34
+ end
35
+ end
36
+
37
+ it "should unregister handlers" do
38
+ em(1) do
39
+ postman.send_message("test", 'cb', {:value => 'abc'})
40
+ responses = []
41
+ proc = Proc.new {|msg|
42
+ responses << msg
43
+ postman.unlisten(:cb, proc)
44
+ }
45
+ postman.onmessage(:cb, &proc)
46
+
47
+ postman.listen
48
+
49
+ EM.add_timer(0.2) do
50
+ postman.send_message("test", 'cb', {:value => 'cdf'})
51
+ end
52
+
53
+ EM.add_timer(0.4) do
54
+ responses.should == [{"value"=>"abc"}]
55
+ done
56
+ end
57
+ end
58
+ end
59
+
60
+ it "should gracefully handle a Redis.rpop error" do
61
+ em(1) do
62
+ redis.lpush("postman:inbox_test", MultiJson.encode({:method => 'cb', :body => 'a'}))
63
+ redis.lpush("postman:inbox_test", "error")
64
+ redis.lpush("postman:inbox_test", MultiJson.encode({:method => 'cb', :body => 'b'}))
65
+
66
+ responses = []
67
+ postman.onmessage(:cb) do |msg|
68
+ responses << msg
69
+ end
70
+ postman.listen
71
+
72
+ EM.add_timer(0.2) do
73
+ responses.should == ["a", "b"]
74
+ done
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,8 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'em-postman'
3
+ require 'rspec'
4
+ require 'em-spec/rspec'
5
+
6
+ RSpec.configure do |config|
7
+ config.include EventMachine::SpecHelper
8
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'em-synchrony'
3
+ require 'em-synchrony/em-hiredis'
4
+ require 'em-postman/synchrony'
5
+
6
+ describe EventMachine::Postman do
7
+ describe "when listening for messages" do
8
+
9
+ it "should send and receive messages using em-sychrony" do
10
+ EventMachine.synchrony do
11
+
12
+ redis = EM::Hiredis::Client.connect('localhost', 6379)
13
+ redis.flushall
14
+
15
+ postman = EM::Postman.new('test', :redis => redis)
16
+ postman.logger = Logger.new(nil)
17
+
18
+ postman.send_message("test", 'cb', {:value => 'abc'})
19
+
20
+ responses = []
21
+ postman.onmessage(:cb) do |msg|
22
+ responses << msg
23
+ end
24
+
25
+ postman.listen
26
+
27
+ responses.should == [{"value"=>"abc"}]
28
+ EventMachine.stop
29
+ end
30
+ end
31
+
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-postman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Vivien Schilis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-05 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: em-hiredis
16
+ requirement: &70337885041680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70337885041680
25
+ - !ruby/object:Gem::Dependency
26
+ name: multi_json
27
+ requirement: &70337885041120 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70337885041120
36
+ - !ruby/object:Gem::Dependency
37
+ name: yajl-ruby
38
+ requirement: &70337885037840 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70337885037840
47
+ - !ruby/object:Gem::Dependency
48
+ name: em-spec
49
+ requirement: &70337885037340 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70337885037340
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &70337885036920 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70337885036920
69
+ description: EventMachine pub/sub using Redis list, resilient to failure
70
+ email:
71
+ - vivien.schilis@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - LICENCE
79
+ - README.md
80
+ - Rakefile
81
+ - em-postman.gemspec
82
+ - examples/client.rb
83
+ - examples/server.rb
84
+ - lib/em-postman.rb
85
+ - lib/em-postman/postman.rb
86
+ - lib/em-postman/synchrony.rb
87
+ - lib/em-postman/version.rb
88
+ - spec/postman_spec.rb
89
+ - spec/spec_helper.rb
90
+ - spec/synchrony_spec.rb
91
+ homepage: http://github.com/vivienschilis/em-postman
92
+ licenses: []
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ segments:
104
+ - 0
105
+ hash: -2381599360813571691
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ segments:
113
+ - 0
114
+ hash: -2381599360813571691
115
+ requirements: []
116
+ rubyforge_project: em-postman
117
+ rubygems_version: 1.8.6
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: EventMachine pub/sub using Redis list, resilient to failure
121
+ test_files:
122
+ - spec/postman_spec.rb
123
+ - spec/spec_helper.rb
124
+ - spec/synchrony_spec.rb