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.
@@ -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