firehose 1.3.6 → 2.1.0
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +31 -10
- data/{LICENSE.txt → LICENSE} +6 -6
- data/README.md +379 -187
- data/app/assets/javascripts/firehose.js +258 -0
- data/config/importmap.rb +1 -0
- data/db/migrate/20260214185244_create_firehose_events.rb +23 -0
- data/lib/commands/firehose_command.rb +119 -0
- data/lib/firehose/channel.rb +40 -0
- data/lib/firehose/cleanup_job.rb +21 -0
- data/lib/firehose/engine.rb +36 -0
- data/lib/firehose/helper.rb +40 -0
- data/lib/firehose/models/channel.rb +12 -0
- data/lib/firehose/models/message.rb +11 -0
- data/lib/firehose/queue.rb +44 -0
- data/lib/firehose/server.rb +930 -17
- data/lib/firehose/sse.rb +187 -0
- data/lib/firehose/stream.rb +32 -0
- data/lib/firehose/streamable.rb +25 -0
- data/lib/firehose/websocket.rb +242 -0
- data/lib/firehose.rb +67 -15
- data/lib/generators/firehose/install/install_generator.rb +43 -0
- data/lib/generators/firehose/install/templates/firehose_controller.rb +16 -0
- metadata +39 -366
- data/.codeclimate.yml +0 -29
- data/.dockerignore +0 -2
- data/.env.sample +0 -17
- data/.gitignore +0 -14
- data/.rspec +0 -1
- data/.rubocop.yml +0 -1156
- data/.ruby-version +0 -1
- data/.travis.yml +0 -12
- data/Dockerfile +0 -11
- data/Gemfile +0 -6
- data/Procfile +0 -1
- data/Procfile.dev +0 -3
- data/Rakefile +0 -64
- data/bin/firehose +0 -9
- data/coffeelint.json +0 -129
- data/config/rainbows.rb +0 -17
- data/docker-compose.yml +0 -17
- data/firehose.gemspec +0 -45
- data/karma.config.coffee +0 -89
- data/lib/assets/javascripts/firehose/base.js.coffee +0 -4
- data/lib/assets/javascripts/firehose/consumer.js.coffee +0 -86
- data/lib/assets/javascripts/firehose/core.js.coffee +0 -6
- data/lib/assets/javascripts/firehose/long_poll.js.coffee +0 -210
- data/lib/assets/javascripts/firehose/multiplexed_consumer.js.coffee +0 -74
- data/lib/assets/javascripts/firehose/transport.js.coffee +0 -44
- data/lib/assets/javascripts/firehose/version.js.coffee.erb +0 -2
- data/lib/assets/javascripts/firehose/web_socket.js.coffee +0 -148
- data/lib/assets/javascripts/firehose.js.coffee +0 -2
- data/lib/firehose/assets.rb +0 -56
- data/lib/firehose/cli.rb +0 -79
- data/lib/firehose/client/consumer.rb +0 -94
- data/lib/firehose/client/producer.rb +0 -114
- data/lib/firehose/client.rb +0 -10
- data/lib/firehose/logging.rb +0 -32
- data/lib/firehose/rack/app.rb +0 -49
- data/lib/firehose/rack/consumer/http_long_poll.rb +0 -159
- data/lib/firehose/rack/consumer/web_socket.rb +0 -200
- data/lib/firehose/rack/consumer.rb +0 -78
- data/lib/firehose/rack/ping.rb +0 -88
- data/lib/firehose/rack/publisher.rb +0 -59
- data/lib/firehose/rack.rb +0 -32
- data/lib/firehose/rails.rb +0 -7
- data/lib/firehose/server/app.rb +0 -52
- data/lib/firehose/server/channel.rb +0 -72
- data/lib/firehose/server/message_buffer.rb +0 -59
- data/lib/firehose/server/publisher.rb +0 -133
- data/lib/firehose/server/redis.rb +0 -32
- data/lib/firehose/server/subscriber.rb +0 -51
- data/lib/firehose/version.rb +0 -4
- data/package.json +0 -17
- data/spec/integrations/integration_test_helper.rb +0 -22
- data/spec/integrations/rainbows_spec.rb +0 -7
- data/spec/integrations/shared_examples.rb +0 -202
- data/spec/integrations/thin_spec.rb +0 -7
- data/spec/javascripts/firehose/base_spec.coffee +0 -2
- data/spec/javascripts/firehose/consumer_spec.coffee +0 -45
- data/spec/javascripts/firehose/long_poll_spec.coffee +0 -44
- data/spec/javascripts/firehose/multiplexed_consumer_spec.coffee +0 -72
- data/spec/javascripts/firehose/transport_spec.coffee +0 -30
- data/spec/javascripts/firehose/websocket_spec.coffee +0 -27
- data/spec/javascripts/helpers/spec_helper.js +0 -31
- data/spec/javascripts/support/jquery-1.11.1.js +0 -10308
- data/spec/javascripts/support/json2.js +0 -487
- data/spec/javascripts/support/sinon-1.7.3.js +0 -4290
- data/spec/javascripts/support/spec_helper.coffee +0 -3
- data/spec/lib/assets_spec.rb +0 -35
- data/spec/lib/client/consumer_spec.rb +0 -13
- data/spec/lib/client/producer_spec.rb +0 -71
- data/spec/lib/firehose_spec.rb +0 -7
- data/spec/lib/rack/consumer/http_long_poll_spec.rb +0 -30
- data/spec/lib/rack/consumer_spec.rb +0 -19
- data/spec/lib/rack/ping_spec.rb +0 -28
- data/spec/lib/rack/publisher_spec.rb +0 -34
- data/spec/lib/server/app_spec.rb +0 -15
- data/spec/lib/server/channel_spec.rb +0 -178
- data/spec/lib/server/message_buffer_spec.rb +0 -148
- data/spec/lib/server/publisher_spec.rb +0 -91
- data/spec/lib/server/redis_spec.rb +0 -13
- data/spec/lib/server/subscriber_spec.rb +0 -70
- data/spec/spec_helper.rb +0 -84
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
# TODO - Spec this thing out. Unfortunately its not tested at all, mostly because the JSON client
|
|
4
|
-
# is more important (and tested). Still, this should be tested, at least used by the specs
|
|
5
|
-
# to test HTTP functionality of the server.
|
|
6
|
-
module Firehose
|
|
7
|
-
module Client
|
|
8
|
-
module Consumer
|
|
9
|
-
# TODO - Test this libs. I had to throw these quickly into our app so that we could get
|
|
10
|
-
# some stress testing out of the way.
|
|
11
|
-
# TODO - Replace the integration test clients with these guys. You'll want to refactor each
|
|
12
|
-
# transport to use on(:message), on(:conncect), and on(:disconnect) callbacks.
|
|
13
|
-
TransportNotSupportedError = Class.new(RuntimeError)
|
|
14
|
-
|
|
15
|
-
# Build up a benchmark client based on a given URI. Accepts ws:// and http:// for now.
|
|
16
|
-
def self.parse(uri)
|
|
17
|
-
case transport = ::URI.parse(uri).scheme
|
|
18
|
-
# TODO - Fix ws:// transport! See class WebSocket below to udnerstand
|
|
19
|
-
# why this doesn't work and support is dropped from the CLI.
|
|
20
|
-
# when 'ws'
|
|
21
|
-
# Consumer::WebSocket.new(uri)
|
|
22
|
-
when 'http'
|
|
23
|
-
Consumer::HttpLongPoll.new(uri)
|
|
24
|
-
else
|
|
25
|
-
raise TransportNotSupportedError.new("Transport #{transport.inspect} not supported.")
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# TODO - This won't even work. Dropping ws:// above until this is tested. This thing
|
|
30
|
-
# should be sending message sequences to Firehose.
|
|
31
|
-
# Connect to Firehose via WebSockets and consume messages.
|
|
32
|
-
class WebSocket
|
|
33
|
-
attr_reader :url, :logger
|
|
34
|
-
|
|
35
|
-
def initialize(url, logger = Firehose.logger)
|
|
36
|
-
@url, @logger = url, logger
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def request
|
|
40
|
-
ws = Faye::WebSocket::Client.new(url)
|
|
41
|
-
ws.onmessage = lambda do |event|
|
|
42
|
-
logger.info "WS | #{event.data[0...40].inspect}"
|
|
43
|
-
end
|
|
44
|
-
ws.onclose = lambda do |event|
|
|
45
|
-
logger.info "WS | Closed"
|
|
46
|
-
end
|
|
47
|
-
ws.onerror do
|
|
48
|
-
logger.error "WS | Failed"
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Connect to Firehose via HTTP Long Polling and consume messages.
|
|
54
|
-
class HttpLongPoll
|
|
55
|
-
JITTER = 0.003
|
|
56
|
-
|
|
57
|
-
attr_reader :url, :logger
|
|
58
|
-
|
|
59
|
-
def initialize(url, logger = Firehose.logger)
|
|
60
|
-
@url, @logger = url, logger
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def request(last_sequence=0)
|
|
64
|
-
http = EM::HttpRequest.new(url, :inactivity_timeout => 0).get(:query => {'last_message_sequence' => last_sequence})
|
|
65
|
-
http.callback do
|
|
66
|
-
case status = http.response_header.status
|
|
67
|
-
when 200
|
|
68
|
-
json = JSON.parse(http.response)
|
|
69
|
-
next_sequence = json['last_sequence'].to_i
|
|
70
|
-
message = json['message']
|
|
71
|
-
|
|
72
|
-
logger.info "HTTP 200 | Next Sequence: #{next_sequence} - #{message[0...40].inspect}"
|
|
73
|
-
EM::add_timer(jitter) { request next_sequence }
|
|
74
|
-
when 204
|
|
75
|
-
logger.info "HTTP 204 | Last Sequence #{last_sequence}"
|
|
76
|
-
EM::add_timer(jitter) { request last_sequence }
|
|
77
|
-
else
|
|
78
|
-
logger.error "HTTP #{status} | Failed"
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
http.errback do
|
|
82
|
-
logger.error "Connection Failed"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
# Random jitter between long poll requests.
|
|
88
|
-
def jitter
|
|
89
|
-
rand*JITTER
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
require "faraday"
|
|
2
|
-
require "uri"
|
|
3
|
-
|
|
4
|
-
module Firehose
|
|
5
|
-
module Client
|
|
6
|
-
module Producer
|
|
7
|
-
# Publish messages to Firehose via an HTTP interface.
|
|
8
|
-
class Http
|
|
9
|
-
# Exception gets raised when a 202 is _not_ received from the server after a message is published.
|
|
10
|
-
PublishError = Class.new(RuntimeError)
|
|
11
|
-
TimeoutError = Class.new(Faraday::Error::TimeoutError)
|
|
12
|
-
DEFAULT_TIMEOUT = 1 # How many seconds should we wait for a publish to take?
|
|
13
|
-
DEFAULT_ERROR_HANDLER = ->(e) { raise e }
|
|
14
|
-
|
|
15
|
-
# A DSL for publishing requests. This doesn't so much, but lets us call
|
|
16
|
-
# Firehose::Client::Producer::Http#publish('message').to('channel'). Slick eh? If you don't like it,
|
|
17
|
-
# just all Firehose::Client::Producer::Http#put('message', 'channel')
|
|
18
|
-
class Builder
|
|
19
|
-
def initialize(producer, message)
|
|
20
|
-
@producer, @message = producer, message
|
|
21
|
-
self
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def to(channel, opts={}, &callback)
|
|
25
|
-
@producer.put(@message, channel, opts, &callback)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# URI for the Firehose server. This URI does not include the path of the channel.
|
|
30
|
-
attr_reader :uri, :timeout
|
|
31
|
-
|
|
32
|
-
def initialize(uri = Firehose::URI, timeout=DEFAULT_TIMEOUT)
|
|
33
|
-
@uri = ::URI.parse(uri.to_s)
|
|
34
|
-
@uri.scheme ||= 'http'
|
|
35
|
-
@timeout = timeout
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# A DSL for publishing messages.
|
|
39
|
-
def publish(message)
|
|
40
|
-
Builder.new(self, message)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Publish the message via HTTP.
|
|
44
|
-
def put(message, channel, opts, &block)
|
|
45
|
-
ttl = opts[:ttl]
|
|
46
|
-
timeout = opts[:timeout] || @timeout || DEFAULT_TIMEOUT
|
|
47
|
-
buffer_size = opts[:buffer_size]
|
|
48
|
-
|
|
49
|
-
response = conn.put do |req|
|
|
50
|
-
req.options[:timeout] = timeout
|
|
51
|
-
if conn.path_prefix.nil? || conn.path_prefix == '/'
|
|
52
|
-
# This avoids a double / if the channel starts with a / too (which is expected).
|
|
53
|
-
req.path = channel
|
|
54
|
-
else
|
|
55
|
-
if conn.path_prefix =~ /\/\Z/ || channel =~ /\A\//
|
|
56
|
-
req.path = [conn.path_prefix, channel].compact.join
|
|
57
|
-
else
|
|
58
|
-
# Add a / so the prefix and channel aren't just rammed together.
|
|
59
|
-
req.path = [conn.path_prefix, channel].compact.join('/')
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
req.body = message
|
|
63
|
-
req.headers['Cache-Control'] = "max-age=#{ttl.to_i}" if ttl
|
|
64
|
-
req.headers["X-Firehose-Buffer-Size"] = buffer_size.to_s if buffer_size
|
|
65
|
-
end
|
|
66
|
-
response.on_complete do
|
|
67
|
-
case response.status
|
|
68
|
-
when 202 # Fire off the callback if everything worked out OK.
|
|
69
|
-
block.call(response) if block
|
|
70
|
-
else
|
|
71
|
-
# don't pass along basic auth header, if present
|
|
72
|
-
response_data = response.inspect.gsub(/"Authorization"=>"Basic \S+"/, '"Authorization" => "Basic [HIDDEN]"')
|
|
73
|
-
endpoint = "#{uri}/#{channel}".gsub(/:\/\/\S+@/, "://")
|
|
74
|
-
error_handler.call PublishError.new("Could not publish #{message.inspect} to '#{endpoint}': #{response_data}")
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Hide Faraday with this Timeout exception, and through the error handler.
|
|
79
|
-
rescue Faraday::Error::TimeoutError => e
|
|
80
|
-
error_handler.call TimeoutError.new(e)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Handle errors that could happen while publishing a message.
|
|
84
|
-
def on_error(&block)
|
|
85
|
-
@error_handler = block
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Raise an exception if an error occurs when connecting to the Firehose.
|
|
89
|
-
def error_handler
|
|
90
|
-
@error_handler || DEFAULT_ERROR_HANDLER
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# What adapter should Firehose use to PUT the message? List of adapters is
|
|
94
|
-
# available at https://github.com/technoweenie/faraday.
|
|
95
|
-
def self.adapter=(adapter)
|
|
96
|
-
@adapter = adapter
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Use :net_http for the default Faraday adapter.
|
|
100
|
-
def self.adapter
|
|
101
|
-
@adapter ||= Faraday.default_adapter
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
# Build out a Faraday connection
|
|
106
|
-
def conn
|
|
107
|
-
@conn ||= Faraday.new(:url => uri.to_s) do |builder|
|
|
108
|
-
builder.adapter self.class.adapter
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
data/lib/firehose/client.rb
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
require 'em-http'
|
|
2
|
-
require 'faye/websocket'
|
|
3
|
-
|
|
4
|
-
module Firehose
|
|
5
|
-
# Ruby clients that connect to Firehose to either publish or consume messages.
|
|
6
|
-
module Client
|
|
7
|
-
autoload :Consumer, 'firehose/client/consumer'
|
|
8
|
-
autoload :Producer, 'firehose/client/producer'
|
|
9
|
-
end
|
|
10
|
-
end
|
data/lib/firehose/logging.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
require 'logger'
|
|
2
|
-
|
|
3
|
-
module Firehose
|
|
4
|
-
# Sets up logging
|
|
5
|
-
def self.logger
|
|
6
|
-
@logger ||= Logger.new($stdout)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def self.logger=(logger)
|
|
10
|
-
@logger = logger
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
self.logger.level = if ENV['LOG_LEVEL']
|
|
14
|
-
Logger.const_get(ENV['LOG_LEVEL'].upcase)
|
|
15
|
-
else
|
|
16
|
-
case ENV['RACK_ENV']
|
|
17
|
-
when 'test' then Logger::ERROR
|
|
18
|
-
when 'development' then Logger::DEBUG
|
|
19
|
-
else Logger::INFO
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# TODO: Provide some way to allow this to be configured via an ENV variable.
|
|
24
|
-
self.logger.formatter = lambda do |severity, time, name, msg|
|
|
25
|
-
out_time = time.utc.strftime "%Y-%m-%d %H:%M:%S.%L"
|
|
26
|
-
"[#{out_time} ##$$] #{severity} : #{msg}\n"
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# stdout gets "lost" in Foreman if this isn't here
|
|
31
|
-
# https://github.com/ddollar/foreman/wiki/Missing-Output
|
|
32
|
-
$stdout.sync = true if ENV['RACK_ENV'] == 'development' || ENV['SYNC_LOGGING']
|
data/lib/firehose/rack/app.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Rack
|
|
3
|
-
# Acts as the glue between the HTTP/WebSocket world and the Firehose::Server class,
|
|
4
|
-
# which talks directly to the Redis server. Also dispatches between HTTP and WebSocket
|
|
5
|
-
# transport handlers depending on the clients' request.
|
|
6
|
-
class App
|
|
7
|
-
def initialize
|
|
8
|
-
yield self if block_given?
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def call(env)
|
|
12
|
-
# Cache the parsed request so we don't need to re-parse it when we pass
|
|
13
|
-
# control onto another app.
|
|
14
|
-
req = env['parsed_request'] ||= ::Rack::Request.new(env)
|
|
15
|
-
method = req.request_method
|
|
16
|
-
|
|
17
|
-
case method
|
|
18
|
-
when 'PUT'
|
|
19
|
-
# Firehose::Client::Publisher PUT's payloads to the server.
|
|
20
|
-
publisher.call(env)
|
|
21
|
-
when 'HEAD'
|
|
22
|
-
# HEAD requests are used to prevent sockets from timing out
|
|
23
|
-
# from inactivity
|
|
24
|
-
ping.call(env)
|
|
25
|
-
else
|
|
26
|
-
# TODO - 'harden' this up with a GET request and throw a "Bad Request"
|
|
27
|
-
# HTTP error code. I'd do it now but I'm in a plane and can't think of it.
|
|
28
|
-
consumer.call(env)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# The consumer pulls messages off of the backend and passes messages to the
|
|
33
|
-
# connected HTTP or WebSocket client. This can be configured from the initialization
|
|
34
|
-
# method of the rack app.
|
|
35
|
-
def consumer
|
|
36
|
-
@consumer ||= Consumer.new
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
def publisher
|
|
41
|
-
@publisher ||= Publisher.new
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def ping
|
|
45
|
-
@ping ||= Ping.new
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
module Firehose
|
|
4
|
-
module Rack
|
|
5
|
-
class Consumer
|
|
6
|
-
class HttpLongPoll
|
|
7
|
-
# How long should we wait before closing out the consuming clients web connection
|
|
8
|
-
# for long polling? Most browsers timeout after a connection has been idle for 30s.
|
|
9
|
-
TIMEOUT = 20
|
|
10
|
-
|
|
11
|
-
attr_accessor :timeout
|
|
12
|
-
|
|
13
|
-
def initialize(timeout=TIMEOUT)
|
|
14
|
-
@timeout = timeout
|
|
15
|
-
yield self if block_given?
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(env)
|
|
19
|
-
if Consumer.multiplexing_request?(env)
|
|
20
|
-
MultiplexingHandler.new(@timeout).call(env)
|
|
21
|
-
else
|
|
22
|
-
DefaultHandler.new(@timeout).call(env)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
class Handler
|
|
27
|
-
include Firehose::Rack::Helpers
|
|
28
|
-
|
|
29
|
-
def initialize(timeout=TIMEOUT)
|
|
30
|
-
@timeout = timeout
|
|
31
|
-
yield self if block_given?
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def call(env)
|
|
35
|
-
request = request(env)
|
|
36
|
-
method = request.request_method
|
|
37
|
-
|
|
38
|
-
case method
|
|
39
|
-
# GET is how clients subscribe to the queue. When a messages comes in, we flush out a response,
|
|
40
|
-
# close down the requeust, and the client then reconnects.
|
|
41
|
-
when "GET"
|
|
42
|
-
handle_request(request, env)
|
|
43
|
-
return ASYNC_RESPONSE
|
|
44
|
-
# we use post messages for http long poll multiplexing
|
|
45
|
-
when "POST"
|
|
46
|
-
if Consumer.multiplexing_request?(env)
|
|
47
|
-
handle_request(request, env)
|
|
48
|
-
return ASYNC_RESPONSE
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
Firehose.logger.debug "HTTP #{method} not supported"
|
|
53
|
-
response(405, "#{method} not supported.", "Allow" => "GET")
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
# If the request is a CORS request, return those headers, otherwise don't worry 'bout it
|
|
59
|
-
def response_headers(env)
|
|
60
|
-
cors_origin(env) ? cors_headers(env) : {}
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def cors_origin(env)
|
|
64
|
-
env['HTTP_ORIGIN']
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def cors_headers(env)
|
|
68
|
-
# TODO seperate out CORS logic as an async middleware with a Goliath web server.
|
|
69
|
-
{'Access-Control-Allow-Origin' => cors_origin(env)}
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def request(env)
|
|
73
|
-
env['parsed_request'] ||= ::Rack::Request.new(env)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def async_callback(env, code, message = "", headers = nil)
|
|
77
|
-
resp_headers = response_headers(env)
|
|
78
|
-
|
|
79
|
-
if headers
|
|
80
|
-
resp_headers.merge!(headers)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
if cb = env["async.callback"]
|
|
84
|
-
cb.call response(code, message, resp_headers)
|
|
85
|
-
else
|
|
86
|
-
Firehose.logger.error "async.callback not set for response: #{message.inspect}"
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def respond_async(channel, last_sequence, env)
|
|
91
|
-
EM.next_tick do
|
|
92
|
-
if last_sequence < 0
|
|
93
|
-
async_callback env, 400, "The last_message_sequence parameter may not be less than zero"
|
|
94
|
-
else
|
|
95
|
-
Server::Channel.new(channel).next_messages(last_sequence, :timeout => @timeout).callback do |messages|
|
|
96
|
-
# TODO: Can we send all of these messages down in one request? Sending one message per
|
|
97
|
-
# request is slow and inefficient. If we change the protocol (3.0?) we could batch the
|
|
98
|
-
# messages and send them all down the pipe, then close the conneciton.
|
|
99
|
-
message = messages.first
|
|
100
|
-
async_callback env, 200, wrap_frame(channel, message)
|
|
101
|
-
end.errback do |e|
|
|
102
|
-
if e == :timeout
|
|
103
|
-
async_callback env, 204
|
|
104
|
-
else
|
|
105
|
-
Firehose.logger.error "Unexpected error when trying to GET last_sequence #{last_sequence} for path #{channel}: #{e.inspect}"
|
|
106
|
-
async_callback env, 500, "Unexpected error"
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
class DefaultHandler < Handler
|
|
115
|
-
def wrap_frame(channel, message)
|
|
116
|
-
JSON.generate :message => message.payload, :last_sequence => message.sequence
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def log_request(path, last_sequence, env)
|
|
120
|
-
Firehose.logger.debug "HTTP GET with last_sequence #{last_sequence} for path #{path} with query #{env["QUERY_STRING"].inspect}"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def handle_request(request, env)
|
|
124
|
-
# Get the Last Message Sequence from the query string.
|
|
125
|
-
# Ideally we'd use an HTTP header, but android devices don't let us
|
|
126
|
-
# set any HTTP headers for CORS requests.
|
|
127
|
-
last_sequence = request.params['last_message_sequence'].to_i
|
|
128
|
-
channel = request.path
|
|
129
|
-
|
|
130
|
-
log_request channel, last_sequence, env
|
|
131
|
-
respond_async channel, last_sequence, env
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
class MultiplexingHandler < Handler
|
|
136
|
-
def wrap_frame(channel, message)
|
|
137
|
-
JSON.generate channel: channel, :message => message.payload, :last_sequence => message.sequence
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def log_request(request, subscriptions, env)
|
|
141
|
-
if request.post?
|
|
142
|
-
Firehose.logger.debug "HTTP multiplexing POST, subscribing #{subscriptions.inspect}"
|
|
143
|
-
else
|
|
144
|
-
Firehose.logger.debug "HTTP multiplexing GET with query #{env["QUERY_STRING"].inspect}"
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def handle_request(request, env)
|
|
149
|
-
subscriptions = Consumer.multiplex_subscriptions(request)
|
|
150
|
-
log_request request, subscriptions, env
|
|
151
|
-
subscriptions.each do |sub|
|
|
152
|
-
respond_async(sub[:channel], sub[:message_sequence], env)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
require 'faye/websocket'
|
|
2
|
-
require 'json'
|
|
3
|
-
require "rack/utils"
|
|
4
|
-
|
|
5
|
-
module Firehose
|
|
6
|
-
module Rack
|
|
7
|
-
class Consumer
|
|
8
|
-
class WebSocket
|
|
9
|
-
# Setup a handler for the websocket connection.
|
|
10
|
-
def call(env)
|
|
11
|
-
ws = Faye::WebSocket.new(env)
|
|
12
|
-
if Consumer.multiplexing_request?(env)
|
|
13
|
-
MultiplexingHandler.new(ws)
|
|
14
|
-
else
|
|
15
|
-
DefaultHandler.new(ws)
|
|
16
|
-
end
|
|
17
|
-
ws.rack_response
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Determine if the rack request is a WebSocket request.
|
|
21
|
-
def self.request?(env)
|
|
22
|
-
Faye::WebSocket.websocket?(env)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
class Handler
|
|
26
|
-
def initialize(ws)
|
|
27
|
-
@ws = ws
|
|
28
|
-
@req = ::Rack::Request.new ws.env
|
|
29
|
-
# Setup the event handlers from this class.
|
|
30
|
-
@ws.onopen = method :open
|
|
31
|
-
@ws.onclose = method :close
|
|
32
|
-
@ws.onerror = method :error
|
|
33
|
-
@ws.onmessage = method :message
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def parse_message(event)
|
|
37
|
-
JSON.parse(event.data, :symbolize_names => true) rescue {}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Send a JSON message to the client
|
|
41
|
-
# Expects message to be a Hash
|
|
42
|
-
def send_message(message)
|
|
43
|
-
@ws.send JSON.generate(message)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Log errors if a socket fails. `close` will fire after this to clean up any
|
|
47
|
-
# remaining connectons.
|
|
48
|
-
def error(event)
|
|
49
|
-
Firehose.logger.error "WS connection `#{@req.path}` error. Message: `#{event.message.inspect}`"
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Manages connection state for the web socket that's connected
|
|
54
|
-
# by the Consumer::WebSocket class. Deals with message sequence,
|
|
55
|
-
# connection, failures, and subscription state.
|
|
56
|
-
class DefaultHandler < Handler
|
|
57
|
-
# Manages messages sent from the connect client to the server. This is mostly
|
|
58
|
-
# used to handle heart-beats that are designed to prevent the WebSocket connection
|
|
59
|
-
# from timing out from inactivity.
|
|
60
|
-
def message(event)
|
|
61
|
-
msg = parse_message(event)
|
|
62
|
-
seq = msg[:message_sequence]
|
|
63
|
-
if msg[:ping] == 'PING'
|
|
64
|
-
Firehose.logger.debug "WS ping received, sending pong"
|
|
65
|
-
send_message pong: "PONG"
|
|
66
|
-
elsif !@subscribed && seq.kind_of?(Integer)
|
|
67
|
-
Firehose.logger.debug "Subscribing at message_sequence #{seq}"
|
|
68
|
-
subscribe seq
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Log a message that the client has connected.
|
|
73
|
-
def open(event)
|
|
74
|
-
Firehose.logger.debug "WebSocket subscribed to `#{@req.path}`. Waiting for message_sequence..."
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Log a message that hte client has disconnected and reset the state for the class. Clean
|
|
78
|
-
# up the subscribers to the channels.
|
|
79
|
-
def close(event)
|
|
80
|
-
if @deferrable
|
|
81
|
-
@deferrable.fail :disconnect
|
|
82
|
-
@channel.unsubscribe(@deferrable) if @channel
|
|
83
|
-
end
|
|
84
|
-
Firehose.logger.debug "WS connection `#{@req.path}` closing. Code: #{event.code.inspect}; Reason #{event.reason.inspect}"
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Subscribe the client to the channel on the server. Asks for
|
|
88
|
-
# the last sequence for clients that reconnect.
|
|
89
|
-
def subscribe(last_sequence)
|
|
90
|
-
@subscribed = true
|
|
91
|
-
@channel = Server::Channel.new @req.path
|
|
92
|
-
@deferrable = @channel.next_messages last_sequence
|
|
93
|
-
@deferrable.callback do |messages|
|
|
94
|
-
messages.each do |message|
|
|
95
|
-
Firehose.logger.debug "WS sent `#{message.payload}` to `#{@req.path}` with sequence `#{message.sequence}`"
|
|
96
|
-
send_message message: message.payload, last_sequence: message.sequence
|
|
97
|
-
end
|
|
98
|
-
subscribe messages.last.sequence
|
|
99
|
-
end
|
|
100
|
-
@deferrable.errback do |e|
|
|
101
|
-
unless e == :disconnect
|
|
102
|
-
Firehose.logger.error "WS Error: #{e}"
|
|
103
|
-
EM.next_tick { raise e.inspect }
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
class MultiplexingHandler < Handler
|
|
110
|
-
class Subscription < Struct.new(:channel, :deferrable)
|
|
111
|
-
def close
|
|
112
|
-
deferrable.fail :disconnect
|
|
113
|
-
channel.unsubscribe(deferrable)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def initialize(ws)
|
|
118
|
-
super(ws)
|
|
119
|
-
@subscriptions = {}
|
|
120
|
-
subscribe_multiplexed Consumer.multiplex_subscriptions(@req)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def message(event)
|
|
124
|
-
msg = parse_message(event)
|
|
125
|
-
|
|
126
|
-
if subscriptions = msg[:multiplex_subscribe]
|
|
127
|
-
subscriptions = [subscriptions] unless subscriptions.is_a?(Array)
|
|
128
|
-
return subscribe_multiplexed(subscriptions)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
if channel_names = msg[:multiplex_unsubscribe]
|
|
132
|
-
return unsubscribe(channel_names)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
if msg[:ping] == 'PING'
|
|
136
|
-
Firehose.logger.debug "WS ping received, sending pong"
|
|
137
|
-
return send_message pong: "PONG"
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def open(event)
|
|
142
|
-
Firehose.logger.debug "Multiplexing Websocket connected: #{@req.path}"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def close(event)
|
|
146
|
-
@subscriptions.each_value(&:close)
|
|
147
|
-
@subscriptions.clear
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def subscribe_multiplexed(subscriptions)
|
|
151
|
-
subscriptions.each do |sub|
|
|
152
|
-
Firehose.logger.debug "Subscribing multiplexed to: #{sub}"
|
|
153
|
-
|
|
154
|
-
channel, sequence = sub[:channel], sub[:message_sequence]
|
|
155
|
-
next if channel.nil?
|
|
156
|
-
|
|
157
|
-
subscribe(channel, sequence.to_i)
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Subscribe the client to the channel on the server. Asks for
|
|
162
|
-
# the last sequence for clients that reconnect.
|
|
163
|
-
def subscribe(channel_name, last_sequence)
|
|
164
|
-
channel = Server::Channel.new channel_name
|
|
165
|
-
deferrable = channel.next_messages last_sequence
|
|
166
|
-
subscription = Subscription.new(channel, deferrable)
|
|
167
|
-
|
|
168
|
-
@subscriptions[channel_name] = subscription
|
|
169
|
-
|
|
170
|
-
deferrable.callback do |messages|
|
|
171
|
-
messages.each do |message|
|
|
172
|
-
send_message(
|
|
173
|
-
channel: channel_name,
|
|
174
|
-
message: message.payload,
|
|
175
|
-
last_sequence: message.sequence
|
|
176
|
-
)
|
|
177
|
-
Firehose.logger.debug "WS sent `#{message.payload}` to `#{channel_name}` with sequence `#{message.sequence}`"
|
|
178
|
-
end
|
|
179
|
-
subscribe channel_name, messages.last.sequence
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
deferrable.errback do |e|
|
|
183
|
-
EM.next_tick { raise e.inspect } unless e == :disconnect
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def unsubscribe(channel_names)
|
|
188
|
-
Firehose.logger.debug "Unsubscribing from channels: #{channel_names}"
|
|
189
|
-
Array(channel_names).each do |chan|
|
|
190
|
-
if sub = @subscriptions[chan]
|
|
191
|
-
sub.close
|
|
192
|
-
@subscriptions.delete(chan)
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|