Redis + ActionController::Live threads not dying

前端 未结 6 1312
自闭症患者
自闭症患者 2020-11-28 04:09

Background: We\'ve built a chat feature in to one of our existing Rails applications. We\'re using the new ActionController::Live module and ru

相关标签:
6条回答
  • 2020-11-28 04:12

    I'm currently making an app that revolves around ActionController:Live, EventSource and Puma and for those that are encountering problems closing streams and such, instead of rescuing an IOError, in Rails 4.2 you need to rescue ClientDisconnected. Example:

    def stream
      #Begin is not required
      twitter_client = Twitter::Streaming::Client.new(config_params) do |obj|
        # Do something
      end
    rescue ClientDisconnected
      # Do something when disconnected
    ensure
      # Do something else to ensure the stream is closed
    end
    

    I found this handy tip from this forum post (all the way at the bottom): http://railscasts.com/episodes/401-actioncontroller-live?view=comments

    0 讨论(0)
  • 2020-11-28 04:17

    Here you are solution with timeout that will exit blocking Redis.(p)subscribe call and kill unused connection tread.

    class Stream::FixedController < StreamController
      def events
        # Rails reserve a db connection from connection pool for
        # each request, lets put it back into connection pool.
        ActiveRecord::Base.clear_active_connections!
    
        # Last time of any (except heartbeat) activity on stream
        # it mean last time of any message was send from server to client
        # or time of setting new connection
        @last_active = Time.zone.now
    
        # Redis (p)subscribe is blocking request so we need do some trick
        # to prevent it freeze request forever.
        redis.psubscribe("messages:*", 'heartbeat') do |on|
          on.pmessage do |pattern, event, data|
            # capture heartbeat from Redis pub/sub
            if event == 'heartbeat'
              # calculate idle time (in secounds) for this stream connection
              idle_time = (Time.zone.now - @last_active).to_i
    
              # Now we need to relase connection with Redis.(p)subscribe
              # chanel to allow go of any Exception (like connection closed)
              if idle_time > 4.minutes
                # unsubscribe from Redis because of idle time was to long
                # that's all - fix in (almost)one line :)
                redis.punsubscribe
              end
            else
              # save time of this (last) activity
              @last_active = Time.zone.now
            end
            # write to stream - even heartbeat - it's sometimes chance to
            # capture dissconection error before idle_time
            response.stream.write("event: #{event}\ndata: #{data}\n\n")
          end
        end
        # blicking end (no chance to get below this line without unsubscribe)
      rescue IOError
        Logs::Stream.info "Stream closed"
      rescue ClientDisconnected
        Logs::Stream.info "ClientDisconnected"
      rescue ActionController::Live::ClientDisconnected
        Logs::Stream.info "Live::ClientDisconnected"
      ensure
        Logs::Stream.info "Stream ensure close"
        redis.quit
        response.stream.close
      end
    end
    

    You have to use reds.(p)unsubscribe to end this blocking call. No exception can break this.

    My simple app with information about this fix: https://github.com/piotr-kedziak/redis-subscribe-stream-puma-fix

    0 讨论(0)
  • 2020-11-28 04:20

    Building on @James Boutcher, I used the following in clustered Puma with 2 workers, so that I have only 1 thread created for the heartbeat in config/initializers/redis.rb:

    config/puma.rb

    on_worker_boot do |index|
      puts "worker nb #{index.to_s} booting"
      create_heartbeat if index.to_i==0
    end
    
    def create_heartbeat
      puts "creating heartbeat"
      $redis||=Redis.new
      heartbeat = Thread.new do
        ActiveRecord::Base.connection_pool.release_connection
        begin
          while true
            hash={event: "heartbeat",data: "heartbeat"}
            $redis.publish("heartbeat",hash.to_json)
            sleep 20.seconds
          end
        ensure
          #no db connection anyway
        end
      end
    end
    
    0 讨论(0)
  • 2020-11-28 04:24

    Here's a potentially simpler solution which does not use a heartbeat. After much research and experimentation, here's the code I'm using with sinatra + sinatra sse gem (which should be easily adapted to Rails 4):

    class EventServer < Sinatra::Base
     include Sinatra::SSE
     set :connections, []
     .
     .
     .
     get '/channel/:channel' do
     .
     .
     .
      sse_stream do |out|
        settings.connections << out
        out.callback {
          puts 'Client disconnected from sse';
          settings.connections.delete(out);
        }
      redis.subscribe(channel) do |on|
          on.subscribe do |channel, subscriptions|
            puts "Subscribed to redis ##{channel}\n"
          end
          on.message do |channel, message|
            puts "Message from redis ##{channel}: #{message}\n"
            message = JSON.parse(message)
            .
            .
            .
            if settings.connections.include?(out)
              out.push(message)
            else
              puts 'closing orphaned redis connection'
              redis.unsubscribe
            end
          end
        end
      end
    end
    

    The redis connection blocks on.message and only accepts (p)subscribe/(p)unsubscribe commands. Once you unsubscribe, the redis connection is no longer blocked and can be released by the web server object which was instantiated by the initial sse request. It automatically clears when you receive a message on redis and sse connection to the browser no longer exists in the collection array.

    0 讨论(0)
  • 2020-11-28 04:34

    Instead of sending a heartbeat to all the clients, it might be easier to just set a watchdog for each connection. [Thanks to @NeilJewers]

    class Stream::FixedController < StreamController
      def events
        # Rails reserve a db connection from connection pool for
        # each request, lets put it back into connection pool.
        ActiveRecord::Base.clear_active_connections!
    
        redis = Redis.new
    
        watchdog = Doberman::WatchDog.new(:timeout => 20.seconds)
        watchdog.start
    
        # Redis (p)subscribe is blocking request so we need do some trick
        # to prevent it freeze request forever.
        redis.psubscribe("messages:*") do |on|
          on.pmessage do |pattern, event, data|
            begin
              # write to stream - even heartbeat - it's sometimes chance to
              response.stream.write("event: #{event}\ndata: #{data}\n\n")
              watchdog.ping
    
            rescue Doberman::WatchDog::Timeout => e
              raise ClientDisconnected if response.stream.closed?
              watchdog.ping
            end
          end
        end
    
      rescue IOError
      rescue ClientDisconnected
    
      ensure
        response.stream.close
        redis.quit
        watchdog.stop
      end
    end
    
    0 讨论(0)
  • 2020-11-28 04:35

    A solution I just did (borrowing a lot from @teeg) which seems to work okay (haven't failure tested it, tho)

    config/initializers/redis.rb

    $redis = Redis.new(:host => "xxxx.com", :port => 6379)
    
    heartbeat_thread = Thread.new do
      while true
        $redis.publish("heartbeat","thump")
        sleep 30.seconds
      end
    end
    
    at_exit do
      # not sure this is needed, but just in case
      heartbeat_thread.kill
      $redis.quit
    end
    

    And then in my controller:

    def events
        response.headers["Content-Type"] = "text/event-stream"
        redis = Redis.new(:host => "xxxxxxx.com", :port => 6379)
        logger.info "New stream starting, connecting to redis"
        redis.subscribe(['parse.new','heartbeat']) do |on|
          on.message do |event, data|
            if event == 'parse.new'
              response.stream.write("event: parse\ndata: #{data}\n\n")
            elsif event == 'heartbeat'
              response.stream.write("event: heartbeat\ndata: heartbeat\n\n")
            end
          end
        end
      rescue IOError
        logger.info "Stream closed"
      ensure
        logger.info "Stopping stream thread"
        redis.quit
        response.stream.close
      end
    
    0 讨论(0)
提交回复
热议问题