redisse 0.4.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.
- 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>
|