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
@@ -1,84 +0,0 @@
1
- module Firehose
2
- class Channel
3
- attr_reader :channel_key, :redis, :subscriber, :list_key, :sequence_key
4
-
5
- def self.redis
6
- @redis ||= EM::Hiredis.connect
7
- end
8
-
9
- def self.subscriber
10
- @subscriber ||= Subscriber.new(EM::Hiredis.connect)
11
- end
12
-
13
-
14
- def initialize(channel_key, redis=self.class.redis, subscriber=self.class.subscriber)
15
- @channel_key, @redis, @subscriber = channel_key, redis, subscriber
16
-
17
- @list_key, @sequence_key = key(channel_key, :list), key(channel_key, :sequence)
18
- end
19
-
20
- def next_message(last_sequence=nil, options={})
21
- last_sequence = last_sequence.to_i
22
-
23
- deferrable = EM::DefaultDeferrable.new
24
- # TODO - Think this through a little harder... maybe some tests ol buddy!
25
- deferrable.errback {|e| EM.next_tick { raise e } unless [:timeout, :disconnect].include?(e) }
26
-
27
-
28
- # TODO: Use HSET so we don't have to pull 100 messages back every time.
29
- redis.multi
30
- redis.get(sequence_key).
31
- errback {|e| deferrable.fail e }
32
- redis.lrange(list_key, 0, Firehose::Publisher::MAX_MESSAGES).
33
- errback {|e| deferrable.fail e }
34
- redis.exec.callback do |(sequence, message_list)|
35
- Firehose.logger.debug "exec returened: `#{sequence}` and `#{message_list.inspect}`"
36
- sequence = sequence.to_i
37
-
38
- if sequence.nil? || (diff = sequence - last_sequence) <= 0
39
- Firehose.logger.debug "No message available yet, subscribing. sequence: `#{sequence}`"
40
- # Either this resource has never been seen before or we are all caught up.
41
- # Subscribe and hope something gets published to this end-point.
42
- subscribe(deferrable, options[:timeout])
43
- elsif last_sequence > 0 && diff < Firehose::Publisher::MAX_MESSAGES
44
- # The client is kinda-sorta running behind, but has a chance to catch
45
- # up. Catch them up FTW.
46
- # But we won't "catch them up" if last_sequence was zero/nil because
47
- # that implies the client is connecting for the 1st time.
48
- message = message_list[diff-1]
49
- Firehose.logger.debug "Sending old message `#{message}` and sequence `#{sequence}` to client directly. Client is `#{diff}` behind, at `#{last_sequence}`."
50
- deferrable.succeed message, last_sequence + 1
51
- else
52
- # The client is hopelessly behind and underwater. Just reset
53
- # their whole world with the lastest message.
54
- message = message_list[0]
55
- Firehose.logger.debug "Sending latest message `#{message}` and sequence `#{sequence}` to client directly."
56
- deferrable.succeed message, sequence
57
- end
58
- end.errback {|e| deferrable.fail e }
59
-
60
- deferrable
61
- end
62
-
63
- def unsubscribe(deferrable)
64
- subscriber.unsubscribe channel_key, deferrable
65
- end
66
-
67
- private
68
- def key(*segments)
69
- segments.unshift(:firehose).join(':')
70
- end
71
-
72
- def subscribe(deferrable, timeout=nil)
73
- subscriber.subscribe(channel_key, deferrable)
74
- if timeout
75
- timer = EventMachine::Timer.new(timeout) do
76
- deferrable.fail :timeout
77
- unsubscribe deferrable
78
- end
79
- # Cancel the timer if when the deferrable succeeds
80
- deferrable.callback { timer.cancel }
81
- end
82
- end
83
- end
84
- end
@@ -1,8 +0,0 @@
1
- require 'uri'
2
-
3
- module Firehose
4
- module Default
5
- # Default URI for the Firehose server. Consider the port "well-known" and bindable from other apps.
6
- URI = URI.parse("//0.0.0.0:7474").freeze
7
- end
8
- end
@@ -1,104 +0,0 @@
1
- # TODO Move this into the Firehose::Client:Producer namespace and rename the class to Http (its an HTTP publisher dumby!)
2
- require "faraday"
3
- require "uri"
4
-
5
- module Firehose
6
- # Publish messages to Firehose via an HTTP interface.
7
- class Producer
8
-
9
- # Exception gets raised when a 202 is _not_ received from the server after a message is published.
10
- PublishError = Class.new(RuntimeError)
11
- TimeoutError = Class.new(Faraday::Error::TimeoutError)
12
- Timeout = 1 # How many seconds should we wait for a publish to take?
13
-
14
- # A DSL for publishing requests. This doesn't so much, but lets us call
15
- # Firehose::Producer#publish('message').to('channel'). Slick eh? If you don't like it,
16
- # just all Firehose::Producer#put('message', 'channel')
17
- class Builder
18
- def initialize(producer, message)
19
- @producer, @message = producer, message
20
- self
21
- end
22
-
23
- def to(channel, opts={}, &callback)
24
- @producer.put(@message, channel, opts, &callback)
25
- end
26
- end
27
-
28
- # URI for the Firehose server. This URI does not include the path of the channel.
29
- attr_reader :uri
30
-
31
- def initialize(uri = Firehose::Default::URI)
32
- @uri = URI.parse(uri.to_s)
33
- @uri.scheme ||= 'http'
34
- end
35
-
36
- # A DSL for publishing messages.
37
- def publish(message)
38
- Builder.new(self, message)
39
- end
40
-
41
- # Publish the message via HTTP.
42
- def put(message, channel, opts, &block)
43
- ttl = opts[:ttl]
44
-
45
- response = conn.put do |req|
46
- req.options[:timeout] = Timeout
47
- if conn.path_prefix.nil? || conn.path_prefix == '/'
48
- # This avoids a double / if the channel starts with a / too (which is expected).
49
- req.path = channel
50
- else
51
- if conn.path_prefix =~ /\/\Z/ || channel =~ /\A\//
52
- req.path = [conn.path_prefix, channel].compact.join
53
- else
54
- # Add a / so the prefix and channel aren't just rammed together.
55
- req.path = [conn.path_prefix, channel].compact.join('/')
56
- end
57
- end
58
- req.body = message
59
- req.headers['Cache-Control'] = "max-age=#{ttl.to_i}" if ttl
60
- end
61
- response.on_complete do
62
- case response.status
63
- when 202 # Fire off the callback if everything worked out OK.
64
- block.call(response) if block
65
- else
66
- error_handler.call PublishError.new("Could not publish #{message.inspect} to '#{uri.to_s}/#{channel}': #{response.inspect}")
67
- end
68
- end
69
-
70
- # Hide Faraday with this Timeout exception, and through the error handler.
71
- rescue Faraday::Error::TimeoutError => e
72
- error_handler.call TimeoutError.new(e)
73
- end
74
-
75
- # Handle errors that could happen while publishing a message.
76
- def on_error(&block)
77
- @error_handler = block
78
- end
79
-
80
- # Raise an exception if an error occurs when connecting to the Firehose.
81
- def error_handler
82
- @error_handler || Proc.new{ |e| raise e }
83
- end
84
-
85
- # What adapter should Firehose use to PUT the message? List of adapters is
86
- # available at https://github.com/technoweenie/faraday.
87
- def self.adapter=(adapter)
88
- @adapter = adapter
89
- end
90
-
91
- # Use :net_http for the default Faraday adapter.
92
- def self.adapter
93
- @adapter ||= Faraday.default_adapter
94
- end
95
-
96
- private
97
- # Build out a Faraday connection
98
- def conn
99
- @conn ||= Faraday.new(:url => uri.to_s) do |builder|
100
- builder.adapter self.class.adapter
101
- end
102
- end
103
- end
104
- end
@@ -1,127 +0,0 @@
1
- module Firehose
2
- class Publisher
3
-
4
- MAX_MESSAGES = 100
5
- TTL = 60*60*24 # 1 day of time, yay!
6
- PAYLOAD_DELIMITER = "\n"
7
-
8
- def publish(channel_key, message, opts={})
9
- # How long should we hang on to the resource once is published?
10
- ttl = (opts[:ttl] || TTL).to_i
11
-
12
- # TODO hi-redis isn't that awesome... we have to setup an errback per even for wrong
13
- # commands because of the lack of a method_missing whitelist. Perhaps implement a whitelist in
14
- # em-hiredis or us a diff lib?
15
- if (deferrable = opts[:deferrable]).nil?
16
- deferrable = EM::DefaultDeferrable.new
17
- deferrable.errback do |e|
18
- # Handle missing Lua publishing script in cache
19
- # (such as Redis restarting or someone executing SCRIPT FLUSH)
20
- if e.message =~ /NOSCRIPT/
21
- deferrable.succeed
22
- EM.next_tick do
23
- @publish_script_digest = nil
24
- combined_opts = opts.merge :deferrable => deferrable
25
- self.publish channel_key, message, combined_opts
26
- end
27
- else
28
- EM.next_tick { raise e }
29
- end
30
- end
31
- end
32
-
33
- if @publish_script_digest.nil?
34
- register_publish_script.errback do |e|
35
- deferrable.fail e
36
- end.callback do |digest|
37
- @publish_script_digest = digest
38
- Firehose.logger.debug "Registered Lua publishing script with Redis => #{digest}"
39
- eval_publish_script channel_key, message, ttl, deferrable
40
- end
41
- else
42
- eval_publish_script channel_key, message, ttl, deferrable
43
- end
44
-
45
- deferrable
46
- end
47
-
48
- private
49
- def key(*segments)
50
- segments.unshift(:firehose).join(':')
51
- end
52
-
53
- def redis
54
- @redis ||= EM::Hiredis.connect
55
- end
56
-
57
- def self.to_payload(channel_key, sequence, message)
58
- [channel_key, sequence, message].join(PAYLOAD_DELIMITER)
59
- end
60
-
61
- def self.from_payload(payload)
62
- payload.split(PAYLOAD_DELIMITER, method(:to_payload).arity)
63
- end
64
-
65
- # TODO: Make this FAR more robust. Ideally we'd whitelist the permitted
66
- # characters and then escape or remove everything else.
67
- # See: http://en.wikibooks.org/wiki/Lua_Programming/How_to_Lua/escape_sequence
68
- def lua_escape(str)
69
- str.gsub(/\\/,'\\\\\\').gsub(/"/,'\"').gsub(/\n/,'\n').gsub(/\r/,'\r')
70
- end
71
-
72
- def register_publish_script
73
- redis.script 'LOAD', REDIS_PUBLISH_SCRIPT
74
- end
75
-
76
- def eval_publish_script(channel_key, message, ttl, deferrable)
77
- list_key = key(channel_key, :list)
78
- script_args = [
79
- key(channel_key, :sequence),
80
- list_key,
81
- key(:channel_updates),
82
- ttl,
83
- message,
84
- MAX_MESSAGES,
85
- PAYLOAD_DELIMITER,
86
- channel_key
87
- ]
88
- redis.evalsha(
89
- @publish_script_digest, script_args.length, *script_args
90
- ).errback do |e|
91
- deferrable.fail e
92
- end.callback do |sequence|
93
- Firehose.logger.debug "Redis stored/published `#{message}` to list `#{list_key}` with sequence `#{sequence}`"
94
- deferrable.succeed
95
- end
96
- end
97
-
98
- REDIS_PUBLISH_SCRIPT = <<-LUA
99
- local sequence_key = KEYS[1]
100
- local list_key = KEYS[2]
101
- local channel_key = KEYS[3]
102
- local ttl = KEYS[4]
103
- local message = KEYS[5]
104
- local max_messages = KEYS[6]
105
- local payload_delimiter = KEYS[7]
106
- local firehose_resource = KEYS[8]
107
-
108
- local current_sequence = redis.call('get', sequence_key)
109
- if current_sequence == nil or current_sequence == false then
110
- current_sequence = 0
111
- end
112
-
113
- local sequence = current_sequence + 1
114
- local message_payload = firehose_resource .. payload_delimiter .. sequence .. payload_delimiter .. message
115
-
116
- redis.call('set', sequence_key, sequence)
117
- redis.call('expire', sequence_key, ttl)
118
- redis.call('lpush', list_key, message)
119
- redis.call('ltrim', list_key, 0, max_messages - 1)
120
- redis.call('expire', list_key, ttl)
121
- redis.call('publish', channel_key, message_payload)
122
-
123
- return sequence
124
- LUA
125
-
126
- end
127
- end
@@ -1,54 +0,0 @@
1
- module Firehose
2
- # Setups a connetion to Redis to listen for new resources...
3
- class Subscriber
4
- attr_reader :pubsub
5
-
6
- def initialize(redis)
7
- @pubsub = redis.pubsub
8
-
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 = Firehose::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
-
45
- private
46
- def subscriptions
47
- @subscriptions ||= Hash.new{|h,k| h[k] = []}
48
- end
49
-
50
- def key(*segments)
51
- segments.unshift(:firehose).join(':')
52
- end
53
- end
54
- end
@@ -1,23 +0,0 @@
1
- module Firehose
2
- module SwfPolicyRequest
3
-
4
- # Borrowed from: https://github.com/igrigorik/em-websocket/blob/3e7f7d7760cc23b9d1d34fc1c17bab4423b5d11a/lib/em-websocket/connection.rb#L104
5
- def handle_swf_policy_request(data)
6
- if data =~ /\A<policy-file-request\s*\/>/
7
- Firehose.logger.debug "Received SWF Policy request: #{data.inspect}"
8
- send_data policy
9
- close_connection_after_writing
10
- true
11
- end
12
- end
13
-
14
- def policy
15
- <<-EOS
16
- <?xml version="1.0"?>
17
- <cross-domain-policy>
18
- <allow-access-from domain="*" to-ports="*"/>
19
- </cross-domain-policy>
20
- EOS
21
- end
22
- end
23
- end
@@ -1,30 +0,0 @@
1
- # require 'spec_helper'
2
-
3
- # describe Firehose::Broker do
4
- # include EM::TestHelper
5
-
6
- # let(:broker) { Firehose::Broker.new }
7
-
8
- # it "should unsubscibe consumers and remove them from the collection" do
9
- # stats = nil
10
-
11
- # em do
12
- # broker.consumer('1').subscribe_to('/the-channel')
13
- # broker.consumer('2').subscribe_to('/the-channel')
14
- # broker.consumer('2').subscribe_to('/a-channel')
15
-
16
- # em.add_timer(1) do
17
- # stats = broker.stats
18
- # broker.stop
19
- # em.stop
20
- # end
21
- # end
22
-
23
- # stats.should == {
24
- # '1' => {'subscriptions' => ['/the-channel'] },
25
- # '2' => {'subscriptions' => ['/the-channel', '/a-channel']}
26
- # }
27
-
28
- # broker.stats.should == {}
29
- # end
30
- # end
@@ -1,66 +0,0 @@
1
- # require 'spec_helper'
2
-
3
- # describe Firehose::Consumer do
4
- # include EM::TestHelper
5
-
6
- # let(:consumer) { Firehose::Consumer.new }
7
- # let(:publisher) { Firehose::Publisher.new }
8
- # let(:channel) { '/papa-smurf' }
9
- # let(:another_channel) { '/mama-smurf' }
10
-
11
- # describe "subscriptions" do
12
- # it "should subscribe to channel" do
13
- # sent, recieved = 'hi', nil
14
-
15
- # em do
16
- # consumer.subscribe_to channel do |msg|
17
- # recieved = msg
18
- # em.stop
19
- # end
20
- # em.add_timer(1) do
21
- # publisher.publish(channel, sent)
22
- # end
23
- # end
24
-
25
- # recieved.should == sent
26
- # end
27
-
28
- # it "should track subscriptions" do
29
- # lambda{
30
- # em do
31
- # consumer.subscribe_to channel
32
- # consumer.subscribe_to another_channel
33
- # em.add_timer(1){ em.stop }
34
- # end
35
- # }.should change{ consumer.subscriptions.size }.by(2)
36
- # end
37
-
38
- # it "should only allow one subscription per channel" do
39
- # lambda{
40
- # em do
41
- # 3.times { consumer.subscribe_to channel }
42
- # em.add_timer(1){ em.stop }
43
- # end
44
- # }.should change{ consumer.subscriptions.size }.by(1)
45
- # end
46
-
47
- # it "should unsubscribe from all channels" do
48
- # subscribed_count, after_unsubscribe_count = 0, nil
49
-
50
- # em do
51
- # consumer.subscribe_to channel
52
- # consumer.subscribe_to another_channel
53
- # subscribed_count = consumer.subscriptions.size
54
- # em.add_timer(1) do
55
- # consumer.unsubscribe
56
- # em.add_timer(1) do
57
- # em.stop
58
- # end
59
- # end
60
- # end
61
-
62
- # subscribed_count.should == 2
63
- # consumer.subscriptions.size.should == 0
64
- # end
65
- # end
66
- # end