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
@@ -0,0 +1,39 @@
|
|
1
|
+
module Redisse
|
2
|
+
module Server::Redis
|
3
|
+
def redis
|
4
|
+
@redis ||= EM::Hiredis.connect(redisse.redis_server)
|
5
|
+
end
|
6
|
+
|
7
|
+
def pubsub(&on_disconnected)
|
8
|
+
ensure_pubsub
|
9
|
+
return false unless @pubsub.connected?
|
10
|
+
@pubsub_errbacks << on_disconnected
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def ensure_pubsub
|
15
|
+
return if defined? @pubsub
|
16
|
+
@pubsub = redis.pubsub
|
17
|
+
@pubsub_errbacks = []
|
18
|
+
@pubsub.on(:disconnected, &method(:on_redis_close))
|
19
|
+
EM::Synchrony.sync(@pubsub)
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_redis_close
|
23
|
+
@pubsub_errbacks.each(&:call)
|
24
|
+
@pubsub_errbacks.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
def pubsub_subcribe(channels, callback)
|
28
|
+
channels.each do |channel|
|
29
|
+
@pubsub.subscribe(channel, callback)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pubsub_unsubscribe_proc(channels, callback)
|
34
|
+
channels.each do |channel|
|
35
|
+
@pubsub.unsubscribe_proc(channel, callback)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Redisse
|
2
|
+
module Server::Responses
|
3
|
+
def plain_response(message = nil, code)
|
4
|
+
message ||= "#{code} #{Goliath::HTTP_STATUS_CODES.fetch(code)}\n"
|
5
|
+
Rack::Response.new(message, code, 'Content-Type' => 'text/plain')
|
6
|
+
end
|
7
|
+
|
8
|
+
def not_acceptable
|
9
|
+
plain_response "406 Not Acceptable\n" \
|
10
|
+
"This resource can only be represented as text/event-stream.\n",
|
11
|
+
406
|
12
|
+
end
|
13
|
+
|
14
|
+
def not_found
|
15
|
+
plain_response 404
|
16
|
+
end
|
17
|
+
|
18
|
+
def service_unavailable
|
19
|
+
plain_response 503
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Redisse
|
2
|
+
module ServerSentEvents
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def server_sent_event(data, type: nil, id: nil, **options)
|
7
|
+
data = String(data)
|
8
|
+
str = ''
|
9
|
+
str << "retry: #{options[:retry]}\n" if options[:retry]
|
10
|
+
str << "id: #{id}\n" if id
|
11
|
+
str << "event: #{type}\n" if type
|
12
|
+
str << "data: " + data.gsub("\n", "\ndata: ") + "\n"
|
13
|
+
str << "\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
data/redisse.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'redisse/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "redisse"
|
8
|
+
spec.version = Redisse::VERSION
|
9
|
+
spec.authors = ["Étienne Barrié", "Julien Blanchard"]
|
10
|
+
spec.email = ["etienne.barrie@gmail.com", "julien@sideburns.eu"]
|
11
|
+
spec.summary = %q{Server-Sent Events via Redis}
|
12
|
+
spec.homepage = "https://github.com/tigerlily/redisse"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
spec.add_development_dependency "yard-tomdoc"
|
24
|
+
spec.add_development_dependency "rack-test"
|
25
|
+
spec.add_development_dependency "dotenv"
|
26
|
+
spec.add_runtime_dependency "goliath"
|
27
|
+
spec.add_runtime_dependency "em-hiredis"
|
28
|
+
spec.add_runtime_dependency "redis"
|
29
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'spec_system_helper'
|
2
|
+
require 'dotenv'
|
3
|
+
Dotenv.load 'example/.env'
|
4
|
+
|
5
|
+
REDIS_PORT = ENV['REDIS_PORT']
|
6
|
+
REDISSE_PORT = ENV['REDISSE_PORT']
|
7
|
+
REDISSE_REDIS = ENV['REDISSE_REDIS']
|
8
|
+
|
9
|
+
describe "Example" do
|
10
|
+
BIN = __dir__ + '/../example/bin/'
|
11
|
+
GEM_BIN = __dir__ + '/../bin/'
|
12
|
+
|
13
|
+
include_context "system"
|
14
|
+
|
15
|
+
describe "basic tests" do
|
16
|
+
before :context do
|
17
|
+
@redis = run_server "#{BIN}redis", REDIS_PORT, pidfile: 'redis.pid'
|
18
|
+
@redisse = run_server "#{GEM_BIN}redisse", REDISSE_PORT
|
19
|
+
@redis.wait_tcp
|
20
|
+
@redisse.wait_tcp
|
21
|
+
end
|
22
|
+
|
23
|
+
after :context do
|
24
|
+
@redis.stop
|
25
|
+
@redisse.stop
|
26
|
+
end
|
27
|
+
|
28
|
+
it "refuses a connection with 406 without proper Accept header" do
|
29
|
+
uri = URI(redisse_url)
|
30
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
31
|
+
request = Net::HTTP::Get.new uri
|
32
|
+
response = http.request request
|
33
|
+
expect(response.code).to be == "406"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "refuses a connection with 404 without channels" do
|
38
|
+
reader = EventReader.open redisse_url
|
39
|
+
expect(reader).not_to be_connected
|
40
|
+
expect(reader.response.code).to be == "404"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "receives a message" do
|
44
|
+
reader = EventReader.open redisse_url :global
|
45
|
+
expect(reader).to be_connected
|
46
|
+
publish :global, :foo, :bar
|
47
|
+
reader.each do |event|
|
48
|
+
expect(event.type).to be == 'foo'
|
49
|
+
expect(event.data).to be == 'bar'
|
50
|
+
reader.close
|
51
|
+
end
|
52
|
+
expect(reader).not_to be_connected
|
53
|
+
end
|
54
|
+
|
55
|
+
it "receives different messages on different channels" do
|
56
|
+
reader_1 = EventReader.open redisse_url :global, :channel_1
|
57
|
+
reader_2 = EventReader.open redisse_url :global, :channel_2
|
58
|
+
expect(reader_1).to be_connected
|
59
|
+
expect(reader_2).to be_connected
|
60
|
+
publish :global, :foo, :foo_data
|
61
|
+
publish :channel_1, :bar, :bar_data
|
62
|
+
publish :channel_2, :baz, :baz_data
|
63
|
+
events_1 = reader_1.each.take(2)
|
64
|
+
events_2 = reader_2.each.take(2)
|
65
|
+
expect(events_1.map(&:type)).to be == %w(foo bar)
|
66
|
+
expect(events_1.map(&:data)).to be == %w(foo_data bar_data)
|
67
|
+
expect(events_2.map(&:type)).to be == %w(foo baz)
|
68
|
+
expect(events_2.map(&:data)).to be == %w(foo_data baz_data)
|
69
|
+
reader_1.close
|
70
|
+
reader_2.close
|
71
|
+
end
|
72
|
+
|
73
|
+
it "closes the connection after a second with long polling" do
|
74
|
+
reader = EventReader.open redisse_url :global, :polling
|
75
|
+
expect(reader).to be_connected
|
76
|
+
publish :global, :foo, :bar
|
77
|
+
time = Time.now.to_f
|
78
|
+
publish :global, :foo, :baz
|
79
|
+
received = nil
|
80
|
+
expect {
|
81
|
+
begin
|
82
|
+
Timeout.timeout(2) do
|
83
|
+
received = reader.each.to_a
|
84
|
+
end
|
85
|
+
rescue Timeout::Error
|
86
|
+
end
|
87
|
+
time = Time.now.to_f
|
88
|
+
}.to change { time }.by(a_value_within(0.2).of(1.0))
|
89
|
+
expect(reader).not_to be_connected
|
90
|
+
expect(received.size).to be == 2
|
91
|
+
expect(received.map(&:data)).to be == %w(bar baz)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "sends a heartbeat", :slow do
|
95
|
+
reader = EventReader.open redisse_url :global
|
96
|
+
expect(reader).to be_connected
|
97
|
+
expect(reader.full_stream).to be_empty
|
98
|
+
sleep(16)
|
99
|
+
expect(reader.full_stream).to match(/^: hb$/)
|
100
|
+
reader.close
|
101
|
+
end
|
102
|
+
|
103
|
+
it "sends history" do
|
104
|
+
event_id = publish :global, :foo, :foo_data
|
105
|
+
expect(event_id).not_to be_nil
|
106
|
+
publish :global, :foo, :hist_1
|
107
|
+
publish :channel_1, :foo, :hist_2
|
108
|
+
events = EventReader.open redisse_url(:global, :channel_1), event_id do |reader|
|
109
|
+
reader.each.take(2)
|
110
|
+
end
|
111
|
+
expect(events.map(&:data)).to be == %w(hist_1 hist_2)
|
112
|
+
end
|
113
|
+
|
114
|
+
let(:history_size) { 100 }
|
115
|
+
|
116
|
+
describe "sends a missedevents events" do
|
117
|
+
example "if full history could not be fetched" do
|
118
|
+
event_id = publish :global, :foo, :foo_data
|
119
|
+
publish :global, :foo, :missed
|
120
|
+
publish :global, :foo, :first
|
121
|
+
publish :global, :foo, :foo_data, count: history_size - 2
|
122
|
+
publish :global, :foo, :'last straw'
|
123
|
+
events = EventReader.open redisse_url(:global), event_id do |reader|
|
124
|
+
enum = reader.each
|
125
|
+
event = enum.next
|
126
|
+
expect(event.type).to be == 'missedevents'
|
127
|
+
enum.take(history_size)
|
128
|
+
end
|
129
|
+
expect(events.first.data).to be == 'first'
|
130
|
+
expect(events[1...-1].map(&:data)).to all be == 'foo_data'
|
131
|
+
expect(events.last.data).to be == 'last straw'
|
132
|
+
end
|
133
|
+
|
134
|
+
example "if full history was fetched but the server can't know if there were missed events" do
|
135
|
+
event_id = publish :global, :foo, :foo_data
|
136
|
+
publish :global, :foo, :first
|
137
|
+
publish :global, :foo, :foo_data, count: history_size - 2
|
138
|
+
publish :global, :foo, :'last straw'
|
139
|
+
events = EventReader.open redisse_url(:global), event_id do |reader|
|
140
|
+
enum = reader.each
|
141
|
+
event = enum.next
|
142
|
+
expect(event.type).to be == 'missedevents'
|
143
|
+
enum.take(history_size)
|
144
|
+
end
|
145
|
+
expect(events.first.data).to be == 'first'
|
146
|
+
expect(events[1...-1].map(&:data)).to all be == 'foo_data'
|
147
|
+
expect(events.last.data).to be == 'last straw'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
it "stores 100 events per channel for history" do
|
152
|
+
event_id = publish :global, :foo, :seen
|
153
|
+
publish :global, :foo, :foo_data, count: history_size - 1
|
154
|
+
publish :channel_1, :bar, :bar_data, count: history_size
|
155
|
+
events = EventReader.open redisse_url(:global, :channel_1), event_id do |reader|
|
156
|
+
reader.each.take(2 * history_size - 1)
|
157
|
+
end
|
158
|
+
expect(events.first(history_size - 1).map(&:type)).to all be == 'foo'
|
159
|
+
expect(events.last(history_size).map(&:type)) .to all be == 'bar'
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "Redis failures" do
|
164
|
+
before :context do
|
165
|
+
@redis = run_server "#{BIN}redis", REDIS_PORT, pidfile: 'redis.pid'
|
166
|
+
@redisse = run_server "#{GEM_BIN}redisse", REDISSE_PORT
|
167
|
+
@redis.wait_tcp
|
168
|
+
@redisse.wait_tcp
|
169
|
+
end
|
170
|
+
|
171
|
+
after :context do
|
172
|
+
@redis.stop
|
173
|
+
@redisse.stop
|
174
|
+
end
|
175
|
+
|
176
|
+
it "disconnects then refuses connections with 503" do
|
177
|
+
reader = EventReader.open redisse_url :global
|
178
|
+
expect(reader).to be_connected
|
179
|
+
@redis.stop
|
180
|
+
Timeout.timeout(0.1) do
|
181
|
+
reader.each.to_a
|
182
|
+
end
|
183
|
+
expect(reader).not_to be_connected
|
184
|
+
reader = EventReader.open redisse_url :global
|
185
|
+
expect(reader).not_to be_connected
|
186
|
+
expect(reader.response.code).to be == "503"
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
def publish(channel, type, data, count: nil)
|
192
|
+
count = "N=#{count}" if count
|
193
|
+
output = `#{BIN}/publish '#{channel}' '#{type}' '#{data}' #{count}`
|
194
|
+
output[/(\d+)/, 1]
|
195
|
+
end
|
196
|
+
|
197
|
+
def redisse_url(*channels)
|
198
|
+
"http://localhost:#{REDISSE_PORT}/?#{URI.encode_www_form(channels)}"
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'redisse'
|
3
|
+
|
4
|
+
module Events
|
5
|
+
extend Redisse
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "Publishing events" do
|
9
|
+
context "basic usage" do
|
10
|
+
before do
|
11
|
+
Events.test_mode!
|
12
|
+
end
|
13
|
+
|
14
|
+
it "has no events initially" do
|
15
|
+
expect(Events.published.size).to be == 0
|
16
|
+
end
|
17
|
+
|
18
|
+
it "keeps the published events" do
|
19
|
+
Events.publish :global, foo: 'bar'
|
20
|
+
expect(Events.published.size).to be == 1
|
21
|
+
Events.publish :global, foo: 'baz'
|
22
|
+
expect(Events.published.size).to be == 2
|
23
|
+
end
|
24
|
+
|
25
|
+
it "gives access to the event channel, type and data" do
|
26
|
+
Events.publish :global, foo: 'bar'
|
27
|
+
event = Events.published.first
|
28
|
+
expect(event.channel).to be == :global
|
29
|
+
expect(event.type).to be == :foo
|
30
|
+
expect(event.data).to be == 'bar'
|
31
|
+
end
|
32
|
+
|
33
|
+
it "parses data as JSON" do
|
34
|
+
Events.publish :global, foo: JSON.dump(bar: 'baz')
|
35
|
+
json = Events.published.first.json
|
36
|
+
expect(json['bar']).to be == 'baz'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with a filter" do
|
41
|
+
it "filters by a simple event type" do
|
42
|
+
Events.test_filter = :foo
|
43
|
+
Events.publish :global, foo: 'bar'
|
44
|
+
Events.publish :global, bar: 'bar'
|
45
|
+
expect(Events.published.size).to be == 1
|
46
|
+
expect(Events.published.first.type).to be == :foo
|
47
|
+
end
|
48
|
+
|
49
|
+
it "filters with a Proc" do
|
50
|
+
Events.test_filter = -> type { %i(foo bar).include? type }
|
51
|
+
Events.publish :global, foo: 'bar'
|
52
|
+
Events.publish :global, bar: 'bar'
|
53
|
+
Events.publish :global, baz: 'bar'
|
54
|
+
expect(Events.published.size).to be == 2
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails if test mode is not set" do
|
59
|
+
events = Module.new.extend Redisse
|
60
|
+
expect {
|
61
|
+
events.published
|
62
|
+
}.to raise_error(/\.test_mode!/)
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "TestEvent" do
|
66
|
+
require 'yaml'
|
67
|
+
|
68
|
+
class Redisse::TestEvent
|
69
|
+
def yml
|
70
|
+
YAML.load data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it "can be extended" do
|
75
|
+
Events.test_mode!
|
76
|
+
Events.publish :global, foo: YAML.dump(bar: 'baz')
|
77
|
+
yml = Events.published.first.yml
|
78
|
+
expect(yml[:bar]).to be == 'baz'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'redisse'
|
3
|
+
require 'rack/test'
|
4
|
+
|
5
|
+
describe "Redirect endpoint" do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
let :redisse do
|
9
|
+
Module.new do
|
10
|
+
extend Redisse
|
11
|
+
|
12
|
+
def self.channels(env)
|
13
|
+
%w(global)
|
14
|
+
end
|
15
|
+
|
16
|
+
self.nginx_internal_url = '/internal'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def app
|
21
|
+
redirect_endpoint = redisse.redirect_endpoint
|
22
|
+
Rack::Builder.new do
|
23
|
+
use Rack::Lint
|
24
|
+
run redirect_endpoint
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "is a Rack app" do
|
29
|
+
expect(redisse.redirect_endpoint).to respond_to(:call)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "returns a 200 OK" do
|
33
|
+
get '/'
|
34
|
+
expect(last_response).to be_ok
|
35
|
+
end
|
36
|
+
|
37
|
+
def redirect_url(uri = '/')
|
38
|
+
get uri
|
39
|
+
last_response['X-Accel-Redirect']
|
40
|
+
end
|
41
|
+
|
42
|
+
it "redirects to the nginx_internal_url" do
|
43
|
+
redisse.nginx_internal_url = '/foo/'
|
44
|
+
expect(redirect_url).to start_with "/foo/"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "forces a slash at the end" do
|
48
|
+
redisse.nginx_internal_url = '/foo'
|
49
|
+
expect(redirect_url).to start_with "/foo/"
|
50
|
+
end
|
51
|
+
|
52
|
+
def redirect_params(uri = '/')
|
53
|
+
query = redirect_url(uri).split('?', 2).last
|
54
|
+
URI.decode_www_form(query)
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "passing channels" do
|
58
|
+
it "passes the channels as query params" do
|
59
|
+
def redisse.channels(env)
|
60
|
+
%w(foo bar)
|
61
|
+
end
|
62
|
+
expect(redirect_params.map(&:first)).to be == %w(foo bar)
|
63
|
+
end
|
64
|
+
|
65
|
+
%w(lastEventId polling).each do |reserved|
|
66
|
+
it "fails for the reserved channel name '#{reserved}'" do
|
67
|
+
def redisse.channels(env)
|
68
|
+
[reserved]
|
69
|
+
end
|
70
|
+
expect { get '/' }.to raise_error(/reserved/i)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "query params" do
|
76
|
+
it "passes the lastEventId param" do
|
77
|
+
params = redirect_params('/?lastEventId=42')
|
78
|
+
expect(params.assoc('lastEventId')).not_to be_nil
|
79
|
+
end
|
80
|
+
|
81
|
+
it "passes the polling param" do
|
82
|
+
params = redirect_params('/?polling')
|
83
|
+
expect(params.assoc('polling')).not_to be_nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it "passes lastEventId and polling params" do
|
87
|
+
params = redirect_params('/?lastEventId=42&polling')
|
88
|
+
expect(params.assoc('lastEventId')).not_to be_nil
|
89
|
+
expect(params.assoc('polling')).not_to be_nil
|
90
|
+
end
|
91
|
+
|
92
|
+
it "ignores other params" do
|
93
|
+
params = redirect_params('/?foo')
|
94
|
+
expect(params.assoc('foo')).to be_nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|