redisse 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ module Redisse
2
+ class Server::Stats
3
+ def initialize(address, port, config, status, logger)
4
+ status[:stats] = Hash.new(0)
5
+ end
6
+
7
+ def run
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ module Redisse
2
+ VERSION = "0.4.0"
3
+ end
@@ -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