faye-redis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,58 @@
1
+ = Faye::Redis {<img src="https://secure.travis-ci.org/faye/faye-redis-ruby.png?branch=master" alt="Build Status" />}[http://travis-ci.org/faye/faye-redis-ruby]
2
+
3
+ This plugin provides a Redis-based backend for the {Faye}[http://faye.jcoglan.com]
4
+ messaging server. It allows a single Faye service to be distributed across many
5
+ front-end web servers by storing state and routing messages through a
6
+ {Redis}[http://redis.io] database server.
7
+
8
+
9
+ == Usage
10
+
11
+ Pass in the engine and any settings you need when setting up your Faye server.
12
+
13
+ require 'faye'
14
+ require 'faye/redis'
15
+
16
+ bayeux = Faye::RackAdapter.new(
17
+ :mount => '/',
18
+ :timeout => 25,
19
+ :engine => {
20
+ :type => Faye::Redis,
21
+ :host => 'redis.example.com',
22
+ # more options
23
+ }
24
+ )
25
+
26
+ The full list of settings is as follows.
27
+
28
+ * <b><tt>:host</tt></b> - hostname of your Redis instance
29
+ * <b><tt>:port</tt></b> - port number, default is +6379+
30
+ * <b><tt>:password</tt></b> - password, if +requirepass+ is set
31
+ * <b><tt>:database</tt></b> - number of database to use, default is +0+
32
+ * <b><tt>:namespace</tt></b> - prefix applied to all keys, default is <tt>''</tt>
33
+ * <b><tt>:socket</tt></b> - path to Unix socket if +unixsocket+ is set
34
+
35
+
36
+ == License
37
+
38
+ (The MIT License)
39
+
40
+ Copyright (c) 2011-2012 James Coglan
41
+
42
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
43
+ this software and associated documentation files (the 'Software'), to deal in
44
+ the Software without restriction, including without limitation the rights to use,
45
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
46
+ Software, and to permit persons to whom the Software is furnished to do so,
47
+ subject to the following conditions:
48
+
49
+ The above copyright notice and this permission notice shall be included in all
50
+ copies or substantial portions of the Software.
51
+
52
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
54
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
55
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
56
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
57
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
58
+
data/lib/faye/redis.rb ADDED
@@ -0,0 +1,222 @@
1
+ require 'em-hiredis'
2
+ require 'yajl'
3
+
4
+ module Faye
5
+ class Redis
6
+
7
+ DEFAULT_HOST = 'localhost'
8
+ DEFAULT_PORT = 6379
9
+ DEFAULT_DATABASE = 0
10
+ DEFAULT_GC = 60
11
+ LOCK_TIMEOUT = 120
12
+
13
+ def self.create(server, options)
14
+ new(server, options)
15
+ end
16
+
17
+ def initialize(server, options)
18
+ @server = server
19
+ @options = options
20
+ end
21
+
22
+ def init
23
+ return if @redis
24
+
25
+ host = @options[:host] || DEFAULT_HOST
26
+ port = @options[:port] || DEFAULT_PORT
27
+ db = @options[:database] || DEFAULT_DATABASE
28
+ auth = @options[:password]
29
+ gc = @options[:gc] || DEFAULT_GC
30
+ @ns = @options[:namespace] || ''
31
+ socket = @options[:socket]
32
+
33
+ if socket
34
+ @redis = EventMachine::Hiredis::Client.connect(socket, nil)
35
+ @subscriber = EventMachine::Hiredis::Client.connect(socket, nil)
36
+ else
37
+ @redis = EventMachine::Hiredis::Client.connect(host, port)
38
+ @subscriber = EventMachine::Hiredis::Client.connect(host, port)
39
+ end
40
+ if auth
41
+ @redis.auth(auth)
42
+ @subscriber.auth(auth)
43
+ end
44
+ @redis.select(db)
45
+ @subscriber.select(db)
46
+
47
+ @subscriber.subscribe(@ns + '/notifications')
48
+ @subscriber.on(:message) do |topic, message|
49
+ empty_queue(message) if topic == @ns + '/notifications'
50
+ end
51
+
52
+ @gc = EventMachine.add_periodic_timer(gc, &method(:gc))
53
+ end
54
+
55
+ def disconnect
56
+ @subscriber.unsubscribe(@ns + '/notifications')
57
+ EventMachine.cancel_timer(@gc)
58
+ end
59
+
60
+ def create_client(&callback)
61
+ init
62
+ client_id = @server.generate_id
63
+ @redis.zadd(@ns + '/clients', 0, client_id) do |added|
64
+ next create_client(&callback) if added == 0
65
+ @server.debug 'Created new client ?', client_id
66
+ ping(client_id)
67
+ @server.trigger(:handshake, client_id)
68
+ callback.call(client_id)
69
+ end
70
+ end
71
+
72
+ def destroy_client(client_id, &callback)
73
+ init
74
+ @redis.zrem(@ns + '/clients', client_id)
75
+ @redis.del(@ns + "/clients/#{client_id}/messages")
76
+
77
+ @redis.smembers(@ns + "/clients/#{client_id}/channels") do |channels|
78
+ i, n = 0, channels.size
79
+ next after_destroy(client_id, &callback) if i == n
80
+
81
+ channels.each do |channel|
82
+ unsubscribe(client_id, channel) do
83
+ i += 1
84
+ after_destroy(client_id, &callback) if i == n
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def after_destroy(client_id, &callback)
91
+ @server.debug 'Destroyed client ?', client_id
92
+ @server.trigger(:disconnect, client_id)
93
+ callback.call if callback
94
+ end
95
+
96
+ def client_exists(client_id, &callback)
97
+ init
98
+ @redis.zscore(@ns + '/clients', client_id) do |score|
99
+ callback.call(score != nil)
100
+ end
101
+ end
102
+
103
+ def ping(client_id)
104
+ init
105
+ timeout = @server.timeout
106
+ return unless Numeric === timeout
107
+
108
+ time = get_current_time
109
+ @server.debug 'Ping ?, ?', client_id, time
110
+ @redis.zadd(@ns + '/clients', time, client_id)
111
+ end
112
+
113
+ def subscribe(client_id, channel, &callback)
114
+ init
115
+ @redis.sadd(@ns + "/clients/#{client_id}/channels", channel) do |added|
116
+ @server.trigger(:subscribe, client_id, channel) if added == 1
117
+ end
118
+ @redis.sadd(@ns + "/channels#{channel}", client_id) do
119
+ @server.debug 'Subscribed client ? to channel ?', client_id, channel
120
+ callback.call if callback
121
+ end
122
+ end
123
+
124
+ def unsubscribe(client_id, channel, &callback)
125
+ init
126
+ @redis.srem(@ns + "/clients/#{client_id}/channels", channel) do |removed|
127
+ @server.trigger(:unsubscribe, client_id, channel) if removed == 1
128
+ end
129
+ @redis.srem(@ns + "/channels#{channel}", client_id) do
130
+ @server.debug 'Unsubscribed client ? from channel ?', client_id, channel
131
+ callback.call if callback
132
+ end
133
+ end
134
+
135
+ def publish(message, channels)
136
+ init
137
+ @server.debug 'Publishing message ?', message
138
+
139
+ json_message = Yajl::Encoder.encode(message)
140
+ channels = Channel.expand(message['channel'])
141
+ keys = channels.map { |c| @ns + "/channels#{c}" }
142
+
143
+ @redis.sunion(*keys) do |clients|
144
+ clients.each do |client_id|
145
+ @server.debug 'Queueing for client ?: ?', client_id, message
146
+ @redis.rpush(@ns + "/clients/#{client_id}/messages", json_message)
147
+ @redis.publish(@ns + '/notifications', client_id)
148
+ end
149
+ end
150
+
151
+ @server.trigger(:publish, message['clientId'], message['channel'], message['data'])
152
+ end
153
+
154
+ def empty_queue(client_id)
155
+ return unless @server.has_connection?(client_id)
156
+ init
157
+
158
+ key = @ns + "/clients/#{client_id}/messages"
159
+
160
+ @redis.multi
161
+ @redis.lrange(key, 0, -1)
162
+ @redis.del(key)
163
+ @redis.exec.callback do |json_messages, deleted|
164
+ messages = json_messages.map { |json| Yajl::Parser.parse(json) }
165
+ @server.deliver(client_id, messages)
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ def get_current_time
172
+ (Time.now.to_f * 1000).to_i
173
+ end
174
+
175
+ def gc
176
+ timeout = @server.timeout
177
+ return unless Numeric === timeout
178
+
179
+ with_lock 'gc' do |release_lock|
180
+ cutoff = get_current_time - 1000 * 2 * timeout
181
+ @redis.zrangebyscore(@ns + '/clients', 0, cutoff) do |clients|
182
+ i, n = 0, clients.size
183
+ next release_lock.call if i == n
184
+
185
+ clients.each do |client_id|
186
+ destroy_client(client_id) do
187
+ i += 1
188
+ release_lock.call if i == n
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ def with_lock(lock_name, &block)
196
+ lock_key = @ns + '/locks/' + lock_name
197
+ current_time = get_current_time
198
+ expiry = current_time + LOCK_TIMEOUT * 1000 + 1
199
+
200
+ release_lock = lambda do
201
+ @redis.del(lock_key) if get_current_time < expiry
202
+ end
203
+
204
+ @redis.setnx(lock_key, expiry) do |set|
205
+ next block.call(release_lock) if set == 1
206
+
207
+ @redis.get(lock_key) do |timeout|
208
+ next unless timeout
209
+
210
+ lock_timeout = timeout.to_i(10)
211
+ next if current_time < lock_timeout
212
+
213
+ @redis.getset(lock_key, expiry) do |old_value|
214
+ block.call(release_lock) if old_value == timeout
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ end
221
+ end
222
+
@@ -0,0 +1,26 @@
1
+ require "spec_helper"
2
+
3
+ describe Faye::Redis do
4
+ let(:engine_opts) do
5
+ pw = ENV["TRAVIS"] ? nil : "foobared"
6
+ {:type => Faye::Redis, :password => pw, :namespace => Time.now.to_i.to_s}
7
+ end
8
+
9
+ after do
10
+ engine.disconnect
11
+ redis = EM::Hiredis::Client.connect('localhost', 6379)
12
+ redis.auth(engine_opts[:password])
13
+ redis.flushall
14
+ end
15
+
16
+ it_should_behave_like "faye engine"
17
+ it_should_behave_like "distributed engine"
18
+
19
+ next if ENV["TRAVIS"]
20
+
21
+ describe "using a Unix socket" do
22
+ before { engine_opts[:socket] = "/tmp/redis.sock" }
23
+ it_should_behave_like "faye engine"
24
+ end
25
+ end
26
+
data/spec/redis.conf ADDED
@@ -0,0 +1,42 @@
1
+ daemonize no
2
+ pidfile /tmp/redis.pid
3
+ port 6379
4
+ unixsocket /tmp/redis.sock
5
+ timeout 300
6
+ loglevel verbose
7
+ logfile stdout
8
+ databases 16
9
+
10
+ save 900 1
11
+ save 300 10
12
+ save 60 10000
13
+
14
+ rdbcompression yes
15
+ dbfilename dump.rdb
16
+ dir ./
17
+
18
+ slave-serve-stale-data yes
19
+
20
+ requirepass foobared
21
+
22
+ appendonly no
23
+ appendfsync everysec
24
+ no-appendfsync-on-rewrite no
25
+
26
+ vm-enabled no
27
+ vm-swap-file /tmp/redis.swap
28
+ vm-max-memory 0
29
+ vm-page-size 32
30
+ vm-pages 134217728
31
+ vm-max-threads 4
32
+
33
+ hash-max-zipmap-entries 512
34
+ hash-max-zipmap-value 64
35
+
36
+ list-max-ziplist-entries 512
37
+ list-max-ziplist-value 64
38
+
39
+ set-max-intset-entries 512
40
+
41
+ activerehashing yes
42
+
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../../lib/faye/redis', __FILE__)
2
+ require File.expand_path('../../vendor/faye/spec/ruby/engine_examples', __FILE__)
3
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faye-redis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James Coglan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &10557300 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.12.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *10557300
25
+ - !ruby/object:Gem::Dependency
26
+ name: em-hiredis
27
+ requirement: &10556820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.0.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *10556820
36
+ - !ruby/object:Gem::Dependency
37
+ name: yajl-ruby
38
+ requirement: &10556360 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 1.0.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *10556360
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &10555900 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.8.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *10555900
58
+ description:
59
+ email: jcoglan@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files:
63
+ - README.rdoc
64
+ files:
65
+ - README.rdoc
66
+ - spec/redis.conf
67
+ - spec/faye_redis_spec.rb
68
+ - spec/spec_helper.rb
69
+ - lib/faye/redis.rb
70
+ homepage: http://github.com/faye/faye-redis-ruby
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --main
75
+ - README.rdoc
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.10
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Redis backend engine for Faye
96
+ test_files: []