firehose 0.0.16 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec CHANGED
@@ -1 +1 @@
1
- --colour
1
+ --colour --backtrace
data/README.md CHANGED
@@ -13,11 +13,11 @@ Firehose is both a Rack application and JavasScript library that makes building
13
13
 
14
14
  # Getting Started
15
15
 
16
- First, you'll need to install and run RabbitMQ.
16
+ First, you'll need to install and run Redis.
17
17
 
18
18
  ```sh
19
- $ apt-get install rabbitmq # Install on Ubuntu
20
- $ brew install rabbitmq # Install on Mac Homebrew
19
+ $ apt-get install redis # Install on Ubuntu
20
+ $ brew install redis # Install on Mac Homebrew
21
21
  ```
22
22
 
23
23
  Then install the gem.
@@ -34,7 +34,7 @@ Now fire up the server.
34
34
  $ firehose server
35
35
  >> Thin web server (v1.3.1 codename Triple Espresso)
36
36
  >> Maximum connections set to 1024
37
- >> Listening on 127.0.0.1:7478, CTRL+C to stop
37
+ >> Listening on 127.0.0.1:7474, CTRL+C to stop
38
38
  ```
39
39
 
40
40
  In case you're wondering, the Firehose application server runs the Rack app `Firehose::Rack::App.new` inside of Thin.
@@ -87,15 +87,14 @@ new Firehose.Consumer({
87
87
  },
88
88
  // Note that we do NOT specify a protocol here because we don't
89
89
  // know that yet.
90
- uri: '//localhost:7478/hello'
90
+ uri: '//localhost:7474/hello'
91
91
  }).connect();
92
92
  ```
93
93
 
94
94
  Then publish another message.
95
95
 
96
-
97
96
  ```sh
98
- $ curl -X PUT -d "This is almost magical" "http://localhost:7478/hello"
97
+ $ curl -X PUT -d "\"This is almost magical\"" "http://localhost:7474/hello"
99
98
  ```
100
99
 
101
100
  # How is it different from socket.io?
data/firehose.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # specify any dependencies here; for example:
22
22
  s.add_runtime_dependency "eventmachine", ">= 1.0.0.beta"
23
- s.add_runtime_dependency "amqp", ">= 0.9.4"
23
+ s.add_runtime_dependency "em-hiredis"
24
24
  s.add_runtime_dependency "thin"
25
25
  s.add_runtime_dependency "thor"
26
26
  s.add_runtime_dependency "faraday"
data/lib/firehose.rb CHANGED
@@ -1,31 +1,16 @@
1
1
  require 'firehose/version'
2
2
 
3
- require 'amqp'
3
+ require 'em-hiredis'
4
4
  require 'logger'
5
5
 
6
6
  module Firehose
7
7
  autoload :Subscription, 'firehose/subscription'
8
8
  autoload :Publisher, 'firehose/publisher'
9
9
  autoload :Producer, 'firehose/producer'
10
- autoload :Consumer, 'firehose/consumer'
11
10
  autoload :Default, 'firehose/default'
12
- autoload :Broker, 'firehose/broker'
13
11
  autoload :Rack, 'firehose/rack'
14
12
  autoload :CLI, 'firehose/cli'
15
13
 
16
- # TODO move this into a configuration or session class.
17
- # Hang on to AMQP configuration settings.
18
- def self.amqp
19
- @amqp ||= Struct.new(:connection).new(AMQP.connect)
20
- end
21
-
22
- # TODO figure out a better way to memoize AMQP connection for production runtimes, and
23
- # make it resetable for testing environment. Some sort of Firehose::Session object is probably
24
- # in order
25
- def self.reset!
26
- @amqp = nil
27
- end
28
-
29
14
  # Logging
30
15
  def self.logger
31
16
  @logger ||= Logger.new($stdout)
data/lib/firehose/cli.rb CHANGED
@@ -8,27 +8,15 @@ module Firehose
8
8
  method_option :host, :type => :string, :default => '0.0.0.0', :required => true, :aliases => '-h'
9
9
 
10
10
  def server
11
- broker = Firehose::Broker.new
12
-
13
11
  server = Thin::Server.new(options[:host], options[:port]) do
14
- # TODO move this into a socket... this is really janky, but I need to to troubleshoot
15
- # connection reference issues. I'd like to have this ancillary stuff accessiable via
16
- # a different port or even a socket.
17
- map '/_firehose/stats' do
18
- run Proc.new {
19
- [200, {'Content-Type' => 'text/plain'}, [broker.stats.inspect]]
20
- }
21
- end
22
-
23
- map '/' do
24
- run Firehose::Rack::App.new(broker)
25
- end
12
+ run Firehose::Rack::App.new
26
13
  end
27
14
 
28
15
  begin
29
16
  server.start!
30
- rescue AMQP::TCPConnectionFailed => e
31
- Firehose.logger.error "Unable to connect to AMQP, are you sure it's running?"
17
+ rescue RuntimeError
18
+ Firehose.logger.error "Unable to connect to Redis, are you sure it's running?"
19
+ raise
32
20
  end
33
21
  end
34
22
  end
@@ -1,13 +1,13 @@
1
1
  module Firehose
2
2
  class Publisher
3
- def publish(path, message)
4
- channel = AMQP::Channel.new(Firehose.amqp.connection)
5
- exchange = AMQP::Exchange.new(channel, :fanout, path, :auto_delete => true)
6
- # TODO How do I clean up this exchange at this point? Do I close it somehow or the channel?
7
- # The exchange just hangs out indefinitely now.
8
- Firehose.logger.debug "AMQP publishing `#{message}` to `#{path}`"
9
- exchange.publish(message)
10
- exchange.delete(:if_unused => true)
3
+ def publish(channel, message)
4
+ Firehose.logger.debug "Redis publishing `#{message}` to `#{channel}`"
5
+ redis.publish(channel, message).errback { raise 'Error publishing' }
6
+ end
7
+
8
+ private
9
+ def redis
10
+ @redis ||= EM::Hiredis.connect
11
11
  end
12
12
  end
13
13
  end
data/lib/firehose/rack.rb CHANGED
@@ -5,16 +5,14 @@ module Firehose
5
5
  AsyncResponse = [-1, {}, []]
6
6
 
7
7
  class HttpLongPoll
8
- def initialize(broker)
9
- @broker = broker
10
- end
11
-
12
8
  def call(env)
13
9
  req = ::Rack::Request.new(env)
14
10
  cid = req.params['cid']
15
11
  path = req.path
16
12
  method = req.request_method
17
13
  timeout = 30
14
+ queue_name = "#{cid}@#{path}"
15
+
18
16
  # TODO seperate out CORS logic as an async middleware with a Goliath web server.
19
17
  cors_origin = env['HTTP_ORIGIN']
20
18
  cors_headers = {
@@ -33,20 +31,23 @@ module Firehose
33
31
  response_headers = cors_origin ? cors_headers : {}
34
32
 
35
33
  # Setup a subscription with a client id. We haven't subscribed yet here.
36
- consumer = @broker.consumer(cid)
34
+ if queue = queues[queue_name]
35
+ queue.live
36
+ else
37
+ queue = queues[queue_name] = Firehose::Subscription::Queue.new(cid, path)
38
+ end
37
39
 
38
40
  # Setup a timeout timer to tell clients that time out that everything is OK
39
41
  # and they should come back for more
40
- timer = EventMachine::Timer.new(timeout) do
42
+ long_poll_timer = EM::Timer.new(timeout) do
41
43
  # We send a 204 OK to tell the client to reconnect.
42
44
  env['async.callback'].call [204, response_headers, []]
43
45
  Firehose.logger.debug "HTTP wait `#{cid}@#{path}` timed out"
44
46
  end
45
47
 
46
48
  # Ok, now subscribe to the subscription.
47
- consumer.subscribe_to path do |message, subscription|
48
- timer.cancel # Turn off the heart beat so we don't execute any of that business.
49
- consumer.unsubscribe
49
+ queue.pop do |message, subscription|
50
+ long_poll_timer.cancel # Turn off the heart beat so we don't execute any of that business.
50
51
  env['async.callback'].call [200, response_headers, [message]]
51
52
  Firehose.logger.debug "HTTP sent `#{message}` to `#{cid}@#{path}`"
52
53
  end
@@ -55,7 +56,11 @@ module Firehose
55
56
  # Unsubscribe from the subscription if its still open and something bad happened
56
57
  # or the heart beat triggered before we could finish.
57
58
  env['async.close'].callback do
58
- consumer.unsubscribe if consumer
59
+ # Kill queue if we don't hear back in 30s
60
+ queue.kill timeout do
61
+ Firehose.logger.debug "Deleting queue to `#{queue_name}`"
62
+ queues.delete queue_name
63
+ end
59
64
  Firehose.logger.debug "HTTP connection `#{cid}@#{path}` closing"
60
65
  end
61
66
  end
@@ -67,7 +72,7 @@ module Firehose
67
72
  when 'PUT'
68
73
  body = env['rack.input'].read
69
74
  Firehose.logger.debug "HTTP published `#{body}` to `#{path}`"
70
- Firehose::Publisher.new.publish(path, body)
75
+ publisher.publish(path, body)
71
76
 
72
77
  [202, {}, []]
73
78
  else
@@ -75,23 +80,28 @@ module Firehose
75
80
  [501, {'Content-Type' => 'text/plain'}, ["#{method} not supported."]]
76
81
  end
77
82
  end
83
+
84
+ private
85
+ def publisher
86
+ @publisher ||= Firehose::Publisher.new
87
+ end
88
+
89
+ def queues
90
+ @queues ||= {}
91
+ end
78
92
  end
79
93
 
80
94
  class WebSocket < ::Rack::WebSocket::Application
81
- attr_reader :cid, :path
95
+ attr_reader :cid, :path, :subscription
82
96
 
83
- def initialize(broker)
84
- @broker = broker
85
- end
86
-
87
97
  # Subscribe to a path and make some magic happen, mmkmay?
88
98
  def on_open(env)
89
99
  req = ::Rack::Request.new(env)
90
100
  @cid = req.params['cid']
91
101
  @path = req.path
102
+ @subscription = Firehose::Subscription.new(cid, path)
92
103
 
93
- @consumer = @broker.consumer(@cid)
94
- @consumer.subscribe_to path do |message|
104
+ subscription.subscribe do |message, subscription|
95
105
  Firehose.logger.debug "WS sent `#{message}` to `#{cid}@#{path}`"
96
106
  send_data message
97
107
  end
@@ -100,37 +110,28 @@ module Firehose
100
110
 
101
111
  # Delete the subscription if the thing even happened.
102
112
  def on_close(env)
103
- @consumer.unsubscribe if @consumer
113
+ subscription.unsubscribe
104
114
  Firehose.logger.debug "WS connection `#{cid}@#{path}` closing"
105
115
  end
106
116
 
107
117
  # Log websocket level errors
108
118
  def on_error(env, error)
109
- Firehose.logger.error "WS connection `#{cid}@#{path}` error `#{error}`: #{env.inspect}"
110
- @consumer.unsubscribe if @consumer
119
+ Firehose.logger.error "WS connection `#{cid}@#{path}` error `#{error}`: #{error.backtrace}"
111
120
  end
112
121
  end
113
122
 
114
123
  class App
115
- # Firehose broker that will be used to pub/sub messages.
116
- attr_reader :broker
117
-
118
- # Fire up a default broker if one is not specified.
119
- def initialize(broker = Firehose::Broker.new)
120
- @broker = broker
121
- end
122
-
123
124
  def call(env)
124
125
  websocket_request?(env) ? websocket.call(env) : http_long_poll.call(env)
125
126
  end
126
127
 
127
128
  private
128
129
  def websocket
129
- @websocket ||= WebSocket.new(broker)
130
+ WebSocket.new
130
131
  end
131
132
 
132
133
  def http_long_poll
133
- @http_long_poll ||= HttpLongPoll.new(broker)
134
+ @http_long_poll ||= HttpLongPoll.new
134
135
  end
135
136
 
136
137
  def websocket_request?(env)
@@ -1,65 +1,75 @@
1
1
  module Firehose
2
2
  class Subscription
3
- # Default TTL for how long a subscription should live on the server when the
4
- # consumer disconnects.
5
- # TODO should the Consumer handle TTL?
6
- TTL = 15000
7
-
8
- # Time to live for the amqp_queue on the server after the subscription is canceled. This
9
- # is mostly for flakey connections where the client may reconnect after *ttl* and continue
10
- # receiving messages.
11
- attr_accessor :ttl
12
-
13
- # Consumer and channel for the subscription.
14
- attr_reader :consumer
3
+ # consumer_id and channel for the subscription.
4
+ attr_reader :consumer_id
15
5
 
16
6
  # Channel that we'll use for the pub-sub activity. This probably maps to an URL
17
7
  attr_reader :channel
18
8
 
19
- def initialize(consumer, channel)
20
- @consumer, @channel = consumer, channel
9
+ def initialize(consumer_id, channel)
10
+ @consumer_id, @channel = consumer_id, channel
21
11
  end
22
12
 
23
- # TODO - Move the channel to an initializer so that we can force on AMQP subscription per one
24
- # Firehose subscription. As it stands now, you could fire off multple subscriptions to diff amqp_channels
13
+ # Subscribe to messages on the backend to fill up the subscription queue. consumer_ids of the messages
14
+ # will queue up units of "work" to process data from the subscription.
25
15
  def subscribe(&block)
26
- amqp_queue_name = "#{consumer.guid}@#{channel}"
27
- amqp_channel = AMQP::Channel.new(Firehose.amqp.connection).prefetch(1)
28
- amqp_exchange = AMQP::Exchange.new(amqp_channel, :fanout, channel, :auto_delete => true)
29
- amqp_queue = AMQP::Queue.new(amqp_channel, amqp_queue_name, :arguments => {'x-expires' => ttl})
30
- amqp_queue.bind(amqp_exchange)
31
-
32
- # When we get a message, we want to remove the consumer from the amqp_queue so that the x-expires
33
- # ttl starts ticking down. On the reconnect, the consumer connects to the amqp_queue and resets the
34
- # timer on x-expires... in theory at least.
35
- @amqp_consumer = AMQP::Consumer.new(amqp_channel, amqp_queue, consumer.guid)
36
- @amqp_consumer.on_delivery do |metadata, message|
37
- Firehose.logger.debug "AMQP delivering `#{message}` to `#{consumer.guid}@#{channel}`"
38
- block.call(message, self)
39
- # The ack needs to go after the block is called. This makes sure that all processing
40
- # happens downstream before we remove it from the amqp_queue entirely.
41
- metadata.ack
42
- end.consume
43
- Firehose.logger.debug "AMQP subscribed to `#{consumer.guid}@#{channel}`"
16
+ redis.subscribe(channel)
17
+ redis.on(:message) do |channel, message|
18
+ Firehose.logger.debug "Redis recieved `#{message}` to `#{consumer_id}@#{channel}`"
19
+ block.call message, self
20
+ end
21
+ Firehose.logger.debug "Redis subscribed to `#{consumer_id}@#{channel}`"
44
22
  self # Return the subscription for chaining.
45
23
  end
46
24
 
47
- def unsubscribe
48
- Firehose.logger.debug "AMQP unsubscribed"
49
- @amqp_consumer.cancel if @amqp_consumer
50
- @unsubscribe_callback.call self if @unsubscribe_callback
25
+ def unsubscribe(&block)
26
+ Firehose.logger.debug "Redis unsubscribed from `#{consumer_id}@#{channel}`"
27
+ redis.close
28
+ block.call(self) if block
29
+ self
30
+ end
31
+
32
+ private
33
+ def redis
34
+ @redis ||= EM::Hiredis.connect
35
+ end
36
+ end
37
+
38
+ # Queue subscription messages so that we can remember and/or operate on them
39
+ class Subscription::Queue
40
+ attr_reader :subscription, :channel
41
+
42
+ def initialize(consumer_id, channel)
43
+ @subscription = Subscription.new(consumer_id, channel)
44
+ # Start the subscription and start dropping mesasge onto the queue
45
+ subscription.subscribe do |message|
46
+ queue.push message
47
+ end
48
+ end
49
+
50
+ # Pop an item off the subscription queue so we can work on it.
51
+ def pop(&block)
52
+ queue.pop do |message|
53
+ block.call message, subscription
54
+ end
55
+ end
56
+
57
+ # Kill the queue in n seconds.
58
+ def kill(ttl=0, &block)
59
+ if ttl.zero?
60
+ subscription.unsubscribe &block
61
+ else
62
+ @timer = EM::Timer.new(ttl){ kill 0 }
63
+ end
51
64
  end
52
65
 
53
- # Callback when consumer unsubscribes from subscription. The consumer uses this to remove
54
- # subscriptions from itself when an unsubscribe happens.
55
- def on_unsubscribe(&block)
56
- @unsubscribe_callback = block
66
+ def live
67
+ @timer.cancel if @timer
57
68
  end
58
69
 
59
- # The time that a amqp_queue should live *after* the client unsubscribes. This is useful for
60
- # flakey network connections, like HTTP Long Polling or even broken web sockets.
61
- def ttl
62
- @ttl ||= TTL
70
+ private
71
+ def queue
72
+ @queue ||= EM::Queue.new
63
73
  end
64
74
  end
65
75
  end
@@ -1,4 +1,4 @@
1
1
  module Firehose
2
- VERSION = "0.0.16"
3
- CODENAME = "Statistical Sam"
2
+ VERSION = "0.1.0"
3
+ CODENAME = "Redis Rodeo"
4
4
  end
@@ -12,12 +12,11 @@ describe Firehose::Rack do
12
12
  end
13
13
 
14
14
  let(:app) { Firehose::Rack::App.new }
15
- let(:messages) { (1..1000).map(&:to_s) }
15
+ let(:messages) { (1..2000).map(&:to_s) }
16
16
  let(:channel) { "/firehose/integration/#{Time.now.to_i}" }
17
17
  let(:uri) { Firehose::Default::URI }
18
18
  let(:http_url) { "http://#{uri.host}:#{uri.port}#{channel}" }
19
19
  let(:ws_url) { "ws://#{uri.host}:#{uri.port}#{channel}" }
20
- let(:cid) { "client-#{Time.now.to_i}" }
21
20
 
22
21
  it "should pub-sub http and websockets" do
23
22
  # Setup variables that we'll use after we turn off EM to validate our
@@ -39,7 +38,7 @@ describe Firehose::Rack do
39
38
 
40
39
  # Lets have an HTTP Long poll client
41
40
  http_long_poll = Proc.new do
42
- http = EM::HttpRequest.new(http_url).get(:query => {'cid' => cid})
41
+ http = EM::HttpRequest.new(http_url).get(:query => {'cid' => 'alpha'})
43
42
  http.errback { em.stop }
44
43
  http.callback do
45
44
  received_http << http.response
@@ -53,7 +52,7 @@ describe Firehose::Rack do
53
52
 
54
53
  # And test a web socket client too, at the same time.
55
54
  websocket = Proc.new do
56
- ws = EventMachine::WebSocketClient.connect(ws_url)
55
+ ws = EventMachine::WebSocketClient.connect("#{ws_url}?cid=bravo")
57
56
  ws.errback { em.stop }
58
57
  ws.stream do |msg|
59
58
  received_ws << msg
@@ -64,7 +63,8 @@ describe Firehose::Rack do
64
63
  # Great, we have all the pieces in order, lets run this thing in the reactor.
65
64
  em do
66
65
  # Start the server
67
- ::Thin::Server.new('0.0.0.0', uri.port, app).start
66
+ server = ::Thin::Server.new('0.0.0.0', uri.port, app)
67
+ server.start
68
68
 
69
69
  # Start the http_long_pollr.
70
70
  http_long_poll.call
@@ -75,7 +75,7 @@ describe Firehose::Rack do
75
75
  end
76
76
 
77
77
  # When EM stops, these assertions will be made.
78
- received_http.should =~ messages
79
78
  received_ws.should =~ messages
79
+ received_http.should =~ messages
80
80
  end
81
81
  end
@@ -1,30 +1,30 @@
1
- require 'spec_helper'
1
+ # require 'spec_helper'
2
2
 
3
- describe Firehose::Broker do
4
- include EM::TestHelper
3
+ # describe Firehose::Broker do
4
+ # include EM::TestHelper
5
5
 
6
- let(:broker) { Firehose::Broker.new }
6
+ # let(:broker) { Firehose::Broker.new }
7
7
 
8
- it "should unsubscibe consumers and remove them from the collection" do
9
- stats = nil
8
+ # it "should unsubscibe consumers and remove them from the collection" do
9
+ # stats = nil
10
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')
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
15
 
16
- em.add_timer(1) do
17
- stats = broker.stats
18
- broker.stop
19
- em.stop
20
- end
21
- end
16
+ # em.add_timer(1) do
17
+ # stats = broker.stats
18
+ # broker.stop
19
+ # em.stop
20
+ # end
21
+ # end
22
22
 
23
- stats.should == {
24
- '1' => {'subscriptions' => ['/the-channel'] },
25
- '2' => {'subscriptions' => ['/the-channel', '/a-channel']}
26
- }
23
+ # stats.should == {
24
+ # '1' => {'subscriptions' => ['/the-channel'] },
25
+ # '2' => {'subscriptions' => ['/the-channel', '/a-channel']}
26
+ # }
27
27
 
28
- broker.stats.should == {}
29
- end
30
- end
28
+ # broker.stats.should == {}
29
+ # end
30
+ # end
@@ -1,66 +1,66 @@
1
- require 'spec_helper'
1
+ # require 'spec_helper'
2
2
 
3
- describe Firehose::Consumer do
4
- include EM::TestHelper
3
+ # describe Firehose::Consumer do
4
+ # include EM::TestHelper
5
5
 
6
- let(:consumer) { Firehose::Consumer.new }
7
- let(:publisher) { Firehose::Publisher.new }
8
- let(:channel) { '/papa-smurf' }
9
- let(:another_channel) { '/mama-smurf' }
6
+ # let(:consumer) { Firehose::Consumer.new }
7
+ # let(:publisher) { Firehose::Publisher.new }
8
+ # let(:channel) { '/papa-smurf' }
9
+ # let(:another_channel) { '/mama-smurf' }
10
10
 
11
- describe "subscriptions" do
12
- it "should subscribe to channel" do
13
- sent, recieved = 'hi', nil
11
+ # describe "subscriptions" do
12
+ # it "should subscribe to channel" do
13
+ # sent, recieved = 'hi', nil
14
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
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
24
 
25
- recieved.should == sent
26
- end
25
+ # recieved.should == sent
26
+ # end
27
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
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
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
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
46
 
47
- it "should unsubscribe from all channels" do
48
- subscribed_count, after_unsubscribe_count = 0, nil
47
+ # it "should unsubscribe from all channels" do
48
+ # subscribed_count, after_unsubscribe_count = 0, nil
49
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
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
61
 
62
- subscribed_count.should == 2
63
- consumer.subscriptions.size.should == 0
64
- end
65
- end
66
- end
62
+ # subscribed_count.should == 2
63
+ # consumer.subscriptions.size.should == 0
64
+ # end
65
+ # end
66
+ # end
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,7 @@ require 'logger'
2
2
  require 'em-http'
3
3
  require 'em-websocket-client'
4
4
 
5
+ # Skip logging if VERBOSE isn't set to true.
5
6
  require 'firehose'
6
7
  Firehose.logger = Logger.new('/dev/null') unless ENV['VERBOSE']
7
8
 
@@ -37,8 +38,4 @@ RSpec.configure do |config|
37
38
  config.treat_symbols_as_metadata_keys_with_true_values = true
38
39
  config.run_all_when_everything_filtered = true
39
40
  config.filter_run :focus
40
- config.before(:each) do
41
- # For now, this resets the AMQP configuration between runs.
42
- Firehose.reset!
43
- end
44
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: firehose
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-05-04 00:00:00.000000000 Z
13
+ date: 2012-05-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: eventmachine
17
- requirement: &70349732213400 !ruby/object:Gem::Requirement
17
+ requirement: &70097385746880 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,21 +22,21 @@ dependencies:
22
22
  version: 1.0.0.beta
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *70349732213400
25
+ version_requirements: *70097385746880
26
26
  - !ruby/object:Gem::Dependency
27
- name: amqp
28
- requirement: &70349732212540 !ruby/object:Gem::Requirement
27
+ name: em-hiredis
28
+ requirement: &70097385746460 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
32
32
  - !ruby/object:Gem::Version
33
- version: 0.9.4
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *70349732212540
36
+ version_requirements: *70097385746460
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: thin
39
- requirement: &70349732211460 !ruby/object:Gem::Requirement
39
+ requirement: &70097385746000 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: '0'
45
45
  type: :runtime
46
46
  prerelease: false
47
- version_requirements: *70349732211460
47
+ version_requirements: *70097385746000
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: thor
50
- requirement: &70349732210400 !ruby/object:Gem::Requirement
50
+ requirement: &70097385745580 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,10 +55,10 @@ dependencies:
55
55
  version: '0'
56
56
  type: :runtime
57
57
  prerelease: false
58
- version_requirements: *70349732210400
58
+ version_requirements: *70097385745580
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: faraday
61
- requirement: &70349732209120 !ruby/object:Gem::Requirement
61
+ requirement: &70097385745160 !ruby/object:Gem::Requirement
62
62
  none: false
63
63
  requirements:
64
64
  - - ! '>='
@@ -66,10 +66,10 @@ dependencies:
66
66
  version: '0'
67
67
  type: :runtime
68
68
  prerelease: false
69
- version_requirements: *70349732209120
69
+ version_requirements: *70097385745160
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: websocket-rack
72
- requirement: &70349732223780 !ruby/object:Gem::Requirement
72
+ requirement: &70097385744740 !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
75
  - - ! '>='
@@ -77,10 +77,10 @@ dependencies:
77
77
  version: '0'
78
78
  type: :runtime
79
79
  prerelease: false
80
- version_requirements: *70349732223780
80
+ version_requirements: *70097385744740
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: em-http-request
83
- requirement: &70349732222860 !ruby/object:Gem::Requirement
83
+ requirement: &70097385744240 !ruby/object:Gem::Requirement
84
84
  none: false
85
85
  requirements:
86
86
  - - ~>
@@ -88,10 +88,10 @@ dependencies:
88
88
  version: 1.0.0
89
89
  type: :runtime
90
90
  prerelease: false
91
- version_requirements: *70349732222860
91
+ version_requirements: *70097385744240
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: rspec
94
- requirement: &70349732221260 !ruby/object:Gem::Requirement
94
+ requirement: &70097385743820 !ruby/object:Gem::Requirement
95
95
  none: false
96
96
  requirements:
97
97
  - - ! '>='
@@ -99,10 +99,10 @@ dependencies:
99
99
  version: '0'
100
100
  type: :development
101
101
  prerelease: false
102
- version_requirements: *70349732221260
102
+ version_requirements: *70097385743820
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: webmock
105
- requirement: &70349732220520 !ruby/object:Gem::Requirement
105
+ requirement: &70097385743360 !ruby/object:Gem::Requirement
106
106
  none: false
107
107
  requirements:
108
108
  - - ! '>='
@@ -110,10 +110,10 @@ dependencies:
110
110
  version: '0'
111
111
  type: :development
112
112
  prerelease: false
113
- version_requirements: *70349732220520
113
+ version_requirements: *70097385743360
114
114
  - !ruby/object:Gem::Dependency
115
115
  name: guard-rspec
116
- requirement: &70349732219740 !ruby/object:Gem::Requirement
116
+ requirement: &70097385742940 !ruby/object:Gem::Requirement
117
117
  none: false
118
118
  requirements:
119
119
  - - ! '>='
@@ -121,10 +121,10 @@ dependencies:
121
121
  version: '0'
122
122
  type: :development
123
123
  prerelease: false
124
- version_requirements: *70349732219740
124
+ version_requirements: *70097385742940
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: guard-bundler
127
- requirement: &70349732219100 !ruby/object:Gem::Requirement
127
+ requirement: &70097385742520 !ruby/object:Gem::Requirement
128
128
  none: false
129
129
  requirements:
130
130
  - - ! '>='
@@ -132,10 +132,10 @@ dependencies:
132
132
  version: '0'
133
133
  type: :development
134
134
  prerelease: false
135
- version_requirements: *70349732219100
135
+ version_requirements: *70097385742520
136
136
  - !ruby/object:Gem::Dependency
137
137
  name: guard-coffeescript
138
- requirement: &70349732218080 !ruby/object:Gem::Requirement
138
+ requirement: &70097385742100 !ruby/object:Gem::Requirement
139
139
  none: false
140
140
  requirements:
141
141
  - - ! '>='
@@ -143,10 +143,10 @@ dependencies:
143
143
  version: '0'
144
144
  type: :development
145
145
  prerelease: false
146
- version_requirements: *70349732218080
146
+ version_requirements: *70097385742100
147
147
  - !ruby/object:Gem::Dependency
148
148
  name: em-websocket-client
149
- requirement: &70349732217260 !ruby/object:Gem::Requirement
149
+ requirement: &70097385741680 !ruby/object:Gem::Requirement
150
150
  none: false
151
151
  requirements:
152
152
  - - ! '>='
@@ -154,7 +154,7 @@ dependencies:
154
154
  version: '0'
155
155
  type: :development
156
156
  prerelease: false
157
- version_requirements: *70349732217260
157
+ version_requirements: *70097385741680
158
158
  description: Firehose is a realtime web application toolkit for building realtime
159
159
  Ruby web applications.
160
160
  email:
@@ -184,16 +184,13 @@ files:
184
184
  - lib/assets/javascripts/firehose/transport.js.coffee
185
185
  - lib/assets/javascripts/firehose/web_socket.js.coffee
186
186
  - lib/firehose.rb
187
- - lib/firehose/broker.rb
188
187
  - lib/firehose/cli.rb
189
- - lib/firehose/consumer.rb
190
188
  - lib/firehose/default.rb
191
189
  - lib/firehose/producer.rb
192
190
  - lib/firehose/publisher.rb
193
191
  - lib/firehose/rack.rb
194
192
  - lib/firehose/subscription.rb
195
193
  - lib/firehose/version.rb
196
- - spec/integrations/amqp_resources_spec.rb
197
194
  - spec/integrations/thin_spec.rb
198
195
  - spec/lib/broker_spec.rb
199
196
  - spec/lib/consumer_spec.rb
@@ -225,7 +222,6 @@ signing_key:
225
222
  specification_version: 3
226
223
  summary: Build realtime Ruby web applications
227
224
  test_files:
228
- - spec/integrations/amqp_resources_spec.rb
229
225
  - spec/integrations/thin_spec.rb
230
226
  - spec/lib/broker_spec.rb
231
227
  - spec/lib/consumer_spec.rb
@@ -1,34 +0,0 @@
1
- module Firehose
2
- # TODO add support to broker for publishing, then abstract this out into a backend. A
3
- # broker will eventually be passed into a web server front-end to serve up the web requests.
4
- class Broker
5
- def consumers
6
- @consumers ||= Hash.new do |consumers, consumer_id|
7
- consumer = Firehose::Consumer.new(consumer_id)
8
- consumer.on_unsubscribe do
9
- consumers.delete consumer_id
10
- end
11
- consumers[consumer_id] = consumer
12
- end
13
- end
14
-
15
- # Don't like the [] syntax to get at consumers? No worries mate!
16
- def consumer(consumer_id)
17
- consumers[consumer_id]
18
- end
19
-
20
- # Gracefully unsubscribe all of the consumers and get rid of them from the consumers
21
- # collection.
22
- def stop
23
- consumers.values.each(&:unsubscribe)
24
- end
25
-
26
- # Returns a hash of the connected consumers with the number of their subscriptions
27
- def stats
28
- consumers.inject({}) do |memo, (consumer_id, consumer)|
29
- memo[consumer_id] = { 'subscriptions' => consumer.subscriptions.keys }
30
- memo
31
- end
32
- end
33
- end
34
- end
@@ -1,49 +0,0 @@
1
- module Firehose
2
- class Consumer
3
- # Unique identifier for a consumer. Note that a consumer does not map directly to
4
- # a user_id. In a web browser world, you might have a user with multiple tabs open,
5
- # so you'll went to send each users tab a seperate message stream. Consider a convention
6
- # such as :user_id-:guid for your application.
7
- attr_reader :guid
8
-
9
- def initialize(guid = self.class.next_guid)
10
- @guid = guid
11
- end
12
-
13
- # Create a subscription and subscribe to a channel.
14
- def subscribe_to(channel, &block)
15
- subscriptions[channel].subscribe(&block)
16
- end
17
-
18
- # Active subscriptions to the backend.
19
- def subscriptions
20
- @subscriptions ||= Hash.new do |subscriptions, channel|
21
- # Setup the hash to generate subscriptions that can delete themselves from
22
- # their own collection on an unsubscription event.
23
- subscription = Subscription.new(self, channel)
24
- subscription.on_unsubscribe do
25
- # Remove the subscription from the consumer.subscriptions
26
- # list when unsubscribe.
27
- subscriptions.delete channel
28
- end
29
- subscriptions[channel] = subscription
30
- end
31
- end
32
-
33
- # Unsubscribe from all subscriptions.
34
- def unsubscribe
35
- subscriptions.values.each(&:unsubscribe)
36
- @on_unsubscribe.call(self) if @on_unsubscribe
37
- end
38
-
39
- # Define callback for when unsubscribe is called from the consumer.
40
- def on_unsubscribe(&block)
41
- @on_unsubscribe = block
42
- end
43
-
44
- protected
45
- def self.next_guid
46
- rand(999_999_999_999).to_s
47
- end
48
- end
49
- end
@@ -1,51 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe "Firehose amqp resources" do
4
-
5
- let(:channel) { "/resource-test-#{Time.now.to_i}" }
6
- let(:consumer) { Firehose::Consumer.new }
7
-
8
- it "should clean up exchanges and queues" do
9
- sent, received = 'howdy!', nil
10
-
11
- before_exchange_count = `rabbitmqctl list_exchanges`.lines.count
12
- before_queue_count = `rabbitmqctl list_queues`.lines.count
13
-
14
- during_exchange_count = nil
15
- during_queue_count = nil
16
-
17
- EM.run do
18
- # Kill test if it runs longer than 5s
19
- EM.add_timer(5) { EM.stop }
20
-
21
- subscription = Firehose::Subscription.new(consumer, channel)
22
- subscription.ttl = 1
23
-
24
- subscription.subscribe do |payload|
25
- received = payload
26
- subscription.unsubscribe
27
-
28
- during_exchange_count = `rabbitmqctl list_exchanges`.lines.count
29
- during_queue_count = `rabbitmqctl list_queues`.lines.count
30
-
31
- # I wait 1 second before killing em so that unsubscribe
32
- # can talk to AMQP before the whole thing dies.
33
- EM.add_timer(1){ EM.stop }
34
- end
35
-
36
- # Let the subscriber subscribe before publishing messages.
37
- EM.add_timer(1){ Firehose::Publisher.new.publish(channel, sent) }
38
- end
39
-
40
- after_exchange_count = `rabbitmqctl list_exchanges`.lines.count
41
- after_queue_count = `rabbitmqctl list_queues`.lines.count
42
-
43
- received.should == sent
44
-
45
- after_exchange_count.should == before_exchange_count
46
- after_queue_count.should == before_queue_count
47
-
48
- during_exchange_count.should == before_exchange_count + 1
49
- during_queue_count.should == before_queue_count + 1
50
- end
51
- end