firehose 1.1.1 → 1.2.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.
Files changed (48) hide show
  1. checksums.yaml +15 -0
  2. data/.rbenv-version +1 -1
  3. data/README.md +1 -1
  4. data/config/rainbows.rb +4 -3
  5. data/lib/firehose.rb +6 -8
  6. data/lib/firehose/assets.rb +1 -3
  7. data/lib/firehose/cli.rb +6 -8
  8. data/lib/firehose/client.rb +2 -84
  9. data/lib/firehose/client/consumer.rb +94 -0
  10. data/lib/firehose/client/producer.rb +106 -0
  11. data/lib/firehose/logging.rb +1 -4
  12. data/lib/{rainbows_em_swf_policy.rb → firehose/patches/rainbows.rb} +4 -2
  13. data/lib/firehose/patches/swf_policy_request.rb +26 -0
  14. data/lib/{thin_em_swf_policy.rb → firehose/patches/thin.rb} +2 -1
  15. data/lib/firehose/rack.rb +12 -39
  16. data/lib/firehose/rack/app.rb +42 -0
  17. data/lib/firehose/rack/{consumer_app.rb → consumer.rb} +7 -5
  18. data/lib/firehose/rack/{ping_app.rb → ping.rb} +4 -2
  19. data/lib/firehose/rack/{publisher_app.rb → publisher.rb} +3 -3
  20. data/lib/firehose/server.rb +16 -42
  21. data/lib/firehose/server/app.rb +53 -0
  22. data/lib/firehose/server/channel.rb +80 -0
  23. data/lib/firehose/server/publisher.rb +134 -0
  24. data/lib/firehose/server/subscriber.rb +50 -0
  25. data/lib/firehose/version.rb +2 -2
  26. data/spec/integrations/integration_test_helper.rb +2 -2
  27. data/spec/integrations/shared_examples.rb +3 -3
  28. data/spec/lib/{client_spec.rb → client/consumer_spec.rb} +0 -0
  29. data/spec/lib/{producer_spec.rb → client/producer_spec.rb} +13 -13
  30. data/spec/lib/firehose_spec.rb +7 -0
  31. data/spec/lib/rack/{consumer_app_spec.rb → consumer_spec.rb} +2 -2
  32. data/spec/lib/rack/{ping_app_spec.rb → ping_spec.rb} +3 -3
  33. data/spec/lib/rack/{publisher_app_spec.rb → publisher_spec.rb} +3 -3
  34. data/spec/lib/server/app_spec.rb +1 -0
  35. data/spec/lib/{channel_spec.rb → server/channel_spec.rb} +4 -4
  36. data/spec/lib/{publisher_spec.rb → server/publisher_spec.rb} +9 -9
  37. data/spec/lib/{subscriber_spec.rb → server/subscriber_spec.rb} +4 -4
  38. data/spec/spec_helper.rb +0 -5
  39. metadata +38 -77
  40. data/lib/firehose/channel.rb +0 -84
  41. data/lib/firehose/default.rb +0 -8
  42. data/lib/firehose/producer.rb +0 -104
  43. data/lib/firehose/publisher.rb +0 -127
  44. data/lib/firehose/subscriber.rb +0 -54
  45. data/lib/firehose/swf_policy_request.rb +0 -23
  46. data/spec/lib/broker_spec.rb +0 -30
  47. data/spec/lib/consumer_spec.rb +0 -66
  48. data/spec/lib/default_spec.rb +0 -7
@@ -0,0 +1,134 @@
1
+ module Firehose
2
+ module Server
3
+ class Publisher
4
+ # Number of messages that Redis buffers for the client if its
5
+ # connection drops, then reconnects.
6
+ MAX_MESSAGES = 100
7
+
8
+ # Seconds that the message buffer should live before Redis expires it.
9
+ TTL = 60*60*24
10
+
11
+ # Delimited used to frame different parts of a message that's published
12
+ # over Firehose.
13
+ PAYLOAD_DELIMITER = "\n"
14
+
15
+ # Publish a message to a Firehose channel via Redis.
16
+ def publish(channel_key, message, opts={})
17
+ # How long should we hang on to the resource once is published?
18
+ ttl = (opts[:ttl] || TTL).to_i
19
+
20
+ # TODO hi-redis isn't that awesome... we have to setup an errback per even for wrong
21
+ # commands because of the lack of a method_missing whitelist. Perhaps implement a whitelist in
22
+ # em-hiredis or us a diff lib?
23
+ if (deferrable = opts[:deferrable]).nil?
24
+ deferrable = EM::DefaultDeferrable.new
25
+ deferrable.errback do |e|
26
+ # Handle missing Lua publishing script in cache
27
+ # (such as Redis restarting or someone executing SCRIPT FLUSH)
28
+ if e.message =~ /NOSCRIPT/
29
+ deferrable.succeed
30
+ EM.next_tick do
31
+ @publish_script_digest = nil
32
+ combined_opts = opts.merge :deferrable => deferrable
33
+ self.publish channel_key, message, combined_opts
34
+ end
35
+ else
36
+ EM.next_tick { raise e }
37
+ end
38
+ end
39
+ end
40
+
41
+ if @publish_script_digest.nil?
42
+ register_publish_script.errback do |e|
43
+ deferrable.fail e
44
+ end.callback do |digest|
45
+ @publish_script_digest = digest
46
+ Firehose.logger.debug "Registered Lua publishing script with Redis => #{digest}"
47
+ eval_publish_script channel_key, message, ttl, deferrable
48
+ end
49
+ else
50
+ eval_publish_script channel_key, message, ttl, deferrable
51
+ end
52
+
53
+ deferrable
54
+ end
55
+
56
+ private
57
+ def redis
58
+ @redis ||= EM::Hiredis.connect
59
+ end
60
+
61
+ # Serialize components of a message into something that can be dropped into Redis.
62
+ def self.to_payload(channel_key, sequence, message)
63
+ [channel_key, sequence, message].join(PAYLOAD_DELIMITER)
64
+ end
65
+
66
+ # Deserealize components of a message back into Ruby.
67
+ def self.from_payload(payload)
68
+ payload.split(PAYLOAD_DELIMITER, method(:to_payload).arity)
69
+ end
70
+
71
+ # TODO: Make this FAR more robust. Ideally we'd whitelist the permitted
72
+ # characters and then escape or remove everything else.
73
+ # See: http://en.wikibooks.org/wiki/Lua_Programming/How_to_Lua/escape_sequence
74
+ def lua_escape(str)
75
+ str.gsub(/\\/,'\\\\\\').gsub(/"/,'\"').gsub(/\n/,'\n').gsub(/\r/,'\r')
76
+ end
77
+
78
+ def register_publish_script
79
+ redis.script 'LOAD', REDIS_PUBLISH_SCRIPT
80
+ end
81
+
82
+ def eval_publish_script(channel_key, message, ttl, deferrable)
83
+ list_key = Server.key(channel_key, :list)
84
+ script_args = [
85
+ Server.key(channel_key, :sequence),
86
+ list_key,
87
+ Server.key(:channel_updates),
88
+ ttl,
89
+ message,
90
+ MAX_MESSAGES,
91
+ PAYLOAD_DELIMITER,
92
+ channel_key
93
+ ]
94
+ redis.evalsha(
95
+ @publish_script_digest, script_args.length, *script_args
96
+ ).errback do |e|
97
+ deferrable.fail e
98
+ end.callback do |sequence|
99
+ Firehose.logger.debug "Redis stored/published `#{message}` to list `#{list_key}` with sequence `#{sequence}`"
100
+ deferrable.succeed
101
+ end
102
+ end
103
+
104
+ REDIS_PUBLISH_SCRIPT = <<-LUA
105
+ local sequence_key = KEYS[1]
106
+ local list_key = KEYS[2]
107
+ local channel_key = KEYS[3]
108
+ local ttl = KEYS[4]
109
+ local message = KEYS[5]
110
+ local max_messages = KEYS[6]
111
+ local payload_delimiter = KEYS[7]
112
+ local firehose_resource = KEYS[8]
113
+
114
+ local current_sequence = redis.call('get', sequence_key)
115
+ if current_sequence == nil or current_sequence == false then
116
+ current_sequence = 0
117
+ end
118
+
119
+ local sequence = current_sequence + 1
120
+ local message_payload = firehose_resource .. payload_delimiter .. sequence .. payload_delimiter .. message
121
+
122
+ redis.call('set', sequence_key, sequence)
123
+ redis.call('expire', sequence_key, ttl)
124
+ redis.call('lpush', list_key, message)
125
+ redis.call('ltrim', list_key, 0, max_messages - 1)
126
+ redis.call('expire', list_key, ttl)
127
+ redis.call('publish', channel_key, message_payload)
128
+
129
+ return sequence
130
+ LUA
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,50 @@
1
+ module Firehose
2
+ module Server
3
+ # Setups a connetion to Redis to listen for new resources...
4
+ class Subscriber
5
+ attr_reader :pubsub
6
+
7
+ def initialize(redis)
8
+ @pubsub = redis.pubsub
9
+ # TODO: Instead of just raising an exception, it would probably be better
10
+ # for the errback to set some sort of 'disconnected' state. Then
11
+ # whenever a deferrable was 'subscribed' we could instantly fail
12
+ # the deferrable with whatever connection error we had.
13
+ # An alternative which would have a similar result would be to
14
+ # subscribe lazily (i.e. not until we have a deferrable to subscribe).
15
+ # Then, if connecting failed, it'd be super easy to fail the deferrable
16
+ # with the same error.
17
+ # The final goal is to allow the failed deferrable bubble back up
18
+ # so we can send back a nice, clean 500 error to the client.
19
+ pubsub.subscribe('firehose:channel_updates').
20
+ errback{|e| EM.next_tick { raise e } }.
21
+ callback { Firehose.logger.debug "Redis subscribed to `firehose:channel_updates`" }
22
+ pubsub.on(:message) do |_, payload|
23
+ channel_key, sequence, message = Server::Publisher.from_payload(payload)
24
+
25
+ if deferrables = subscriptions.delete(channel_key)
26
+ Firehose.logger.debug "Redis notifying #{deferrables.count} deferrable(s) at `#{channel_key}` with sequence `#{sequence}` and message `#{message}`"
27
+ deferrables.each do |deferrable|
28
+ Firehose.logger.debug "Sending message #{message} and sequence #{sequence} to client from subscriber"
29
+ deferrable.succeed message, sequence.to_i
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def subscribe(channel_key, deferrable)
36
+ subscriptions[channel_key].push deferrable
37
+ end
38
+
39
+ def unsubscribe(channel_key, deferrable)
40
+ subscriptions[channel_key].delete deferrable
41
+ subscriptions.delete(channel_key) if subscriptions[channel_key].empty?
42
+ end
43
+
44
+ private
45
+ def subscriptions
46
+ @subscriptions ||= Hash.new{|h,k| h[k] = []}
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,4 +1,4 @@
1
1
  module Firehose
2
- VERSION = "1.1.1"
3
- CODENAME = "Radtastical Redis"
2
+ VERSION = "1.2.0"
3
+ CODENAME = "Spring Cleaning"
4
4
  end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  module IntegrationTestHelper
4
4
  def start_server
5
5
  @server_pid = fork do
6
- Firehose::Server.new(:server => server, :port => uri.port).start
6
+ Firehose::Server::App.new(:server => server, :port => uri.port).start
7
7
  end
8
8
 
9
9
  # Need to give the server a chance to boot up.
@@ -17,6 +17,6 @@ module IntegrationTestHelper
17
17
 
18
18
  # Let isn't allowed in before(:all)
19
19
  def uri
20
- Firehose::Default::URI
20
+ Firehose::URI
21
21
  end
22
22
  end
@@ -7,12 +7,12 @@ shared_examples_for 'Firehose::Rack::App' do
7
7
  include IntegrationTestHelper
8
8
 
9
9
  before(:all) do
10
- Firehose::Producer.adapter = :em_http
10
+ Firehose::Client::Producer::Http.adapter = :em_http
11
11
  start_server
12
12
  end
13
13
 
14
14
  after(:all) do
15
- Firehose::Producer.adapter = nil
15
+ Firehose::Client::Producer::Http.adapter = nil
16
16
  stop_server
17
17
  end
18
18
 
@@ -41,7 +41,7 @@ shared_examples_for 'Firehose::Rack::App' do
41
41
 
42
42
  # Setup a publisher
43
43
  publish = Proc.new do
44
- Firehose::Producer.new.publish(outgoing.shift).to(channel) do
44
+ Firehose::Client::Producer::Http.new.publish(outgoing.shift).to(channel) do
45
45
  # The random timer ensures that sometimes the clients will be behind
46
46
  # and sometimes they will be caught up.
47
47
  EM::add_timer(rand*0.005) { publish.call } unless outgoing.empty?
@@ -1,34 +1,34 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Firehose::Producer do
3
+ describe Firehose::Client::Producer::Http do
4
4
  let(:channel) { "/channel-awesome" }
5
- let(:url) { "#{Firehose::Default::URI}#{channel}"}
5
+ let(:url) { "#{Firehose::URI}#{channel}"}
6
6
  let(:publish_stub) { stub_request(:put, url) }
7
7
  let(:message) { "hey dude" }
8
8
 
9
9
  before(:all) do
10
- Firehose::Producer.adapter = :net_http
10
+ Firehose::Client::Producer::Http.adapter = :net_http
11
11
  end
12
12
 
13
13
  after(:all) do
14
- Firehose::Producer.adapter = nil
14
+ Firehose::Client::Producer::Http.adapter = nil
15
15
  end
16
16
 
17
17
  it "should publish message to channel" do
18
18
  publish_stub.to_return(:body => "", :status => 202)
19
19
 
20
- Firehose::Producer.new.publish(message).to(channel)
20
+ Firehose::Client::Producer::Http.new.publish(message).to(channel)
21
21
  WebMock.should have_requested(:put, url).with { |req| req.body == message }
22
22
  end
23
23
 
24
24
  context 'prefix is specified in URI' do
25
- let(:firehose_uri) {"#{Firehose::Default::URI}/prefix"}
25
+ let(:firehose_uri) {"#{Firehose::URI}/prefix"}
26
26
  let(:url) { "#{firehose_uri}#{channel}"}
27
27
 
28
28
  it "should publish message to channel" do
29
29
  publish_stub.to_return(:body => "", :status => 202)
30
30
 
31
- Firehose::Producer.new(firehose_uri).publish(message).to(channel)
31
+ Firehose::Client::Producer::Http.new(firehose_uri).publish(message).to(channel)
32
32
  WebMock.should have_requested(:put, url).with { |req| req.body == message }
33
33
  end
34
34
  end
@@ -37,7 +37,7 @@ describe Firehose::Producer do
37
37
  publish_stub.to_return(:body => "", :status => 202)
38
38
  ttl = 20
39
39
 
40
- Firehose::Producer.new.publish(message).to(channel, :ttl => ttl)
40
+ Firehose::Client::Producer::Http.new.publish(message).to(channel, :ttl => ttl)
41
41
  WebMock.should have_requested(:put, url).with { |req| req.body == message and req.headers['Cache-Control'] == "max-age=#{ttl}" }
42
42
  end
43
43
 
@@ -46,14 +46,14 @@ describe Firehose::Producer do
46
46
  publish_stub.to_return(:body => "", :status => 500)
47
47
 
48
48
  lambda{
49
- Firehose::Producer.new.publish(message).to(channel)
50
- }.should raise_exception(Firehose::Producer::PublishError)
49
+ Firehose::Client::Producer::Http.new.publish(message).to(channel)
50
+ }.should raise_exception(Firehose::Client::Producer::Http::PublishError)
51
51
  end
52
52
 
53
53
  it "should use .error_handler if not 201" do
54
54
  publish_stub.to_return(:body => "", :status => 500)
55
55
 
56
- producer = Firehose::Producer.new
56
+ producer = Firehose::Client::Producer::Http.new
57
57
  producer.on_error do |e|
58
58
  e.message.should =~ /could not publish.+to/i
59
59
  end
@@ -64,8 +64,8 @@ describe Firehose::Producer do
64
64
  publish_stub.to_timeout
65
65
 
66
66
  lambda{
67
- Firehose::Producer.new.publish(message).to(channel)
68
- }.should raise_exception(Firehose::Producer::TimeoutError)
67
+ Firehose::Client::Producer::Http.new.publish(message).to(channel)
68
+ }.should raise_exception(Firehose::Client::Producer::Http::TimeoutError)
69
69
  end
70
70
  end
71
71
  end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Firehose do
4
+ it "should have 7474 for default port" do
5
+ Firehose::URI.to_s.should == '//0.0.0.0:7474'
6
+ end
7
+ end
@@ -1,9 +1,9 @@
1
1
  require 'spec_helper'
2
2
  require 'rack/test'
3
3
 
4
- describe Firehose::Rack::ConsumerApp, :type => :request do
4
+ describe Firehose::Rack::Consumer, :type => :request do
5
5
  include Rack::Test::Methods
6
- let(:app) { Firehose::Rack::ConsumerApp.new }
6
+ let(:app) { Firehose::Rack::Consumer.new }
7
7
  let(:path) { "/test/path/#{Time.now.to_i}" }
8
8
 
9
9
  it "should have Content-Length on OPTIONS request"
@@ -2,15 +2,15 @@ require 'spec_helper'
2
2
  require 'rack/test'
3
3
  require 'async_rack_test'
4
4
 
5
- describe Firehose::Rack::PingApp, :type => :request do
5
+ describe Firehose::Rack::Ping, :type => :request do
6
6
  include AsyncRackTest::Methods
7
- let(:app) { Firehose::Rack::PingApp.new dummy_redis }
7
+ let(:app) { Firehose::Rack::Ping.new dummy_redis }
8
8
  let(:path) { "/test/path/#{Time.now.to_i}" }
9
9
  let(:deferrable) { EM::DefaultDeferrable.new }
10
10
  let(:dummy_redis) { double 'redis', :set => deferrable, :get => deferrable, :expire => deferrable }
11
11
 
12
12
  context 'redis is available' do
13
- before { deferrable.succeed Firehose::Rack::PingApp::PingCheck::TEST_VALUE }
13
+ before { deferrable.succeed Firehose::Rack::Ping::PingCheck::TEST_VALUE }
14
14
 
15
15
  it "should return 200" do
16
16
  ahead path
@@ -2,9 +2,9 @@ require 'spec_helper'
2
2
  require 'rack/test'
3
3
  require 'async_rack_test'
4
4
 
5
- describe Firehose::Rack::PublisherApp, :type => :request do
5
+ describe Firehose::Rack::Publisher, :type => :request do
6
6
  include AsyncRackTest::Methods
7
- let(:app) { Firehose::Rack::PublisherApp.new }
7
+ let(:app) { Firehose::Rack::Publisher.new }
8
8
  let(:path) { "/test/path/#{Time.now.to_i}" }
9
9
  let(:deferrable) { EM::DefaultDeferrable.new }
10
10
 
@@ -27,7 +27,7 @@ describe Firehose::Rack::PublisherApp, :type => :request do
27
27
  body = "howdy dude!"
28
28
  ttl = '92'
29
29
 
30
- Firehose::Publisher.any_instance.stub(:publish).with(path, body, :ttl => ttl).and_return(deferrable)
30
+ Firehose::Server::Publisher.any_instance.stub(:publish).with(path, body, :ttl => ttl).and_return(deferrable)
31
31
  aput path, body, 'HTTP_CACHE_CONTROL' => 'max-age=92'
32
32
  end
33
33
  end
@@ -0,0 +1 @@
1
+ # The Firehose::Server::App class is tested via the spec/integrations suite.
@@ -1,13 +1,13 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Firehose::Channel do
3
+ describe Firehose::Server::Channel do
4
4
  include EM::TestHelper
5
5
 
6
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) }
7
+ let(:channel) { Firehose::Server::Channel.new(channel_key, EM::Hiredis.connect, subscriber) }
8
+ let(:subscriber) { Firehose::Server::Subscriber.new(EM::Hiredis.connect) }
9
9
  let(:message) { 'Raaaarrrrrr!!!!' }
10
- let(:publisher) { Firehose::Publisher.new }
10
+ let(:publisher) { Firehose::Server::Publisher.new }
11
11
 
12
12
  describe "#next_message" do
13
13
  it "should wait for message if message was not published before subscription" do
@@ -1,18 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Firehose::Publisher do
3
+ describe Firehose::Server::Publisher do
4
4
  include EM::TestHelper
5
5
 
6
- let(:publisher) { Firehose::Publisher.new }
6
+ let(:publisher) { Firehose::Server::Publisher.new }
7
7
  let(:channel_key) { "/firehose/publisher/test/#{Time.now.to_i}" }
8
8
  let(:message) { "howdy friends!" }
9
9
 
10
10
  it "should have 100 MAX_MESSAGES" do
11
- Firehose::Publisher::MAX_MESSAGES.should == 100
11
+ Firehose::Server::Publisher::MAX_MESSAGES.should == 100
12
12
  end
13
13
 
14
14
  it "should have 1 day TTL" do
15
- Firehose::Publisher::TTL.should == 86400
15
+ Firehose::Server::Publisher::TTL.should == 86400
16
16
  end
17
17
 
18
18
  describe "#publish" do
@@ -24,7 +24,7 @@ describe Firehose::Publisher do
24
24
  msg.should == "#{channel_key}\n1\n#{message}"
25
25
  em.next_tick { em.stop }
26
26
  }
27
- Firehose::Publisher.new.publish channel_key, message
27
+ Firehose::Server::Publisher.new.publish channel_key, message
28
28
  end
29
29
  end
30
30
 
@@ -32,7 +32,7 @@ describe Firehose::Publisher do
32
32
  it "should publish messages with the '#{char.inspect}' character" do
33
33
  msg = [char, message, char].join
34
34
  em 1 do
35
- Firehose::Publisher.new.publish(channel_key, msg).callback { em.stop }
35
+ Firehose::Server::Publisher.new.publish(channel_key, msg).callback { em.stop }
36
36
  end
37
37
  redis_exec('lpop', "firehose:#{channel_key}:list").should == msg
38
38
  end
@@ -40,19 +40,19 @@ describe Firehose::Publisher do
40
40
 
41
41
  it "should add message to list" do
42
42
  em do
43
- Firehose::Publisher.new.publish(channel_key, message).callback { em.stop }
43
+ Firehose::Server::Publisher.new.publish(channel_key, message).callback { em.stop }
44
44
  end
45
45
  redis_exec('lpop', "firehose:#{channel_key}:list").should == message
46
46
  end
47
47
 
48
48
  it "should limit list to MAX_MESSAGES messages" do
49
49
  em do
50
- Firehose::Publisher::MAX_MESSAGES.times do |n|
50
+ Firehose::Server::Publisher::MAX_MESSAGES.times do |n|
51
51
  publisher.publish(channel_key, message)
52
52
  end
53
53
  publisher.publish(channel_key, message).callback { em.stop }
54
54
  end
55
- redis_exec('llen', "firehose:#{channel_key}:list").should == Firehose::Publisher::MAX_MESSAGES
55
+ redis_exec('llen', "firehose:#{channel_key}:list").should == Firehose::Server::Publisher::MAX_MESSAGES
56
56
  end
57
57
 
58
58
  it "should increment sequence" do