firehose 0.1.1 → 0.2.alpha.2

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.
Files changed (49) hide show
  1. data/.env.sample +10 -0
  2. data/.gitignore +2 -0
  3. data/Procfile +1 -1
  4. data/README.md +117 -11
  5. data/config/rainbows.rb +20 -0
  6. data/firehose.gemspec +9 -6
  7. data/lib/assets/flash/firehose/WebSocketMain.swf +0 -0
  8. data/lib/assets/javascripts/firehose.js.coffee +4 -1
  9. data/lib/assets/javascripts/firehose/consumer.js.coffee +3 -11
  10. data/lib/assets/javascripts/firehose/lib/jquery.cors.headers.js.coffee +16 -0
  11. data/lib/assets/javascripts/firehose/lib/swfobject.js +4 -0
  12. data/lib/assets/javascripts/firehose/lib/web_socket.js +389 -0
  13. data/lib/assets/javascripts/firehose/long_poll.js.coffee +42 -39
  14. data/lib/assets/javascripts/firehose/transport.js.coffee +1 -1
  15. data/lib/assets/javascripts/firehose/web_socket.js.coffee +8 -14
  16. data/lib/firehose.rb +12 -17
  17. data/lib/firehose/channel.rb +84 -0
  18. data/lib/firehose/cli.rb +57 -13
  19. data/lib/firehose/client.rb +92 -0
  20. data/lib/firehose/default.rb +2 -2
  21. data/lib/firehose/logging.rb +35 -0
  22. data/lib/firehose/producer.rb +1 -0
  23. data/lib/firehose/publisher.rb +56 -4
  24. data/lib/firehose/rack.rb +37 -120
  25. data/lib/firehose/rack/consumer_app.rb +143 -0
  26. data/lib/firehose/rack/ping_app.rb +84 -0
  27. data/lib/firehose/rack/publisher_app.rb +40 -0
  28. data/lib/firehose/server.rb +48 -0
  29. data/lib/firehose/subscriber.rb +54 -0
  30. data/lib/firehose/swf_policy_request.rb +23 -0
  31. data/lib/firehose/version.rb +2 -2
  32. data/lib/rainbows_em_swf_policy.rb +33 -0
  33. data/lib/thin_em_swf_policy.rb +26 -0
  34. data/spec/integrations/integration_test_helper.rb +16 -0
  35. data/spec/integrations/rainbows_spec.rb +7 -0
  36. data/spec/integrations/shared_examples.rb +111 -0
  37. data/spec/integrations/thin_spec.rb +5 -79
  38. data/spec/lib/channel_spec.rb +164 -0
  39. data/spec/lib/client_spec.rb +9 -0
  40. data/spec/lib/default_spec.rb +2 -2
  41. data/spec/lib/publisher_spec.rb +82 -0
  42. data/spec/lib/rack/consumer_app_spec.rb +11 -0
  43. data/spec/lib/rack/ping_app_spec.rb +28 -0
  44. data/spec/lib/rack/publisher_app_spec.rb +26 -0
  45. data/spec/lib/subscriber_spec.rb +69 -0
  46. data/spec/spec_helper.rb +49 -8
  47. metadata +114 -45
  48. data/config.ru +0 -6
  49. data/lib/firehose/subscription.rb +0 -77
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+
3
+ describe Firehose::Channel do
4
+ include EM::TestHelper
5
+
6
+ let(:channel_key) { '/bears/are/mean' }
7
+ let(:channel) { Firehose::Channel.new(channel_key, EM::Hiredis.connect, subscriber) }
8
+ let(:subscriber) { Firehose::Subscriber.new(EM::Hiredis.connect) }
9
+ let(:message) { 'Raaaarrrrrr!!!!' }
10
+ let(:publisher) { Firehose::Publisher.new }
11
+
12
+ describe "#next_message" do
13
+ it "should wait for message if message was not published before subscription" do
14
+ em do
15
+ channel.next_message.callback do |msg, seq|
16
+ msg.should == message
17
+ seq.should == 1
18
+ em.next_tick { em.stop }
19
+ end
20
+
21
+ publisher.publish(channel_key, message)
22
+ end
23
+ end
24
+
25
+ it "should return the latest message and sequence if no sequence is given" do
26
+ redis_exec 'lpush', "firehose:#{channel_key}:list", message
27
+ redis_exec 'set', "firehose:#{channel_key}:sequence", '100'
28
+
29
+ em do
30
+ channel.next_message.callback do |msg, seq|
31
+ msg.should == message
32
+ seq.should == 100
33
+
34
+ # This must happen _after_ the callback runs in order to pass consistently.
35
+ em.next_tick { em.stop }
36
+ end
37
+ end
38
+ end
39
+
40
+ it "should wait for message if most recent sequence is given" do
41
+ redis_exec 'lpush', "firehose:#{channel_key}:list", message
42
+ redis_exec 'set', "firehose:#{channel_key}:sequence", '100'
43
+
44
+ em 3 do
45
+ channel.next_message(100).callback do |msg, seq|
46
+ msg.should == message
47
+ seq.should == 101
48
+ em.next_tick { em.stop }
49
+ end.errback
50
+
51
+ publisher.publish(channel_key, message)
52
+ end
53
+ end
54
+
55
+ it "should wait for message if a future sequence is given" do
56
+ redis_exec 'lpush', "firehose:#{channel_key}:list", message
57
+ redis_exec 'set', "firehose:#{channel_key}:sequence", '100'
58
+
59
+ em 3 do
60
+ channel.next_message(101).callback do |msg, seq|
61
+ msg.should == message
62
+ seq.should == 101
63
+ em.next_tick { em.stop }
64
+ end.errback
65
+
66
+ publisher.publish(channel_key, message)
67
+ end
68
+ end
69
+
70
+ it "should immediatly get a message if message sequence is behind and in list" do
71
+ messages = %w[a b c d e]
72
+
73
+ em 3 do
74
+ publish_messages(messages) do
75
+ channel.next_message(2).callback do |msg, seq|
76
+ msg.should == 'c'
77
+ seq.should == 3
78
+
79
+ # This must happen _after_ the callback runs in order to pass consistently.
80
+ em.next_tick { em.stop }
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ it "should get current message if sequence is really far behind in list" do
87
+ messages = ('aa'..'zz').to_a
88
+
89
+ em 3 do
90
+ publish_messages(messages) do
91
+ channel.next_message(2).callback do |msg, seq|
92
+ msg.should == messages.last
93
+ seq.should == messages.size
94
+
95
+ # This must happen _after_ the callback runs in order to pass consistently.
96
+ em.next_tick { em.stop }
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ context "a timeout is set" do
103
+ it "should timeout if message isn't published in time" do
104
+ redis_exec 'lpush', "firehose:#{channel_key}:list", message
105
+ redis_exec 'set', "firehose:#{channel_key}:sequence", '100'
106
+
107
+ em 3 do
108
+ channel.next_message(100, :timeout => 1).callback do |msg, seq|
109
+ raise 'test failed'
110
+ end.errback do |e|
111
+ e.should == :timeout
112
+ em.next_tick { em.stop }
113
+ end
114
+
115
+ EM::add_timer(2) do
116
+ publisher.publish(channel_key, message)
117
+ end
118
+ end
119
+ end
120
+
121
+ it "should not timeout if message is published in time" do
122
+ redis_exec 'lpush', "firehose:#{channel_key}:list", message
123
+ redis_exec 'set', "firehose:#{channel_key}:sequence", '100'
124
+
125
+ em 3 do
126
+ d = channel.next_message(100, :timeout => 2).callback do |msg, seq|
127
+ msg.should == message
128
+ seq.should == 101
129
+ EM::add_timer(1) do
130
+ em.stop
131
+ end
132
+ end.errback do |e|
133
+ raise 'test failed'
134
+ end
135
+
136
+ d.should_not_receive(:fail)
137
+
138
+ EM::add_timer(1) do
139
+ publisher.publish(channel_key, message)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+
147
+ # Publishes the given messages and executes the given block when finished.
148
+ def publish_messages(all_messages)
149
+ publish = Proc.new do |messages_to_publish|
150
+ msg = messages_to_publish.shift
151
+ publisher.publish(channel_key, msg).callback do
152
+ if messages_to_publish.empty?
153
+ # Publishing is done, proceed with the test
154
+ yield
155
+ else
156
+ # Publish the next message
157
+ publish.call messages_to_publish
158
+ end
159
+ end
160
+ end
161
+
162
+ publish.call all_messages.dup
163
+ end
164
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Firehose::Client::Consumer::WebSocket do
4
+ it "should be tested" # probably inside of the integrations
5
+ end
6
+
7
+ describe Firehose::Client::Consumer::HttpLongPoll do
8
+ it "should be tested" # probably inside of the integrations
9
+ end
@@ -2,6 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  describe Firehose::Default do
4
4
  it "should have 7474 for default port" do
5
- Firehose::Default::URI.to_s.should == '//127.0.0.1:7474'
5
+ Firehose::Default::URI.to_s.should == '//0.0.0.0:7474'
6
6
  end
7
- end
7
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe Firehose::Publisher do
4
+ include EM::TestHelper
5
+
6
+ let(:publisher) { Firehose::Publisher.new }
7
+ let(:channel_key) { "/firehose/publisher/test/#{Time.now.to_i}" }
8
+ let(:message) { "howdy friends!" }
9
+
10
+ it "should have 100 MAX_MESSAGES" do
11
+ Firehose::Publisher::MAX_MESSAGES.should == 100
12
+ end
13
+
14
+ it "should have 1 day TTL" do
15
+ Firehose::Publisher::TTL.should == 86400
16
+ end
17
+
18
+ describe "#publish" do
19
+ it "should publish message change" do
20
+ em do
21
+ hiredis = EM::Hiredis.connect
22
+ hiredis.subscribe "firehose:channel_updates"
23
+ hiredis.on(:message) {|_, msg|
24
+ msg.should == "#{channel_key}\n1\n#{message}"
25
+ em.next_tick { em.stop }
26
+ }
27
+ Firehose::Publisher.new.publish channel_key, message
28
+ end
29
+ end
30
+
31
+
32
+ "\"'\r\t\n!@\#$%^&*()[]\v\f\a\b\e{}/=?+\\|".each_char do |char|
33
+ it "should publish messages with the '#{char.inspect}' character" do
34
+ msg = [char, message, char].join
35
+ em 1 do
36
+ Firehose::Publisher.new.publish(channel_key, msg).callback { em.stop }
37
+ end
38
+ redis_exec('lpop', "firehose:#{channel_key}:list").should == msg
39
+ end
40
+ end
41
+
42
+ it "should add message to list" do
43
+ em do
44
+ Firehose::Publisher.new.publish(channel_key, message).callback { em.stop }
45
+ end
46
+ redis_exec('lpop', "firehose:#{channel_key}:list").should == message
47
+ end
48
+
49
+ it "should limit list to MAX_MESSAGES messages" do
50
+ em do
51
+ Firehose::Publisher::MAX_MESSAGES.times do |n|
52
+ publisher.publish(channel_key, message)
53
+ end
54
+ publisher.publish(channel_key, message).callback { em.stop }
55
+ end
56
+ redis_exec('llen', "firehose:#{channel_key}:list").should == Firehose::Publisher::MAX_MESSAGES
57
+ end
58
+
59
+ it "should increment sequence" do
60
+ sequence_key = "firehose:#{channel_key}:sequence"
61
+
62
+ @done_counter = 0
63
+ redis_exec('get', sequence_key).should be_nil
64
+ em do
65
+ publisher.publish(channel_key, message).callback { @done_counter += 1; em.next_tick { em.stop } if @done_counter > 1 }
66
+ publisher.publish(channel_key, message).callback { @done_counter += 1; em.next_tick { em.stop } if @done_counter > 1 }
67
+ end
68
+ redis_exec('get', sequence_key).to_i.should == 2
69
+ end
70
+
71
+ it "should set expirery on sequence and list keys" do
72
+ em do
73
+ publisher.publish(channel_key, message).callback do
74
+ # Allow for 1 second of slippage/delay
75
+ redis_exec('TTL', "firehose:#{channel_key}:sequence").should > (Firehose::Publisher::TTL - 1)
76
+ redis_exec('TTL', "firehose:#{channel_key}:list").should > (Firehose::Publisher::TTL - 1)
77
+ em.stop
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe Firehose::Rack::ConsumerApp, :type => :request do
5
+ include Rack::Test::Methods
6
+ let(:app) { Firehose::Rack::ConsumerApp.new }
7
+ let(:path) { "/test/path/#{Time.now.to_i}" }
8
+
9
+ it "should have Content-Length on OPTIONS request"
10
+ it "should have Content-Length on GET request"
11
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+ require 'async_rack_test'
4
+
5
+ describe Firehose::Rack::PingApp, :type => :request do
6
+ include AsyncRackTest::Methods
7
+ let(:app) { Firehose::Rack::PingApp.new dummy_redis }
8
+ let(:path) { "/test/path/#{Time.now.to_i}" }
9
+ let(:deferrable) { EM::DefaultDeferrable.new }
10
+ let(:dummy_redis) { double 'redis', :set => deferrable, :get => deferrable, :expire => deferrable }
11
+
12
+ context 'redis is available' do
13
+ before { deferrable.succeed Firehose::Rack::PingApp::PingCheck::TEST_VALUE }
14
+
15
+ it "should return 200" do
16
+ ahead path
17
+ last_response.status.should == 200
18
+ end
19
+ end
20
+
21
+ context 'redis is not available' do
22
+ before { deferrable.fail 'some error' }
23
+ it "should return 500" do
24
+ ahead path
25
+ last_response.status.should == 500
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+ require 'async_rack_test'
4
+
5
+ describe Firehose::Rack::PublisherApp, :type => :request do
6
+ include AsyncRackTest::Methods
7
+ let(:app) { Firehose::Rack::PublisherApp.new }
8
+ let(:path) { "/test/path/#{Time.now.to_i}" }
9
+ let(:deferrable) { EM::DefaultDeferrable.new }
10
+
11
+ context 'publishing is successful' do
12
+ before { deferrable.succeed }
13
+
14
+ it "should return 202" do
15
+ app.stub(:publisher => double('publisher', :publish => deferrable))
16
+ aput path, :body => "some nice little message"
17
+ last_response.status.should == 202
18
+ end
19
+
20
+ it "should have Content-Length of zero" do
21
+ app.stub(:publisher => double('publisher', :publish => deferrable))
22
+ aput path, :body => "some nice little message"
23
+ last_response.headers['Content-Length'].should == '0'
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Firehose::Subscriber do
4
+ include EM::TestHelper
5
+
6
+ let(:channel_key) { '/bears/are/mean' }
7
+ let(:subscriber) { Firehose::Subscriber.new(EM::Hiredis.connect) }
8
+ let(:dummy_subscriber){ Firehose::Subscriber.new(double('redis', :subscribe => EM::DefaultDeferrable.new, :on => nil)) }
9
+ let(:message) { 'Raaaarrrrrr!!!!' }
10
+ let(:publisher) { Firehose::Publisher.new }
11
+
12
+ describe "#subscribe" do
13
+ it "should add the deferrable to the subscriptions hash" do
14
+ deferrable = EM::DefaultDeferrable.new
15
+ dummy_subscriber.subscribe(channel_key, deferrable)
16
+ dummy_subscriber.send(:subscriptions)[channel_key].should == [deferrable]
17
+ end
18
+
19
+ it "should call succeed on the deferrable when a message is published" do
20
+ deferrable = EM::DefaultDeferrable.new
21
+ deferrable.callback do |msg, sequence|
22
+ msg.should == message
23
+ sequence.should == 1 # The publisher is fresh, so the sequence ID will be 1.
24
+ em.next_tick { em.stop }
25
+ end
26
+
27
+ em do
28
+ subscriber.subscribe(channel_key, deferrable)
29
+ publisher.publish(channel_key, message)
30
+ end
31
+ end
32
+
33
+ it "shouldn't call succeed on the deferrable when a 2nd message is published" do
34
+ deferrable = EM::DefaultDeferrable.new
35
+ deferrable.should_receive(:succeed).with(message, 1) # The publisher is fresh, so the sequence ID will be 1.
36
+ deferrable.should_not_receive(:succeed).with('2nd message', 2)
37
+ em do
38
+ subscriber.subscribe(channel_key, deferrable)
39
+ publisher.publish(channel_key, message).callback do
40
+ publisher.publish(channel_key, '2nd message').callback do
41
+ em.stop
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+
49
+ describe "#unsubscribe" do
50
+ it "should remove the deferrable from the subscriptions hash" do
51
+ deferrable = EM::DefaultDeferrable.new
52
+ dummy_subscriber.subscribe(channel_key, deferrable)
53
+ dummy_subscriber.unsubscribe(channel_key, deferrable)
54
+ dummy_subscriber.send(:subscriptions).has_key?(channel_key).should be_false
55
+ end
56
+
57
+ it "shouldn't call succeed on the deferrable when a message is published" do
58
+ deferrable = EM::DefaultDeferrable.new
59
+ deferrable.should_not_receive(:succeed).with(message, 1) # The publisher is fresh, so the sequence ID will be 1.
60
+ em do
61
+ subscriber.subscribe(channel_key, deferrable)
62
+ subscriber.unsubscribe(channel_key, deferrable)
63
+ publisher.publish(channel_key, message).callback do
64
+ em.stop
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,17 +1,16 @@
1
+ ENV['RACK_ENV'] ||= 'test'
2
+
1
3
  require 'logger'
2
4
  require 'em-http'
3
- require 'em-websocket-client'
5
+ require 'faye/websocket'
6
+ require 'hiredis'
4
7
 
5
- # Skip logging if VERBOSE isn't set to true.
6
8
  require 'firehose'
7
- Firehose.logger = Logger.new('/dev/null') unless ENV['VERBOSE']
8
9
 
9
- # Lets skip the verbosity of the thin for the test output.
10
- require 'thin'
11
- Thin::Logging.silent = true unless ENV['VERBOSE']
12
10
 
13
11
  # We use both EM::Http and Net::Http in this test lib. When EM:Http is fired up
14
- # we're usually hitting thins for integrations, and when Net::Http we want to mock that up.
12
+ # we're usually hitting Rainbows! for integrations, and when Net::Http we want
13
+ # to mock that up.
15
14
  require 'webmock/rspec'
16
15
  WebMock.allow_net_connect!
17
16
 
@@ -33,9 +32,51 @@ module EM::TestHelper
33
32
  end
34
33
  end
35
34
 
35
+ module Hiredis::TestHelper
36
+ def redis
37
+ @conn ||= begin
38
+ conn = Hiredis::Connection.new
39
+ conn.connect("127.0.0.1", 6379)
40
+ conn
41
+ end
42
+ end
43
+
44
+ def reset_redis
45
+ redis_exec 'flushdb'
46
+ end
47
+
48
+ def redis_exec(*tokens)
49
+ redis.write tokens
50
+ redis.read
51
+ end
52
+ end
53
+
36
54
  # Configure RSpec runner. See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
37
55
  RSpec.configure do |config|
38
56
  config.treat_symbols_as_metadata_keys_with_true_values = true
39
57
  config.run_all_when_everything_filtered = true
40
58
  config.filter_run :focus
41
- end
59
+ config.include Hiredis::TestHelper
60
+ config.before(:each) do
61
+ reset_redis
62
+ end
63
+ end
64
+
65
+
66
+
67
+
68
+ # Allow async responses to get through rack/lint
69
+ require 'rack/lint'
70
+ class Rack::Lint
71
+ def check_status_with_async(status)
72
+ check_status_without_async(status) unless status == -1
73
+ end
74
+ alias_method :check_status_without_async, :check_status
75
+ alias_method :check_status, :check_status_with_async
76
+
77
+ def check_content_type_with_async(status, headers)
78
+ check_content_type_without_async(status, headers) unless status == -1
79
+ end
80
+ alias_method :check_content_type_without_async, :check_content_type
81
+ alias_method :check_content_type, :check_content_type_with_async
82
+ end