firehose 1.2.20 → 1.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +29 -0
- data/.dockerignore +2 -0
- data/.gitignore +3 -1
- data/.rubocop.yml +1156 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -7
- data/CHANGELOG.md +15 -0
- data/Dockerfile +11 -0
- data/Gemfile +4 -2
- data/Procfile.dev +0 -1
- data/README.md +66 -8
- data/Rakefile +43 -32
- data/coffeelint.json +129 -0
- data/docker-compose.yml +17 -0
- data/firehose.gemspec +5 -9
- data/karma.config.coffee +89 -0
- data/lib/assets/javascripts/firehose.js.coffee +1 -2
- data/lib/assets/javascripts/firehose/consumer.js.coffee +18 -2
- data/lib/assets/javascripts/firehose/core.js.coffee +2 -1
- data/lib/assets/javascripts/firehose/long_poll.js.coffee +69 -8
- data/lib/assets/javascripts/firehose/multiplexed_consumer.js.coffee +74 -0
- data/lib/assets/javascripts/firehose/transport.js.coffee +4 -2
- data/lib/assets/javascripts/firehose/web_socket.js.coffee +51 -5
- data/lib/firehose/cli.rb +2 -1
- data/lib/firehose/client/producer.rb +10 -4
- data/lib/firehose/rack/consumer.rb +39 -0
- data/lib/firehose/rack/consumer/http_long_poll.rb +118 -45
- data/lib/firehose/rack/consumer/web_socket.rb +133 -28
- data/lib/firehose/rack/ping.rb +1 -1
- data/lib/firehose/rack/publisher.rb +10 -4
- data/lib/firehose/server.rb +9 -9
- data/lib/firehose/server/channel.rb +23 -31
- data/lib/firehose/server/message_buffer.rb +59 -0
- data/lib/firehose/server/publisher.rb +16 -17
- data/lib/firehose/server/redis.rb +32 -0
- data/lib/firehose/server/subscriber.rb +7 -7
- data/lib/firehose/version.rb +2 -2
- data/package.json +14 -2
- data/spec/integrations/shared_examples.rb +89 -7
- data/spec/javascripts/firehose/multiplexed_consumer_spec.coffee +72 -0
- data/spec/javascripts/firehose/transport_spec.coffee +0 -2
- data/spec/javascripts/firehose/websocket_spec.coffee +2 -0
- data/spec/javascripts/helpers/spec_helper.js +1 -0
- data/spec/javascripts/support/jquery-1.11.1.js +10308 -0
- data/{lib/assets/javascripts/vendor → spec/javascripts/support}/json2.js +0 -0
- data/spec/javascripts/support/spec_helper.coffee +3 -0
- data/spec/lib/assets_spec.rb +8 -8
- data/spec/lib/client/producer_spec.rb +14 -14
- data/spec/lib/firehose_spec.rb +2 -2
- data/spec/lib/rack/consumer/http_long_poll_spec.rb +21 -3
- data/spec/lib/rack/consumer_spec.rb +4 -4
- data/spec/lib/rack/ping_spec.rb +4 -4
- data/spec/lib/rack/publisher_spec.rb +5 -5
- data/spec/lib/server/app_spec.rb +2 -2
- data/spec/lib/server/channel_spec.rb +58 -44
- data/spec/lib/server/message_buffer_spec.rb +148 -0
- data/spec/lib/server/publisher_spec.rb +29 -22
- data/spec/lib/server/redis_spec.rb +13 -0
- data/spec/lib/server/subscriber_spec.rb +14 -13
- data/spec/spec_helper.rb +8 -1
- metadata +34 -95
- data/.rbenv-version +0 -1
- data/Guardfile +0 -31
- data/config/evergreen.rb +0 -9
data/lib/firehose/rack/ping.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
+
require "rack/utils"
|
2
|
+
|
1
3
|
module Firehose
|
2
4
|
module Rack
|
3
5
|
class Publisher
|
4
6
|
include Firehose::Rack::Helpers
|
5
7
|
|
6
8
|
def call(env)
|
7
|
-
req
|
8
|
-
path
|
9
|
-
method
|
9
|
+
req = env['parsed_request'] ||= ::Rack::Request.new(env)
|
10
|
+
path = req.path
|
11
|
+
method = req.request_method
|
10
12
|
cache_control = {}
|
11
13
|
|
12
14
|
# Parse out cache control directives from the Cache-Control header.
|
@@ -26,7 +28,11 @@ module Firehose
|
|
26
28
|
EM.next_tick do
|
27
29
|
body = env['rack.input'].read
|
28
30
|
Firehose.logger.debug "HTTP published #{body.inspect} to #{path.inspect} with ttl #{ttl.inspect}"
|
29
|
-
|
31
|
+
opts = { :ttl => ttl }
|
32
|
+
if buffer_size = env["HTTP_X_FIREHOSE_BUFFER_SIZE"]
|
33
|
+
opts[:buffer_size] = buffer_size.to_i
|
34
|
+
end
|
35
|
+
publisher.publish(path, body, opts).callback do
|
30
36
|
env['async.callback'].call [202, {'Content-Type' => 'text/plain', 'Content-Length' => '0'}, []]
|
31
37
|
env['async.callback'].call response(202, '', 'Content-Type' => 'text/plain')
|
32
38
|
end.errback do |e|
|
data/lib/firehose/server.rb
CHANGED
@@ -8,15 +8,15 @@ module Firehose
|
|
8
8
|
# Firehose components that sit between the Rack HTTP software and the Redis server.
|
9
9
|
# This mostly handles message sequencing and different HTTP channel names.
|
10
10
|
module Server
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :
|
14
|
-
autoload :
|
11
|
+
autoload :MessageBuffer, 'firehose/server/message_buffer'
|
12
|
+
autoload :Subscriber, 'firehose/server/subscriber'
|
13
|
+
autoload :Publisher, 'firehose/server/publisher'
|
14
|
+
autoload :Channel, 'firehose/server/channel'
|
15
|
+
autoload :App, 'firehose/server/app'
|
16
|
+
autoload :Redis, 'firehose/server/redis'
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
def self.key(*segments)
|
19
|
-
segments.unshift(:firehose).join(':')
|
18
|
+
def self.redis
|
19
|
+
@redis ||= Redis.new
|
20
20
|
end
|
21
21
|
end
|
22
|
-
end
|
22
|
+
end
|
@@ -2,57 +2,49 @@ module Firehose
|
|
2
2
|
module Server
|
3
3
|
# Connects to a specific channel on Redis and listens for messages to notify subscribers.
|
4
4
|
class Channel
|
5
|
-
attr_reader :channel_key, :
|
5
|
+
attr_reader :channel_key, :list_key, :sequence_key
|
6
|
+
attr_reader :redis, :subscriber
|
6
7
|
|
7
8
|
def self.redis
|
8
|
-
@redis ||=
|
9
|
+
@redis ||= Firehose::Server.redis.connection
|
9
10
|
end
|
10
11
|
|
11
12
|
def self.subscriber
|
12
|
-
@subscriber ||= Server::Subscriber.new(
|
13
|
+
@subscriber ||= Server::Subscriber.new(Firehose::Server.redis.connection)
|
13
14
|
end
|
14
15
|
|
15
16
|
def initialize(channel_key, redis=self.class.redis, subscriber=self.class.subscriber)
|
16
|
-
@
|
17
|
-
@
|
17
|
+
@redis = redis
|
18
|
+
@subscriber = subscriber
|
19
|
+
@channel_key = channel_key
|
20
|
+
@list_key = Server::Redis.key(channel_key, :list)
|
21
|
+
@sequence_key = Server::Redis.key(channel_key, :sequence)
|
18
22
|
end
|
19
23
|
|
20
|
-
def
|
21
|
-
last_sequence = last_sequence.to_i
|
22
|
-
|
24
|
+
def next_messages(consumer_sequence=nil, options={})
|
23
25
|
deferrable = EM::DefaultDeferrable.new
|
24
|
-
# TODO - Think this through a little harder... maybe some tests ol buddy!
|
25
26
|
deferrable.errback {|e| EM.next_tick { raise e } unless [:timeout, :disconnect].include?(e) }
|
26
27
|
|
27
|
-
# TODO: Use HSET so we don't have to pull 100 messages back every time.
|
28
28
|
redis.multi
|
29
29
|
redis.get(sequence_key).
|
30
30
|
errback {|e| deferrable.fail e }
|
31
|
-
|
31
|
+
# Fetch entire list: http://stackoverflow.com/questions/10703019/redis-fetch-all-value-of-list-without-iteration-and-without-popping
|
32
|
+
redis.lrange(list_key, 0, -1).
|
32
33
|
errback {|e| deferrable.fail e }
|
33
|
-
redis.exec.callback do |(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
34
|
+
redis.exec.callback do |(channel_sequence, message_list)|
|
35
|
+
# Reverse the messages so they can be correctly procesed by the MessageBuffer class. There's
|
36
|
+
# a patch in the message-buffer-redis branch that moves this concern into the Publisher LUA
|
37
|
+
# script. We kept it out of this for now because it represents a deployment risk and `reverse!`
|
38
|
+
# is a cheap operation in Ruby.
|
39
|
+
message_list.reverse!
|
40
|
+
buffer = MessageBuffer.new(message_list, channel_sequence, consumer_sequence)
|
41
|
+
if buffer.remaining_messages.empty?
|
42
|
+
Firehose.logger.debug "No messages in buffer, subscribing. sequence: `#{channel_sequence}` consumer_sequence: #{consumer_sequence}"
|
39
43
|
# Either this resource has never been seen before or we are all caught up.
|
40
44
|
# Subscribe and hope something gets published to this end-point.
|
41
45
|
subscribe(deferrable, options[:timeout])
|
42
|
-
|
43
|
-
|
44
|
-
# up. Catch them up FTW.
|
45
|
-
# But we won't "catch them up" if last_sequence was zero/nil because
|
46
|
-
# that implies the client is connecting for the 1st time.
|
47
|
-
message = message_list[diff-1]
|
48
|
-
Firehose.logger.debug "Sending old message `#{message}` and sequence `#{sequence}` to client directly. Client is `#{diff}` behind, at `#{last_sequence}`."
|
49
|
-
deferrable.succeed message, last_sequence + 1
|
50
|
-
else
|
51
|
-
# The client is hopelessly behind and underwater. Just reset
|
52
|
-
# their whole world with the lastest message.
|
53
|
-
message = message_list[0]
|
54
|
-
Firehose.logger.debug "Sending latest message `#{message}` and sequence `#{sequence}` to client directly."
|
55
|
-
deferrable.succeed message, sequence
|
46
|
+
else # Either the client is under water or caught up to head.
|
47
|
+
deferrable.succeed buffer.remaining_messages
|
56
48
|
end
|
57
49
|
end.errback {|e| deferrable.fail e }
|
58
50
|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Firehose
|
2
|
+
module Server
|
3
|
+
# Encapsulates a sequence of messages from the server along with their
|
4
|
+
# consumer_sequences calculate by offset.
|
5
|
+
class MessageBuffer
|
6
|
+
# Number of messages that Redis buffers for the client if its
|
7
|
+
# connection drops, then reconnects.
|
8
|
+
DEFAULT_SIZE = 100
|
9
|
+
|
10
|
+
Message = Struct.new(:payload, :sequence)
|
11
|
+
|
12
|
+
def initialize(message_list, channel_sequence, consumer_sequence = nil)
|
13
|
+
@message_list = message_list
|
14
|
+
@channel_sequence = channel_sequence.to_i
|
15
|
+
@consumer_sequence = consumer_sequence.to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def remaining_messages
|
19
|
+
messages.last(remaining_message_count)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def remaining_message_count
|
25
|
+
# Special case to always get the latest message.
|
26
|
+
return 1 unless @consumer_sequence > 0
|
27
|
+
|
28
|
+
count = @channel_sequence - @consumer_sequence
|
29
|
+
|
30
|
+
if count < 0
|
31
|
+
# UNEXPECTED: Somehow the sequence is ahead of the channel.
|
32
|
+
# It is likely a bug in the consumer, but we'll assume
|
33
|
+
# the consumer has all the messages.
|
34
|
+
0
|
35
|
+
elsif count > @message_list.size
|
36
|
+
# Consumer is under water since the last request. Just send the most recent message.
|
37
|
+
1
|
38
|
+
else
|
39
|
+
count
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Calculates the last_message_sequence per message.
|
44
|
+
# [a b c e f]
|
45
|
+
def messages
|
46
|
+
@messages ||= @message_list.map.with_index do |payload, index|
|
47
|
+
Message.new(payload, starting_channel_sequence + index)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Channel sequence is 10
|
52
|
+
# Buffer size of 5
|
53
|
+
# Start of sequence in buffer ... which would be 6
|
54
|
+
def starting_channel_sequence
|
55
|
+
@channel_sequence - @message_list.size + 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,10 +1,6 @@
|
|
1
1
|
module Firehose
|
2
2
|
module Server
|
3
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
4
|
# Seconds that the message buffer should live before Redis expires it.
|
9
5
|
TTL = 60*60*24
|
10
6
|
|
@@ -16,6 +12,7 @@ module Firehose
|
|
16
12
|
def publish(channel_key, message, opts={})
|
17
13
|
# How long should we hang on to the resource once is published?
|
18
14
|
ttl = (opts[:ttl] || TTL).to_i
|
15
|
+
buffer_size = (opts[:buffer_size] || MessageBuffer::DEFAULT_SIZE).to_i
|
19
16
|
|
20
17
|
# TODO hi-redis isn't that awesome... we have to setup an errback per even for wrong
|
21
18
|
# commands because of the lack of a method_missing whitelist. Perhaps implement a whitelist in
|
@@ -44,10 +41,10 @@ module Firehose
|
|
44
41
|
end.callback do |digest|
|
45
42
|
@publish_script_digest = digest
|
46
43
|
Firehose.logger.debug "Registered Lua publishing script with Redis => #{digest}"
|
47
|
-
eval_publish_script channel_key, message, ttl, deferrable
|
44
|
+
eval_publish_script channel_key, message, ttl, buffer_size, deferrable
|
48
45
|
end
|
49
46
|
else
|
50
|
-
eval_publish_script channel_key, message, ttl, deferrable
|
47
|
+
eval_publish_script channel_key, message, ttl, buffer_size, deferrable
|
51
48
|
end
|
52
49
|
|
53
50
|
deferrable
|
@@ -55,7 +52,7 @@ module Firehose
|
|
55
52
|
|
56
53
|
private
|
57
54
|
def redis
|
58
|
-
@redis ||=
|
55
|
+
@redis ||= Firehose::Server.redis.connection
|
59
56
|
end
|
60
57
|
|
61
58
|
# Serialize components of a message into something that can be dropped into Redis.
|
@@ -63,9 +60,10 @@ module Firehose
|
|
63
60
|
[channel_key, sequence, message].join(PAYLOAD_DELIMITER)
|
64
61
|
end
|
65
62
|
|
66
|
-
#
|
63
|
+
# Deserialize components of a message back into Ruby.
|
67
64
|
def self.from_payload(payload)
|
68
|
-
|
65
|
+
@payload_size ||= method(:to_payload).arity
|
66
|
+
payload.split(PAYLOAD_DELIMITER, @payload_size)
|
69
67
|
end
|
70
68
|
|
71
69
|
# TODO: Make this FAR more robust. Ideally we'd whitelist the permitted
|
@@ -79,18 +77,19 @@ module Firehose
|
|
79
77
|
redis.script 'LOAD', REDIS_PUBLISH_SCRIPT
|
80
78
|
end
|
81
79
|
|
82
|
-
def eval_publish_script(channel_key, message, ttl, deferrable)
|
83
|
-
list_key = Server.key(channel_key, :list)
|
80
|
+
def eval_publish_script(channel_key, message, ttl, buffer_size, deferrable)
|
81
|
+
list_key = Server::Redis.key(channel_key, :list)
|
84
82
|
script_args = [
|
85
|
-
Server.key(channel_key, :sequence),
|
83
|
+
Server::Redis.key(channel_key, :sequence),
|
86
84
|
list_key,
|
87
|
-
Server.key(:channel_updates),
|
85
|
+
Server::Redis.key(:channel_updates),
|
88
86
|
ttl,
|
89
87
|
message,
|
90
|
-
|
88
|
+
buffer_size,
|
91
89
|
PAYLOAD_DELIMITER,
|
92
90
|
channel_key
|
93
91
|
]
|
92
|
+
|
94
93
|
redis.evalsha(
|
95
94
|
@publish_script_digest, script_args.length, *script_args
|
96
95
|
).errback do |e|
|
@@ -107,7 +106,7 @@ module Firehose
|
|
107
106
|
local channel_key = KEYS[3]
|
108
107
|
local ttl = KEYS[4]
|
109
108
|
local message = KEYS[5]
|
110
|
-
local
|
109
|
+
local buffer_size = KEYS[6]
|
111
110
|
local payload_delimiter = KEYS[7]
|
112
111
|
local firehose_resource = KEYS[8]
|
113
112
|
|
@@ -122,7 +121,7 @@ module Firehose
|
|
122
121
|
redis.call('set', sequence_key, sequence)
|
123
122
|
redis.call('expire', sequence_key, ttl)
|
124
123
|
redis.call('lpush', list_key, message)
|
125
|
-
redis.call('ltrim', list_key, 0,
|
124
|
+
redis.call('ltrim', list_key, 0, buffer_size - 1)
|
126
125
|
redis.call('expire', list_key, ttl)
|
127
126
|
redis.call('publish', channel_key, message_payload)
|
128
127
|
|
@@ -131,4 +130,4 @@ module Firehose
|
|
131
130
|
|
132
131
|
end
|
133
132
|
end
|
134
|
-
end
|
133
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Firehose
|
4
|
+
module Server
|
5
|
+
# Manages redis configuration and connections.
|
6
|
+
class Redis
|
7
|
+
DEFAULT_URL = "redis://127.0.0.1:6379/0".freeze
|
8
|
+
KEY_DELIMITER = ":".freeze
|
9
|
+
ROOT_KEY = "firehose".freeze
|
10
|
+
|
11
|
+
attr_reader :url
|
12
|
+
|
13
|
+
def initialize(url = self.class.url)
|
14
|
+
@url = URI(url)
|
15
|
+
end
|
16
|
+
|
17
|
+
def connection
|
18
|
+
EM::Hiredis.connect(@url)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Generates keys for all firehose interactions with Redis. Ensures a root
|
22
|
+
# key of `firehose`
|
23
|
+
def self.key(*segments)
|
24
|
+
segments.flatten.unshift(ROOT_KEY).join(KEY_DELIMITER)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.url
|
28
|
+
ENV.fetch("REDIS_URL", DEFAULT_URL)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -16,18 +16,18 @@ module Firehose
|
|
16
16
|
# with the same error.
|
17
17
|
# The final goal is to allow the failed deferrable bubble back up
|
18
18
|
# so we can send back a nice, clean 500 error to the client.
|
19
|
-
channel_updates_key = Server.key('channel_updates')
|
19
|
+
channel_updates_key = Server::Redis.key('channel_updates')
|
20
20
|
pubsub.subscribe(channel_updates_key).
|
21
21
|
errback{|e| EM.next_tick { raise e } }.
|
22
22
|
callback { Firehose.logger.debug "Redis subscribed to `#{channel_updates_key}`" }
|
23
23
|
pubsub.on(:message) do |_, payload|
|
24
|
-
channel_key,
|
25
|
-
|
24
|
+
channel_key, channel_sequence, message = Server::Publisher.from_payload(payload)
|
25
|
+
messages = [ MessageBuffer::Message.new(message, channel_sequence.to_i) ]
|
26
26
|
if deferrables = subscriptions.delete(channel_key)
|
27
|
-
Firehose.logger.debug "Redis notifying #{deferrables.count} deferrable(s) at `#{channel_key}` with
|
27
|
+
Firehose.logger.debug "Redis notifying #{deferrables.count} deferrable(s) at `#{channel_key}` with channel_sequence `#{channel_sequence}` and message `#{message}`"
|
28
28
|
deferrables.each do |deferrable|
|
29
|
-
Firehose.logger.debug "Sending message #{message} and
|
30
|
-
deferrable.succeed
|
29
|
+
Firehose.logger.debug "Sending message #{message} and channel_sequence #{channel_sequence} to client from subscriber"
|
30
|
+
deferrable.succeed messages
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -48,4 +48,4 @@ module Firehose
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
51
|
-
end
|
51
|
+
end
|
data/lib/firehose/version.rb
CHANGED
data/package.json
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"name": "firehose",
|
3
|
-
"version": "1.
|
4
|
-
"main": "lib/assets/javascripts/firehose.js.coffee"
|
3
|
+
"version": "1.3.3",
|
4
|
+
"main": "lib/assets/javascripts/firehose.js.coffee",
|
5
|
+
"devDependencies": {
|
6
|
+
"coffee-script": "*",
|
7
|
+
"jasmine-jquery": "git://github.com/velesin/jasmine-jquery.git",
|
8
|
+
"karma": "~0.12.16",
|
9
|
+
"karma-chrome-launcher": "~0.1.4",
|
10
|
+
"karma-coffee-preprocessor": "~0.2.1",
|
11
|
+
"karma-jasmine": "~0.2.0",
|
12
|
+
"karma-junit-reporter": "^0.2.2",
|
13
|
+
"karma-phantomjs-launcher": "^0.1.4",
|
14
|
+
"karma-safari-launcher": "*",
|
15
|
+
"karma-sprockets-mincer": "0.1.2"
|
16
|
+
}
|
5
17
|
}
|
@@ -23,9 +23,12 @@ shared_examples_for 'Firehose::Rack::App' do
|
|
23
23
|
let(:messages) { (1..200).map{|n| "msg-#{n}" } }
|
24
24
|
let(:channel) { "/firehose/integration/#{Time.now.to_i}" }
|
25
25
|
let(:http_url) { "http://#{uri.host}:#{uri.port}#{channel}" }
|
26
|
+
let(:http_multi_url) { "http://#{uri.host}:#{uri.port}/channels@firehose" }
|
26
27
|
let(:ws_url) { "ws://#{uri.host}:#{uri.port}#{channel}" }
|
28
|
+
let(:multiplex_channels) { ["/foo/bar", "/bar/baz", "/baz/quux"] }
|
29
|
+
let(:subscription_query) { multiplex_channels.map{|c| "#{c}!0"}.join(",") }
|
27
30
|
|
28
|
-
it "
|
31
|
+
it "supports pub-sub http and websockets" do
|
29
32
|
# Setup variables that we'll use after we turn off EM to validate our
|
30
33
|
# test assertions.
|
31
34
|
outgoing, received = messages.dup, Hash.new{|h,k| h[k] = []}
|
@@ -41,7 +44,7 @@ shared_examples_for 'Firehose::Rack::App' do
|
|
41
44
|
|
42
45
|
# Setup a publisher
|
43
46
|
publish = Proc.new do
|
44
|
-
Firehose::Client::Producer::Http.new.publish(outgoing.shift).to(channel) do
|
47
|
+
Firehose::Client::Producer::Http.new.publish(outgoing.shift).to(channel, buffer_size: rand(100)) do
|
45
48
|
# The random timer ensures that sometimes the clients will be behind
|
46
49
|
# and sometimes they will be caught up.
|
47
50
|
EM::add_timer(rand*0.005) { publish.call } unless outgoing.empty?
|
@@ -100,19 +103,98 @@ shared_examples_for 'Firehose::Rack::App' do
|
|
100
103
|
end
|
101
104
|
|
102
105
|
# When EM stops, these assertions will be made.
|
103
|
-
received.size.
|
104
|
-
received.
|
105
|
-
arr.
|
106
|
+
expect(received.size).to eql(4)
|
107
|
+
received.each_value do |arr|
|
108
|
+
expect(arr.size).to eql(messages.size)
|
109
|
+
expect(arr.sort).to eql(messages.sort)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it "supports channel multiplexing for http_long_poll and websockets" do
|
114
|
+
# Setup variables that we'll use after we turn off EM to validate our
|
115
|
+
# test assertions.
|
116
|
+
outgoing, received = messages.dup, Hash.new{|h,k| h[k] = []}
|
117
|
+
|
118
|
+
# Our WS and Http clients call this when they have received their messages to determine
|
119
|
+
# when to turn off EM and make the test assertion at the very bottom.
|
120
|
+
succeed = Proc.new do
|
121
|
+
# TODO: For some weird reason the `add_timer` call causes up to 20 seconds of delay after
|
122
|
+
# the test finishes running. However, without it the test will randomly fail with a
|
123
|
+
# "Redis disconnected" error.
|
124
|
+
em.add_timer(1) { em.stop } if received.values.all?{|arr| arr.size == messages.size }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Lets have an HTTP Long poll client using channel multiplexing
|
128
|
+
multiplexed_http_long_poll = Proc.new do |cid, last_sequence|
|
129
|
+
http = EM::HttpRequest.new(http_multi_url).get(:query => {'subscribe' => subscription_query})
|
130
|
+
|
131
|
+
http.errback { em.stop }
|
132
|
+
http.callback do
|
133
|
+
frame = JSON.parse(http.response, :symbolize_names => true)
|
134
|
+
received[cid] << frame[:message]
|
135
|
+
if received[cid].size < messages.size
|
136
|
+
# Add some jitter so the clients aren't syncronized
|
137
|
+
EM::add_timer(rand*0.001) { multiplexed_http_long_poll.call cid, frame[:last_sequence] }
|
138
|
+
else
|
139
|
+
succeed.call cid
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Test multiplexed web socket client
|
145
|
+
outgoing = messages.dup
|
146
|
+
publish_multi = Proc.new do
|
147
|
+
msg = outgoing.shift
|
148
|
+
chan = multiplex_channels[rand(multiplex_channels.size)]
|
149
|
+
Firehose::Client::Producer::Http.new.publish(msg).to(chan) do
|
150
|
+
EM::add_timer(rand*0.005) { publish_multi.call } unless outgoing.empty?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
multiplexed_websocket = Proc.new do |cid|
|
155
|
+
ws = Faye::WebSocket::Client.new("ws://#{uri.host}:#{uri.port}/channels@firehose?subscribe=#{subscription_query}")
|
156
|
+
|
157
|
+
ws.onmessage = lambda do |event|
|
158
|
+
frame = JSON.parse(event.data, :symbolize_names => true)
|
159
|
+
received[cid] << frame[:message]
|
160
|
+
succeed.call cid unless received[cid].size < messages.size
|
161
|
+
end
|
162
|
+
|
163
|
+
ws.onclose = lambda do |event|
|
164
|
+
ws = nil
|
165
|
+
end
|
166
|
+
|
167
|
+
ws.onerror = lambda do |event|
|
168
|
+
raise 'ws failed' + "\n" + event.inspect
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
em 180 do
|
173
|
+
# Start the clients.
|
174
|
+
multiplexed_http_long_poll.call(5)
|
175
|
+
multiplexed_http_long_poll.call(6)
|
176
|
+
multiplexed_websocket.call(7)
|
177
|
+
multiplexed_websocket.call(8)
|
178
|
+
|
179
|
+
# Wait a sec to let our clients set up.
|
180
|
+
em.add_timer(1){ publish_multi.call }
|
181
|
+
end
|
182
|
+
|
183
|
+
# When EM stops, these assertions will be made.
|
184
|
+
expect(received.size).to eql(4)
|
185
|
+
received.each_value do |arr|
|
186
|
+
expect(arr.size).to be <= messages.size
|
187
|
+
# expect(arr.sort).to eql(messages.sort)
|
106
188
|
end
|
107
189
|
end
|
108
190
|
|
109
191
|
|
110
|
-
it "
|
192
|
+
it "returns 400 error for long-polling when using http long polling and sequence header is < 0" do
|
111
193
|
em 5 do
|
112
194
|
http = EM::HttpRequest.new(http_url).get(:query => {'last_message_sequence' => -1})
|
113
195
|
http.errback { |e| raise e.inspect }
|
114
196
|
http.callback do
|
115
|
-
http.response_header.status.
|
197
|
+
expect(http.response_header.status).to eql(400)
|
116
198
|
em.stop
|
117
199
|
end
|
118
200
|
end
|