deb-python-pyngus/pyngus/endpoint.py

194 lines
7.2 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.
#
# 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 logging
import proton
LOG = logging.getLogger(__name__)
_PROTON_VERSION = (int(getattr(proton, "VERSION_MAJOR", 0)),
int(getattr(proton, "VERSION_MINOR", 0)))
class Endpoint(object):
"""AMQP Endpoint state machine."""
# Endpoint States:
STATE_UNINIT = 0 # initial state
STATE_PENDING = 1 # local opened, waiting for remote to open
STATE_REQUESTED = 2 # remote opened, waiting for local to open
STATE_CANCELLED = 3 # local closed before remote opened
STATE_ABANDONED = 4 # remote closed before local opened
STATE_ACTIVE = 5
STATE_NEED_CLOSE = 6 # remote closed, waiting for local close
STATE_CLOSING = 7 # locally closed, pending remote close
STATE_CLOSED = 8 # terminal state
STATE_ERROR = 9 # unexpected state transition
STATE_NAMES = ["STATE_UNINIT", "STATE_PENDING", "STATE_REQUESTED",
"STATE_CANCELLED", "STATE_ABANDONED", "STATE_ACTIVE",
"STATE_NEED_CLOSE", "STATE_CLOSING", "STATE_CLOSED",
"STATE_ERROR"]
# Events:
# These correspond to endpoint events generated by the Proton Engine
LOCAL_OPENED = 0
LOCAL_CLOSED = 1
REMOTE_OPENED = 2
REMOTE_CLOSED = 3
EVENT_NAMES = ["LOCAL_OPENED", "LOCAL_CLOSED",
"REMOTE_OPENED", "REMOTE_CLOSED"]
# Endpoint Finite State Machine:
# Indexed by current state, each entry is indexed by the event received and
# returns a tuple of (next-state, action). If there is no entry for a
# given event, _ep_error() is invoked and the endpoint moves to the
# terminal STATE_ERROR state.
_FSM = {}
_FSM[STATE_UNINIT] = {
LOCAL_OPENED: (STATE_PENDING, None),
REMOTE_OPENED: (STATE_REQUESTED, lambda s: s._ep_requested())
}
_FSM[STATE_PENDING] = {
LOCAL_CLOSED: (STATE_CANCELLED, None),
REMOTE_OPENED: (STATE_ACTIVE, lambda s: s._ep_active())
}
_FSM[STATE_REQUESTED] = {
LOCAL_OPENED: (STATE_ACTIVE, lambda s: s._ep_active()),
REMOTE_CLOSED: (STATE_ABANDONED, None)
}
_FSM[STATE_CANCELLED] = {
REMOTE_OPENED: (STATE_CLOSING, None)
}
_FSM[STATE_ABANDONED] = {
LOCAL_OPENED: (STATE_NEED_CLOSE, lambda s: s._ep_need_close()),
LOCAL_CLOSED: (STATE_CLOSED, lambda s: s._ep_closed())
}
_FSM[STATE_ACTIVE] = {
LOCAL_CLOSED: (STATE_CLOSING, None),
REMOTE_CLOSED: (STATE_NEED_CLOSE, lambda s: s._ep_need_close())
}
_FSM[STATE_NEED_CLOSE] = {
LOCAL_CLOSED: (STATE_CLOSED, lambda s: s._ep_closed())
}
_FSM[STATE_CLOSING] = {
REMOTE_CLOSED: (STATE_CLOSED, lambda s: s._ep_closed())
}
_FSM[STATE_CLOSED] = {
REMOTE_CLOSED: (STATE_CLOSED, None)
}
_FSM[STATE_ERROR] = { # terminal state
LOCAL_OPENED: (STATE_ERROR, None),
LOCAL_CLOSED: (STATE_ERROR, None),
REMOTE_OPENED: (STATE_ERROR, None),
REMOTE_CLOSED: (STATE_ERROR, None)
}
def __init__(self, name):
self._name = name
self._state = Endpoint.STATE_UNINIT
if (_PROTON_VERSION < (0, 8)):
# The old proton event model did not generate specific endpoint
# events. Rather it simply indicated local or remote state change
# occured without giving the value of the state (opened/closed).
# Map these events to open and close events, assuming the Proton
# endpoint state transitions are fixed to the following sequence:
# UNINIT --> ACTIVE --> CLOSED
self._remote_events = [Endpoint.REMOTE_OPENED,
Endpoint.REMOTE_CLOSED]
self._local_events = [Endpoint.LOCAL_OPENED,
Endpoint.LOCAL_CLOSED]
def _process_endpoint_event(self, event):
"""Called when the Proton Engine generates an endpoint state change
event.
"""
LOG.debug("Endpoint %s event: %s",
self._name, Endpoint.EVENT_NAMES[event])
state_fsm = Endpoint._FSM[self._state]
entry = state_fsm.get(event)
if not entry:
# protocol error: invalid event for current state
old_state = self._state
self._state = Endpoint.STATE_ERROR
self._ep_error("invalid event=%s in state=%s" %
(Endpoint.EVENT_NAMES[event],
Endpoint.STATE_NAMES[old_state]))
return
LOG.debug("Endpoint %s Old State: %s New State: %s",
self._name,
Endpoint.STATE_NAMES[self._state],
Endpoint.STATE_NAMES[entry[0]])
self._state = entry[0]
if entry[1]:
entry[1](self)
if (_PROTON_VERSION < (0, 8)):
def _process_remote_state(self):
"""Call when remote endpoint state changes."""
try:
event = self._remote_events.pop(0)
self._process_endpoint_event(event)
except IndexError:
LOG.debug("Endpoint %s: ignoring unexpected remote event",
self._name)
def _process_local_state(self):
"""Call when local endpoint state changes."""
try:
event = self._local_events.pop(0)
self._process_endpoint_event(event)
except IndexError:
LOG.debug("Endpoint %s: ignoring unexpected local event",
self._name)
else:
def _process_remote_state(self):
pass
def _process_local_state(self):
pass
@property
def _endpoint_state(self):
"""Returns the current endpoint state."""
raise NotImplementedError("Must Override")
# state entry actions - overridden by endpoint subclass:
def _ep_requested(self):
"""Remote has activated a new endpoint."""
LOG.debug("endpoint_requested - ignored")
def _ep_active(self):
"""Both ends of the Endpoint have become active."""
LOG.debug("endpoint_active - ignored")
def _ep_need_close(self):
"""The remote has closed its end of the endpoint."""
LOG.debug("endpoint_need_close - ignored")
def _ep_closed(self):
"""Both ends of the endpoint have closed."""
LOG.debug("endpoint_closed - ignored")
def _ep_error(self, error):
"""Unanticipated/illegal state change."""
LOG.error("Endpoint state error: endpoint=%s, error=%s",
self._name, error)