redis_message_capsule 0.0.1 → 0.8.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.
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *debug.log
data/README.md CHANGED
@@ -1,58 +1,182 @@
1
1
  # RedisMessageCapsule
2
2
 
3
- Send and receive real-time messages between applications (via redis).
3
+ Send messages between node or rails apps asynchronously (via redis).
4
+ * [report an issue with the ruby version] (https://github.com/arbind/redis_message_capsule/issues)
5
+ * [report an issue with the node version] (https://github.com/arbind/redis_message_capsule-node/issues)
4
6
 
5
- ## Installation
7
+ ## Installation (with npm for node) (as a gem for ruby)
6
8
 
7
- Add this line to your application's Gemfile:
9
+ $ npm install redis_message_capsule
10
+ $ gem install redis_message_capsule
8
11
 
9
- gem 'redis_message_capsule'
12
+ ## Usage
13
+ RedisMessageCapsule is used in the same way for both node and ruby (just the syntax is different, naturally).
10
14
 
11
- And then execute:
15
+ The pseudo code goes something like this:
12
16
 
13
- $ bundle
17
+ # 1. Create a capsule that is bound to the redis DB:
18
+ > load RedisMessageCapsule
19
+ > capsule = materializeCapsule(redisURL, dbNumber)
14
20
 
15
- Or install it yourself as:
21
+ # 2. Create a named channel that you want to send or receive messages on
22
+ > catChannel = capsule.materializeChannel('cat')
16
23
 
17
- $ gem install redis_message_capsule
24
+ # 3.a Either: Send messages on the channel:
25
+ > catChannel.send('meeeooow')
26
+ > catChannel.send('roooaaar')
18
27
 
19
- ## Usage
28
+ # 3.b Or: Handle the messages that are recieved on the channel:
29
+ > messageHandler = { |message| doSomethingWith(message) }
30
+ > catChannel.register(messageHandler)
31
+
32
+ # 4. Create as many channels and listeners as you want, using the capsule.
33
+
34
+ # 5. Create as many capsules as you want (only need one capsule for each redisURL:dbNumber )
35
+
36
+ The purpose of RedisMessageCapsule is to enable separate apps to communicate with each other asynchronously.
37
+
38
+ So, normally, one app would do the sending and another would listen for the message.
39
+ However, nothing restricts the same app from doing the sending as well as receiving its own message.
40
+
41
+ This is not your typical pub/sub model as messages are not broadcast to all apps that are listening on a channel.
42
+
43
+ ## Advanced Usage
44
+ If multiple apps are listening on a channel, only one will actually receive the message.
45
+ Each app can register multiple handlers for a channel, but only one app will actually get the message for processing.
46
+
47
+ In ruby, the listeners will block waiting for a message, so the app that is waiting the longest would get the message.
48
+
49
+ With node, however, there is no guarentee of order, since the listeners are not blocking the event loop.
50
+
51
+ ## Demonstration
52
+ Below are code demonstrations for sending messages between 2 apps (node apps, ruby apps and a combination of both).
20
53
 
21
- Open terminal window one (to send messages):
54
+ * Make sure redis is running
55
+ * Data or object being sent will automatically be serialized to json (in order to teleport through redis)
56
+ * in node: using obj.toJSON() or JSON.stringify(obj)
57
+ * in rails: using obj.to_json
58
+ * Demo for node <-> node: Open 2 terminal windows to send messages between 2 node apps (outlined below)
59
+ * Demo for ruby <-> ruby: Open 2 terminal windows to send messages between 2 ruby apps (outlined below)
60
+ * Demo for node <-> ruby: Open 4 terminal windows and do both of the demos above (which are outlined below)
61
+ * Think of each window that you open as a stand-alone app
62
+
63
+ ### Demo for node <-> node
64
+ In node window 1 - send cat messages:
65
+
66
+ $ node
67
+ RedisMessageCapsule = require('redis-message-capsule')
68
+ redisURL = process.env.REDIS_URL || process.env.REDISTOGO_URL || 'redis://127.0.0.1:6379/'
69
+ capsule = RedisMessageCapsule.materializeCapsule(redisURL)
70
+ cat = capsule.materializeChannel('cat')
71
+ cat.send('meow') // will show up in node window 2 (listening to cat)
72
+ cat.send('meow') // will show up in node window 2 (listening to cat)
73
+
74
+ In node window 2 - listen for cat messages, send messages from dog:
75
+
76
+ $ node
77
+ RedisMessageCapsule = require('redis-message-capsule')
78
+ redisURL = process.env.REDIS_URL || process.env.REDISTOGO_URL || 'redis://127.0.0.1:6379/'
79
+ capsule = RedisMessageCapsule.materializeCapsule(redisURL)
80
+ cat = capsule.materializeChannel('cat')
81
+ cat.register( function(err, message){ console.log(message) } )
82
+ // => meow
83
+ // => meow
84
+ // create a dog channel to send messages to ruby, and start barking
85
+ dog = capsule.materializeChannel('dog')
86
+ dog.send('woof') // will show up in ruby window 2 (listening to dog)
87
+
88
+ In node window 1 - send more cat messages:
89
+
90
+ talk = { say: 'roar', time: new Date() }
91
+ cat.send(9)
92
+ cat.send( talk )
93
+ // Watch for real-time messages show up in node window 2:
94
+ // => 9
95
+ // => {"say"=>"roar", "time"=>"2012-11-21T208:08:08.808Z"}
96
+
97
+ ### Demo for ruby <-> ruby
98
+ In ruby window 1 - send dog messages:
22
99
 
23
100
  $ irb
24
101
  require 'redis_message_capsule'
25
- channel_cat = RedisMessageCapsule.channel('cat')
26
- channel_cat.send('meow')
27
- channel_cat.send('meow')
102
+ redisURL = ENV["REDIS_URL"] || ENV["REDISTOGO_URL"] || "redis://127.0.0.1:6379/"
103
+ capsule = RedisMessageCapsule.materialize_capsule redisURL
104
+ dog = capsule.materialize_channel 'dog'
105
+ dog.send 'bark'
28
106
 
29
- Open terminal window two (to listen for messages):
107
+ In ruby window 2 - listen for dog messages in ruby and send cat messages:
30
108
 
31
109
  $ irb
32
110
  require 'redis_message_capsule'
33
- RedisMessageCapsule.listen('cat') do |msg|
34
- puts msg
35
- end
36
- # => meow
111
+ redisURL = ENV["REDIS_URL"] || ENV["REDISTOGO_URL"] || "redis://127.0.0.1:6379/"
112
+ capsule = RedisMessageCapsule.materialize_capsule redisURL
113
+ dog = capsule.materialize_channel 'dog'
114
+ dog.register { |msg| puts "#{msg}!" * 2 }
115
+ # => woof!woof!
116
+ # => bark!bark!
117
+ # create a cat channel to send messages to node
118
+ cat = capsule.materialize_channel 'cat'
119
+ cat.send 'purrr' # will show up in node window 2 (listening to cat)
120
+
121
+ Back in ruby window 1 - send more dog messages:
122
+
123
+ talk = { say: 'grrrrrr', time: Time.now }
124
+ dog.send 2 # will show up in window 4 (listening to dog)
125
+ dog.send talk # will show up in window 4 (listening to dog)
126
+ # Watch for real-time messages to show up in ruby window 2:
127
+ # => 2
128
+ # => {"say"=>"grrrrrr", "time"=>"2012-11-21 08:08:08 -0800"}
129
+ # => purr
37
130
 
38
- Go back to terminal window one:
131
+ ## Environment
132
+ In order for 2 apps to send messages to each other, they must bind a capsule to the same redis DB and select the same db number.
39
133
 
40
- channel_cat.send 9
41
- channel_cat.send say: 'roar', time: Time.now
42
- channel_cat.send :purr
134
+ By default, RedisMessageCapsule will:
135
+ * select 9 for test environment
136
+ * select 8 for development environment
137
+ * select 7 for production environment
138
+ The selected db can also be overriden when materializing a capsule (examples are below).
43
139
 
44
- Watch for messages in terminal window two:
140
+ If 2 apps are not sending messages to each other:
141
+ * check that they are both in the same environment (test, development, production)
142
+ * or be sure to override using same dbNumber when materializing a capsule.
45
143
 
46
- # => 9
47
- # => {"say"=>"roar", "time"=>"2012-11-19 23:16:08 -0800"}
48
- # => purr
144
+ ## Environment for node
145
+ In node use one of the following to set your environment
146
+
147
+ process.env.NODE_ENV = 'test' // redisDB.select 9
148
+ process.env.NODE_ENV = 'development' // redisDB.select 8
149
+ process.env.NODE_ENV = 'production' // redisDB.select 7
49
150
 
50
- (Make sure you have redis running)
151
+ Alternatively, you can override these defaults for the redis db when materializing a capsule:
51
152
 
52
- ## Comming Soon
153
+ RedisMessageCapsule = require('redis-message-capsule')
154
+ redisURL = process.env.REDIS_URL || process.env.REDISTOGO_URL || 'redis://127.0.0.1:6379/'
155
+ dbNumber = 5
156
+ capsule = RedisMessageCapsule.materializeCapsule(redisURL, dbNumber)
53
157
 
54
- A node.js version you can use to send messages back and forth between node and rails apps.
158
+ ## Environment for ruby
159
+ In ruby use one of the following to set your environment
55
160
 
161
+ ENV["RACK_ENV"] = 'test' // redisDB.select 9
162
+ ENV["RACK_ENV"] = 'development' // redisDB.select 8
163
+ ENV["RACK_ENV"] = 'production' // redisDB.select 7
164
+
165
+ Alternatively, you can override these defaults for the redis db when materializing a capsule:
166
+
167
+ require 'redis_message_capsule'
168
+ redisURL = ENV["REDIS_URL"] || ENV["REDISTOGO_URL"] || "redis://127.0.0.1:6379/"
169
+ dbNumber = 5
170
+ capsule = RedisMessageCapsule.materialize_capsule redisURL, dbNumber
171
+
172
+ ## To build the gem locally:
173
+ git clone git@github.com:arbind/redis_message_capsule-gem.git
174
+ cd redis_message_capsule-gem
175
+ bundle
176
+
177
+ gem uninstall redis_message_capsule
178
+ gem build redis_message_capsule.gemspec
179
+ rake install
56
180
 
57
181
  ## Contributing
58
182
 
@@ -1,3 +1,3 @@
1
1
  module RedisMessageCapsule
2
- VERSION = "0.0.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -3,144 +3,179 @@ require 'redis'
3
3
  require 'json'
4
4
  require 'uri'
5
5
 
6
+
7
+ # class declaration
6
8
  module RedisMessageCapsule
7
9
  class Configuration
8
- attr_accessor :environment, :db_number, :redis_url
9
-
10
- def initialize
11
- self.environment = ENV["RACK_ENV"] || "development"
12
- self.db_number = 7 if self.environment.eql? 'production'
13
- self.db_number = 8 if self.environment.eql? 'development'
14
- self.db_number = 9 if self.environment.eql? 'test'
15
- self.db_number ||= 9
16
- self.redis_url = ENV["REDIS_URL"] || ENV["REDISTOGO_URL"] || "redis://localhost:6379/"
10
+ end
11
+ class Capsule
12
+ class Channel
13
+ class Listener
14
+ end
17
15
  end
18
16
  end
17
+ end
18
+
19
19
 
20
+ ###
21
+ # module
22
+ ###
23
+ module RedisMessageCapsule
20
24
  class << self
21
- attr_accessor :configuration, :redis_clients, :capsule_channels, :listener_threads, :handlers
25
+ attr_accessor :configuration, :capsules
22
26
  end
23
27
 
24
- def self.redis_clients
25
- @redis_clients ||= {}
28
+ def self.config() configuration end
29
+ def self.configure() yield(configuration) if block_given? end
30
+ def self.configuration() @configuration ||= Configuration.new end
31
+
32
+ def self.capsules() @capsules ||= {} end
33
+ def self.make_capsule_key(url, db_num) "#{url}.#{db_num}" end
34
+ def self.materialize_capsule(redis_url=nil, db_number=-1)
35
+ url = redis_url || config.redis_url
36
+ redis_uri = URI.parse(url) rescue nil
37
+ return nil if redis_uri.nil?
38
+
39
+ db_num = db_number
40
+ db_num = config.db_number if db_num < 0
41
+ key = (make_capsule_key url, db_num)
42
+
43
+ capsule = capsules[key] || (Capsule.new redis_uri, db_num)
44
+ capsules[key] = capsule
26
45
  end
46
+ def self.capsule(redis_url=nil, db_number=-1) materialize_capsule(redis_url, db_number) end # method alias)
27
47
 
28
- def self.capsule_channels
29
- @capsule_channels ||= {}
48
+ def self.materialize_redis_client(redis_uri, db_number)
49
+ redis_client = Redis.new(:host => redis_uri.host, :port => redis_uri.port, :password => redis_uri.password) rescue nil
50
+ if redis_client.nil?
51
+ puts "!!!\n!!! Can not connect to redis server at #{uri}\n!!!"
52
+ return nil
53
+ end
54
+ redis_client.select db_number rescue nil
55
+ redis_client
30
56
  end
31
57
 
32
- def self.listener_threads
33
- @listener_threads ||= {}
58
+ end
59
+
60
+ class RedisMessageCapsule::Configuration
61
+ attr_accessor :environment, :redis_url, :db_number
62
+ def initialize
63
+ self.environment = ENV["RACK_ENV"] || "development"
64
+ self.db_number = 7 if self.environment.eql? 'production'
65
+ self.db_number = 8 if self.environment.eql? 'development'
66
+ self.db_number = 9 if self.environment.eql? 'test'
67
+ self.db_number ||= 9
68
+ self.redis_url = ENV["REDIS_URL"] || ENV["REDISTOGO_URL"] || "redis://127.0.0.1:6379/"
34
69
  end
70
+ end
71
+
72
+ class RedisMessageCapsule::Capsule
73
+ attr_accessor :redis_client, :redis_uri, :db_number, :channels
35
74
 
36
- def self.configuration
37
- @configuration ||= Configuration.new
75
+ def initialize(redis_uri, db_number)
76
+ @redis_uri = redis_uri
77
+ @db_number = db_number
78
+ @channels = {}
79
+ @redis_client = RedisMessageCapsule.materialize_redis_client @redis_uri, @db_number
38
80
  end
39
- def self.config() configuration end
40
81
 
41
- def self.configure
42
- yield(configuration) if block_given?
82
+ def materialize_channel(channel_name)
83
+ channels[channel_name] ||= (Channel.new channel_name, redis_client, redis_uri, db_number)
43
84
  end
44
85
 
45
- class Channel
46
- attr_accessor :name, :redis_client
47
- def initialize(name, redis_client)
48
- self.name = name
49
- self.redis_client = redis_client
50
- end
86
+ end
51
87
 
52
- def send (message)
53
- payload = { 'data' => message }
54
- redis_client.rpush name, payload.to_json
55
- end
88
+ class RedisMessageCapsule::Capsule::Channel
89
+ attr_accessor :channel_name, :listener, :redis_client, :redis_uri, :db_number
56
90
 
91
+ def initialize(channel_name, redis_client, redis_uri, db_number )
92
+ self.channel_name = channel_name
93
+ self.redis_client = redis_client
94
+ self.redis_uri = redis_uri
95
+ self.db_number = db_number
96
+ @listener = nil
57
97
  end
58
98
 
59
- def self.make_client_key(url, db_num)
60
- "#{url}.#{db_num}"
99
+ def send (message)
100
+ payload = { 'data' => message }
101
+ redis_client.rpush channel_name, payload.to_json
102
+ rescue Exception => e
103
+ puts e.message
104
+ puts e.backtrace
105
+ ensure
106
+ self # chainability
61
107
  end
62
-
63
- def self.make_channel_key(name, url, db_num)
64
- "#{name}.#{url}.#{db_num}"
108
+ alias_method :emit, :send
109
+ alias_method :message, :send
110
+
111
+ def register(&block)
112
+ raise "listen_for(#{channel_name}): No callback was specified!" unless block_given?
113
+ @listener ||= (Listener.new channel_name, redis_uri, db_number) # listener needs its own connection since it blocks
114
+ @listener.register(&block)
115
+ self # chainability
65
116
  end
117
+ alias_method :on, :register
118
+ alias_method :listen, :register
66
119
 
67
- def self.make_listener_key(channels, url, db_num)
68
- [ *channels, url, db_num].join('.')
120
+ def unregister(&block)
121
+ # +++
69
122
  end
70
123
 
71
- def self.channel(name, redis_url=nil, db_number=-1)
72
- url = redis_url || config.redis_url
73
- db_num = db_number
74
- db_num = config.db_number if db_num < 0
75
-
76
- channel_key = make_channel_key(name, url, db_num)
77
- return capsule_channels[channel_key] unless capsule_channels[channel_key].nil?
124
+ def stop_listening
125
+ # +++
126
+ end
78
127
 
79
- client_key = make_client_key(url, db_num)
80
- redis_client = redis_clients[client_key]
128
+ end
81
129
 
82
- if redis_client.nil?
83
- uri = URI.parse(url)
84
- redis_client = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password) rescue nil
85
- if redis_client.nil?
86
- puts "!!!\n!!! Can not connect to redis server at #{uri}\n!!!"
87
- return nil
88
- end
89
- redis_client.select db_num
90
- redis_clients[client_key] = redis_client
91
- end
92
- channel = Channel.new(name, redis_client)
93
- capsule_channels[channel_key] = channel
94
- channel
130
+ class RedisMessageCapsule::Capsule::Channel::Listener
131
+ def initialize(channel_name, redis_uri, db_number)
132
+ @channel_name = channel_name
133
+ @redis_uri = redis_uri
134
+ @db_number = db_number
135
+ @handlers = []
136
+ @listener_thread = nil
95
137
  end
96
138
 
139
+ def register(&handler)
140
+ @handlers << handler
141
+ launch_listener if @listener_thread.nil?
142
+ handler
143
+ end
97
144
 
98
- def self.listen(channels_array, redis_url=nil, db_number=-1)
99
- url = redis_url || config.redis_url
100
- db_num = db_number
101
- db_num = config.db_number if db_num < 0
102
- channels = *channels_array
103
- key = make_listener_key(channels, url, db_number)
104
- return true unless listener_threads[key].nil?
105
-
106
- listener_threads[key] = Thread.new do
107
- Thread.current[:name] = :RedisMessageCapsule
108
- Thread.current[:description] = "Listening for messages from #{url.to_s} on chanel: #{[*channels].join(',')} "
109
-
110
- redis_client = nil # establish redis connection:
111
- until !redis_client.nil? and redis_client.ping
112
- uri = URI.parse(url)
113
- redis_client = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password) rescue nil
114
- if redis_client.nil?
115
- puts "!!!\n!!! Can not connect to redis server at #{uri}\n!!!"
116
- sleep 10
117
- else
118
- redis_client.select db_num
119
- puts "connected!"
120
- Thread.current[:redis_client] = redis_client
121
- end
122
- end
123
-
124
- puts "Listening for messages on #{[*channels].join(', ')}"
125
- loop do
126
- channel_element = redis_client.blpop *channels, 0
127
- channel = channel_element.first
128
- element = channel_element.last
129
- payload = ( JSON.parse(element) rescue {} )
130
- message = payload['data']
131
- # fire event on channel
132
- puts "#{channel}: #{message}"
133
- yield(message) if block_given?
134
- end
135
- end
145
+ def stop_listening
146
+ @listener_thread[:listening] = false unless @@listener_thread.nil?
136
147
  end
137
148
 
138
- end
149
+ def unregister(&handler) @handlers.delete handler end
139
150
 
151
+ def launch_listener
152
+ @listener_thread ||= Thread.new do
140
153
 
141
- def start_redis_listeners(channels_array, redis_url=nil)
142
- channels = *channels_array
154
+ blocking_redis_client = RedisMessageCapsule.materialize_redis_client @redis_uri, @db_number
155
+ # This redis connection will block when popping, so it is created inside of its own thread
143
156
 
144
- url = redis_url || ENV["REDIS_URL"] || ENV["REDISTOGO_URL"] || "redis://localhost:6379/"
145
- key = url.to_s + channels.to_s
146
- end
157
+ Thread.current[:name] = :RedisMessageCapsule
158
+ Thread.current[:chanel] = @channel_name
159
+ Thread.current[:description] = "Listening for '#{@channel_name}' messages from #{@redis_uri}"
160
+ Thread.current[:redis_client] = blocking_redis_client
161
+ Thread.current[:listening] = true
162
+
163
+ while Thread.current[:listening] do # listen forever
164
+ begin
165
+ channel_element = blocking_redis_client.blpop @channel_name, 0 # pop a message, or block the thread and wait till the next one
166
+ unless channel_element.nil?
167
+ ch = channel_element.first
168
+ element = channel_element.last
169
+ payload = ( JSON.parse(element) rescue {'data' => 'error parsing json!'} )
170
+ message = payload['data']
171
+ puts @handlers.count
172
+ @handlers.each { |handler| handler.call(message) }
173
+ end
174
+ rescue Exception => e
175
+ Thread.current[:listening] = false # stop listening
176
+ end
177
+ end # while
178
+ end # Thread.new
179
+ end # launch_listener
180
+
181
+ end # class
@@ -8,9 +8,9 @@ Gem::Specification.new do |gem|
8
8
  gem.version = RedisMessageCapsule::VERSION
9
9
  gem.authors = ["Arbind"]
10
10
  gem.email = ["arbind@carbonfive.com"]
11
- gem.description = "Send and receive real-time messages between applications (via redis)."
12
- gem.summary = ""
13
- gem.homepage = ""
11
+ gem.description = "Send messages between node or rails apps asynchronously (via redis)."
12
+ gem.summary = "See the README file for code demonstrations"
13
+ gem.homepage = "https://github.com/arbind/redis_message_capsule-gem"
14
14
 
15
15
  gem.files = `git ls-files`.split($/)
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_message_capsule
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.8.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-20 00:00:00.000000000 Z
12
+ date: 2012-11-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -91,7 +91,7 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
- description: Send and receive real-time messages between applications (via redis).
94
+ description: Send messages between node or rails apps asynchronously (via redis).
95
95
  email:
96
96
  - arbind@carbonfive.com
97
97
  executables: []
@@ -107,7 +107,7 @@ files:
107
107
  - lib/redis_message_capsule/version.rb
108
108
  - redis_message_capsule.gemspec
109
109
  - spec/spec_helper.rb
110
- homepage: ''
110
+ homepage: https://github.com/arbind/redis_message_capsule-gem
111
111
  licenses: []
112
112
  post_install_message:
113
113
  rdoc_options: []
@@ -130,6 +130,6 @@ rubyforge_project:
130
130
  rubygems_version: 1.8.24
131
131
  signing_key:
132
132
  specification_version: 3
133
- summary: ''
133
+ summary: See the README file for code demonstrations
134
134
  test_files:
135
135
  - spec/spec_helper.rb