def test_write_when_write_event_and_not_pending(self):
    sock = EventSocket()
    sock._write_event = mock()
    
    expect( sock._write_event.pending ).returns( False )
    expect( sock._write_event.add )
    expect( sock._flag_activity )

    sock.write( 'foo' )
    assert_equals( deque(['foo']), sock._write_buf )
  def test_write_when_write_event_is_pending_and_debugging(self):
    sock = EventSocket()
    sock._write_event = mock()
    sock._peername = 'peername'
    sock._write_buf = deque(['data'])
    sock._debug = 2
    sock._logger = mock()
    
    expect( sock._write_event.pending ).returns( True )
    expect( sock._logger.debug ).args(str, 3, 7, 'peername')
    expect( sock._flag_activity )

    sock.write( 'foo' )
    assert_equals( deque(['data', 'foo']), sock._write_buf )
Exemplo n.º 3
0
class Connection(object):

  class TooManyChannels(ConnectionError): '''This connection has too many channels open.  Non-fatal.'''
  class InvalidChannel(ConnectionError): '''The channel id does not correspond to an existing channel.  Non-fatal.'''

  def __init__(self, **kwargs):
    '''
    Initialize the connection.
    '''
    self._debug = kwargs.get('debug', False)
    self._logger = kwargs.get('logger', root_logger)

    self._user = kwargs.get('user', 'guest')
    self._password = kwargs.get('password', 'guest')
    self._host = kwargs.get('host', 'localhost')
    self._vhost = kwargs.get('vhost', '/')

    self._connect_timeout = kwargs.get('connect_timeout', 5)
    self._sock_opts = kwargs.get('sock_opts')
    self._sock = None
    self._heartbeat = kwargs.get('heartbeat')
    self._reconnect_cb = kwargs.get('reconnect_cb')
    self._close_cb = kwargs.get('close_cb')

    self._login_method = kwargs.get('login_method', 'AMQPLAIN')
    self._locale = kwargs.get('locale', 'en_US')
    self._client_properties = kwargs.get('client_properties')

    self._properties = LIBRARY_PROPERTIES.copy()
    if self._client_properties:
      self._properties.update( self._client_properties )

    self._closed = False
    self._connected = False
    self._close_info = {
      'reply_code'    : 0,
      'reply_text'    : 'first connect',
      'class_id'      : 0,
      'method_id'     : 0
    }
    
    self._channels = {
      0 : ConnectionChannel(self, 0)
    } 
    
    login_response = Writer()
    login_response.write_table({'LOGIN': self._user, 'PASSWORD': self._password})
    #stream = BytesIO()
    #login_response.flush(stream)
    #self._login_response = stream.getvalue()[4:]  #Skip the length
                                                      #at the beginning
    self._login_response = login_response.buffer()[4:]
    
    self._channel_counter = 0
    self._channel_max = 65535
    self._frame_max = 65535

    self._frames_read = 0
    self._frames_written = 0

    self._strategy = kwargs.get('connection_strategy')
    if not self._strategy:
      self._strategy = ConnectionStrategy( self, self._host, reconnect_cb = self._reconnect_cb )
    self._strategy.connect()

    self._output_frame_buffer = []
    
  @property
  def logger(self):
    return self._logger

  @property
  def debug(self):
    return self._debug

  @property
  def frame_max(self):
    return self._frame_max

  @property
  def channel_max(self):
    return self._channel_max

  @property
  def frames_read(self):
    '''Number of frames read in the lifetime of this connection.'''
    return self._frames_read

  @property
  def frames_written(self):
    '''Number of frames written in the lifetime of this connection.'''
    return self._frames_written

  @property
  def close_info(self):
    '''Return dict with information on why this connection is closed.  Will
    return None if the connections is open.'''
    return self._close_info if self._closed else None
  
  def reconnect(self):
    '''Reconnect to the configured host and port.'''
    self._strategy.connect()
  
  def connect(self, host, port):
    '''
    Connect to a host and port. Can be called directly, or is called by the
    strategy as it tries to find and connect to hosts.
    '''
    # Clear the connect state immediately since we're no longer connected
    # at this point.
    self._connected = False
    
    # NOTE: purposefully leave output_frame_buffer alone so that pending writes can
    # still occur.  this allows the reconnect to occur silently without
    # completely breaking any pending data on, say, a channel that was just
    # opened.
    self._sock = EventSocket( read_cb=self._sock_read_cb,
      close_cb=self._sock_close_cb, error_cb=self._sock_error_cb,
      debug=self._debug, logger=self._logger )
    self._sock.settimeout( self._connect_timeout )
    if self._sock_opts:
      for k,v in self._sock_opts.iteritems():
        family,type = k
        self._sock.setsockopt(family, type, v)
    self._sock.connect( (host,port) )
    self._sock.setblocking( False )

    # Only after the socket has connected do we clear this state; closed must
    # be False so that writes can be buffered in writePacket().  The closed
    # state might have been set to True due to a socket error or a redirect.
    self._host = "%s:%d"%(host,port)
    self._closed = False
    self._close_info = {
      'reply_code'    : 0,
      'reply_text'    : 'failed to connect to %s'%(self._host),
      'class_id'      : 0,
      'method_id'     : 0
    }

    self._sock.write( PROTOCOL_HEADER )
  
  def disconnect(self):
    '''
    Disconnect from the current host, but otherwise leave this object "open"
    so that it can be reconnected.
    '''
    self._connected = False
    if self._sock!=None:
      self._sock.close_cb = None
      try:
        self._sock.close()
      except: 
        self.logger.error("Failed to disconnect socket to %s", self._host, exc_info=True)
      self._sock = None
  
  def add_reconnect_callback(self, callback):
    '''Adds a reconnect callback to the strategy.  This can be used to
    resubscribe to exchanges, etc.'''
    self._strategy.reconnect_callbacks.append(callback)

  ###
  ### EventSocket callbacks
  ###
  def _sock_read_cb(self, sock):
    '''
    Callback when there's data to read on the socket.
    '''
    try:
      self._read_frames()
    except:
      self.logger.error("Failed to read frames from %s", self._host, exc_info=True)
      self.close( reply_code=501, reply_text='Error parsing frames' )

  def _sock_close_cb(self, sock):
    """
    Callback when socket closed.  This is intended to be the callback when the
    closure is unexpected.
    """
    self.logger.warning( 'socket to %s closed unexpectedly', self._host )
    self._close_info = {
      'reply_code'    : 0,
      'reply_text'    : 'socket closed unexpectedly to %s'%(self._host),
      'class_id'      : 0,
      'method_id'     : 0
    }

    # We're not connected any more (we're not closed but we're definitely not
    # connected)
    self._connected = False
    self._sock = None

    # Call back to a user-provided close function
    self._callback_close()

    # Fail and do nothing. If you haven't configured permissions and that's 
    # why the socket is closing, this keeps us from looping.
    self._strategy.fail()
  
  def _sock_error_cb(self, sock, msg, exception=None):
    """
    Callback when there's an error on the socket.
    """
    self.logger.error( 'error on connection to %s: %s', self._host, msg)
    self._close_info = {
      'reply_code'    : 0,
      'reply_text'    : 'socket error on host %s: %s'%(self._host, msg),
      'class_id'      : 0,
      'method_id'     : 0
    }
    
    # we're not connected any more (we're not closed but we're definitely not
    # connected)
    self._connected = False
    self._sock = None

    # Call back to a user-provided close function
    self._callback_close()

    # Fail and try to reconnect, because this is expected to be a transient error.
    self._strategy.fail()
    self._strategy.next_host()

  ###
  ### Connection methods
  ###
  def _next_channel_id(self):
    '''Return the next possible channel id.  Is a circular enumeration.'''
    self._channel_counter += 1
    if self._channel_counter >= self._channel_max:
      self._channel_counter = 1
    return self._channel_counter

  def channel(self, channel_id=None):
    """
    Fetch a Channel object identified by the numeric channel_id, or
    create that object if it doesn't already exist.  If channel_id is not
    None but no channel exists for that id, will raise InvalidChannel.  If
    there are already too many channels open, will raise TooManyChannels.
    """
    if channel_id is None:
      # adjust for channel 0
      if len(self._channels)-1 >= self._channel_max:
        raise Connection.TooManyChannels( "%d channels already open, max %d",
          len(self._channels)-1, self._channel_max )
      channel_id = self._next_channel_id()
      while channel_id in self._channels:
        channel_id = self._next_channel_id()
    elif channel_id in self._channels:
      return self._channels[channel_id]
    else:
      raise Connection.InvalidChannel("%s is not a valid channel id", channel_id )

    # Call open() here so that ConnectionChannel doesn't have it called.  Could
    # also solve this other ways, but it's a HACK regardless.
    rval = Channel(self, channel_id)
    self._channels[ channel_id ] = rval
    rval.open()
    return rval

  def close(self, reply_code=0, reply_text='', class_id=0, method_id=0):
    '''
    Close this connection.
    '''
    self._close_info = {
      'reply_code'    : reply_code,
      'reply_text'    : reply_text,
      'class_id'      : class_id,
      'method_id'     : method_id
    }
    self._channels[0].close()

  def _close_socket(self):
    '''Close the socket.'''
    # The assumption here is that we don't want auto-reconnect to kick in if
    # the socket is purposefully closed.
    self._closed = True

    # By the time we hear about the protocol-level closure, the socket may
    # have already gone away.
    if self._sock != None:
      self._sock.close_cb = None
      try:
        self._sock.close()
      except:
        self.logger.error( 'error closing socket' )
      self._sock = None

  def _callback_close(self):
    '''Callback to any close handler.'''
    if self._close_cb:
      try: self._close_cb()
      except SystemExit: raise
      except: self.logger.error( 'error calling close callback' )


  def _read_frames(self):
    '''
    Read frames from the socket.
    '''
    # Because of the timer callback to dataRead when we re-buffered, there's a
    # chance that in between we've lost the socket.  If that's the case, just
    # silently return as some code elsewhere would have already notified us.
    # That bug could be fixed by improving the message reading so that we consume
    # all possible messages and ensure that only a partial message was rebuffered,
    # so that we can rely on the next read event to read the subsequent message.
    if self._sock is None:
      return
    
    data = self._sock.read()
    reader = Reader( data )
    p_channels = set()
    
    for frame in Frame.read_frames( reader ):
      if self._debug > 1:
        self.logger.debug( "READ: %s", frame )
      self._frames_read += 1
      ch = self.channel( frame.channel_id )
      ch.buffer_frame( frame )
      p_channels.add( ch )

    # Still not clear on what's the best approach here. It seems there's a
    # slight speedup by calling this directly rather than delaying, but the
    # delay allows for pending IO with higher priority to execute.
    self._process_channels( p_channels )
    #event.timeout(0, self._process_channels, p_channels)

    # HACK: read the buffer contents and re-buffer.  Would prefer to pass
    # buffer back, but there's no good way of asking the total size of the
    # buffer, comparing to tell(), and then re-buffering.  There's also no
    # ability to clear the buffer up to the current position.
    # NOTE: This will be cleared up once eventsocket supports the 
    # uber-awesome buffering scheme that will utilize mmap.
    if reader.tell() < len(data):
      self._sock.buffer( data[reader.tell():] )

  def _process_channels(self, channels):
    '''
    Walk through a set of channels and process their frame buffer. Will
    collect all socket output and flush in one write.
    '''
    for channel in channels:
      channel.process_frames()

  def _flush_buffered_frames(self):
    # In the rare case (a bug) where this is called but send_frame thinks
    # they should be buffered, don't clobber.
    frames = self._output_frame_buffer
    self._output_frame_buffer = []
    for frame in frames:
      self.send_frame( frame )
  
  def send_frame(self, frame):
    '''
    Send a single frame. If there is an output buffer, write to that, else send
    immediately to the socket.
    '''
    if self._closed:
      if self._close_info and len(self._close_info['reply_text'])>0:
        raise ConnectionClosed("connection is closed: %s : %s"%\
          (self._close_info['reply_code'],self._close_info['reply_text']) )
      raise ConnectionClosed("connection is closed")

    if self._sock==None or (not self._connected and frame.channel_id!=0):
      self._output_frame_buffer.append( frame )
      return
    
    if self._debug > 1:
      self.logger.debug( "WRITE: %s", frame )

    buf = bytearray()
    frame.write_frame(buf)
    self._sock.write( buf )
    
    self._frames_written += 1
Exemplo n.º 4
0
class Connection(object):
    class TooManyChannels(ConnectionError):
        '''This connection has too many channels open.  Non-fatal.'''

    class InvalidChannel(ConnectionError):
        '''The channel id does not correspond to an existing channel.  Non-fatal.'''

    def __init__(self, **kwargs):
        '''
    Initialize the connection.
    '''
        self._debug = kwargs.get('debug', False)
        self._logger = kwargs.get('logger', root_logger)

        self._user = kwargs.get('user', 'guest')
        self._password = kwargs.get('password', 'guest')
        self._host = kwargs.get('host', 'localhost')
        self._vhost = kwargs.get('vhost', '/')

        self._connect_timeout = kwargs.get('connect_timeout', 5)
        self._sock_opts = kwargs.get('sock_opts')
        self._sock = None
        self._heartbeat = kwargs.get('heartbeat')
        self._reconnect_cb = kwargs.get('reconnect_cb')
        self._close_cb = kwargs.get('close_cb')

        self._login_method = kwargs.get('login_method', 'AMQPLAIN')
        self._locale = kwargs.get('locale', 'en_US')
        self._client_properties = kwargs.get('client_properties')

        self._properties = LIBRARY_PROPERTIES.copy()
        if self._client_properties:
            self._properties.update(self._client_properties)

        self._closed = False
        self._connected = False
        self._close_info = {
            'reply_code': 0,
            'reply_text': 'first connect',
            'class_id': 0,
            'method_id': 0
        }

        self._channels = {0: ConnectionChannel(self, 0)}

        login_response = Writer()
        login_response.write_table({
            'LOGIN': self._user,
            'PASSWORD': self._password
        })
        #stream = BytesIO()
        #login_response.flush(stream)
        #self._login_response = stream.getvalue()[4:]  #Skip the length
        #at the beginning
        self._login_response = login_response.buffer()[4:]

        self._channel_counter = 0
        self._channel_max = 65535
        self._frame_max = 65535

        self._frames_read = 0
        self._frames_written = 0

        self._strategy = kwargs.get('connection_strategy')
        if not self._strategy:
            self._strategy = ConnectionStrategy(
                self, self._host, reconnect_cb=self._reconnect_cb)
        self._strategy.connect()

        self._output_frame_buffer = []

    @property
    def logger(self):
        return self._logger

    @property
    def debug(self):
        return self._debug

    @property
    def frame_max(self):
        return self._frame_max

    @property
    def channel_max(self):
        return self._channel_max

    @property
    def frames_read(self):
        '''Number of frames read in the lifetime of this connection.'''
        return self._frames_read

    @property
    def frames_written(self):
        '''Number of frames written in the lifetime of this connection.'''
        return self._frames_written

    @property
    def close_info(self):
        '''Return dict with information on why this connection is closed.  Will
    return None if the connections is open.'''
        return self._close_info if self._closed else None

    def reconnect(self):
        '''Reconnect to the configured host and port.'''
        self._strategy.connect()

    def connect(self, host, port):
        '''
    Connect to a host and port. Can be called directly, or is called by the
    strategy as it tries to find and connect to hosts.
    '''
        # Clear the connect state immediately since we're no longer connected
        # at this point.
        self._connected = False

        # NOTE: purposefully leave output_frame_buffer alone so that pending writes can
        # still occur.  this allows the reconnect to occur silently without
        # completely breaking any pending data on, say, a channel that was just
        # opened.
        self._sock = EventSocket(read_cb=self._sock_read_cb,
                                 close_cb=self._sock_close_cb,
                                 error_cb=self._sock_error_cb,
                                 debug=self._debug,
                                 logger=self._logger)
        self._sock.settimeout(self._connect_timeout)
        if self._sock_opts:
            for k, v in self._sock_opts.iteritems():
                family, type = k
                self._sock.setsockopt(family, type, v)
        self._sock.connect((host, port))
        self._sock.setblocking(False)

        # Only after the socket has connected do we clear this state; closed must
        # be False so that writes can be buffered in writePacket().  The closed
        # state might have been set to True due to a socket error or a redirect.
        self._host = "%s:%d" % (host, port)
        self._closed = False
        self._close_info = {
            'reply_code': 0,
            'reply_text': 'failed to connect to %s' % (self._host),
            'class_id': 0,
            'method_id': 0
        }

        self._sock.write(PROTOCOL_HEADER)

    def disconnect(self):
        '''
    Disconnect from the current host, but otherwise leave this object "open"
    so that it can be reconnected.
    '''
        self._connected = False
        if self._sock != None:
            self._sock.close_cb = None
            try:
                self._sock.close()
            except:
                self.logger.error("Failed to disconnect socket to %s",
                                  self._host,
                                  exc_info=True)
            self._sock = None

    def add_reconnect_callback(self, callback):
        '''Adds a reconnect callback to the strategy.  This can be used to
    resubscribe to exchanges, etc.'''
        self._strategy.reconnect_callbacks.append(callback)

    ###
    ### EventSocket callbacks
    ###
    def _sock_read_cb(self, sock):
        '''
    Callback when there's data to read on the socket.
    '''
        try:
            self._read_frames()
        except:
            self.logger.error("Failed to read frames from %s",
                              self._host,
                              exc_info=True)
            self.close(reply_code=501, reply_text='Error parsing frames')

    def _sock_close_cb(self, sock):
        """
    Callback when socket closed.  This is intended to be the callback when the
    closure is unexpected.
    """
        self.logger.warning('socket to %s closed unexpectedly', self._host)
        self._close_info = {
            'reply_code': 0,
            'reply_text': 'socket closed unexpectedly to %s' % (self._host),
            'class_id': 0,
            'method_id': 0
        }

        # We're not connected any more (we're not closed but we're definitely not
        # connected)
        self._connected = False
        self._sock = None

        # Call back to a user-provided close function
        self._callback_close()

        # Fail and do nothing. If you haven't configured permissions and that's
        # why the socket is closing, this keeps us from looping.
        self._strategy.fail()

    def _sock_error_cb(self, sock, msg, exception=None):
        """
    Callback when there's an error on the socket.
    """
        self.logger.error('error on connection to %s: %s', self._host, msg)
        self._close_info = {
            'reply_code': 0,
            'reply_text': 'socket error on host %s: %s' % (self._host, msg),
            'class_id': 0,
            'method_id': 0
        }

        # we're not connected any more (we're not closed but we're definitely not
        # connected)
        self._connected = False
        self._sock = None

        # Call back to a user-provided close function
        self._callback_close()

        # Fail and try to reconnect, because this is expected to be a transient error.
        self._strategy.fail()
        self._strategy.next_host()

    ###
    ### Connection methods
    ###
    def _next_channel_id(self):
        '''Return the next possible channel id.  Is a circular enumeration.'''
        self._channel_counter += 1
        if self._channel_counter >= self._channel_max:
            self._channel_counter = 1
        return self._channel_counter

    def channel(self, channel_id=None):
        """
    Fetch a Channel object identified by the numeric channel_id, or
    create that object if it doesn't already exist.  If channel_id is not
    None but no channel exists for that id, will raise InvalidChannel.  If
    there are already too many channels open, will raise TooManyChannels.
    """
        if channel_id is None:
            # adjust for channel 0
            if len(self._channels) - 1 >= self._channel_max:
                raise Connection.TooManyChannels(
                    "%d channels already open, max %d",
                    len(self._channels) - 1, self._channel_max)
            channel_id = self._next_channel_id()
            while channel_id in self._channels:
                channel_id = self._next_channel_id()
        elif channel_id in self._channels:
            return self._channels[channel_id]
        else:
            raise Connection.InvalidChannel("%s is not a valid channel id",
                                            channel_id)

        # Call open() here so that ConnectionChannel doesn't have it called.  Could
        # also solve this other ways, but it's a HACK regardless.
        rval = Channel(self, channel_id)
        self._channels[channel_id] = rval
        rval.open()
        return rval

    def close(self, reply_code=0, reply_text='', class_id=0, method_id=0):
        '''
    Close this connection.
    '''
        self._close_info = {
            'reply_code': reply_code,
            'reply_text': reply_text,
            'class_id': class_id,
            'method_id': method_id
        }
        self._channels[0].close()

    def _close_socket(self):
        '''Close the socket.'''
        # The assumption here is that we don't want auto-reconnect to kick in if
        # the socket is purposefully closed.
        self._closed = True

        # By the time we hear about the protocol-level closure, the socket may
        # have already gone away.
        if self._sock != None:
            self._sock.close_cb = None
            try:
                self._sock.close()
            except:
                self.logger.error('error closing socket')
            self._sock = None

    def _callback_close(self):
        '''Callback to any close handler.'''
        if self._close_cb:
            try:
                self._close_cb()
            except SystemExit:
                raise
            except:
                self.logger.error('error calling close callback')

    def _read_frames(self):
        '''
    Read frames from the socket.
    '''
        # Because of the timer callback to dataRead when we re-buffered, there's a
        # chance that in between we've lost the socket.  If that's the case, just
        # silently return as some code elsewhere would have already notified us.
        # That bug could be fixed by improving the message reading so that we consume
        # all possible messages and ensure that only a partial message was rebuffered,
        # so that we can rely on the next read event to read the subsequent message.
        if self._sock is None:
            return

        data = self._sock.read()
        reader = Reader(data)
        p_channels = set()

        for frame in Frame.read_frames(reader):
            if self._debug > 1:
                self.logger.debug("READ: %s", frame)
            self._frames_read += 1
            ch = self.channel(frame.channel_id)
            ch.buffer_frame(frame)
            p_channels.add(ch)

        # Still not clear on what's the best approach here. It seems there's a
        # slight speedup by calling this directly rather than delaying, but the
        # delay allows for pending IO with higher priority to execute.
        self._process_channels(p_channels)
        #event.timeout(0, self._process_channels, p_channels)

        # HACK: read the buffer contents and re-buffer.  Would prefer to pass
        # buffer back, but there's no good way of asking the total size of the
        # buffer, comparing to tell(), and then re-buffering.  There's also no
        # ability to clear the buffer up to the current position.
        # NOTE: This will be cleared up once eventsocket supports the
        # uber-awesome buffering scheme that will utilize mmap.
        if reader.tell() < len(data):
            self._sock.buffer(data[reader.tell():])

    def _process_channels(self, channels):
        '''
    Walk through a set of channels and process their frame buffer. Will
    collect all socket output and flush in one write.
    '''
        for channel in channels:
            channel.process_frames()

    def _flush_buffered_frames(self):
        # In the rare case (a bug) where this is called but send_frame thinks
        # they should be buffered, don't clobber.
        frames = self._output_frame_buffer
        self._output_frame_buffer = []
        for frame in frames:
            self.send_frame(frame)

    def send_frame(self, frame):
        '''
    Send a single frame. If there is an output buffer, write to that, else send
    immediately to the socket.
    '''
        if self._closed:
            if self._close_info and len(self._close_info['reply_text']) > 0:
                raise ConnectionClosed("connection is closed: %s : %s"%\
                  (self._close_info['reply_code'],self._close_info['reply_text']) )
            raise ConnectionClosed("connection is closed")

        if self._sock == None or (not self._connected
                                  and frame.channel_id != 0):
            self._output_frame_buffer.append(frame)
            return

        if self._debug > 1:
            self.logger.debug("WRITE: %s", frame)

        buf = bytearray()
        frame.write_frame(buf)
        self._sock.write(buf)

        self._frames_written += 1