redisse 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /example/nginx.log
19
+ /example/nginx.pid
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --order rand
@@ -0,0 +1 @@
1
+ 2.1.2
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redisse.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'redisse/server'
4
+ Redisse.run
@@ -0,0 +1,3 @@
1
+ REDIS_PORT=6380
2
+ REDISSE_REDIS=redis://localhost:$REDIS_PORT/
3
+ REDISSE_PORT=8082
@@ -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'
@@ -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
@@ -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'
@@ -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
@@ -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>