firehose 1.2.20 → 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 +36 -0
- data/{LICENSE.txt → LICENSE} +6 -6
- data/README.md +389 -139
- 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 +931 -18
- 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 +40 -428
- data/.env.sample +0 -17
- data/.gitignore +0 -12
- data/.rbenv-version +0 -1
- data/.rspec +0 -1
- data/.travis.yml +0 -16
- data/Gemfile +0 -4
- data/Guardfile +0 -31
- data/Procfile +0 -1
- data/Procfile.dev +0 -4
- data/Rakefile +0 -53
- data/bin/firehose +0 -9
- data/config/evergreen.rb +0 -9
- data/config/rainbows.rb +0 -17
- data/firehose.gemspec +0 -49
- data/lib/assets/javascripts/firehose/base.js.coffee +0 -4
- data/lib/assets/javascripts/firehose/consumer.js.coffee +0 -70
- data/lib/assets/javascripts/firehose/core.js.coffee +0 -5
- data/lib/assets/javascripts/firehose/long_poll.js.coffee +0 -149
- data/lib/assets/javascripts/firehose/transport.js.coffee +0 -42
- data/lib/assets/javascripts/firehose/version.js.coffee.erb +0 -2
- data/lib/assets/javascripts/firehose/web_socket.js.coffee +0 -102
- data/lib/assets/javascripts/firehose.js.coffee +0 -3
- data/lib/assets/javascripts/vendor/json2.js +0 -487
- data/lib/firehose/assets.rb +0 -56
- data/lib/firehose/cli.rb +0 -78
- data/lib/firehose/client/consumer.rb +0 -94
- data/lib/firehose/client/producer.rb +0 -108
- 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 -86
- data/lib/firehose/rack/consumer/web_socket.rb +0 -95
- data/lib/firehose/rack/consumer.rb +0 -39
- data/lib/firehose/rack/ping.rb +0 -88
- data/lib/firehose/rack/publisher.rb +0 -53
- 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 -80
- data/lib/firehose/server/publisher.rb +0 -134
- data/lib/firehose/server/subscriber.rb +0 -51
- data/lib/firehose/version.rb +0 -4
- data/package.json +0 -5
- data/spec/integrations/integration_test_helper.rb +0 -22
- data/spec/integrations/rainbows_spec.rb +0 -7
- data/spec/integrations/shared_examples.rb +0 -120
- 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/transport_spec.coffee +0 -32
- data/spec/javascripts/firehose/websocket_spec.coffee +0 -25
- data/spec/javascripts/helpers/spec_helper.js +0 -30
- data/spec/javascripts/support/sinon-1.7.3.js +0 -4290
- 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 -12
- 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 -164
- data/spec/lib/server/publisher_spec.rb +0 -84
- data/spec/lib/server/subscriber_spec.rb +0 -69
- data/spec/spec_helper.rb +0 -77
|
@@ -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,108 +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
|
-
|
|
14
|
-
# A DSL for publishing requests. This doesn't so much, but lets us call
|
|
15
|
-
# Firehose::Client::Producer::Http#publish('message').to('channel'). Slick eh? If you don't like it,
|
|
16
|
-
# just all Firehose::Client::Producer::Http#put('message', 'channel')
|
|
17
|
-
class Builder
|
|
18
|
-
def initialize(producer, message)
|
|
19
|
-
@producer, @message = producer, message
|
|
20
|
-
self
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def to(channel, opts={}, &callback)
|
|
24
|
-
@producer.put(@message, channel, opts, &callback)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# URI for the Firehose server. This URI does not include the path of the channel.
|
|
29
|
-
attr_reader :uri, :timeout
|
|
30
|
-
|
|
31
|
-
def initialize(uri = Firehose::URI, timeout=DEFAULT_TIMEOUT)
|
|
32
|
-
@uri = ::URI.parse(uri.to_s)
|
|
33
|
-
@uri.scheme ||= 'http'
|
|
34
|
-
@timeout = timeout
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# A DSL for publishing messages.
|
|
38
|
-
def publish(message)
|
|
39
|
-
Builder.new(self, message)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Publish the message via HTTP.
|
|
43
|
-
def put(message, channel, opts, &block)
|
|
44
|
-
ttl = opts[:ttl]
|
|
45
|
-
timeout = opts[:timeout] || @timeout || DEFAULT_TIMEOUT
|
|
46
|
-
|
|
47
|
-
response = conn.put do |req|
|
|
48
|
-
req.options[:timeout] = timeout
|
|
49
|
-
if conn.path_prefix.nil? || conn.path_prefix == '/'
|
|
50
|
-
# This avoids a double / if the channel starts with a / too (which is expected).
|
|
51
|
-
req.path = channel
|
|
52
|
-
else
|
|
53
|
-
if conn.path_prefix =~ /\/\Z/ || channel =~ /\A\//
|
|
54
|
-
req.path = [conn.path_prefix, channel].compact.join
|
|
55
|
-
else
|
|
56
|
-
# Add a / so the prefix and channel aren't just rammed together.
|
|
57
|
-
req.path = [conn.path_prefix, channel].compact.join('/')
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
req.body = message
|
|
61
|
-
req.headers['Cache-Control'] = "max-age=#{ttl.to_i}" if ttl
|
|
62
|
-
end
|
|
63
|
-
response.on_complete do
|
|
64
|
-
case response.status
|
|
65
|
-
when 202 # Fire off the callback if everything worked out OK.
|
|
66
|
-
block.call(response) if block
|
|
67
|
-
else
|
|
68
|
-
error_handler.call PublishError.new("Could not publish #{message.inspect} to '#{uri.to_s}/#{channel}': #{response.inspect}")
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Hide Faraday with this Timeout exception, and through the error handler.
|
|
73
|
-
rescue Faraday::Error::TimeoutError => e
|
|
74
|
-
error_handler.call TimeoutError.new(e)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Handle errors that could happen while publishing a message.
|
|
78
|
-
def on_error(&block)
|
|
79
|
-
@error_handler = block
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Raise an exception if an error occurs when connecting to the Firehose.
|
|
83
|
-
def error_handler
|
|
84
|
-
@error_handler || Proc.new{ |e| raise e }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# What adapter should Firehose use to PUT the message? List of adapters is
|
|
88
|
-
# available at https://github.com/technoweenie/faraday.
|
|
89
|
-
def self.adapter=(adapter)
|
|
90
|
-
@adapter = adapter
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Use :net_http for the default Faraday adapter.
|
|
94
|
-
def self.adapter
|
|
95
|
-
@adapter ||= Faraday.default_adapter
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
private
|
|
99
|
-
# Build out a Faraday connection
|
|
100
|
-
def conn
|
|
101
|
-
@conn ||= Faraday.new(:url => uri.to_s) do |builder|
|
|
102
|
-
builder.adapter self.class.adapter
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
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,86 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
module Firehose
|
|
4
|
-
module Rack
|
|
5
|
-
class Consumer
|
|
6
|
-
class HttpLongPoll
|
|
7
|
-
include Firehose::Rack::Helpers
|
|
8
|
-
|
|
9
|
-
# How long should we wait before closing out the consuming clients web connection
|
|
10
|
-
# for long polling? Most browsers timeout after a connection has been idle for 30s.
|
|
11
|
-
TIMEOUT = 20
|
|
12
|
-
|
|
13
|
-
# Configures the timeout for the
|
|
14
|
-
attr_accessor :timeout
|
|
15
|
-
|
|
16
|
-
def initialize(timeout=TIMEOUT)
|
|
17
|
-
@timeout = timeout
|
|
18
|
-
yield self if block_given?
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def call(env)
|
|
22
|
-
req = env['parsed_request'] ||= ::Rack::Request.new(env)
|
|
23
|
-
path = req.path
|
|
24
|
-
method = req.request_method
|
|
25
|
-
# Get the Last Message Sequence from the query string.
|
|
26
|
-
# Ideally we'd use an HTTP header, but android devices don't let us
|
|
27
|
-
# set any HTTP headers for CORS requests.
|
|
28
|
-
last_sequence = req.params['last_message_sequence'].to_i
|
|
29
|
-
|
|
30
|
-
case method
|
|
31
|
-
# GET is how clients subscribe to the queue. When a messages comes in, we flush out a response,
|
|
32
|
-
# close down the requeust, and the client then reconnects.
|
|
33
|
-
when 'GET'
|
|
34
|
-
Firehose.logger.debug "HTTP GET with last_sequence #{last_sequence} for path #{path} with query #{env["QUERY_STRING"].inspect} and params #{req.params.inspect}"
|
|
35
|
-
EM.next_tick do
|
|
36
|
-
|
|
37
|
-
if last_sequence < 0
|
|
38
|
-
env['async.callback'].call response(400, "The last_message_sequence parameter may not be less than zero", response_headers(env))
|
|
39
|
-
else
|
|
40
|
-
Server::Channel.new(path).next_message(last_sequence, :timeout => timeout).callback do |message, sequence|
|
|
41
|
-
env['async.callback'].call response(200, wrap_frame(message, sequence), response_headers(env))
|
|
42
|
-
end.errback do |e|
|
|
43
|
-
if e == :timeout
|
|
44
|
-
env['async.callback'].call response(204, '', response_headers(env))
|
|
45
|
-
else
|
|
46
|
-
Firehose.logger.error "Unexpected error when trying to GET last_sequence #{last_sequence} for path #{path}: #{e.inspect}"
|
|
47
|
-
env['async.callback'].call response(500, 'Unexpected error', response_headers(env))
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Tell the web server that this will be an async response.
|
|
55
|
-
ASYNC_RESPONSE
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
Firehose.logger.debug "HTTP #{method} not supported"
|
|
59
|
-
response(501, "#{method} not supported.")
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def wrap_frame(message, last_sequence)
|
|
67
|
-
JSON.generate :message => message, :last_sequence => last_sequence
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# If the request is a CORS request, return those headers, otherwise don't worry 'bout it
|
|
71
|
-
def response_headers(env)
|
|
72
|
-
cors_origin(env) ? cors_headers(env) : {}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def cors_origin(env)
|
|
76
|
-
env['HTTP_ORIGIN']
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def cors_headers(env)
|
|
80
|
-
# TODO seperate out CORS logic as an async middleware with a Goliath web server.
|
|
81
|
-
{'Access-Control-Allow-Origin' => cors_origin(env)}
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
require 'faye/websocket'
|
|
2
|
-
require 'json'
|
|
3
|
-
|
|
4
|
-
module Firehose
|
|
5
|
-
module Rack
|
|
6
|
-
class Consumer
|
|
7
|
-
class WebSocket
|
|
8
|
-
# Setup a handler for the websocket connection.
|
|
9
|
-
def call(env)
|
|
10
|
-
ws = Faye::WebSocket.new(env)
|
|
11
|
-
Handler.new(ws)
|
|
12
|
-
ws.rack_response
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Determine if the rack request is a WebSocket request.
|
|
16
|
-
def self.request?(env)
|
|
17
|
-
Faye::WebSocket.websocket?(env)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Manages connection state for the web socket that's connected
|
|
21
|
-
# by the Consumer::WebSocket class. Deals with message sequence,
|
|
22
|
-
# connection, failures, and subscription state.
|
|
23
|
-
class Handler
|
|
24
|
-
def initialize(ws)
|
|
25
|
-
@ws = ws
|
|
26
|
-
@req = ::Rack::Request.new ws.env
|
|
27
|
-
# Setup the event handlers from this class.
|
|
28
|
-
@ws.onopen = method :open
|
|
29
|
-
@ws.onclose = method :close
|
|
30
|
-
@ws.onerror = method :error
|
|
31
|
-
@ws.onmessage = method :message
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Subscribe the client to the channel on the server. Asks for
|
|
35
|
-
# the last sequence for clients that reconnect.
|
|
36
|
-
def subscribe(last_sequence)
|
|
37
|
-
@subscribed = true
|
|
38
|
-
@channel = Server::Channel.new @req.path
|
|
39
|
-
@deferrable = @channel.next_message last_sequence
|
|
40
|
-
@deferrable.callback do |message, sequence|
|
|
41
|
-
Firehose.logger.debug "WS sent `#{message}` to `#{@req.path}` with sequence `#{sequence}`"
|
|
42
|
-
@ws.send self.class.wrap_frame(message, last_sequence)
|
|
43
|
-
subscribe sequence
|
|
44
|
-
end
|
|
45
|
-
@deferrable.errback do |e|
|
|
46
|
-
EM.next_tick { raise e.inspect } unless e == :disconnect
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Manages messages sent from the connect client to the server. This is mostly
|
|
51
|
-
# used to handle heart-beats that are designed to prevent the WebSocket connection
|
|
52
|
-
# from timing out from inactivity.
|
|
53
|
-
def message(event)
|
|
54
|
-
msg = JSON.parse(event.data, :symbolize_names => true) rescue {}
|
|
55
|
-
seq = msg[:message_sequence]
|
|
56
|
-
if msg[:ping] == 'PING'
|
|
57
|
-
Firehose.logger.debug "WS ping received, sending pong"
|
|
58
|
-
@ws.send JSON.generate :pong => 'PONG'
|
|
59
|
-
elsif !@subscribed && seq.kind_of?(Integer)
|
|
60
|
-
Firehose.logger.debug "Subscribing at message_sequence #{seq}"
|
|
61
|
-
subscribe seq
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Log a message that the client has connected.
|
|
66
|
-
def open(event)
|
|
67
|
-
Firehose.logger.debug "WebSocket subscribed to `#{@req.path}`. Waiting for message_sequence..."
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Log a message that hte client has disconnected and reset the state for the class. Clean
|
|
71
|
-
# up the subscribers to the channels.
|
|
72
|
-
def close(event)
|
|
73
|
-
if @deferrable
|
|
74
|
-
@deferrable.fail :disconnect
|
|
75
|
-
@channel.unsubscribe(@deferrable) if @channel
|
|
76
|
-
end
|
|
77
|
-
Firehose.logger.debug "WS connection `#{@req.path}` closing. Code: #{event.code.inspect}; Reason #{event.reason.inspect}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Log errors if a socket fails. `close` will fire after this to clean up any
|
|
81
|
-
# remaining connectons.
|
|
82
|
-
def error(event)
|
|
83
|
-
Firehose.logger.error "WS connection `#{@req.path}` error. Message: `#{event.message.inspect}`"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Wrap a message in a sequence so that the client can record this and give us
|
|
87
|
-
# the sequence when it reconnects.
|
|
88
|
-
def self.wrap_frame(message, last_sequence)
|
|
89
|
-
JSON.generate :message => message, :last_sequence => last_sequence
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
module Firehose
|
|
4
|
-
module Rack
|
|
5
|
-
# Handles a subscription request over HTTP or WebSockets depeding on its abilities and
|
|
6
|
-
# binds that to the Firehose::Server::Subscription class, which is bound to a channel that
|
|
7
|
-
# gets published to.
|
|
8
|
-
class Consumer
|
|
9
|
-
# Rack consumer transports
|
|
10
|
-
autoload :HttpLongPoll, 'firehose/rack/consumer/http_long_poll'
|
|
11
|
-
autoload :WebSocket, 'firehose/rack/consumer/web_socket'
|
|
12
|
-
|
|
13
|
-
# Let the client configure the consumer on initialization.
|
|
14
|
-
def initialize
|
|
15
|
-
yield self if block_given?
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(env)
|
|
19
|
-
websocket_request?(env) ? websocket.call(env) : http_long_poll.call(env)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Memoized instance of web socket that can be configured from the rack app.
|
|
23
|
-
def websocket
|
|
24
|
-
@web_socket ||= WebSocket.new
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Memoized instance of http long poll handler that can be configured from the rack app.
|
|
28
|
-
def http_long_poll
|
|
29
|
-
@http_long_poll ||= HttpLongPoll.new
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
# Determine if the incoming request is a websocket request.
|
|
34
|
-
def websocket_request?(env)
|
|
35
|
-
Firehose::Rack::Consumer::WebSocket.request?(env)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
data/lib/firehose/rack/ping.rb
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
require 'em-hiredis'
|
|
2
|
-
|
|
3
|
-
module Firehose
|
|
4
|
-
module Rack
|
|
5
|
-
# Allows the Firehose client to periodically "ping" the server
|
|
6
|
-
# so that the connection isn't timed out by browsers or proxies from
|
|
7
|
-
# inactivity.
|
|
8
|
-
class Ping
|
|
9
|
-
attr_reader :redis
|
|
10
|
-
|
|
11
|
-
def initialize(redis=nil)
|
|
12
|
-
@redis = redis
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call(env)
|
|
16
|
-
PingCheck.new(env, redis).call
|
|
17
|
-
ASYNC_RESPONSE
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Encapsulate this in a class so we aren't passing a bunch of variables around
|
|
21
|
-
class PingCheck
|
|
22
|
-
include Firehose::Rack::Helpers
|
|
23
|
-
|
|
24
|
-
attr_reader :req, :env, :key, :redis
|
|
25
|
-
|
|
26
|
-
TEST_VALUE = 'Firehose Healthcheck Test Value'
|
|
27
|
-
SECONDS_TO_EXPIRE = 60
|
|
28
|
-
|
|
29
|
-
def self.redis
|
|
30
|
-
@redis ||= EM::Hiredis.connect
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def initialize(env, redis=nil)
|
|
34
|
-
@redis = redis || self.class.redis
|
|
35
|
-
@env = env
|
|
36
|
-
@req = env['parsed_request'] ||= ::Rack::Request.new(env)
|
|
37
|
-
@key = "/firehose/ping/#{Time.now.to_i}/#{rand}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def call
|
|
41
|
-
log req, 'started'
|
|
42
|
-
test_redis
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def log(req, msg)
|
|
49
|
-
Firehose.logger.debug "HTTP PING request for path '#{req.path}': #{msg}"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def test_redis
|
|
53
|
-
redis.set(key, TEST_VALUE).
|
|
54
|
-
callback { expire_key }.
|
|
55
|
-
callback { read_and_respond }.
|
|
56
|
-
errback do |e|
|
|
57
|
-
log req, "failed with write value to redis: #{e.inspect}"
|
|
58
|
-
env['async.callback'].call response(500)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def expire_key
|
|
63
|
-
redis.expire(key, SECONDS_TO_EXPIRE).
|
|
64
|
-
errback do
|
|
65
|
-
log req, "failed to expire key #{key.inspect}. If this key is not manually deleted, it may cause a memory leak."
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def read_and_respond
|
|
70
|
-
redis.get(key).
|
|
71
|
-
callback do |val|
|
|
72
|
-
if val == TEST_VALUE
|
|
73
|
-
log req, 'succeeded'
|
|
74
|
-
env['async.callback'].call response(200)
|
|
75
|
-
else
|
|
76
|
-
log req, "failed with unexpected value retrieved from redis: #{val.inspect}"
|
|
77
|
-
env['async.callback'].call response(500)
|
|
78
|
-
end
|
|
79
|
-
end.
|
|
80
|
-
errback do |e|
|
|
81
|
-
log req, "failed with read value from redis: #{e.inspect}"
|
|
82
|
-
env['async.callback'].call response(500)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Rack
|
|
3
|
-
class Publisher
|
|
4
|
-
include Firehose::Rack::Helpers
|
|
5
|
-
|
|
6
|
-
def call(env)
|
|
7
|
-
req = env['parsed_request'] ||= ::Rack::Request.new(env)
|
|
8
|
-
path = req.path
|
|
9
|
-
method = req.request_method
|
|
10
|
-
cache_control = {}
|
|
11
|
-
|
|
12
|
-
# Parse out cache control directives from the Cache-Control header.
|
|
13
|
-
if cache_control_header = env['HTTP_CACHE_CONTROL']
|
|
14
|
-
cache_control = cache_control_header.split(',').map(&:strip).inject({}) do |memo, directive|
|
|
15
|
-
key, value = directive.split('=')
|
|
16
|
-
memo[key.downcase] = value
|
|
17
|
-
memo
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Read the max-age directive from the cache so that we can set a TTL on the redis key. This will
|
|
22
|
-
# prevent stale content from being served up to the client.
|
|
23
|
-
ttl = cache_control['max-age']
|
|
24
|
-
|
|
25
|
-
if method == 'PUT'
|
|
26
|
-
EM.next_tick do
|
|
27
|
-
body = env['rack.input'].read
|
|
28
|
-
Firehose.logger.debug "HTTP published #{body.inspect} to #{path.inspect} with ttl #{ttl.inspect}"
|
|
29
|
-
publisher.publish(path, body, :ttl => ttl).callback do
|
|
30
|
-
env['async.callback'].call [202, {'Content-Type' => 'text/plain', 'Content-Length' => '0'}, []]
|
|
31
|
-
env['async.callback'].call response(202, '', 'Content-Type' => 'text/plain')
|
|
32
|
-
end.errback do |e|
|
|
33
|
-
Firehose.logger.debug "Error publishing: #{e.inspect}"
|
|
34
|
-
env['async.callback'].call response(500, 'Error when trying to publish', 'Content-Type' => 'text/plain')
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Tell the web server that this will be an async response.
|
|
39
|
-
ASYNC_RESPONSE
|
|
40
|
-
else
|
|
41
|
-
Firehose.logger.debug "HTTP #{method} not supported"
|
|
42
|
-
msg = "#{method} not supported."
|
|
43
|
-
[501, {'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s}, [msg]]
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
def publisher
|
|
49
|
-
@publisher ||= Firehose::Server::Publisher.new
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|