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.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +36 -0
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +389 -139
  5. data/app/assets/javascripts/firehose.js +258 -0
  6. data/config/importmap.rb +1 -0
  7. data/db/migrate/20260214185244_create_firehose_events.rb +23 -0
  8. data/lib/commands/firehose_command.rb +119 -0
  9. data/lib/firehose/channel.rb +40 -0
  10. data/lib/firehose/cleanup_job.rb +21 -0
  11. data/lib/firehose/engine.rb +36 -0
  12. data/lib/firehose/helper.rb +40 -0
  13. data/lib/firehose/models/channel.rb +12 -0
  14. data/lib/firehose/models/message.rb +11 -0
  15. data/lib/firehose/queue.rb +44 -0
  16. data/lib/firehose/server.rb +931 -18
  17. data/lib/firehose/sse.rb +187 -0
  18. data/lib/firehose/stream.rb +32 -0
  19. data/lib/firehose/streamable.rb +25 -0
  20. data/lib/firehose/websocket.rb +242 -0
  21. data/lib/firehose.rb +67 -15
  22. data/lib/generators/firehose/install/install_generator.rb +43 -0
  23. data/lib/generators/firehose/install/templates/firehose_controller.rb +16 -0
  24. metadata +40 -428
  25. data/.env.sample +0 -17
  26. data/.gitignore +0 -12
  27. data/.rbenv-version +0 -1
  28. data/.rspec +0 -1
  29. data/.travis.yml +0 -16
  30. data/Gemfile +0 -4
  31. data/Guardfile +0 -31
  32. data/Procfile +0 -1
  33. data/Procfile.dev +0 -4
  34. data/Rakefile +0 -53
  35. data/bin/firehose +0 -9
  36. data/config/evergreen.rb +0 -9
  37. data/config/rainbows.rb +0 -17
  38. data/firehose.gemspec +0 -49
  39. data/lib/assets/javascripts/firehose/base.js.coffee +0 -4
  40. data/lib/assets/javascripts/firehose/consumer.js.coffee +0 -70
  41. data/lib/assets/javascripts/firehose/core.js.coffee +0 -5
  42. data/lib/assets/javascripts/firehose/long_poll.js.coffee +0 -149
  43. data/lib/assets/javascripts/firehose/transport.js.coffee +0 -42
  44. data/lib/assets/javascripts/firehose/version.js.coffee.erb +0 -2
  45. data/lib/assets/javascripts/firehose/web_socket.js.coffee +0 -102
  46. data/lib/assets/javascripts/firehose.js.coffee +0 -3
  47. data/lib/assets/javascripts/vendor/json2.js +0 -487
  48. data/lib/firehose/assets.rb +0 -56
  49. data/lib/firehose/cli.rb +0 -78
  50. data/lib/firehose/client/consumer.rb +0 -94
  51. data/lib/firehose/client/producer.rb +0 -108
  52. data/lib/firehose/client.rb +0 -10
  53. data/lib/firehose/logging.rb +0 -32
  54. data/lib/firehose/rack/app.rb +0 -49
  55. data/lib/firehose/rack/consumer/http_long_poll.rb +0 -86
  56. data/lib/firehose/rack/consumer/web_socket.rb +0 -95
  57. data/lib/firehose/rack/consumer.rb +0 -39
  58. data/lib/firehose/rack/ping.rb +0 -88
  59. data/lib/firehose/rack/publisher.rb +0 -53
  60. data/lib/firehose/rack.rb +0 -32
  61. data/lib/firehose/rails.rb +0 -7
  62. data/lib/firehose/server/app.rb +0 -52
  63. data/lib/firehose/server/channel.rb +0 -80
  64. data/lib/firehose/server/publisher.rb +0 -134
  65. data/lib/firehose/server/subscriber.rb +0 -51
  66. data/lib/firehose/version.rb +0 -4
  67. data/package.json +0 -5
  68. data/spec/integrations/integration_test_helper.rb +0 -22
  69. data/spec/integrations/rainbows_spec.rb +0 -7
  70. data/spec/integrations/shared_examples.rb +0 -120
  71. data/spec/integrations/thin_spec.rb +0 -7
  72. data/spec/javascripts/firehose/base_spec.coffee +0 -2
  73. data/spec/javascripts/firehose/consumer_spec.coffee +0 -45
  74. data/spec/javascripts/firehose/long_poll_spec.coffee +0 -44
  75. data/spec/javascripts/firehose/transport_spec.coffee +0 -32
  76. data/spec/javascripts/firehose/websocket_spec.coffee +0 -25
  77. data/spec/javascripts/helpers/spec_helper.js +0 -30
  78. data/spec/javascripts/support/sinon-1.7.3.js +0 -4290
  79. data/spec/lib/assets_spec.rb +0 -35
  80. data/spec/lib/client/consumer_spec.rb +0 -13
  81. data/spec/lib/client/producer_spec.rb +0 -71
  82. data/spec/lib/firehose_spec.rb +0 -7
  83. data/spec/lib/rack/consumer/http_long_poll_spec.rb +0 -12
  84. data/spec/lib/rack/consumer_spec.rb +0 -19
  85. data/spec/lib/rack/ping_spec.rb +0 -28
  86. data/spec/lib/rack/publisher_spec.rb +0 -34
  87. data/spec/lib/server/app_spec.rb +0 -15
  88. data/spec/lib/server/channel_spec.rb +0 -164
  89. data/spec/lib/server/publisher_spec.rb +0 -84
  90. data/spec/lib/server/subscriber_spec.rb +0 -69
  91. 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
@@ -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
@@ -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']
@@ -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
@@ -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