Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/334.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python Django:在客户端断开与流的连接后清理redis连接_Python_Django_Redis_Gevent - Fatal编程技术网

Python Django:在客户端断开与流的连接后清理redis连接

Python Django:在客户端断开与流的连接后清理redis连接,python,django,redis,gevent,Python,Django,Redis,Gevent,我在Django应用程序中实现了一个API,将实时更新从后端流式传输到浏览器。后端是Redis pubsub。我的Django视图如下所示: def event_stream(request): """ Stream worker events out to browser. """ listener = events.Listener( settings.EVENTS_PUBSUB_URL, channels=[settings.EVENT

我在Django应用程序中实现了一个API,将实时更新从后端流式传输到浏览器。后端是Redis pubsub。我的Django视图如下所示:

def event_stream(request):
   """
   Stream worker events out to browser.
   """

   listener = events.Listener(
       settings.EVENTS_PUBSUB_URL,
       channels=[settings.EVENTS_PUBSUB_CHANNEL],
       buffer_key=settings.EVENTS_BUFFER_KEY,
       last_event_id=request.META.get('HTTP_LAST_EVENT_ID')
   )

   return http.HttpResponse(listener, mimetype='text/event-stream')
class Listener(object):
    def __init__(self, rcon_or_url, channels, buffer_key=None,
                 last_event_id=None):
        if isinstance(rcon_or_url, redis.StrictRedis):
            self.rcon = rcon_or_url
        elif isinstance(rcon_or_url, basestring):
            self.rcon = redis.StrictRedis(**utils.parse_redis_url(rcon_or_url))
        self.channels = channels
        self.buffer_key = buffer_key
        self.last_event_id = last_event_id
        self.pubsub = self.rcon.pubsub()
        self.pubsub.subscribe(channels)

    def __iter__(self):
        # If we've been initted with a buffer key, then get all the events off
        # that and spew them out before blocking on the pubsub.
        if self.buffer_key:
            buffered_events = self.rcon.lrange(self.buffer_key, 0, -1)

            # check whether msg with last_event_id is still in buffer.  If so,
            # trim buffered_events to have only newer messages.
            if self.last_event_id:
                # Note that we're looping through most recent messages first,
                # here
                counter = 0
                for msg in buffered_events:
                    if (json.loads(msg)['id'] == self.last_event_id):
                        break
                    counter += 1
                buffered_events = buffered_events[:counter]

            for msg in reversed(list(buffered_events)):
                # Stream out oldest messages first
                yield to_sse({'data': msg})
        try:
            for msg in self.pubsub.listen():
                if msg['type'] == 'message':
                    yield to_sse(msg)
        finally:
            logging.info('Closing pubsub')
            self.pubsub.close()
            self.rcon.connection_pool.disconnect()
作为迭代器返回的events.Listener类如下所示:

def event_stream(request):
   """
   Stream worker events out to browser.
   """

   listener = events.Listener(
       settings.EVENTS_PUBSUB_URL,
       channels=[settings.EVENTS_PUBSUB_CHANNEL],
       buffer_key=settings.EVENTS_BUFFER_KEY,
       last_event_id=request.META.get('HTTP_LAST_EVENT_ID')
   )

   return http.HttpResponse(listener, mimetype='text/event-stream')
class Listener(object):
    def __init__(self, rcon_or_url, channels, buffer_key=None,
                 last_event_id=None):
        if isinstance(rcon_or_url, redis.StrictRedis):
            self.rcon = rcon_or_url
        elif isinstance(rcon_or_url, basestring):
            self.rcon = redis.StrictRedis(**utils.parse_redis_url(rcon_or_url))
        self.channels = channels
        self.buffer_key = buffer_key
        self.last_event_id = last_event_id
        self.pubsub = self.rcon.pubsub()
        self.pubsub.subscribe(channels)

    def __iter__(self):
        # If we've been initted with a buffer key, then get all the events off
        # that and spew them out before blocking on the pubsub.
        if self.buffer_key:
            buffered_events = self.rcon.lrange(self.buffer_key, 0, -1)

            # check whether msg with last_event_id is still in buffer.  If so,
            # trim buffered_events to have only newer messages.
            if self.last_event_id:
                # Note that we're looping through most recent messages first,
                # here
                counter = 0
                for msg in buffered_events:
                    if (json.loads(msg)['id'] == self.last_event_id):
                        break
                    counter += 1
                buffered_events = buffered_events[:counter]

            for msg in reversed(list(buffered_events)):
                # Stream out oldest messages first
                yield to_sse({'data': msg})
        try:
            for msg in self.pubsub.listen():
                if msg['type'] == 'message':
                    yield to_sse(msg)
        finally:
            logging.info('Closing pubsub')
            self.pubsub.close()
            self.rcon.connection_pool.disconnect()
使用此设置,我能够成功地将事件流式输出到浏览器。然而,似乎侦听器的“finally”中的断开连接调用实际上从未被调用。我猜想他们仍然在露营,等待来自pubsub的消息。当客户端断开连接并重新连接时,我可以看到到我的Redis实例的连接数在不断增加,而且永远不会下降。一旦达到1000左右,Redis就开始崩溃并消耗所有可用的CPU

我希望能够检测到客户端何时不再侦听,并在此时关闭Redis连接

我尝试或思考过的事情:

  • 连接池。但正如自述文件所述,“在线程之间传递PubSub或管道对象是不安全的。”
  • 一个中间件来处理连接,或者可能只是断开连接。这不会起作用,因为中间件的process_response()方法调用得太早(甚至在http头发送到客户端之前)。当客户端处于流媒体内容的中间时,我需要一些被调用的东西。
  • 电话和信号。第一个,就像中间件中的process_response(),似乎启动得太快了。当客户端断开中流连接时,不会调用第二个
  • 最后一个问题:在生产中,我使用Gevent,这样我就可以一次打开很多连接。但是,无论我使用的是普通的旧版“manage.py runserver”,还是Gevent monkeypatched runserver,还是Gunicorn的Gevent workers,都会出现此连接泄漏问题。

    更新:,如果您想像我在本问题/答案中所做的那样懒洋洋地将内容流出来,则需要返回StreamingHttpResponse实例

    原始答案如下

    经过大量的研究和阅读框架代码,我找到了这个问题的正确答案

  • 根据,如果应用程序返回一个带有close()方法的迭代器,则响应完成后WSGI服务器应调用该迭代器。Django也支持这一点。这是一个自然的地方做我需要的Redis连接清理
  • 在Python的wsgiref实现中,以及在Django的“runserver”中的扩展中,都存在一个漏洞,如果客户端从服务器中流断开连接,则会跳过close()。我已经提交了一个补丁
  • 即使服务器接受close(),在对客户机的写入实际失败之前也不会调用它。如果迭代器被阻止在pubsub上等待,并且没有发送任何内容,则不会调用close()。我已经解决了这个问题,每次客户端连接时都会向pubsub发送一条no-op消息。这样,当浏览器执行正常的重新连接时,现在已失效的线程将尝试写入其关闭的连接,引发异常,然后在服务器调用close()时进行清理。表示任何以冒号开头的行都是一条注释,应该忽略,因此我只发送“:\n”作为我的无操作消息,以清除过时的客户端
  • 这是新代码。首先是Django的观点:

    def event_stream(request):
        """
        Stream worker events out to browser.
        """
        return events.SSEResponse(
            settings.EVENTS_PUBSUB_URL,
            channels=[settings.EVENTS_PUBSUB_CHANNEL],
            buffer_key=settings.EVENTS_BUFFER_KEY,
            last_event_id=request.META.get('HTTP_LAST_EVENT_ID')
        )
    
    完成这项工作的Listener类,以及格式化类的helper函数和使视图更清晰的HTTPResponse子类:

    class Listener(object):
        def __init__(self,
                     rcon_or_url=settings.EVENTS_PUBSUB_URL,
                     channels=None,
                     buffer_key=settings.EVENTS_BUFFER_KEY,
                     last_event_id=None):
            if isinstance(rcon_or_url, redis.StrictRedis):
                self.rcon = rcon_or_url
            elif isinstance(rcon_or_url, basestring):
                self.rcon = redis.StrictRedis(**utils.parse_redis_url(rcon_or_url))
            if channels is None:
                channels = [settings.EVENTS_PUBSUB_CHANNEL]
            self.channels = channels
            self.buffer_key = buffer_key
            self.last_event_id = last_event_id
            self.pubsub = self.rcon.pubsub()
            self.pubsub.subscribe(channels)
    
            # Send a superfluous message down the pubsub to flush out stale
            # connections.
            for channel in self.channels:
                # Use buffer_key=None since these pings never need to be remembered
                # and replayed.
                sender = Sender(self.rcon, channel, None)
                sender.publish('_flush', tags=['hidden'])
    
        def __iter__(self):
            # If we've been initted with a buffer key, then get all the events off
            # that and spew them out before blocking on the pubsub.
            if self.buffer_key:
                buffered_events = self.rcon.lrange(self.buffer_key, 0, -1)
    
                # check whether msg with last_event_id is still in buffer.  If so,
                # trim buffered_events to have only newer messages.
                if self.last_event_id:
                    # Note that we're looping through most recent messages first,
                    # here
                    counter = 0
                    for msg in buffered_events:
                        if (json.loads(msg)['id'] == self.last_event_id):
                            break
                        counter += 1
                    buffered_events = buffered_events[:counter]
    
                for msg in reversed(list(buffered_events)):
                    # Stream out oldest messages first
                    yield to_sse({'data': msg})
    
            for msg in self.pubsub.listen():
                if msg['type'] == 'message':
                    yield to_sse(msg)
    
        def close(self):
            self.pubsub.close()
            self.rcon.connection_pool.disconnect()
    
    
    class SSEResponse(HttpResponse):
        def __init__(self, rcon_or_url, channels, buffer_key=None,
                     last_event_id=None, *args, **kwargs):
            self.listener = Listener(rcon_or_url, channels, buffer_key,
                                     last_event_id)
            super(SSEResponse, self).__init__(self.listener,
                                              mimetype='text/event-stream',
                                              *args, **kwargs)
    
        def close(self):
            """
            This will be called by the WSGI server at the end of the request, even
            if the client disconnects midstream.  Unless you're using Django's
            runserver, in which case you should expect to see Redis connections
            build up until http://bugs.python.org/issue16220 is fixed.
            """
            self.listener.close()
    
    
    def to_sse(msg):
        """
        Given a Redis pubsub message that was published by a Sender (ie, has a JSON
        body with time, message, title, tags, and id), return a properly-formatted
        SSE string.
        """
        data = json.loads(msg['data'])
    
        # According to the SSE spec, lines beginning with a colon should be
        # ignored.  We can use that as a way to force zombie listeners to try
        # pushing something down the socket and clean up their redis connections
        # when they get an error.
        # See http://dev.w3.org/html5/eventsource/#event-stream-interpretation
        if data['message'] == '_flush':
            return ":\n"  # Administering colonic!
    
        if 'id' in data:
            out = "id: " + data['id'] + '\n'
        else:
            out = ''
        if 'name' in data:
            out += 'name: ' + data['name'] + '\n'
    
        payload = json.dumps({
            'time': data['time'],
            'message': data['message'],
            'tags': data['tags'],
            'title': data['title'],
        })
        out += 'data: ' + payload + '\n\n'
        return out