firehose 0.0.15 → 0.0.16
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.
- data/lib/firehose.rb +4 -2
- data/lib/firehose/broker.rb +34 -0
- data/lib/firehose/cli.rb +15 -1
- data/lib/firehose/consumer.rb +49 -0
- data/lib/firehose/rack.rb +27 -11
- data/lib/firehose/subscription.rb +38 -29
- data/lib/firehose/version.rb +2 -2
- data/spec/integrations/amqp_resources_spec.rb +4 -3
- data/spec/integrations/thin_spec.rb +8 -9
- data/spec/lib/broker_spec.rb +30 -0
- data/spec/lib/consumer_spec.rb +66 -0
- data/spec/spec_helper.rb +21 -3
- metadata +34 -28
data/lib/firehose.rb
CHANGED
@@ -4,10 +4,12 @@ require 'amqp'
|
|
4
4
|
require 'logger'
|
5
5
|
|
6
6
|
module Firehose
|
7
|
-
autoload :Default, 'firehose/default'
|
8
7
|
autoload :Subscription, 'firehose/subscription'
|
9
|
-
autoload :Producer, 'firehose/producer'
|
10
8
|
autoload :Publisher, 'firehose/publisher'
|
9
|
+
autoload :Producer, 'firehose/producer'
|
10
|
+
autoload :Consumer, 'firehose/consumer'
|
11
|
+
autoload :Default, 'firehose/default'
|
12
|
+
autoload :Broker, 'firehose/broker'
|
11
13
|
autoload :Rack, 'firehose/rack'
|
12
14
|
autoload :CLI, 'firehose/cli'
|
13
15
|
|
@@ -0,0 +1,34 @@
|
|
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
|
data/lib/firehose/cli.rb
CHANGED
@@ -6,9 +6,23 @@ module Firehose
|
|
6
6
|
desc "server", "starts the firehose server"
|
7
7
|
method_option :port, :type => :numeric, :default => Firehose::Default::URI.port, :required => true, :aliases => '-p'
|
8
8
|
method_option :host, :type => :string, :default => '0.0.0.0', :required => true, :aliases => '-h'
|
9
|
+
|
9
10
|
def server
|
11
|
+
broker = Firehose::Broker.new
|
12
|
+
|
10
13
|
server = Thin::Server.new(options[:host], options[:port]) do
|
11
|
-
|
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
26
|
end
|
13
27
|
|
14
28
|
begin
|
@@ -0,0 +1,49 @@
|
|
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
|
data/lib/firehose/rack.rb
CHANGED
@@ -5,12 +5,17 @@ module Firehose
|
|
5
5
|
AsyncResponse = [-1, {}, []]
|
6
6
|
|
7
7
|
class HttpLongPoll
|
8
|
+
def initialize(broker)
|
9
|
+
@broker = broker
|
10
|
+
end
|
11
|
+
|
8
12
|
def call(env)
|
9
13
|
req = ::Rack::Request.new(env)
|
10
14
|
cid = req.params['cid']
|
11
15
|
path = req.path
|
12
16
|
method = req.request_method
|
13
17
|
timeout = 30
|
18
|
+
# TODO seperate out CORS logic as an async middleware with a Goliath web server.
|
14
19
|
cors_origin = env['HTTP_ORIGIN']
|
15
20
|
cors_headers = {
|
16
21
|
'Access-Control-Allow-Origin' => cors_origin,
|
@@ -28,7 +33,7 @@ module Firehose
|
|
28
33
|
response_headers = cors_origin ? cors_headers : {}
|
29
34
|
|
30
35
|
# Setup a subscription with a client id. We haven't subscribed yet here.
|
31
|
-
|
36
|
+
consumer = @broker.consumer(cid)
|
32
37
|
|
33
38
|
# Setup a timeout timer to tell clients that time out that everything is OK
|
34
39
|
# and they should come back for more
|
@@ -39,10 +44,9 @@ module Firehose
|
|
39
44
|
end
|
40
45
|
|
41
46
|
# Ok, now subscribe to the subscription.
|
42
|
-
|
47
|
+
consumer.subscribe_to path do |message, subscription|
|
43
48
|
timer.cancel # Turn off the heart beat so we don't execute any of that business.
|
44
|
-
|
45
|
-
subscription = nil # Set this to nil so that our heart beat timer doesn't try to double unsub.
|
49
|
+
consumer.unsubscribe
|
46
50
|
env['async.callback'].call [200, response_headers, [message]]
|
47
51
|
Firehose.logger.debug "HTTP sent `#{message}` to `#{cid}@#{path}`"
|
48
52
|
end
|
@@ -51,7 +55,7 @@ module Firehose
|
|
51
55
|
# Unsubscribe from the subscription if its still open and something bad happened
|
52
56
|
# or the heart beat triggered before we could finish.
|
53
57
|
env['async.close'].callback do
|
54
|
-
|
58
|
+
consumer.unsubscribe if consumer
|
55
59
|
Firehose.logger.debug "HTTP connection `#{cid}@#{path}` closing"
|
56
60
|
end
|
57
61
|
end
|
@@ -76,14 +80,18 @@ module Firehose
|
|
76
80
|
class WebSocket < ::Rack::WebSocket::Application
|
77
81
|
attr_reader :cid, :path
|
78
82
|
|
83
|
+
def initialize(broker)
|
84
|
+
@broker = broker
|
85
|
+
end
|
86
|
+
|
79
87
|
# Subscribe to a path and make some magic happen, mmkmay?
|
80
88
|
def on_open(env)
|
81
89
|
req = ::Rack::Request.new(env)
|
82
90
|
@cid = req.params['cid']
|
83
91
|
@path = req.path
|
84
92
|
|
85
|
-
@
|
86
|
-
@
|
93
|
+
@consumer = @broker.consumer(@cid)
|
94
|
+
@consumer.subscribe_to path do |message|
|
87
95
|
Firehose.logger.debug "WS sent `#{message}` to `#{cid}@#{path}`"
|
88
96
|
send_data message
|
89
97
|
end
|
@@ -92,29 +100,37 @@ module Firehose
|
|
92
100
|
|
93
101
|
# Delete the subscription if the thing even happened.
|
94
102
|
def on_close(env)
|
95
|
-
@
|
103
|
+
@consumer.unsubscribe if @consumer
|
96
104
|
Firehose.logger.debug "WS connection `#{cid}@#{path}` closing"
|
97
105
|
end
|
98
106
|
|
99
107
|
# Log websocket level errors
|
100
108
|
def on_error(env, error)
|
101
109
|
Firehose.logger.error "WS connection `#{cid}@#{path}` error `#{error}`: #{env.inspect}"
|
102
|
-
@
|
110
|
+
@consumer.unsubscribe if @consumer
|
103
111
|
end
|
104
112
|
end
|
105
113
|
|
106
114
|
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
|
+
|
107
123
|
def call(env)
|
108
124
|
websocket_request?(env) ? websocket.call(env) : http_long_poll.call(env)
|
109
125
|
end
|
110
126
|
|
111
127
|
private
|
112
128
|
def websocket
|
113
|
-
@websocket ||= WebSocket.new
|
129
|
+
@websocket ||= WebSocket.new(broker)
|
114
130
|
end
|
115
131
|
|
116
132
|
def http_long_poll
|
117
|
-
@http_long_poll ||= HttpLongPoll.new
|
133
|
+
@http_long_poll ||= HttpLongPoll.new(broker)
|
118
134
|
end
|
119
135
|
|
120
136
|
def websocket_request?(env)
|
@@ -1,56 +1,65 @@
|
|
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?
|
3
6
|
TTL = 15000
|
4
7
|
|
5
|
-
# Time to live for the
|
8
|
+
# Time to live for the amqp_queue on the server after the subscription is canceled. This
|
6
9
|
# is mostly for flakey connections where the client may reconnect after *ttl* and continue
|
7
10
|
# receiving messages.
|
8
11
|
attr_accessor :ttl
|
9
12
|
|
10
|
-
#
|
11
|
-
attr_reader :
|
13
|
+
# Consumer and channel for the subscription.
|
14
|
+
attr_reader :consumer
|
12
15
|
|
13
|
-
|
14
|
-
|
16
|
+
# Channel that we'll use for the pub-sub activity. This probably maps to an URL
|
17
|
+
attr_reader :channel
|
18
|
+
|
19
|
+
def initialize(consumer, channel)
|
20
|
+
@consumer, @channel = consumer, channel
|
15
21
|
end
|
16
22
|
|
17
|
-
# TODO - Move the
|
18
|
-
# Firehose subscription. As it stands now, you could fire off multple subscriptions to diff
|
19
|
-
def subscribe(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# When we get a message, we want to remove the consumer from the
|
27
|
-
# ttl starts ticking down. On the reconnect, the consumer connects to the
|
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
|
25
|
+
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
|
28
34
|
# timer on x-expires... in theory at least.
|
29
|
-
@
|
30
|
-
@
|
31
|
-
Firehose.logger.debug "AMQP delivering `#{message}` to `#{
|
32
|
-
block.call(message)
|
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)
|
33
39
|
# The ack needs to go after the block is called. This makes sure that all processing
|
34
|
-
# happens downstream before we remove it from the
|
40
|
+
# happens downstream before we remove it from the amqp_queue entirely.
|
35
41
|
metadata.ack
|
36
42
|
end.consume
|
37
|
-
Firehose.logger.debug "AMQP subscribed to `#{
|
43
|
+
Firehose.logger.debug "AMQP subscribed to `#{consumer.guid}@#{channel}`"
|
44
|
+
self # Return the subscription for chaining.
|
38
45
|
end
|
39
46
|
|
40
47
|
def unsubscribe
|
41
48
|
Firehose.logger.debug "AMQP unsubscribed"
|
42
|
-
@
|
49
|
+
@amqp_consumer.cancel if @amqp_consumer
|
50
|
+
@unsubscribe_callback.call self if @unsubscribe_callback
|
51
|
+
end
|
52
|
+
|
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
|
43
57
|
end
|
44
58
|
|
45
|
-
# The time that a
|
59
|
+
# The time that a amqp_queue should live *after* the client unsubscribes. This is useful for
|
46
60
|
# flakey network connections, like HTTP Long Polling or even broken web sockets.
|
47
61
|
def ttl
|
48
62
|
@ttl ||= TTL
|
49
63
|
end
|
50
|
-
|
51
|
-
protected
|
52
|
-
def self.subscriber_id
|
53
|
-
rand(999_999_999_999).to_s
|
54
|
-
end
|
55
64
|
end
|
56
65
|
end
|
data/lib/firehose/version.rb
CHANGED
@@ -2,7 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe "Firehose amqp resources" do
|
4
4
|
|
5
|
-
let(:channel)
|
5
|
+
let(:channel) { "/resource-test-#{Time.now.to_i}" }
|
6
|
+
let(:consumer) { Firehose::Consumer.new }
|
6
7
|
|
7
8
|
it "should clean up exchanges and queues" do
|
8
9
|
sent, received = 'howdy!', nil
|
@@ -17,10 +18,10 @@ describe "Firehose amqp resources" do
|
|
17
18
|
# Kill test if it runs longer than 5s
|
18
19
|
EM.add_timer(5) { EM.stop }
|
19
20
|
|
20
|
-
subscription = Firehose::Subscription.new
|
21
|
+
subscription = Firehose::Subscription.new(consumer, channel)
|
21
22
|
subscription.ttl = 1
|
22
23
|
|
23
|
-
subscription.subscribe
|
24
|
+
subscription.subscribe do |payload|
|
24
25
|
received = payload
|
25
26
|
subscription.unsubscribe
|
26
27
|
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Firehose::Rack do
|
4
|
+
include EM::TestHelper
|
5
|
+
|
4
6
|
before(:all) do
|
5
7
|
Firehose::Producer.adapter = :em_http
|
6
8
|
end
|
@@ -25,7 +27,7 @@ describe Firehose::Rack do
|
|
25
27
|
# Our WS and Http clients call this when they have received their messages to determine
|
26
28
|
# when to turn off EM and make the test assertion at the very bottom.
|
27
29
|
succeed = Proc.new do
|
28
|
-
|
30
|
+
em.stop if received_http.size == messages.size and received_ws.size == messages.size
|
29
31
|
end
|
30
32
|
|
31
33
|
# Setup a publisher
|
@@ -38,7 +40,7 @@ describe Firehose::Rack do
|
|
38
40
|
# Lets have an HTTP Long poll client
|
39
41
|
http_long_poll = Proc.new do
|
40
42
|
http = EM::HttpRequest.new(http_url).get(:query => {'cid' => cid})
|
41
|
-
http.errback {
|
43
|
+
http.errback { em.stop }
|
42
44
|
http.callback do
|
43
45
|
received_http << http.response
|
44
46
|
if received_http.size < messages.size
|
@@ -52,7 +54,7 @@ describe Firehose::Rack do
|
|
52
54
|
# And test a web socket client too, at the same time.
|
53
55
|
websocket = Proc.new do
|
54
56
|
ws = EventMachine::WebSocketClient.connect(ws_url)
|
55
|
-
ws.errback {
|
57
|
+
ws.errback { em.stop }
|
56
58
|
ws.stream do |msg|
|
57
59
|
received_ws << msg
|
58
60
|
succeed.call unless received_ws.size < messages.size
|
@@ -60,10 +62,7 @@ describe Firehose::Rack do
|
|
60
62
|
end
|
61
63
|
|
62
64
|
# Great, we have all the pieces in order, lets run this thing in the reactor.
|
63
|
-
|
64
|
-
# Stop the server no matter what happens.
|
65
|
-
EM.add_timer(30) { EM.stop }
|
66
|
-
|
65
|
+
em do
|
67
66
|
# Start the server
|
68
67
|
::Thin::Server.new('0.0.0.0', uri.port, app).start
|
69
68
|
|
@@ -72,11 +71,11 @@ describe Firehose::Rack do
|
|
72
71
|
websocket.call
|
73
72
|
|
74
73
|
# Wait a sec to let our http_long_poll setup.
|
75
|
-
|
74
|
+
em.add_timer(1){ publish.call }
|
76
75
|
end
|
77
76
|
|
78
77
|
# When EM stops, these assertions will be made.
|
79
78
|
received_http.should =~ messages
|
80
|
-
|
79
|
+
received_ws.should =~ messages
|
81
80
|
end
|
82
81
|
end
|
@@ -0,0 +1,30 @@
|
|
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
|
@@ -0,0 +1,66 @@
|
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -3,18 +3,36 @@ require 'em-http'
|
|
3
3
|
require 'em-websocket-client'
|
4
4
|
|
5
5
|
require 'firehose'
|
6
|
-
Firehose.logger = Logger.new('/dev/null')
|
6
|
+
Firehose.logger = Logger.new('/dev/null') unless ENV['VERBOSE']
|
7
7
|
|
8
8
|
# Lets skip the verbosity of the thin for the test output.
|
9
9
|
require 'thin'
|
10
|
-
Thin::Logging.silent = true
|
10
|
+
Thin::Logging.silent = true unless ENV['VERBOSE']
|
11
11
|
|
12
12
|
# We use both EM::Http and Net::Http in this test lib. When EM:Http is fired up
|
13
13
|
# we're usually hitting thins for integrations, and when Net::Http we want to mock that up.
|
14
14
|
require 'webmock/rspec'
|
15
15
|
WebMock.allow_net_connect!
|
16
16
|
|
17
|
-
|
17
|
+
module EM::TestHelper
|
18
|
+
# Run test inside of reactor.
|
19
|
+
def em(ttl=30, &block)
|
20
|
+
if block
|
21
|
+
# Run the block inside of a reactor
|
22
|
+
EM.run do
|
23
|
+
EM.add_timer(ttl) do
|
24
|
+
EM.stop
|
25
|
+
raise "Test timed-out"
|
26
|
+
end
|
27
|
+
block.call(EM)
|
28
|
+
end
|
29
|
+
else # or just grab em and go nuts.
|
30
|
+
EM
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Configure RSpec runner. See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
18
36
|
RSpec.configure do |config|
|
19
37
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
20
38
|
config.run_all_when_everything_filtered = true
|
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.
|
4
|
+
version: 0.0.16
|
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-
|
13
|
+
date: 2012-05-04 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: eventmachine
|
17
|
-
requirement: &
|
17
|
+
requirement: &70349732213400 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 1.0.0.beta
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70349732213400
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: amqp
|
28
|
-
requirement: &
|
28
|
+
requirement: &70349732212540 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: 0.9.4
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70349732212540
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: thin
|
39
|
-
requirement: &
|
39
|
+
requirement: &70349732211460 !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: *
|
47
|
+
version_requirements: *70349732211460
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: thor
|
50
|
-
requirement: &
|
50
|
+
requirement: &70349732210400 !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: *
|
58
|
+
version_requirements: *70349732210400
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: faraday
|
61
|
-
requirement: &
|
61
|
+
requirement: &70349732209120 !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: *
|
69
|
+
version_requirements: *70349732209120
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: websocket-rack
|
72
|
-
requirement: &
|
72
|
+
requirement: &70349732223780 !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: *
|
80
|
+
version_requirements: *70349732223780
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
82
|
name: em-http-request
|
83
|
-
requirement: &
|
83
|
+
requirement: &70349732222860 !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: *
|
91
|
+
version_requirements: *70349732222860
|
92
92
|
- !ruby/object:Gem::Dependency
|
93
93
|
name: rspec
|
94
|
-
requirement: &
|
94
|
+
requirement: &70349732221260 !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: *
|
102
|
+
version_requirements: *70349732221260
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: webmock
|
105
|
-
requirement: &
|
105
|
+
requirement: &70349732220520 !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: *
|
113
|
+
version_requirements: *70349732220520
|
114
114
|
- !ruby/object:Gem::Dependency
|
115
115
|
name: guard-rspec
|
116
|
-
requirement: &
|
116
|
+
requirement: &70349732219740 !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: *
|
124
|
+
version_requirements: *70349732219740
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: guard-bundler
|
127
|
-
requirement: &
|
127
|
+
requirement: &70349732219100 !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: *
|
135
|
+
version_requirements: *70349732219100
|
136
136
|
- !ruby/object:Gem::Dependency
|
137
137
|
name: guard-coffeescript
|
138
|
-
requirement: &
|
138
|
+
requirement: &70349732218080 !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: *
|
146
|
+
version_requirements: *70349732218080
|
147
147
|
- !ruby/object:Gem::Dependency
|
148
148
|
name: em-websocket-client
|
149
|
-
requirement: &
|
149
|
+
requirement: &70349732217260 !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: *
|
157
|
+
version_requirements: *70349732217260
|
158
158
|
description: Firehose is a realtime web application toolkit for building realtime
|
159
159
|
Ruby web applications.
|
160
160
|
email:
|
@@ -184,7 +184,9 @@ 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
|
187
188
|
- lib/firehose/cli.rb
|
189
|
+
- lib/firehose/consumer.rb
|
188
190
|
- lib/firehose/default.rb
|
189
191
|
- lib/firehose/producer.rb
|
190
192
|
- lib/firehose/publisher.rb
|
@@ -193,6 +195,8 @@ files:
|
|
193
195
|
- lib/firehose/version.rb
|
194
196
|
- spec/integrations/amqp_resources_spec.rb
|
195
197
|
- spec/integrations/thin_spec.rb
|
198
|
+
- spec/lib/broker_spec.rb
|
199
|
+
- spec/lib/consumer_spec.rb
|
196
200
|
- spec/lib/default_spec.rb
|
197
201
|
- spec/lib/producer_spec.rb
|
198
202
|
- spec/spec_helper.rb
|
@@ -223,6 +227,8 @@ summary: Build realtime Ruby web applications
|
|
223
227
|
test_files:
|
224
228
|
- spec/integrations/amqp_resources_spec.rb
|
225
229
|
- spec/integrations/thin_spec.rb
|
230
|
+
- spec/lib/broker_spec.rb
|
231
|
+
- spec/lib/consumer_spec.rb
|
226
232
|
- spec/lib/default_spec.rb
|
227
233
|
- spec/lib/producer_spec.rb
|
228
234
|
- spec/spec_helper.rb
|