148 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			148 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
# -*- coding: utf-8 -*-
 | 
						|
# Copyright 2018 New Vector Ltd
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
# you may not use this file except in compliance with the License.
 | 
						|
# You may obtain a copy of the License at
 | 
						|
#
 | 
						|
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
# See the License for the specific language governing permissions and
 | 
						|
# limitations under the License.
 | 
						|
 | 
						|
import queue
 | 
						|
 | 
						|
from twisted.internet import threads
 | 
						|
 | 
						|
from synapse.logging.context import make_deferred_yieldable, run_in_background
 | 
						|
 | 
						|
 | 
						|
class BackgroundFileConsumer:
 | 
						|
    """A consumer that writes to a file like object. Supports both push
 | 
						|
    and pull producers
 | 
						|
 | 
						|
    Args:
 | 
						|
        file_obj (file): The file like object to write to. Closed when
 | 
						|
            finished.
 | 
						|
        reactor (twisted.internet.reactor): the Twisted reactor to use
 | 
						|
    """
 | 
						|
 | 
						|
    # For PushProducers pause if we have this many unwritten slices
 | 
						|
    _PAUSE_ON_QUEUE_SIZE = 5
 | 
						|
    # And resume once the size of the queue is less than this
 | 
						|
    _RESUME_ON_QUEUE_SIZE = 2
 | 
						|
 | 
						|
    def __init__(self, file_obj, reactor):
 | 
						|
        self._file_obj = file_obj
 | 
						|
 | 
						|
        self._reactor = reactor
 | 
						|
 | 
						|
        # Producer we're registered with
 | 
						|
        self._producer = None
 | 
						|
 | 
						|
        # True if PushProducer, false if PullProducer
 | 
						|
        self.streaming = False
 | 
						|
 | 
						|
        # For PushProducers, indicates whether we've paused the producer and
 | 
						|
        # need to call resumeProducing before we get more data.
 | 
						|
        self._paused_producer = False
 | 
						|
 | 
						|
        # Queue of slices of bytes to be written. When producer calls
 | 
						|
        # unregister a final None is sent.
 | 
						|
        self._bytes_queue = queue.Queue()
 | 
						|
 | 
						|
        # Deferred that is resolved when finished writing
 | 
						|
        self._finished_deferred = None
 | 
						|
 | 
						|
        # If the _writer thread throws an exception it gets stored here.
 | 
						|
        self._write_exception = None
 | 
						|
 | 
						|
    def registerProducer(self, producer, streaming):
 | 
						|
        """Part of IConsumer interface
 | 
						|
 | 
						|
        Args:
 | 
						|
            producer (IProducer)
 | 
						|
            streaming (bool): True if push based producer, False if pull
 | 
						|
                based.
 | 
						|
        """
 | 
						|
        if self._producer:
 | 
						|
            raise Exception("registerProducer called twice")
 | 
						|
 | 
						|
        self._producer = producer
 | 
						|
        self.streaming = streaming
 | 
						|
        self._finished_deferred = run_in_background(
 | 
						|
            threads.deferToThreadPool,
 | 
						|
            self._reactor,
 | 
						|
            self._reactor.getThreadPool(),
 | 
						|
            self._writer,
 | 
						|
        )
 | 
						|
        if not streaming:
 | 
						|
            self._producer.resumeProducing()
 | 
						|
 | 
						|
    def unregisterProducer(self):
 | 
						|
        """Part of IProducer interface
 | 
						|
        """
 | 
						|
        self._producer = None
 | 
						|
        if not self._finished_deferred.called:
 | 
						|
            self._bytes_queue.put_nowait(None)
 | 
						|
 | 
						|
    def write(self, bytes):
 | 
						|
        """Part of IProducer interface
 | 
						|
        """
 | 
						|
        if self._write_exception:
 | 
						|
            raise self._write_exception
 | 
						|
 | 
						|
        if self._finished_deferred.called:
 | 
						|
            raise Exception("consumer has closed")
 | 
						|
 | 
						|
        self._bytes_queue.put_nowait(bytes)
 | 
						|
 | 
						|
        # If this is a PushProducer and the queue is getting behind
 | 
						|
        # then we pause the producer.
 | 
						|
        if self.streaming and self._bytes_queue.qsize() >= self._PAUSE_ON_QUEUE_SIZE:
 | 
						|
            self._paused_producer = True
 | 
						|
            self._producer.pauseProducing()
 | 
						|
 | 
						|
    def _writer(self):
 | 
						|
        """This is run in a background thread to write to the file.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            while self._producer or not self._bytes_queue.empty():
 | 
						|
                # If we've paused the producer check if we should resume the
 | 
						|
                # producer.
 | 
						|
                if self._producer and self._paused_producer:
 | 
						|
                    if self._bytes_queue.qsize() <= self._RESUME_ON_QUEUE_SIZE:
 | 
						|
                        self._reactor.callFromThread(self._resume_paused_producer)
 | 
						|
 | 
						|
                bytes = self._bytes_queue.get()
 | 
						|
 | 
						|
                # If we get a None (or empty list) then that's a signal used
 | 
						|
                # to indicate we should check if we should stop.
 | 
						|
                if bytes:
 | 
						|
                    self._file_obj.write(bytes)
 | 
						|
 | 
						|
                # If its a pull producer then we need to explicitly ask for
 | 
						|
                # more stuff.
 | 
						|
                if not self.streaming and self._producer:
 | 
						|
                    self._reactor.callFromThread(self._producer.resumeProducing)
 | 
						|
        except Exception as e:
 | 
						|
            self._write_exception = e
 | 
						|
            raise
 | 
						|
        finally:
 | 
						|
            self._file_obj.close()
 | 
						|
 | 
						|
    def wait(self):
 | 
						|
        """Returns a deferred that resolves when finished writing to file
 | 
						|
        """
 | 
						|
        return make_deferred_yieldable(self._finished_deferred)
 | 
						|
 | 
						|
    def _resume_paused_producer(self):
 | 
						|
        """Gets called if we should resume producing after being paused
 | 
						|
        """
 | 
						|
        if self._paused_producer and self._producer:
 | 
						|
            self._paused_producer = False
 | 
						|
            self._producer.resumeProducing()
 |