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.
Files changed (104) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +31 -10
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +379 -187
  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 +930 -17
  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 +39 -366
  25. data/.codeclimate.yml +0 -29
  26. data/.dockerignore +0 -2
  27. data/.env.sample +0 -17
  28. data/.gitignore +0 -14
  29. data/.rspec +0 -1
  30. data/.rubocop.yml +0 -1156
  31. data/.ruby-version +0 -1
  32. data/.travis.yml +0 -12
  33. data/Dockerfile +0 -11
  34. data/Gemfile +0 -6
  35. data/Procfile +0 -1
  36. data/Procfile.dev +0 -3
  37. data/Rakefile +0 -64
  38. data/bin/firehose +0 -9
  39. data/coffeelint.json +0 -129
  40. data/config/rainbows.rb +0 -17
  41. data/docker-compose.yml +0 -17
  42. data/firehose.gemspec +0 -45
  43. data/karma.config.coffee +0 -89
  44. data/lib/assets/javascripts/firehose/base.js.coffee +0 -4
  45. data/lib/assets/javascripts/firehose/consumer.js.coffee +0 -86
  46. data/lib/assets/javascripts/firehose/core.js.coffee +0 -6
  47. data/lib/assets/javascripts/firehose/long_poll.js.coffee +0 -210
  48. data/lib/assets/javascripts/firehose/multiplexed_consumer.js.coffee +0 -74
  49. data/lib/assets/javascripts/firehose/transport.js.coffee +0 -44
  50. data/lib/assets/javascripts/firehose/version.js.coffee.erb +0 -2
  51. data/lib/assets/javascripts/firehose/web_socket.js.coffee +0 -148
  52. data/lib/assets/javascripts/firehose.js.coffee +0 -2
  53. data/lib/firehose/assets.rb +0 -56
  54. data/lib/firehose/cli.rb +0 -79
  55. data/lib/firehose/client/consumer.rb +0 -94
  56. data/lib/firehose/client/producer.rb +0 -114
  57. data/lib/firehose/client.rb +0 -10
  58. data/lib/firehose/logging.rb +0 -32
  59. data/lib/firehose/rack/app.rb +0 -49
  60. data/lib/firehose/rack/consumer/http_long_poll.rb +0 -159
  61. data/lib/firehose/rack/consumer/web_socket.rb +0 -200
  62. data/lib/firehose/rack/consumer.rb +0 -78
  63. data/lib/firehose/rack/ping.rb +0 -88
  64. data/lib/firehose/rack/publisher.rb +0 -59
  65. data/lib/firehose/rack.rb +0 -32
  66. data/lib/firehose/rails.rb +0 -7
  67. data/lib/firehose/server/app.rb +0 -52
  68. data/lib/firehose/server/channel.rb +0 -72
  69. data/lib/firehose/server/message_buffer.rb +0 -59
  70. data/lib/firehose/server/publisher.rb +0 -133
  71. data/lib/firehose/server/redis.rb +0 -32
  72. data/lib/firehose/server/subscriber.rb +0 -51
  73. data/lib/firehose/version.rb +0 -4
  74. data/package.json +0 -17
  75. data/spec/integrations/integration_test_helper.rb +0 -22
  76. data/spec/integrations/rainbows_spec.rb +0 -7
  77. data/spec/integrations/shared_examples.rb +0 -202
  78. data/spec/integrations/thin_spec.rb +0 -7
  79. data/spec/javascripts/firehose/base_spec.coffee +0 -2
  80. data/spec/javascripts/firehose/consumer_spec.coffee +0 -45
  81. data/spec/javascripts/firehose/long_poll_spec.coffee +0 -44
  82. data/spec/javascripts/firehose/multiplexed_consumer_spec.coffee +0 -72
  83. data/spec/javascripts/firehose/transport_spec.coffee +0 -30
  84. data/spec/javascripts/firehose/websocket_spec.coffee +0 -27
  85. data/spec/javascripts/helpers/spec_helper.js +0 -31
  86. data/spec/javascripts/support/jquery-1.11.1.js +0 -10308
  87. data/spec/javascripts/support/json2.js +0 -487
  88. data/spec/javascripts/support/sinon-1.7.3.js +0 -4290
  89. data/spec/javascripts/support/spec_helper.coffee +0 -3
  90. data/spec/lib/assets_spec.rb +0 -35
  91. data/spec/lib/client/consumer_spec.rb +0 -13
  92. data/spec/lib/client/producer_spec.rb +0 -71
  93. data/spec/lib/firehose_spec.rb +0 -7
  94. data/spec/lib/rack/consumer/http_long_poll_spec.rb +0 -30
  95. data/spec/lib/rack/consumer_spec.rb +0 -19
  96. data/spec/lib/rack/ping_spec.rb +0 -28
  97. data/spec/lib/rack/publisher_spec.rb +0 -34
  98. data/spec/lib/server/app_spec.rb +0 -15
  99. data/spec/lib/server/channel_spec.rb +0 -178
  100. data/spec/lib/server/message_buffer_spec.rb +0 -148
  101. data/spec/lib/server/publisher_spec.rb +0 -91
  102. data/spec/lib/server/redis_spec.rb +0 -13
  103. data/spec/lib/server/subscriber_spec.rb +0 -70
  104. data/spec/spec_helper.rb +0 -84
@@ -1,78 +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
- MULTIPLEX_CHANNEL = "channels@firehose"
14
-
15
- def self.multiplexing_request?(env)
16
- env["PATH_INFO"].include? MULTIPLEX_CHANNEL
17
- end
18
-
19
- def self.multiplex_subscriptions(request)
20
- if request.get?
21
- query_string_subscriptions(request.env)
22
- elsif request.post?
23
- post_subscriptions(request)
24
- end
25
- end
26
-
27
- def self.query_string_subscriptions(env)
28
- query_params = ::Rack::Utils.parse_query(env["QUERY_STRING"])
29
-
30
- query_params["subscribe"].to_s.split(",").map do |sub|
31
- chan, last_sequence = sub.split("!")
32
- last_sequence = last_sequence.to_i
33
- last_sequence = 0 if last_sequence < 0
34
- {
35
- channel: chan,
36
- message_sequence: last_sequence
37
- }
38
- end
39
- end
40
-
41
- def self.post_subscriptions(request)
42
- body = request.body.read
43
- subs = JSON.parse(body).map do |chan, last_sequence|
44
- last_sequence = 0 if last_sequence < 0
45
- {
46
- channel: chan,
47
- message_sequence: last_sequence
48
- }
49
- end
50
- end
51
-
52
- # Let the client configure the consumer on initialization.
53
- def initialize
54
- yield self if block_given?
55
- end
56
-
57
- def call(env)
58
- websocket_request?(env) ? websocket.call(env) : http_long_poll.call(env)
59
- end
60
-
61
- # Memoized instance of web socket that can be configured from the rack app.
62
- def websocket
63
- @web_socket ||= WebSocket.new
64
- end
65
-
66
- # Memoized instance of http long poll handler that can be configured from the rack app.
67
- def http_long_poll
68
- @http_long_poll ||= HttpLongPoll.new
69
- end
70
-
71
- private
72
- # Determine if the incoming request is a websocket request.
73
- def websocket_request?(env)
74
- Firehose::Rack::Consumer::WebSocket.request?(env)
75
- end
76
- end
77
- end
78
- 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 ||= Firehose::Server.redis.connection
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,59 +0,0 @@
1
- require "rack/utils"
2
-
3
- module Firehose
4
- module Rack
5
- class Publisher
6
- include Firehose::Rack::Helpers
7
-
8
- def call(env)
9
- req = env['parsed_request'] ||= ::Rack::Request.new(env)
10
- path = req.path
11
- method = req.request_method
12
- cache_control = {}
13
-
14
- # Parse out cache control directives from the Cache-Control header.
15
- if cache_control_header = env['HTTP_CACHE_CONTROL']
16
- cache_control = cache_control_header.split(',').map(&:strip).inject({}) do |memo, directive|
17
- key, value = directive.split('=')
18
- memo[key.downcase] = value
19
- memo
20
- end
21
- end
22
-
23
- # Read the max-age directive from the cache so that we can set a TTL on the redis key. This will
24
- # prevent stale content from being served up to the client.
25
- ttl = cache_control['max-age']
26
-
27
- if method == 'PUT'
28
- EM.next_tick do
29
- body = env['rack.input'].read
30
- Firehose.logger.debug "HTTP published #{body.inspect} to #{path.inspect} with ttl #{ttl.inspect}"
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
36
- env['async.callback'].call [202, {'Content-Type' => 'text/plain', 'Content-Length' => '0'}, []]
37
- env['async.callback'].call response(202, '', 'Content-Type' => 'text/plain')
38
- end.errback do |e|
39
- Firehose.logger.debug "Error publishing: #{e.inspect}"
40
- env['async.callback'].call response(500, 'Error when trying to publish', 'Content-Type' => 'text/plain')
41
- end
42
- end
43
-
44
- # Tell the web server that this will be an async response.
45
- ASYNC_RESPONSE
46
- else
47
- Firehose.logger.debug "HTTP #{method} not supported"
48
- msg = "#{method} not supported."
49
- [501, {'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s}, [msg]]
50
- end
51
- end
52
-
53
- private
54
- def publisher
55
- @publisher ||= Firehose::Server::Publisher.new
56
- end
57
- end
58
- end
59
- end
data/lib/firehose/rack.rb DELETED
@@ -1,32 +0,0 @@
1
- module Firehose
2
- module Rack
3
- autoload :Consumer, 'firehose/rack/consumer'
4
- autoload :Publisher, 'firehose/rack/publisher'
5
- autoload :Ping, 'firehose/rack/ping'
6
- autoload :App, 'firehose/rack/app'
7
-
8
- # Evented web servers recognize the -1 HTTP code as a response deferral, which
9
- # is needed to stream responses via WebSockets or HTTP long polling.
10
- ASYNC_RESPONSE = [-1, {}, []].freeze
11
-
12
- # Normally we'd want to use a custom header to reduce the likelihood of some
13
- # HTTP middleware clobbering the value. But Safari seems to ignore our CORS
14
- # header instructions, so we are using 'pragma' because it is always allowed.
15
- LAST_MESSAGE_SEQUENCE_HEADER = 'Pragma'
16
-
17
- # Rack wants the header to start with HTTP, so we deal with that here.
18
- RACK_LAST_MESSAGE_SEQUENCE_HEADER = "HTTP_#{LAST_MESSAGE_SEQUENCE_HEADER.upcase.gsub('-', '_')}"
19
-
20
- # Disable CORS preflight caches for requests in development mode.
21
- CORS_OPTIONS_MAX_AGE = ENV['RACK_ENV'] == 'development' ? '1' : '1728000'
22
-
23
- module Helpers
24
- # Calculates the content of a message body for the response so that HTTP Keep-Alive
25
- # connections work.
26
- def response(status, body='', headers={})
27
- headers = {'Content-Length' => body.size.to_s}.merge(headers)
28
- [status, headers, [body]]
29
- end
30
- end
31
- end
32
- end
@@ -1,7 +0,0 @@
1
- module Firehose
2
- module Rails
3
- class Engine < ::Rails::Engine
4
- # enable rails to pick up lib/assets/* via sprockets.
5
- end
6
- end
7
- end
@@ -1,52 +0,0 @@
1
- module Firehose
2
- module Server
3
- # Configure servers that are booted with-out going through Rack. This is mostly used by
4
- # the `firehose server` CLI command or for testing. Production configurations are likely
5
- # to boot with custom rack configurations.
6
- class App
7
- def initialize(opts={})
8
- @port = opts[:port] || Firehose::URI.port
9
- @host = opts[:host] || Firehose::URI.host
10
- @server = opts[:server] || :rainbows
11
-
12
- Firehose.logger.info "Starting #{Firehose::VERSION} '#{Firehose::CODENAME}', in #{ENV['RACK_ENV']}"
13
- end
14
-
15
- def start
16
- self.send("start_#{@server}")
17
- end
18
-
19
- private
20
- # Boot the Firehose server with the Rainbows app server.
21
- def start_rainbows
22
- require 'rainbows'
23
- Faye::WebSocket.load_adapter('rainbows')
24
-
25
- rackup = Unicorn::Configurator::RACKUP
26
- rackup[:port] = @port if @port
27
- rackup[:host] = @host if @host
28
- rackup[:set_listener] = true
29
- opts = rackup[:options]
30
- opts[:config_file] = File.expand_path('../../../../config/rainbows.rb', __FILE__)
31
-
32
- server = Rainbows::HttpServer.new(Firehose::Rack::App.new, opts)
33
- server.start.join
34
- end
35
-
36
- # Boot the Firehose server with the Thin app server.
37
- def start_thin
38
- require 'thin'
39
-
40
- Faye::WebSocket.load_adapter('thin')
41
-
42
- # TODO: See if we can just set Thin to use Firehose.logger instead of
43
- # printing out messages by itself.
44
- Thin::Logging.silent = true if Firehose.logger.level == Logger::ERROR
45
-
46
- server = Thin::Server.new(@host, @port) do
47
- run Firehose::Rack::App.new
48
- end.start
49
- end
50
- end
51
- end
52
- end
@@ -1,72 +0,0 @@
1
- module Firehose
2
- module Server
3
- # Connects to a specific channel on Redis and listens for messages to notify subscribers.
4
- class Channel
5
- attr_reader :channel_key, :list_key, :sequence_key
6
- attr_reader :redis, :subscriber
7
-
8
- def self.redis
9
- @redis ||= Firehose::Server.redis.connection
10
- end
11
-
12
- def self.subscriber
13
- @subscriber ||= Server::Subscriber.new(Firehose::Server.redis.connection)
14
- end
15
-
16
- def initialize(channel_key, redis=self.class.redis, subscriber=self.class.subscriber)
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)
22
- end
23
-
24
- def next_messages(consumer_sequence=nil, options={})
25
- deferrable = EM::DefaultDeferrable.new
26
- deferrable.errback {|e| EM.next_tick { raise e } unless [:timeout, :disconnect].include?(e) }
27
-
28
- redis.multi
29
- redis.get(sequence_key).
30
- errback {|e| deferrable.fail e }
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).
33
- errback {|e| deferrable.fail e }
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}"
43
- # Either this resource has never been seen before or we are all caught up.
44
- # Subscribe and hope something gets published to this end-point.
45
- subscribe(deferrable, options[:timeout])
46
- else # Either the client is under water or caught up to head.
47
- deferrable.succeed buffer.remaining_messages
48
- end
49
- end.errback {|e| deferrable.fail e }
50
-
51
- deferrable
52
- end
53
-
54
- def unsubscribe(deferrable)
55
- subscriber.unsubscribe channel_key, deferrable
56
- end
57
-
58
- private
59
- def subscribe(deferrable, timeout=nil)
60
- subscriber.subscribe(channel_key, deferrable)
61
- if timeout
62
- timer = EventMachine::Timer.new(timeout) do
63
- deferrable.fail :timeout
64
- unsubscribe deferrable
65
- end
66
- # Cancel the timer if when the deferrable succeeds
67
- deferrable.callback { timer.cancel }
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,59 +0,0 @@
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,133 +0,0 @@
1
- module Firehose
2
- module Server
3
- class Publisher
4
- # Seconds that the message buffer should live before Redis expires it.
5
- TTL = 60*60*24
6
-
7
- # Delimited used to frame different parts of a message that's published
8
- # over Firehose.
9
- PAYLOAD_DELIMITER = "\n"
10
-
11
- # Publish a message to a Firehose channel via Redis.
12
- def publish(channel_key, message, opts={})
13
- # How long should we hang on to the resource once is published?
14
- ttl = (opts[:ttl] || TTL).to_i
15
- buffer_size = (opts[:buffer_size] || MessageBuffer::DEFAULT_SIZE).to_i
16
-
17
- # TODO hi-redis isn't that awesome... we have to setup an errback per even for wrong
18
- # commands because of the lack of a method_missing whitelist. Perhaps implement a whitelist in
19
- # em-hiredis or us a diff lib?
20
- if (deferrable = opts[:deferrable]).nil?
21
- deferrable = EM::DefaultDeferrable.new
22
- deferrable.errback do |e|
23
- # Handle missing Lua publishing script in cache
24
- # (such as Redis restarting or someone executing SCRIPT FLUSH)
25
- if e.message =~ /NOSCRIPT/
26
- deferrable.succeed
27
- EM.next_tick do
28
- @publish_script_digest = nil
29
- combined_opts = opts.merge :deferrable => deferrable
30
- self.publish channel_key, message, combined_opts
31
- end
32
- else
33
- EM.next_tick { raise e }
34
- end
35
- end
36
- end
37
-
38
- if @publish_script_digest.nil?
39
- register_publish_script.errback do |e|
40
- deferrable.fail e
41
- end.callback do |digest|
42
- @publish_script_digest = digest
43
- Firehose.logger.debug "Registered Lua publishing script with Redis => #{digest}"
44
- eval_publish_script channel_key, message, ttl, buffer_size, deferrable
45
- end
46
- else
47
- eval_publish_script channel_key, message, ttl, buffer_size, deferrable
48
- end
49
-
50
- deferrable
51
- end
52
-
53
- private
54
- def redis
55
- @redis ||= Firehose::Server.redis.connection
56
- end
57
-
58
- # Serialize components of a message into something that can be dropped into Redis.
59
- def self.to_payload(channel_key, sequence, message)
60
- [channel_key, sequence, message].join(PAYLOAD_DELIMITER)
61
- end
62
-
63
- # Deserialize components of a message back into Ruby.
64
- def self.from_payload(payload)
65
- @payload_size ||= method(:to_payload).arity
66
- payload.split(PAYLOAD_DELIMITER, @payload_size)
67
- end
68
-
69
- # TODO: Make this FAR more robust. Ideally we'd whitelist the permitted
70
- # characters and then escape or remove everything else.
71
- # See: http://en.wikibooks.org/wiki/Lua_Programming/How_to_Lua/escape_sequence
72
- def lua_escape(str)
73
- str.gsub(/\\/,'\\\\\\').gsub(/"/,'\"').gsub(/\n/,'\n').gsub(/\r/,'\r')
74
- end
75
-
76
- def register_publish_script
77
- redis.script 'LOAD', REDIS_PUBLISH_SCRIPT
78
- end
79
-
80
- def eval_publish_script(channel_key, message, ttl, buffer_size, deferrable)
81
- list_key = Server::Redis.key(channel_key, :list)
82
- script_args = [
83
- Server::Redis.key(channel_key, :sequence),
84
- list_key,
85
- Server::Redis.key(:channel_updates),
86
- ttl,
87
- message,
88
- buffer_size,
89
- PAYLOAD_DELIMITER,
90
- channel_key
91
- ]
92
-
93
- redis.evalsha(
94
- @publish_script_digest, script_args.length, *script_args
95
- ).errback do |e|
96
- deferrable.fail e
97
- end.callback do |sequence|
98
- Firehose.logger.debug "Redis stored/published `#{message}` to list `#{list_key}` with sequence `#{sequence}`"
99
- deferrable.succeed
100
- end
101
- end
102
-
103
- REDIS_PUBLISH_SCRIPT = <<-LUA
104
- local sequence_key = KEYS[1]
105
- local list_key = KEYS[2]
106
- local channel_key = KEYS[3]
107
- local ttl = KEYS[4]
108
- local message = KEYS[5]
109
- local buffer_size = KEYS[6]
110
- local payload_delimiter = KEYS[7]
111
- local firehose_resource = KEYS[8]
112
-
113
- local current_sequence = redis.call('get', sequence_key)
114
- if current_sequence == nil or current_sequence == false then
115
- current_sequence = 0
116
- end
117
-
118
- local sequence = current_sequence + 1
119
- local message_payload = firehose_resource .. payload_delimiter .. sequence .. payload_delimiter .. message
120
-
121
- redis.call('set', sequence_key, sequence)
122
- redis.call('expire', sequence_key, ttl)
123
- redis.call('lpush', list_key, message)
124
- redis.call('ltrim', list_key, 0, buffer_size - 1)
125
- redis.call('expire', list_key, ttl)
126
- redis.call('publish', channel_key, message_payload)
127
-
128
- return sequence
129
- LUA
130
-
131
- end
132
- end
133
- end
@@ -1,32 +0,0 @@
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
@@ -1,51 +0,0 @@
1
- module Firehose
2
- module Server
3
- # Setups a connetion to Redis to listen for new resources...
4
- class Subscriber
5
- attr_reader :pubsub
6
-
7
- def initialize(redis)
8
- @pubsub = redis.pubsub
9
- # TODO: Instead of just raising an exception, it would probably be better
10
- # for the errback to set some sort of 'disconnected' state. Then
11
- # whenever a deferrable was 'subscribed' we could instantly fail
12
- # the deferrable with whatever connection error we had.
13
- # An alternative which would have a similar result would be to
14
- # subscribe lazily (i.e. not until we have a deferrable to subscribe).
15
- # Then, if connecting failed, it'd be super easy to fail the deferrable
16
- # with the same error.
17
- # The final goal is to allow the failed deferrable bubble back up
18
- # so we can send back a nice, clean 500 error to the client.
19
- channel_updates_key = Server::Redis.key('channel_updates')
20
- pubsub.subscribe(channel_updates_key).
21
- errback{|e| EM.next_tick { raise e } }.
22
- callback { Firehose.logger.debug "Redis subscribed to `#{channel_updates_key}`" }
23
- pubsub.on(:message) do |_, payload|
24
- channel_key, channel_sequence, message = Server::Publisher.from_payload(payload)
25
- messages = [ MessageBuffer::Message.new(message, channel_sequence.to_i) ]
26
- if deferrables = subscriptions.delete(channel_key)
27
- Firehose.logger.debug "Redis notifying #{deferrables.count} deferrable(s) at `#{channel_key}` with channel_sequence `#{channel_sequence}` and message `#{message}`"
28
- deferrables.each do |deferrable|
29
- Firehose.logger.debug "Sending message #{message} and channel_sequence #{channel_sequence} to client from subscriber"
30
- deferrable.succeed messages
31
- end
32
- end
33
- end
34
- end
35
-
36
- def subscribe(channel_key, deferrable)
37
- subscriptions[channel_key].push deferrable
38
- end
39
-
40
- def unsubscribe(channel_key, deferrable)
41
- subscriptions[channel_key].delete deferrable
42
- subscriptions.delete(channel_key) if subscriptions[channel_key].empty?
43
- end
44
-
45
- private
46
- def subscriptions
47
- @subscriptions ||= Hash.new{|h,k| h[k] = []}
48
- end
49
- end
50
- end
51
- end
@@ -1,4 +0,0 @@
1
- module Firehose
2
- VERSION = "1.3.6"
3
- CODENAME = "Oops I did it again"
4
- end