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 +58 -0
- data/lib/faye/redis.rb +222 -0
- data/spec/faye_redis_spec.rb +26 -0
- data/spec/redis.conf +42 -0
- data/spec/spec_helper.rb +3 -0
- metadata +96 -0
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
|
+
|
data/spec/spec_helper.rb
ADDED
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: []
|