redisse 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +28 -0
- data/bin/redisse +4 -0
- data/example/.env +3 -0
- data/example/README.md +37 -0
- data/example/bin/publish +15 -0
- data/example/bin/redis +13 -0
- data/example/config.ru +40 -0
- data/example/nginx.conf +29 -0
- data/example/public/index.html +46 -0
- data/example/spec/app_spec.rb +39 -0
- data/lib/redisse.rb +181 -0
- data/lib/redisse/configuration.rb +47 -0
- data/lib/redisse/publisher.rb +70 -0
- data/lib/redisse/redirect_endpoint.rb +45 -0
- data/lib/redisse/server.rb +205 -0
- data/lib/redisse/server/redis.rb +39 -0
- data/lib/redisse/server/responses.rb +22 -0
- data/lib/redisse/server/stats.rb +10 -0
- data/lib/redisse/server_sent_events.rb +17 -0
- data/lib/redisse/version.rb +3 -0
- data/redisse.gemspec +29 -0
- data/spec/example_spec.rb +200 -0
- data/spec/publisher_spec.rb +81 -0
- data/spec/redirect_endpoint_spec.rb +98 -0
- data/spec/server_sent_events_spec.rb +53 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/spec_system_helper.rb +204 -0
- metadata +214 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5799fb06db34c512846d7e6d11fd403c7bc2b8a7
|
4
|
+
data.tar.gz: 798680edb40005c798ab67fc4d13dc1d690209ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 61edfb6b8f496dfe5497d565dc0e6a7de62d9d1a104e827f589c364188c70126f0a521d597667802a183c3be4e33df1c8387a73ef6dcd9935ed31b7c7e619f87
|
7
|
+
data.tar.gz: 2014e85400758f2a0a1b0eb149668ad36b5b20b95dc18a32822c30f307aede8a879075f4de88ff626fa1efc76255c0056a130d413f09710e2e7d68be63036757
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.2
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Tigerlily
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Redisse
|
2
|
+
|
3
|
+
Redisse is a Redis-backed Ruby library for creating [Server-Sent
|
4
|
+
Events](http://www.w3.org/TR/eventsource/), publishing them from your
|
5
|
+
application, and serving them to your clients.
|
6
|
+
|
7
|
+
* **Homepage:**
|
8
|
+
[github.com/tigerlily/redisse](https://github.com/tigerlily/redisse)
|
9
|
+
* **Documentation:**
|
10
|
+
[tigerlily.github.io/redisse](https://tigerlily.github.io/redisse/)
|
11
|
+
|
12
|
+
## Features
|
13
|
+
|
14
|
+
* Pub/Sub split into **channels** for privacy & access rights handling.
|
15
|
+
|
16
|
+
* **SSE history** via the `Last-Event-Id` header and the `lastEventId` query
|
17
|
+
parameter, with a limit of 100 events per channel.
|
18
|
+
|
19
|
+
* **Long-polling** via the `polling` query parameter. Allows to send several
|
20
|
+
events at once for long-polling clients by waiting one second before closing
|
21
|
+
the connection.
|
22
|
+
|
23
|
+
* **Lightweight**: only one Redis connection for history and one for all
|
24
|
+
subscriptions, no matter the number of connected clients.
|
25
|
+
|
26
|
+
* **`missedevents` event fired** when the full requested history could not be
|
27
|
+
found, to allow the client to handle the case where events were missed.
|
28
|
+
|
29
|
+
* **Event types** from SSE are left untouched for your application code, but
|
30
|
+
keep in mind that a client will receive events of all types from their
|
31
|
+
channels. To handle access rights, use channels instead.
|
32
|
+
|
33
|
+
## Rationale
|
34
|
+
|
35
|
+
Redisse’s design comes from these requirements:
|
36
|
+
|
37
|
+
* The client wants to listen to several channels but use only one connection.
|
38
|
+
(e.g. a single `EventSource` object is created in the browser but you want
|
39
|
+
events coming from different Redis channels.)
|
40
|
+
|
41
|
+
* A server handles the concurrent connections so that the application servers
|
42
|
+
don't need to (e.g. Unicorn workers).
|
43
|
+
|
44
|
+
* The application is written in Ruby, so there needs to be a Ruby API to
|
45
|
+
publish events.
|
46
|
+
|
47
|
+
* The application is written on top of Rack, so the code that lists the Redis
|
48
|
+
Pub/Sub channels to subscribe to needs to be able to use Rack middlewares and
|
49
|
+
should receive a Rack environment. (e.g. you can use
|
50
|
+
[Warden](https://github.com/hassox/warden) as a middleware and simply use
|
51
|
+
`env['warden'].user` to decide which channels the user can access.)
|
52
|
+
|
53
|
+
### Redirect endpoint
|
54
|
+
|
55
|
+
The simplest way that last point can be fulfilled is by actually loading and
|
56
|
+
running your code in the Redisse server. Unfortunately since it’s
|
57
|
+
EventMachine-based, if your method takes a while to returns the channels, all
|
58
|
+
the other connected clients will be blocked too. You'll also have some
|
59
|
+
duplication between your [Rack config](https://github.com/tigerlily/redisse/blob/9052630e57081714365188a8f55f0549aee03d56/example/config.ru#L30)
|
60
|
+
and [Redisse server config](https://github.com/tigerlily/redisse/blob/9052630e57081714365188a8f55f0549aee03d56/example/lib/sse_server.rb#L15).
|
61
|
+
|
62
|
+
Another way if you use nginx is instead to use a endpoint in your main
|
63
|
+
application that will use the header X-Accel-Redirect to redirect to the
|
64
|
+
Redisse server, which is now free from your blocking code. The channels will be
|
65
|
+
sent instead via the redirect URL. See the [section on nginx](#behind-nginx)
|
66
|
+
for more info.
|
67
|
+
|
68
|
+
## Installation
|
69
|
+
|
70
|
+
Add this line to your application's Gemfile:
|
71
|
+
|
72
|
+
gem 'redisse', '~> 0.4.0'
|
73
|
+
|
74
|
+
## Usage
|
75
|
+
|
76
|
+
Configure Redisse (e.g. in `config/initializers/redisse.rb`):
|
77
|
+
|
78
|
+
require 'redisse'
|
79
|
+
|
80
|
+
Redisse.channels do |env|
|
81
|
+
%w[ global ]
|
82
|
+
end
|
83
|
+
|
84
|
+
Use the endpoint in your main application (in config.ru or your router):
|
85
|
+
|
86
|
+
# config.ru Rack
|
87
|
+
map "/events" do
|
88
|
+
run Redisse.redirect_endpoint
|
89
|
+
end
|
90
|
+
|
91
|
+
# config/routes.rb Rails
|
92
|
+
get "/events" => Redisse.redirect_endpoint
|
93
|
+
|
94
|
+
Run the server:
|
95
|
+
|
96
|
+
$ redisse --stdout --verbose
|
97
|
+
|
98
|
+
Get ready to receive events:
|
99
|
+
|
100
|
+
$ curl localhost:8080 -H 'Accept: text/event-stream'
|
101
|
+
|
102
|
+
Send a Server-Sent Event:
|
103
|
+
|
104
|
+
Redisse.publish('global', success: "It's working!")
|
105
|
+
|
106
|
+
### Testing
|
107
|
+
|
108
|
+
In the traditional Rack app specs or tests, use `Redisse.test_mode!`:
|
109
|
+
|
110
|
+
describe "SSE" do
|
111
|
+
before do
|
112
|
+
Redisse.test_mode!
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should send a Server-Sent Event" do
|
116
|
+
post '/publish', channel: 'global', message: 'Hello'
|
117
|
+
expect(Redisse.published.size).to be == 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
See [the example app
|
122
|
+
specs](https://github.com/tigerlily/redisse/blob/master/example/spec/app_spec.rb).
|
123
|
+
|
124
|
+
### Behind nginx
|
125
|
+
|
126
|
+
When running behind nginx as a reverse proxy, you should disable buffering
|
127
|
+
(`proxy_buffering off`) and close the connection to the server when the client
|
128
|
+
disconnects (`proxy_ignore_client_abort on`) to preserve resources (otherwise
|
129
|
+
connections to Redis will be kept alive longer than necessary).
|
130
|
+
|
131
|
+
You should take advantage of the [redirect endpoint](#redirect-endpoint)
|
132
|
+
instead of directing the SSE requests to the SSE server. Let your Rack
|
133
|
+
application determine the channels, but have the request served by the SSE
|
134
|
+
server with a redirect (X-Accel-Redirect) to an internal location.
|
135
|
+
|
136
|
+
In this case, and if you have a large number of long-named channels, the
|
137
|
+
internal redirect URL will be long and you might need to increase
|
138
|
+
`proxy_buffer_size` from its default in your Rack application location
|
139
|
+
configuration. For example, 8k will allow you about 200 channels with UUIDs as
|
140
|
+
names, which is quite a lot.
|
141
|
+
|
142
|
+
You can check the [nginx conf of the
|
143
|
+
example](https://github.com/tigerlily/redisse/blob/master/example/nginx.conf)
|
144
|
+
for all the details.
|
145
|
+
|
146
|
+
## Contributing
|
147
|
+
|
148
|
+
1. Fork it
|
149
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
150
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
151
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
152
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'yard'
|
5
|
+
YARD::Config.load_plugin 'tomdoc'
|
6
|
+
YARD::Rake::YardocTask.new do |t|
|
7
|
+
t.name = :doc
|
8
|
+
end
|
9
|
+
|
10
|
+
task :gh_pages => :doc do
|
11
|
+
sh 'git checkout gh-pages'
|
12
|
+
sh 'rsync -a doc/* .'
|
13
|
+
sh 'git add .'
|
14
|
+
version = "v#{Bundler::GemHelper.gemspec.version}"
|
15
|
+
sh "git commit -m 'Documentation for #{version}'"
|
16
|
+
sh 'git checkout -'
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
end
|
20
|
+
|
21
|
+
begin
|
22
|
+
require 'rspec/core/rake_task'
|
23
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
24
|
+
task.pattern = "{example/,}" + task.pattern
|
25
|
+
end
|
26
|
+
task :default => :spec
|
27
|
+
rescue LoadError
|
28
|
+
end
|
data/bin/redisse
ADDED
data/example/.env
ADDED
data/example/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Redisse example: Rack app
|
2
|
+
|
3
|
+
Get the dependencies:
|
4
|
+
|
5
|
+
$ bundle
|
6
|
+
|
7
|
+
Note that the example uses [dotenv](https://github.com/bkeepers/dotenv):
|
8
|
+
|
9
|
+
$ cat .env
|
10
|
+
|
11
|
+
Change .env to point to a running Redis server, or simply run the dedicated
|
12
|
+
Redis server:
|
13
|
+
|
14
|
+
$ bin/redis
|
15
|
+
|
16
|
+
Run the Rack application server:
|
17
|
+
|
18
|
+
$ bundle exec dotenv rackup --port 8081
|
19
|
+
|
20
|
+
Run the SSE server:
|
21
|
+
|
22
|
+
$ bundle exec dotenv redisse --stdout --verbose
|
23
|
+
|
24
|
+
Finally run nginx to glue them together:
|
25
|
+
|
26
|
+
$ nginx -p $PWD -c nginx.conf
|
27
|
+
|
28
|
+
Open [http://localhost:8080/](http://localhost:8080/) in multiple browsers and
|
29
|
+
tabs and then send messages to see them replicated.
|
30
|
+
|
31
|
+
A Rack session cookie is used to randomly select one of four channels
|
32
|
+
(`channel_1` to `channel_4`) and simulate different access rights for
|
33
|
+
different users of your application.
|
34
|
+
|
35
|
+
You can also send events from the command line:
|
36
|
+
|
37
|
+
$ bin/publish global message 'Hello CLI'
|
data/example/bin/publish
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'dotenv'
|
4
|
+
Dotenv.load
|
5
|
+
require 'redisse'
|
6
|
+
|
7
|
+
if count = ARGV.detect {|arg| arg.start_with?('N=') }
|
8
|
+
ARGV.delete(count)
|
9
|
+
count = count[/(\d+)/, 1].to_i
|
10
|
+
else
|
11
|
+
count = 1
|
12
|
+
end
|
13
|
+
count.times do
|
14
|
+
puts "Event ##{ Redisse.publish ARGV[0], ARGV[1] => ARGV[2] } published"
|
15
|
+
end
|
data/example/bin/redis
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'dotenv'
|
4
|
+
Dotenv.load
|
5
|
+
|
6
|
+
puts "Starting Redis server on 127.0.0.1:#{ENV['REDIS_PORT']}"
|
7
|
+
Process.exec 'redis-server ' \
|
8
|
+
'--daemonize yes ' \
|
9
|
+
'--pidfile redis.pid ' \
|
10
|
+
'--port $REDIS_PORT ' \
|
11
|
+
'--bind 127.0.0.1 ' \
|
12
|
+
'--logfile /dev/null ' \
|
13
|
+
'--databases 1'
|
data/example/config.ru
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'dotenv'
|
3
|
+
Dotenv.load
|
4
|
+
|
5
|
+
require 'redisse'
|
6
|
+
|
7
|
+
Redisse.channels do |env|
|
8
|
+
env['rack.session']['channels'] ||=
|
9
|
+
%w[ global ] << "channel_#{rand(4)+1}"
|
10
|
+
end
|
11
|
+
|
12
|
+
class Application
|
13
|
+
def call(env)
|
14
|
+
request = Rack::Request.new env
|
15
|
+
|
16
|
+
if publish?(request)
|
17
|
+
Redisse.publish(request['channel'], request['message'])
|
18
|
+
return Rack::Response.new "No Content", 204
|
19
|
+
elsif subscriptions?(request)
|
20
|
+
return Rack::Response.new Redisse.channels(env).join(", "), 200
|
21
|
+
end
|
22
|
+
|
23
|
+
Rack::Response.new "Not Found", 404
|
24
|
+
end
|
25
|
+
|
26
|
+
def publish?(request)
|
27
|
+
request.post? && request.path_info == '/publish'
|
28
|
+
end
|
29
|
+
|
30
|
+
def subscriptions?(request)
|
31
|
+
request.get? && request.path_info == '/subscriptions'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
use Rack::Session::Cookie, secret: 'not a secret'
|
36
|
+
use Rack::Static, urls: {"/" => 'index.html'}, root: 'public', index: 'index.html'
|
37
|
+
map "/events" do
|
38
|
+
run Redisse.redirect_endpoint
|
39
|
+
end
|
40
|
+
run Application.new
|
data/example/nginx.conf
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
pid nginx.pid;
|
2
|
+
error_log nginx.log error;
|
3
|
+
daemon off;
|
4
|
+
|
5
|
+
events {
|
6
|
+
worker_connections 1024;
|
7
|
+
}
|
8
|
+
|
9
|
+
http {
|
10
|
+
server {
|
11
|
+
listen 8080;
|
12
|
+
|
13
|
+
location / {
|
14
|
+
proxy_pass http://localhost:8081;
|
15
|
+
|
16
|
+
# necessary if lots of long-named channels
|
17
|
+
#proxy_buffer_size 8k;
|
18
|
+
}
|
19
|
+
|
20
|
+
location /redisse/ {
|
21
|
+
internal;
|
22
|
+
proxy_pass http://localhost:8082;
|
23
|
+
proxy_buffering off;
|
24
|
+
proxy_ignore_client_abort on;
|
25
|
+
proxy_http_version 1.1;
|
26
|
+
}
|
27
|
+
}
|
28
|
+
access_log off;
|
29
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head><title>Redisse example: Rack app</title></head>
|
4
|
+
<body>
|
5
|
+
<h1>Redisse example: Rack app</h1>
|
6
|
+
|
7
|
+
<p>Subscribed to: <span id=subscriptions></span></p>
|
8
|
+
|
9
|
+
<form id=publish method=POST action=publish>
|
10
|
+
<label>Channel
|
11
|
+
<input name=channel value=global>
|
12
|
+
</label>
|
13
|
+
<label>Message
|
14
|
+
<input name=message value=Hello>
|
15
|
+
</label>
|
16
|
+
<input type=submit>
|
17
|
+
</form>
|
18
|
+
|
19
|
+
<pre id=log></pre>
|
20
|
+
|
21
|
+
<script>
|
22
|
+
var subscriptions = document.getElementById('subscriptions')
|
23
|
+
var xhr = new XMLHttpRequest()
|
24
|
+
xhr.open('GET', '/subscriptions', false)
|
25
|
+
xhr.send(null)
|
26
|
+
subscriptions.innerHTML = xhr.responseText
|
27
|
+
|
28
|
+
var form = document.getElementById('publish')
|
29
|
+
form.addEventListener('submit', function(e) {
|
30
|
+
e.preventDefault()
|
31
|
+
var data = new FormData(form)
|
32
|
+
var xhr = new XMLHttpRequest()
|
33
|
+
xhr.open(form.method, form.action, false)
|
34
|
+
xhr.send(data)
|
35
|
+
return false
|
36
|
+
}, false)
|
37
|
+
|
38
|
+
var log = document.getElementById('log')
|
39
|
+
var source = new EventSource('/events')
|
40
|
+
source.addEventListener('message', function(e) {
|
41
|
+
log.innerHTML += e.data + "\n"
|
42
|
+
}, false)
|
43
|
+
</script>
|
44
|
+
|
45
|
+
</body>
|
46
|
+
</html>
|