firehose 1.2.20 → 1.3.6

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +29 -0
  3. data/.dockerignore +2 -0
  4. data/.gitignore +3 -1
  5. data/.rubocop.yml +1156 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +3 -7
  8. data/CHANGELOG.md +15 -0
  9. data/Dockerfile +11 -0
  10. data/Gemfile +4 -2
  11. data/Procfile.dev +0 -1
  12. data/README.md +66 -8
  13. data/Rakefile +43 -32
  14. data/coffeelint.json +129 -0
  15. data/docker-compose.yml +17 -0
  16. data/firehose.gemspec +5 -9
  17. data/karma.config.coffee +89 -0
  18. data/lib/assets/javascripts/firehose.js.coffee +1 -2
  19. data/lib/assets/javascripts/firehose/consumer.js.coffee +18 -2
  20. data/lib/assets/javascripts/firehose/core.js.coffee +2 -1
  21. data/lib/assets/javascripts/firehose/long_poll.js.coffee +69 -8
  22. data/lib/assets/javascripts/firehose/multiplexed_consumer.js.coffee +74 -0
  23. data/lib/assets/javascripts/firehose/transport.js.coffee +4 -2
  24. data/lib/assets/javascripts/firehose/web_socket.js.coffee +51 -5
  25. data/lib/firehose/cli.rb +2 -1
  26. data/lib/firehose/client/producer.rb +10 -4
  27. data/lib/firehose/rack/consumer.rb +39 -0
  28. data/lib/firehose/rack/consumer/http_long_poll.rb +118 -45
  29. data/lib/firehose/rack/consumer/web_socket.rb +133 -28
  30. data/lib/firehose/rack/ping.rb +1 -1
  31. data/lib/firehose/rack/publisher.rb +10 -4
  32. data/lib/firehose/server.rb +9 -9
  33. data/lib/firehose/server/channel.rb +23 -31
  34. data/lib/firehose/server/message_buffer.rb +59 -0
  35. data/lib/firehose/server/publisher.rb +16 -17
  36. data/lib/firehose/server/redis.rb +32 -0
  37. data/lib/firehose/server/subscriber.rb +7 -7
  38. data/lib/firehose/version.rb +2 -2
  39. data/package.json +14 -2
  40. data/spec/integrations/shared_examples.rb +89 -7
  41. data/spec/javascripts/firehose/multiplexed_consumer_spec.coffee +72 -0
  42. data/spec/javascripts/firehose/transport_spec.coffee +0 -2
  43. data/spec/javascripts/firehose/websocket_spec.coffee +2 -0
  44. data/spec/javascripts/helpers/spec_helper.js +1 -0
  45. data/spec/javascripts/support/jquery-1.11.1.js +10308 -0
  46. data/{lib/assets/javascripts/vendor → spec/javascripts/support}/json2.js +0 -0
  47. data/spec/javascripts/support/spec_helper.coffee +3 -0
  48. data/spec/lib/assets_spec.rb +8 -8
  49. data/spec/lib/client/producer_spec.rb +14 -14
  50. data/spec/lib/firehose_spec.rb +2 -2
  51. data/spec/lib/rack/consumer/http_long_poll_spec.rb +21 -3
  52. data/spec/lib/rack/consumer_spec.rb +4 -4
  53. data/spec/lib/rack/ping_spec.rb +4 -4
  54. data/spec/lib/rack/publisher_spec.rb +5 -5
  55. data/spec/lib/server/app_spec.rb +2 -2
  56. data/spec/lib/server/channel_spec.rb +58 -44
  57. data/spec/lib/server/message_buffer_spec.rb +148 -0
  58. data/spec/lib/server/publisher_spec.rb +29 -22
  59. data/spec/lib/server/redis_spec.rb +13 -0
  60. data/spec/lib/server/subscriber_spec.rb +14 -13
  61. data/spec/spec_helper.rb +8 -1
  62. metadata +34 -95
  63. data/.rbenv-version +0 -1
  64. data/Guardfile +0 -31
  65. data/config/evergreen.rb +0 -9
@@ -27,7 +27,7 @@ module Firehose
27
27
  SECONDS_TO_EXPIRE = 60
28
28
 
29
29
  def self.redis
30
- @redis ||= EM::Hiredis.connect
30
+ @redis ||= Firehose::Server.redis.connection
31
31
  end
32
32
 
33
33
  def initialize(env, redis=nil)
@@ -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 = env['parsed_request'] ||= ::Rack::Request.new(env)
8
- path = req.path
9
- method = req.request_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
- publisher.publish(path, body, :ttl => ttl).callback do
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|
@@ -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 :Subscriber, 'firehose/server/subscriber'
12
- autoload :Publisher, 'firehose/server/publisher'
13
- autoload :Channel, 'firehose/server/channel'
14
- autoload :App, 'firehose/server/app'
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
- # Generates keys for all firehose interactions with Redis. Ensures a root
17
- # key of `firehose`
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, :redis, :subscriber, :list_key, :sequence_key
5
+ attr_reader :channel_key, :list_key, :sequence_key
6
+ attr_reader :redis, :subscriber
6
7
 
7
8
  def self.redis
8
- @redis ||= EM::Hiredis.connect
9
+ @redis ||= Firehose::Server.redis.connection
9
10
  end
10
11
 
11
12
  def self.subscriber
12
- @subscriber ||= Server::Subscriber.new(EM::Hiredis.connect)
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
- @channel_key, @redis, @subscriber = channel_key, redis, subscriber
17
- @list_key, @sequence_key = Server.key(channel_key, :list), Server.key(channel_key, :sequence)
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 next_message(last_sequence=nil, options={})
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
- redis.lrange(list_key, 0, Server::Publisher::MAX_MESSAGES).
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 |(sequence, message_list)|
34
- Firehose.logger.debug "exec returned: `#{sequence}` and `#{message_list.inspect}`"
35
- sequence = sequence.to_i
36
-
37
- if sequence.nil? || (diff = sequence - last_sequence) <= 0
38
- Firehose.logger.debug "No message available yet, subscribing. sequence: `#{sequence}`"
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
- elsif last_sequence > 0 && diff < Server::Publisher::MAX_MESSAGES
43
- # The client is kinda-sorta running behind, but has a chance to catch
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 ||= EM::Hiredis.connect
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
- # Deserealize components of a message back into Ruby.
63
+ # Deserialize components of a message back into Ruby.
67
64
  def self.from_payload(payload)
68
- payload.split(PAYLOAD_DELIMITER, method(:to_payload).arity)
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
- MAX_MESSAGES,
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 max_messages = KEYS[6]
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, max_messages - 1)
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, sequence, message = Server::Publisher.from_payload(payload)
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 sequence `#{sequence}` and message `#{message}`"
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 sequence #{sequence} to client from subscriber"
30
- deferrable.succeed message, sequence.to_i
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
@@ -1,4 +1,4 @@
1
1
  module Firehose
2
- VERSION = "1.2.20"
3
- CODENAME = "Onprogress of Doom"
2
+ VERSION = "1.3.6"
3
+ CODENAME = "Oops I did it again"
4
4
  end
@@ -1,5 +1,17 @@
1
1
  {
2
2
  "name": "firehose",
3
- "version": "1.2.20",
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 "should pub-sub http and websockets" do
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.should == 4
104
- received.values.each do |arr|
105
- arr.should == messages
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 "should return 400 error for long-polling when using http long polling and sequence header is < 0" do
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.should == 400
197
+ expect(http.response_header.status).to eql(400)
116
198
  em.stop
117
199
  end
118
200
  end