Example #1
0
    def __init__(self, close_callback=None):
        """
        Initializes a BaseChannel instance.

        @param  transport       Underlying transport used for broker communication. Can be None, if so, will
                                use the AMQPTransport stateless singleton.
        @type   transport       BaseTransport
        @param  close_callback  The method to invoke when close() is called on this BaseChannel. May be left as None,
                                in which case close_impl() will be called. This expects to be a callable taking one
                                param, this channel instance.
        """
        self.set_close_callback(close_callback)
        self._transport = None

        # setup FSM for BaseChannel / SendChannel tree
        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_ATTACH, self.S_INIT, None,
                                 self.S_ACTIVE)
        self._fsm.add_transition(self.I_CLOSE, self.S_ACTIVE, self._on_close,
                                 self.S_CLOSED)
        self._fsm.add_transition(
            self.I_CLOSE, self.S_CLOSED,
            lambda *args: self.on_channel_close(None, 0, ""),
            self.S_CLOSED)  # closed is a goal state, multiple closes are ok
        self._fsm.add_transition(self.I_CLOSE, self.S_INIT, None,
                                 self.S_CLOSED)  # INIT to CLOSED is fine too
Example #2
0
    def on_start(self):
        self.queue_name = self.CFG.get_safe('process.queue_name', self.id)
        self.subscriber = StreamSubscriber(process=self,
                                           exchange_name=self.queue_name,
                                           callback=self.recv_packet)
        self._msg_buffer = []

        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_READY, self.S_INIT, None, self.S_READY)
        self._fsm.add_transition(self.I_FETCH, self.S_READY, None,
                                 self.S_FETCH)
        self._fsm.add_transition(self.I_READY, self.S_FETCH, self._reset,
                                 self.S_READY)
        self._fsm.add_transition(self.I_EXCEPTION, self.S_FETCH, None,
                                 self.S_EXCEPTION)
        self._fsm.add_transition(self.I_READY, self.S_EXCEPTION, self._reset,
                                 self.S_READY)
        self.subscriber.initialize()
        self.done = gevent.event.Event()
        self._fsm.process(self.I_READY)
        self.greenlet = gevent.spawn(self.activate)
Example #3
0
    def on_start(self):
        self.queue_name  = self.CFG.get_safe('process.queue_name', self.id)
        self.subscriber  = StreamSubscriber(process=self, exchange_name=self.queue_name, callback=self.recv_packet)
        self._msg_buffer = []

        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_READY, self.S_INIT, None, self.S_READY)
        self._fsm.add_transition(self.I_FETCH, self.S_READY, None, self.S_FETCH)
        self._fsm.add_transition(self.I_READY, self.S_FETCH, self._reset, self.S_READY)
        self._fsm.add_transition(self.I_EXCEPTION, self.S_FETCH, None, self.S_EXCEPTION)
        self._fsm.add_transition(self.I_READY, self.S_EXCEPTION, self._reset, self.S_READY)
        self.subscriber.initialize()
        self.done = gevent.event.Event()
        self._fsm.process(self.I_READY)
        self.greenlet = gevent.spawn(self.activate)
Example #4
0
    def __init__(self, transport=None, close_callback=None):
        """
        Initializes a BaseChannel instance.

        @param  transport       Underlying transport used for broker communication. Can be None, if so, will
                                use the AMQPTransport stateless singleton.
        @type   transport       BaseTransport
        @param  close_callback  The method to invoke when close() is called on this BaseChannel. May be left as None,
                                in which case close_impl() will be called. This expects to be a callable taking one
                                param, this channel instance.
        """
        self.set_close_callback(close_callback)
        self._transport = transport or AMQPTransport.get_instance()

        # setup FSM for BaseChannel / SendChannel tree
        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_ATTACH, self.S_INIT, None, self.S_ACTIVE)
        self._fsm.add_transition(self.I_CLOSE, self.S_ACTIVE, self._on_close, self.S_CLOSED)
        self._fsm.add_transition(self.I_CLOSE, self.S_CLOSED, None, self.S_CLOSED)              # closed is a goal state, multiple closes are ok and are no-ops
        self._fsm.add_transition(self.I_CLOSE, self.S_INIT, None, self.S_CLOSED)                # INIT to CLOSED is fine too
Example #5
0
class BaseChannel(object):

    _close_callback = None  # close callback to use when closing, not always set (used for pooling)
    _closed_error_callback = None  # callback which triggers when the underlying transport closes with error
    _exchange = None  # exchange (too AMQP specific)
    _close_event = None  # used for giving notice a close was processed

    # exchange related settings @TODO: these should likely come from config instead
    _exchange_type = 'topic'
    _exchange_auto_delete = None
    _exchange_durable = None

    # States, Inputs for FSM
    S_INIT = 'INIT'
    S_ACTIVE = 'ACTIVE'
    S_CLOSED = 'CLOSED'
    I_ATTACH = 'ATTACH'
    I_CLOSE = 'CLOSE'

    def __init__(self, close_callback=None):
        """
        Initializes a BaseChannel instance.

        @param  transport       Underlying transport used for broker communication. Can be None, if so, will
                                use the AMQPTransport stateless singleton.
        @type   transport       BaseTransport
        @param  close_callback  The method to invoke when close() is called on this BaseChannel. May be left as None,
                                in which case close_impl() will be called. This expects to be a callable taking one
                                param, this channel instance.
        """
        self.set_close_callback(close_callback)
        self._transport = None

        # setup FSM for BaseChannel / SendChannel tree
        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_ATTACH, self.S_INIT, None,
                                 self.S_ACTIVE)
        self._fsm.add_transition(self.I_CLOSE, self.S_ACTIVE, self._on_close,
                                 self.S_CLOSED)
        self._fsm.add_transition(
            self.I_CLOSE, self.S_CLOSED,
            lambda *args: self.on_channel_close(None, 0, ""),
            self.S_CLOSED)  # closed is a goal state, multiple closes are ok
        self._fsm.add_transition(self.I_CLOSE, self.S_INIT, None,
                                 self.S_CLOSED)  # INIT to CLOSED is fine too

    @property
    def exchange_auto_delete(self):
        if self._exchange_auto_delete is not None:
            return self._exchange_auto_delete

        if hasattr(self, '_send_name') and hasattr(self._send_name,
                                                   'exchange_auto_delete'):
            return self._send_name.exchange_auto_delete

        if hasattr(self, '_recv_name') and hasattr(self._recv_name,
                                                   'exchange_auto_delete'):
            return self._recv_name.exchange_auto_delete

        return True

    @exchange_auto_delete.setter
    def exchange_auto_delete(self, value):
        self._exchange_auto_delete = value

    @property
    def exchange_durable(self):
        if self._exchange_durable is not None:
            return self._exchange_durable

        if hasattr(self, '_send_name') and hasattr(self._send_name,
                                                   'exchange_durable'):
            return self._send_name.exchange_durable

        if hasattr(self, '_recv_name') and hasattr(self._recv_name,
                                                   'exchange_durable'):
            return self._recv_name.exchange_durable

        return False

    @exchange_durable.setter
    def exchange_durable(self, value):
        self._exchange_durable = value

    def set_close_callback(self, close_callback):
        """
        Sets a callback method to be called when this channel's close method is invoked.

        By default, if no callback is set on this channel, close_impl() is called instead.
        The close callback is chiefly used to pool channels in the Node.

        @param  close_callback  The callback method. Should be a callable taking one argument, this channel.
        """
        self._close_callback = close_callback

    @contextmanager
    def _ensure_transport(self):
        """
        Ensures this Channel has been activated with the Node.
        """
        #        log.debug("BaseChannel._ensure_transport (current: %s)", self._transport is not None)
        if not self._transport:
            raise ChannelError("No transport attached")

        if not self._lock:
            raise ChannelError("No lock available")

        # is lock already acquired? spit out a notice
        if self._lock._is_owned():
            log.warn(
                "INTERLEAVE DETECTED:\n\nCURRENT STACK:\n%s\n\nSTACK THAT LOCKED: %s\n",
                "".join(traceback.format_stack()), "".join(self._lock_trace))

        with self._lock:
            # we could wait and wait, and it gets closed, and unless we check again, we'd never know!
            if not self._transport:
                raise ChannelError("No transport attached")

            self._lock_trace = traceback.format_stack()
            try:
                yield
            finally:
                self._lock_trace = None

    def _declare_exchange(self, exchange):
        """
        Performs an AMQP exchange declare.

        @param  exchange      The name of the exchange to use.
        @TODO: this really shouldn't exist, messaging layer should not make this declaration.  it will be provided.
               perhaps push into an ion layer derivation to help bootstrapping / getting started fast.
        """
        self._exchange = exchange
        assert self._exchange
        assert self._transport

        with self._ensure_transport():

            #           log.debug("Exchange declare: %s, TYPE %s, DUR %s AD %s", self._exchange, self._exchange_type,
            #                                                                 self.exchange_durable, self.exchange_auto_delete)

            self._transport.declare_exchange_impl(
                self._exchange,
                exchange_type=self._exchange_type,
                durable=self.exchange_durable,
                auto_delete=self.exchange_auto_delete)

    def get_channel_id(self):
        """
        Gets the underlying AMQP channel's channel identifier (number).

        @return Channel id, or None.
        """
        if not self._transport:
            return None

        return self._transport.channel_number

    def reset(self):
        """
        Provides a hook for resetting a node (used only by pooling in the node).

        At this base level, is a no-op.
        """
        pass

    def close(self):
        """
        Public close method.

        If a close callback was specified when creating this instance, it will call that,
        otherwise it calls close_impl.

        If created via a Node (99% likely), the Node will take care of
        calling close_impl for you at the proper time.

        @returns    An Event you can wait on for completion of the close. Will be set
                    automatically if the channel came from the Node's pool.
        """
        ev = Event()
        if self._close_callback:
            self._close_callback(self)
            ev.set()
        else:
            self._close_event = ev
            self._fsm.process(self.I_CLOSE)

        return ev

    def _on_close(self, fsm):
        self.close_impl()

    def close_impl(self):
        """
        Closes the AMQP connection.
        """
        log.trace("BaseChannel.close_impl (%s)", self.get_channel_id())
        if self._transport:

            # the close destroys self._transport, so keep a ref here
            transport = self._transport
            with self._ensure_transport():
                transport.close()

            # set to None now so nothing else tries to use the channel during the callback
            self._transport = None

    def on_channel_open(self, transport):
        """
        Node calls here to attach a bound transport.
        """
        transport.add_on_close_callback(self.on_channel_close)
        self.attach_transport(transport)

    def attach_transport(self, transport):
        """
        Attaches a bound transport and indicates this channel is now open.
        """
        self._transport = transport
        self._lock = coros.RLock()
        self._fsm.process(self.I_ATTACH)

    def set_closed_error_callback(self, callback):
        """
        Sets the closed error callback.

        This callback is called when the underlying transport closes early with an error.

        This is typically used for internal operations with the broker and will likely not be
        used by others.

        @param callback     The callback to trigger. Should take three parameters, this channel, the error code, and the error text.
        @returns            The former value of the closed error callback.
        """
        oldval = self._closed_error_callback
        self._closed_error_callback = callback
        return oldval

    @contextmanager
    def push_closed_error_callback(self, callback):
        """
        Context manager based approach to set_closed_error_callback.
        """
        cur_cb = self.set_closed_error_callback(callback)
        try:
            yield callback
        finally:
            self.set_closed_error_callback(cur_cb)

    def on_channel_close(self, transport, code, text):
        """
        Callback for when the Pika channel is closed.
        """
        # make callback to user event if we've closed
        if self._close_event is not None:
            self._close_event.set()
            self._close_event = None

        # remove transport so we don't try to use it again
        # (all?) calls are protected via _ensure_transport, which raise a ChannelError if you try to do anything with it.
        self._transport = None

        # make callback if it exists!
        if not (code == 0 or code == 200) and self._closed_error_callback:
            # run in try block because this can shutter the entire connection
            try:
                self._closed_error_callback(self, code, text)
            except Exception, e:
                log.warn("Closed error callback caught an exception: %s",
                         str(e))

        # fixup channel state fsm, but only if we're not executing a transition right now
        if self._fsm.current_state != self.S_CLOSED and self._fsm.next_state is None:
            self._fsm.current_state = self.S_CLOSED
Example #6
0
class BaseChannel(object):

    _amq_chan                   = None      # underlying transport
    _close_callback             = None      # close callback to use when closing, not always set (used for pooling)
    _closed_error_callback      = None      # callback which triggers when the underlying transport closes with error
    _exchange                   = None      # exchange (too AMQP specific)

    # exchange related settings @TODO: these should likely come from config instead
    _exchange_type              = 'topic'
    _exchange_auto_delete       = True
    _exchange_durable           = False

    # States, Inputs for FSM
    S_INIT                      = 'INIT'
    S_ACTIVE                    = 'ACTIVE'
    S_CLOSED                    = 'CLOSED'
    I_ATTACH                    = 'ATTACH'
    I_CLOSE                     = 'CLOSE'

    def __init__(self, transport=None, close_callback=None):
        """
        Initializes a BaseChannel instance.

        @param  transport       Underlying transport used for broker communication. Can be None, if so, will
                                use the AMQPTransport stateless singleton.
        @type   transport       BaseTransport
        @param  close_callback  The method to invoke when close() is called on this BaseChannel. May be left as None,
                                in which case close_impl() will be called. This expects to be a callable taking one
                                param, this channel instance.
        """
        self.set_close_callback(close_callback)
        self._transport = transport or AMQPTransport.get_instance()

        # setup FSM for BaseChannel / SendChannel tree
        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_ATTACH, self.S_INIT, None, self.S_ACTIVE)
        self._fsm.add_transition(self.I_CLOSE, self.S_ACTIVE, self._on_close, self.S_CLOSED)
        self._fsm.add_transition(self.I_CLOSE, self.S_CLOSED, None, self.S_CLOSED)              # closed is a goal state, multiple closes are ok and are no-ops
        self._fsm.add_transition(self.I_CLOSE, self.S_INIT, None, self.S_CLOSED)                # INIT to CLOSED is fine too

    def set_close_callback(self, close_callback):
        """
        Sets a callback method to be called when this channel's close method is invoked.

        By default, if no callback is set on this channel, close_impl() is called instead.
        The close callback is chiefly used to pool channels in the Node.

        @param  close_callback  The callback method. Should be a callable taking one argument, this channel.
        """
        self._close_callback = close_callback

    def _ensure_amq_chan(self):
        """
        Ensures this Channel has been activated with the Node.
        """
#        log.debug("BaseChannel._ensure_amq_chan (current: %s)", self._amq_chan is not None)
        if not self._amq_chan:
            raise ChannelError("No amq_chan attached")

    def _declare_exchange(self, exchange):
        """
        Performs an AMQP exchange declare.

        @param  exchange      The name of the exchange to use.
        @TODO: this really shouldn't exist, messaging layer should not make this declaration.  it will be provided.
               perhaps push into an ion layer derivation to help bootstrapping / getting started fast.
        """
        self._exchange = exchange
        assert self._exchange

        self._ensure_amq_chan()
        assert self._transport

#        log.debug("Exchange declare: %s, TYPE %s, DUR %s AD %s", self._exchange, self._exchange_type,
#                                                                 self._exchange_durable, self._exchange_auto_delete)

        self._transport.declare_exchange_impl(self._amq_chan,
                                              self._exchange,
                                              exchange_type=self._exchange_type,
                                              durable=self._exchange_durable,
                                              auto_delete=self._exchange_auto_delete)

    def attach_underlying_channel(self, amq_chan):
        """
        Attaches an AMQP channel and indicates this channel is now open.
        """
        self._amq_chan = amq_chan
        self._fsm.process(self.I_ATTACH)

    def get_channel_id(self):
        """
        Gets the underlying AMQP channel's channel identifier (number).

        @return Channel id, or None.
        """
        if not self._amq_chan:
            return None

        return self._amq_chan.channel_number

    def reset(self):
        """
        Provides a hook for resetting a node (used only by pooling in the node).

        At this base level, is a no-op.
        """
        pass

    def close(self):
        """
        Public close method.

        If a close callback was specified when creating this instance, it will call that,
        otherwise it calls close_impl.

        If created via a Node (99% likely), the Node will take care of
        calling close_impl for you at the proper time.
        """
        if self._close_callback:
            self._close_callback(self)
        else:
            self._fsm.process(self.I_CLOSE)

    def _on_close(self, fsm):
        self.close_impl()

    def close_impl(self):
        """
        Closes the AMQP connection.
        """
        log.debug("BaseChannel.close_impl (%s)", self.get_channel_id())
        if self._amq_chan:

            # the close destroys self._amq_chan, so keep a ref here
            amq_chan = self._amq_chan
            amq_chan.close()

            # set to None now so nothing else tries to use the channel during the callback
            self._amq_chan = None

            # PIKA BUG: in v0.9.5, this amq_chan instance will be left around in the callbacks
            # manager, and trips a bug in the handler for on_basic_deliver. We attempt to clean
            # up for Pika here so we don't goof up when reusing a channel number.
            amq_chan.callbacks.remove(amq_chan.channel_number, 'Basic.GetEmpty')
            amq_chan.callbacks.remove(amq_chan.channel_number, 'Channel.Close')
            amq_chan.callbacks.remove(amq_chan.channel_number, '_on_basic_deliver')
            amq_chan.callbacks.remove(amq_chan.channel_number, '_on_basic_get')

            # uncomment these lines to see the full callback list that Pika maintains
            #stro = pprint.pformat(callbacks._callbacks)
            #log.error(str(stro))

    def on_channel_open(self, amq_chan):
        """
        The node calls here to attach an open Pika channel.
        We attach our on_channel_close handler and then call attach_underlying_channel.
        """
        amq_chan.add_on_close_callback(self.on_channel_close)
        self.attach_underlying_channel(amq_chan)

    def set_closed_error_callback(self, callback):
        """
        Sets the closed error callback.

        This callback is called when the underlying transport closes early with an error.

        This is typically used for internal operations with the broker and will likely not be
        used by others.

        @param callback     The callback to trigger. Should take three parameters, this channel, the error code, and the error text.
        @returns            The former value of the closed error callback.
        """
        oldval = self._closed_error_callback
        self._closed_error_callback = callback
        return oldval

    @contextmanager
    def push_closed_error_callback(self, callback):
        """
        Context manager based approach to set_closed_error_callback.
        """
        cur_cb = self.set_closed_error_callback(callback)
        try:
            yield callback
        finally:
            self.set_closed_error_callback(cur_cb)

    def on_channel_close(self, code, text):
        """
        Callback for when the Pika channel is closed.
        """
        logmeth = log.debug
        if not (code == 0 or code == 200):
            logmeth = log.error
        logmeth("BaseChannel.on_channel_close\n\tchannel number: %s\n\tcode: %d\n\ttext: %s", self.get_channel_id(), code, text)

        # remove amq_chan so we don't try to use it again
        # (all?) calls are protected via _ensure_amq_chan, which raise a ChannelError if you try to do anything with it.
        self._amq_chan = None

        # make callback if it exists!
        if not (code == 0 or code == 200) and self._closed_error_callback:
            # run in try block because this can shutter the entire connection
            try:
                self._closed_error_callback(self, code, text)
            except Exception, e:
                log.warn("Closed error callback caught an exception: %s", str(e))
Example #7
0
class BaseChannel(object):

    _amq_chan = None  # underlying transport
    _close_callback = None  # close callback to use when closing, not always set (used for pooling)
    _closed_error_callback = None  # callback which triggers when the underlying transport closes with error
    _exchange = None  # exchange (too AMQP specific)

    # exchange related settings @TODO: these should likely come from config instead
    _exchange_type = 'topic'
    _exchange_auto_delete = True
    _exchange_durable = False

    # States, Inputs for FSM
    S_INIT = 'INIT'
    S_ACTIVE = 'ACTIVE'
    S_CLOSED = 'CLOSED'
    I_ATTACH = 'ATTACH'
    I_CLOSE = 'CLOSE'

    def __init__(self, transport=None, close_callback=None):
        """
        Initializes a BaseChannel instance.

        @param  transport       Underlying transport used for broker communication. Can be None, if so, will
                                use the AMQPTransport stateless singleton.
        @type   transport       BaseTransport
        @param  close_callback  The method to invoke when close() is called on this BaseChannel. May be left as None,
                                in which case close_impl() will be called. This expects to be a callable taking one
                                param, this channel instance.
        """
        self.set_close_callback(close_callback)
        self._transport = transport or AMQPTransport.get_instance()

        # setup FSM for BaseChannel / SendChannel tree
        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_ATTACH, self.S_INIT, None,
                                 self.S_ACTIVE)
        self._fsm.add_transition(self.I_CLOSE, self.S_ACTIVE, self._on_close,
                                 self.S_CLOSED)
        self._fsm.add_transition(
            self.I_CLOSE, self.S_CLOSED, None, self.S_CLOSED
        )  # closed is a goal state, multiple closes are ok and are no-ops
        self._fsm.add_transition(self.I_CLOSE, self.S_INIT, None,
                                 self.S_CLOSED)  # INIT to CLOSED is fine too

    def set_close_callback(self, close_callback):
        """
        Sets a callback method to be called when this channel's close method is invoked.

        By default, if no callback is set on this channel, close_impl() is called instead.
        The close callback is chiefly used to pool channels in the Node.

        @param  close_callback  The callback method. Should be a callable taking one argument, this channel.
        """
        self._close_callback = close_callback

    def _ensure_amq_chan(self):
        """
        Ensures this Channel has been activated with the Node.
        """
        #        log.debug("BaseChannel._ensure_amq_chan (current: %s)", self._amq_chan is not None)
        if not self._amq_chan:
            raise ChannelError("No amq_chan attached")

    def _declare_exchange(self, exchange):
        """
        Performs an AMQP exchange declare.

        @param  exchange      The name of the exchange to use.
        @TODO: this really shouldn't exist, messaging layer should not make this declaration.  it will be provided.
               perhaps push into an ion layer derivation to help bootstrapping / getting started fast.
        """
        self._exchange = exchange
        assert self._exchange

        self._ensure_amq_chan()
        assert self._transport

        #        log.debug("Exchange declare: %s, TYPE %s, DUR %s AD %s", self._exchange, self._exchange_type,
        #                                                                 self._exchange_durable, self._exchange_auto_delete)

        self._transport.declare_exchange_impl(
            self._amq_chan,
            self._exchange,
            exchange_type=self._exchange_type,
            durable=self._exchange_durable,
            auto_delete=self._exchange_auto_delete)

    def attach_underlying_channel(self, amq_chan):
        """
        Attaches an AMQP channel and indicates this channel is now open.
        """
        self._amq_chan = amq_chan
        self._fsm.process(self.I_ATTACH)

    def get_channel_id(self):
        """
        Gets the underlying AMQP channel's channel identifier (number).

        @return Channel id, or None.
        """
        if not self._amq_chan:
            return None

        return self._amq_chan.channel_number

    def reset(self):
        """
        Provides a hook for resetting a node (used only by pooling in the node).

        At this base level, is a no-op.
        """
        pass

    def close(self):
        """
        Public close method.

        If a close callback was specified when creating this instance, it will call that,
        otherwise it calls close_impl.

        If created via a Node (99% likely), the Node will take care of
        calling close_impl for you at the proper time.
        """
        if self._close_callback:
            self._close_callback(self)
        else:
            self._fsm.process(self.I_CLOSE)

    def _on_close(self, fsm):
        self.close_impl()

    def close_impl(self):
        """
        Closes the AMQP connection.
        """
        log.debug("BaseChannel.close_impl (%s)", self.get_channel_id())
        if self._amq_chan:

            # the close destroys self._amq_chan, so keep a ref here
            amq_chan = self._amq_chan
            amq_chan.close()

            # set to None now so nothing else tries to use the channel during the callback
            self._amq_chan = None

            # PIKA BUG: in v0.9.5, this amq_chan instance will be left around in the callbacks
            # manager, and trips a bug in the handler for on_basic_deliver. We attempt to clean
            # up for Pika here so we don't goof up when reusing a channel number.
            amq_chan.callbacks.remove(amq_chan.channel_number,
                                      'Basic.GetEmpty')
            amq_chan.callbacks.remove(amq_chan.channel_number, 'Channel.Close')
            amq_chan.callbacks.remove(amq_chan.channel_number,
                                      '_on_basic_deliver')
            amq_chan.callbacks.remove(amq_chan.channel_number, '_on_basic_get')

            # uncomment these lines to see the full callback list that Pika maintains
            #stro = pprint.pformat(callbacks._callbacks)
            #log.error(str(stro))

    def on_channel_open(self, amq_chan):
        """
        The node calls here to attach an open Pika channel.
        We attach our on_channel_close handler and then call attach_underlying_channel.
        """
        amq_chan.add_on_close_callback(self.on_channel_close)
        self.attach_underlying_channel(amq_chan)

    def set_closed_error_callback(self, callback):
        """
        Sets the closed error callback.

        This callback is called when the underlying transport closes early with an error.

        This is typically used for internal operations with the broker and will likely not be
        used by others.

        @param callback     The callback to trigger. Should take three parameters, this channel, the error code, and the error text.
        @returns            The former value of the closed error callback.
        """
        oldval = self._closed_error_callback
        self._closed_error_callback = callback
        return oldval

    @contextmanager
    def push_closed_error_callback(self, callback):
        """
        Context manager based approach to set_closed_error_callback.
        """
        cur_cb = self.set_closed_error_callback(callback)
        try:
            yield callback
        finally:
            self.set_closed_error_callback(cur_cb)

    def on_channel_close(self, code, text):
        """
        Callback for when the Pika channel is closed.
        """
        logmeth = log.debug
        if not (code == 0 or code == 200):
            logmeth = log.error
        logmeth(
            "BaseChannel.on_channel_close\n\tchannel number: %s\n\tcode: %d\n\ttext: %s",
            self.get_channel_id(), code, text)

        # remove amq_chan so we don't try to use it again
        # (all?) calls are protected via _ensure_amq_chan, which raise a ChannelError if you try to do anything with it.
        self._amq_chan = None

        # make callback if it exists!
        if not (code == 0 or code == 200) and self._closed_error_callback:
            # run in try block because this can shutter the entire connection
            try:
                self._closed_error_callback(self, code, text)
            except Exception, e:
                log.warn("Closed error callback caught an exception: %s",
                         str(e))
Example #8
0
class BaseChannel(object):

    _close_callback             = None      # close callback to use when closing, not always set (used for pooling)
    _closed_error_callback      = None      # callback which triggers when the underlying transport closes with error
    _exchange                   = None      # exchange (too AMQP specific)
    _close_event                = None      # used for giving notice a close was processed

    # exchange related settings @TODO: these should likely come from config instead
    _exchange_type              = 'topic'
    _exchange_auto_delete       = None
    _exchange_durable           = None

    # States, Inputs for FSM
    S_INIT                      = 'INIT'
    S_ACTIVE                    = 'ACTIVE'
    S_CLOSED                    = 'CLOSED'
    I_ATTACH                    = 'ATTACH'
    I_CLOSE                     = 'CLOSE'

    def __init__(self, close_callback=None):
        """
        Initializes a BaseChannel instance.

        @param  transport       Underlying transport used for broker communication. Can be None, if so, will
                                use the AMQPTransport stateless singleton.
        @type   transport       BaseTransport
        @param  close_callback  The method to invoke when close() is called on this BaseChannel. May be left as None,
                                in which case close_impl() will be called. This expects to be a callable taking one
                                param, this channel instance.
        """
        self.set_close_callback(close_callback)
        self._transport = None

        # setup FSM for BaseChannel / SendChannel tree
        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_ATTACH, self.S_INIT, None, self.S_ACTIVE)
        self._fsm.add_transition(self.I_CLOSE, self.S_ACTIVE, self._on_close, self.S_CLOSED)
        self._fsm.add_transition(self.I_CLOSE, self.S_CLOSED, lambda *args: self.on_channel_close(None, 0, ""), self.S_CLOSED)              # closed is a goal state, multiple closes are ok
        self._fsm.add_transition(self.I_CLOSE, self.S_INIT, None, self.S_CLOSED)                # INIT to CLOSED is fine too

    @property
    def exchange_auto_delete(self):
        # Fix OOIION-1710: Added because exchanges get deleted on broker restart
        if CFG.get_safe('container.messaging.names.durable', False):
            self._exchange_auto_delete = False
            return False

        if self._exchange_auto_delete is not None:
            return self._exchange_auto_delete

        if hasattr(self, '_send_name') and hasattr(self._send_name, 'exchange_auto_delete'):
            return self._send_name.exchange_auto_delete

        if hasattr(self, '_recv_name') and hasattr(self._recv_name, 'exchange_auto_delete'):
            return self._recv_name.exchange_auto_delete

        return True

    @exchange_auto_delete.setter
    def exchange_auto_delete(self, value):
        self._exchange_auto_delete = value

    @property
    def exchange_durable(self):
        # Fix OOIION-1710: Added because exchanges get deleted on broker restart
        if CFG.get_safe('container.messaging.names.durable', False):
            self._exchange_durable = True
            return True

        if self._exchange_durable is not None:
            return self._exchange_durable

        if hasattr(self, '_send_name') and hasattr(self._send_name, 'exchange_durable'):
            return self._send_name.exchange_durable

        if hasattr(self, '_recv_name') and hasattr(self._recv_name, 'exchange_durable'):
            return self._recv_name.exchange_durable

        return False

    @exchange_durable.setter
    def exchange_durable(self, value):
        self._exchange_durable = value

    def set_close_callback(self, close_callback):
        """
        Sets a callback method to be called when this channel's close method is invoked.

        By default, if no callback is set on this channel, close_impl() is called instead.
        The close callback is chiefly used to pool channels in the Node.

        @param  close_callback  The callback method. Should be a callable taking one argument, this channel.
        """
        self._close_callback = close_callback

    @contextmanager
    def _ensure_transport(self):
        """
        Ensures this Channel has been activated with the Node.
        """
#        log.debug("BaseChannel._ensure_transport (current: %s)", self._transport is not None)
        if not self._transport:
            raise ChannelError("No transport attached")

        if not self._lock:
            raise ChannelError("No lock available")

        # is lock already acquired? spit out a notice
        if self._lock._is_owned():
            log.warn("INTERLEAVE DETECTED:\n\nCURRENT STACK:\n%s\n\nSTACK THAT LOCKED: %s\n",
                    "".join(traceback.format_stack()), "".join(self._lock_trace))

        with self._lock:
            # we could wait and wait, and it gets closed, and unless we check again, we'd never know!
            if not self._transport:
                raise ChannelError("No transport attached")

            self._lock_trace = traceback.format_stack()
            try:
                yield
            finally:
                self._lock_trace = None

    def _declare_exchange(self, exchange):
        """
        Performs an AMQP exchange declare.

        @param  exchange      The name of the exchange to use.
        @TODO: this really shouldn't exist, messaging layer should not make this declaration.  it will be provided.
               perhaps push into an ion layer derivation to help bootstrapping / getting started fast.
        """
        self._exchange = exchange
        assert self._exchange
        assert self._transport

        with self._ensure_transport():

#           log.debug("Exchange declare: %s, TYPE %s, DUR %s AD %s", self._exchange, self._exchange_type,
#                                                                 self.exchange_durable, self.exchange_auto_delete)

            self._transport.declare_exchange_impl(self._exchange,
                                                  exchange_type=self._exchange_type,
                                                  durable=self.exchange_durable,
                                                  auto_delete=self.exchange_auto_delete)

    def get_channel_id(self):
        """
        Gets the underlying AMQP channel's channel identifier (number).

        @return Channel id, or None.
        """
        if not self._transport:
            return None

        return self._transport.channel_number

    def reset(self):
        """
        Provides a hook for resetting a node (used only by pooling in the node).

        At this base level, is a no-op.
        """
        pass

    def close(self):
        """
        Public close method.

        If a close callback was specified when creating this instance, it will call that,
        otherwise it calls close_impl.

        If created via a Node (99% likely), the Node will take care of
        calling close_impl for you at the proper time.

        @returns    An Event you can wait on for completion of the close. Will be set
                    automatically if the channel came from the Node's pool.
        """
        ev = Event()
        if self._close_callback:
            self._close_callback(self)
            ev.set()
        else:
            self._close_event = ev
            self._fsm.process(self.I_CLOSE)

        return ev

    def _on_close(self, fsm):
        self.close_impl()

    def close_impl(self):
        """
        Closes the AMQP connection.
        """
        #log.trace("BaseChannel.close_impl (%s)", self.get_channel_id())
        if self._transport:

            # the close destroys self._transport, so keep a ref here
            transport = self._transport
            with self._ensure_transport():
                transport.close()

            # set to None now so nothing else tries to use the channel during the callback
            self._transport = None

    def on_channel_open(self, transport):
        """
        Node calls here to attach a bound transport.
        """
        transport.add_on_close_callback(self.on_channel_close)
        self.attach_transport(transport)

    def attach_transport(self, transport):
        """
        Attaches a bound transport and indicates this channel is now open.
        """
        self._transport = transport
        self._lock = RLock()
        self._fsm.process(self.I_ATTACH)

    def set_closed_error_callback(self, callback):
        """
        Sets the closed error callback.

        This callback is called when the underlying transport closes early with an error.

        This is typically used for internal operations with the broker and will likely not be
        used by others.

        @param callback     The callback to trigger. Should take three parameters, this channel, the error code, and the error text.
        @returns            The former value of the closed error callback.
        """
        oldval = self._closed_error_callback
        self._closed_error_callback = callback
        return oldval

    @contextmanager
    def push_closed_error_callback(self, callback):
        """
        Context manager based approach to set_closed_error_callback.
        """
        cur_cb = self.set_closed_error_callback(callback)
        try:
            yield callback
        finally:
            self.set_closed_error_callback(cur_cb)

    def on_channel_close(self, transport, code, text):
        """
        Callback for when the Pika channel is closed.
        """
        # make callback to user event if we've closed
        if self._close_event is not None:
            self._close_event.set()
            self._close_event = None

        # remove transport so we don't try to use it again
        # (all?) calls are protected via _ensure_transport, which raise a ChannelError if you try to do anything with it.
        self._transport = None

        # make callback if it exists!
        if not (code == 0 or code == 200) and self._closed_error_callback:
            # run in try block because this can shutter the entire connection
            try:
                self._closed_error_callback(self, code, text)
            except Exception as e:
                log.warn("Closed error callback caught an exception: %s", str(e))

        # fixup channel state fsm, but only if we're not executing a transition right now
        if self._fsm.current_state != self.S_CLOSED and self._fsm.next_state is None:
            self._fsm.current_state = self.S_CLOSED
Example #9
0
class TransformPoll(TransformStreamListener):
    '''
    Transform Extension of Stream Listener which allows the transform to poll 
    and buffer data from the queue, in lieu of consuming automatically.
    '''
    MESSAGE_BUFFER = 1024
    TIMEOUT        = 2.0
    S_INIT = 'INIT'
    S_READY = 'READY'
    S_FETCH = 'FETCHED'
    S_EXCEPTION = 'EXCEPTION'
    I_READY = 'RESET'
    I_FETCH = 'FETCH'
    I_EXCEPTION = 'EXCEPT'


    def on_start(self):
        self.queue_name  = self.CFG.get_safe('process.queue_name', self.id)
        self.subscriber  = StreamSubscriber(process=self, exchange_name=self.queue_name, callback=self.recv_packet)
        self._msg_buffer = []

        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_READY, self.S_INIT, None, self.S_READY)
        self._fsm.add_transition(self.I_FETCH, self.S_READY, None, self.S_FETCH)
        self._fsm.add_transition(self.I_READY, self.S_FETCH, self._reset, self.S_READY)
        self._fsm.add_transition(self.I_EXCEPTION, self.S_FETCH, None, self.S_EXCEPTION)
        self._fsm.add_transition(self.I_READY, self.S_EXCEPTION, self._reset, self.S_READY)
        self.subscriber.initialize()
        self.done = gevent.event.Event()
        self._fsm.process(self.I_READY)
        self.greenlet = gevent.spawn(self.activate)

    def on_quit(self):
        self._ack_all()
        self.subscriber.close()
        if self.subscriber._chan._amq_chan is not None:
            log.error('Channel is still attached, forcing closure.')
            self.subscriber._chan.close_impl()

        self.done.set()
        self.greenlet.join(5)
        self.greenlet = None


    def _reset(self, fsm):
        self._ack_all()

    def _ack_all(self):
        while self._msg_buffer:
            msg = self._msg_buffer.pop()
            try:
                msg.ack()
            except:
                log.critical('Failed to ack message')

    def ack_all(self):
        '''
        Acknowledge all the messages in the current buffer.
        '''
        self._fsm.process(self.I_READY)

    def reject_all(self):
        '''
        Reject all the messages in the curernt buffer.
        '''
        self._reject_all()
        self._fsm.process(self.I_READY)

    def _reject_all(self):
        while self._msg_buffer:
            msg = self._msg_buffer.pop()
            try:
                msg.reject()
            except:
                log.critical('Failed to reject message')

    def poll_trigger(self):
        '''
        Conditional method for determining when to fetch, meant to be overridden.
        '''
        n = self.poll()
        if n:
            return True
        else:
            return False

    def poll(self):
        '''
        Returns the number of available messages
        '''
        return self.subscriber.get_stats()[0]

    def fetch(self):
        '''
        Method for fetching
        '''
        n = self.poll()
        return self._fetch(n)

    def _fetch(self, n):
        '''
        Fetches n messages from the queue,
        The messages must be acknowledged before another fetch can take place
        '''
        self._fsm.process(self.I_FETCH)
        try:
            if len(self._msg_buffer) + n >= self.MESSAGE_BUFFER:
                raise BadRequest('Request exceeds maximum buffer space')
            try:
                self._msg_buffer.extend( self.subscriber.get_n_msgs(n, self.TIMEOUT))
            except gevent.Timeout:
                raise Timeout
        except:
            self._fsm.process(self.I_EXCEPTION)
            return []
        return [(msg.body, msg.headers) for msg in self._msg_buffer]

    def activate(self):
        pass
Example #10
0
class TransformPoll(TransformStreamListener):
    '''
    Transform Extension of Stream Listener which allows the transform to poll 
    and buffer data from the queue, in lieu of consuming automatically.
    '''
    MESSAGE_BUFFER = 1024
    TIMEOUT = 2.0
    S_INIT = 'INIT'
    S_READY = 'READY'
    S_FETCH = 'FETCHED'
    S_EXCEPTION = 'EXCEPTION'
    I_READY = 'RESET'
    I_FETCH = 'FETCH'
    I_EXCEPTION = 'EXCEPT'

    def on_start(self):
        self.queue_name = self.CFG.get_safe('process.queue_name', self.id)
        self.subscriber = StreamSubscriber(process=self,
                                           exchange_name=self.queue_name,
                                           callback=self.recv_packet)
        self._msg_buffer = []

        self._fsm = FSM(self.S_INIT)
        self._fsm.add_transition(self.I_READY, self.S_INIT, None, self.S_READY)
        self._fsm.add_transition(self.I_FETCH, self.S_READY, None,
                                 self.S_FETCH)
        self._fsm.add_transition(self.I_READY, self.S_FETCH, self._reset,
                                 self.S_READY)
        self._fsm.add_transition(self.I_EXCEPTION, self.S_FETCH, None,
                                 self.S_EXCEPTION)
        self._fsm.add_transition(self.I_READY, self.S_EXCEPTION, self._reset,
                                 self.S_READY)
        self.subscriber.initialize()
        self.done = gevent.event.Event()
        self._fsm.process(self.I_READY)
        self.greenlet = gevent.spawn(self.activate)

    def on_quit(self):
        self._ack_all()
        self.subscriber.close()
        if self.subscriber._chan._amq_chan is not None:
            log.error('Channel is still attached, forcing closure.')
            self.subscriber._chan.close_impl()

        self.done.set()
        self.greenlet.join(5)
        self.greenlet = None

    def _reset(self, fsm):
        self._ack_all()

    def _ack_all(self):
        while self._msg_buffer:
            msg = self._msg_buffer.pop()
            try:
                msg.ack()
            except:
                log.critical('Failed to ack message')

    def ack_all(self):
        '''
        Acknowledge all the messages in the current buffer.
        '''
        self._fsm.process(self.I_READY)

    def reject_all(self):
        '''
        Reject all the messages in the curernt buffer.
        '''
        self._reject_all()
        self._fsm.process(self.I_READY)

    def _reject_all(self):
        while self._msg_buffer:
            msg = self._msg_buffer.pop()
            try:
                msg.reject()
            except:
                log.critical('Failed to reject message')

    def poll_trigger(self):
        '''
        Conditional method for determining when to fetch, meant to be overridden.
        '''
        n = self.poll()
        if n:
            return True
        else:
            return False

    def poll(self):
        '''
        Returns the number of available messages
        '''
        return self.subscriber.get_stats()[0]

    def fetch(self):
        '''
        Method for fetching
        '''
        n = self.poll()
        return self._fetch(n)

    def _fetch(self, n):
        '''
        Fetches n messages from the queue,
        The messages must be acknowledged before another fetch can take place
        '''
        self._fsm.process(self.I_FETCH)
        try:
            if len(self._msg_buffer) + n >= self.MESSAGE_BUFFER:
                raise BadRequest('Request exceeds maximum buffer space')
            try:
                self._msg_buffer.extend(
                    self.subscriber.get_n_msgs(n, self.TIMEOUT))
            except gevent.Timeout:
                raise Timeout
        except:
            self._fsm.process(self.I_EXCEPTION)
            return []
        return [(msg.body, msg.headers) for msg in self._msg_buffer]

    def activate(self):
        pass