firehose 1.2.20 → 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
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