Skip to content

Commit

Permalink
(Re)ConnectionInfo: use attributes, not getter functions
Browse files Browse the repository at this point in the history
Simplies the API somewhat.. none of the properties need functions. Thanks to
meejah for additionally pointing out that providing functions implies the
values are immutable/callee-owned, and providing attributes should make
applications think twice about mutating them (they're actually shared, so
mutation would cause serious problems).
  • Loading branch information
warner committed Dec 7, 2016
1 parent 48d21e4 commit 47351c7
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 152 deletions.
78 changes: 40 additions & 38 deletions doc/using-foolscap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1090,59 +1090,60 @@ not populate this attribute, so applications should use ``getattr()``, guard
access with a ``hasattr()`` check, or catch-and-tolerate ``AttributeError``.


The ``ConnectionInfo`` object has methods to tell you the following:
The ``ConnectionInfo`` object has attributes to tell you the following:

* ``ci.connected()``: returns False until the target is "connected" (meaning
that a ``.callRemote()`` might succeed), then returns True until the
connection is lost, then returns False again. If the connection has been
lost, ``callRemote()`` is sure to fail (until a new connection is
established).
* ``ci.connected``: is False until the target is "connected" (meaning that a
``.callRemote()`` might succeed), then is True until the connection is
lost, then is False again. If the connection has been lost,
``callRemote()`` is sure to fail (until a new connection is established).

These methods can help track progress of outbound connections:

* ``ci.connectorStatuses()``: returns a dictionary, where the keys are
connection hints, one for each hint in the FURL that provoked the outbound
connection attempt. Each value is a string that describes the current
status of this hint: "no handler", "resolving hint", "connecting",
* ``ci.connectorStatuses``: is a dictionary, where the keys are connection
hints, one for each hint in the FURL that provoked the outbound connection
attempt. Each value is a string that describes the current status of this
hint: "no handler", "resolving hint", "connecting",
"ConnectionRefusedError", "cancelled", "InvalidHintError", "negotiation",
"negotiation failed:" (and an exception string), "successful", or some
other error string.
* ``ci.connectionHandlers()``: a dictionary, where the keys are connection
* ``ci.connectionHandlers``: a dictionary, where the keys are connection
hints (like ``connectorStatuses()``, and each value is a string description
of the connection handler that is managing that hint (e.g. "tcp" or "tor").
If not connection handler could be found for the hint, the value will be
None.

Once connected, the following methods become useful to tell you when and how
the connection was established:

* ``ci.connectionEstablishedAt()``: Returns None until the connection is
established, then returns a unix timestamp (seconds since epoch) of the
connection time. That timestamp will continue to be returned, even after
the connection is subsequently lost.
* ``ci.winningHint()``: Returns None until an outbound connection is
successfully negotiated, then returns a string with the connection hint
that succeeded. If the connection was created by an inbound socket, this
will remain None (in which case ``ci.listenerStatus()`` will help).
* ``ci.listenerStatus()``: Returns (None, None) until an inbound connection
is accepted, then returns a tuple of (listener, status), both strings. This
Once connected, the following attributes become useful to tell you when and
how the connection was established:

* ``ci.establishedAt``: is None until the connection is
established, then is a unix timestamp (seconds since epoch) of the
connection time. That timestamp will remain set (non-None) even after the
connection is subsequently lost.
* ``ci.winningHint``: is None until an outbound connection is successfully
negotiated, then is a string with the connection hint that succeeded. If
the connection was created by an inbound socket, this will remain None (in
which case ``ci.listenerStatus`` will help).
* ``ci.listenerStatus``: is (None, None) until an inbound connection is
accepted, then is a tuple of (listener, status), both strings. This
provides a description of the listener and its status ("negotiating",
"successful", or "negotiation failed:" and an exception string, except that
the only observable value is "successful"). If the connection was
established by an *outbound* connection, this will remain (None, None).

Finally, when the connection is lost, this method becomes useful:
Finally, when the connection is lost, this attribute becomes useful:

* ``ci.connectionLostAt()``: Returns None until the connection is established
and then lost, then returns a unix timestamp (seconds since epoch) of the
* ``ci.lostAt``: is None until the connection is established and
then lost, then is a unix timestamp (seconds since epoch) of the
connection-loss time.

Note that the ``ConnectionInfo`` object is not "live": connection
establishment or loss may cause the object to be replaced with a new copy. So
applications should re-obtain a new object each time they want to display the
current status. However ``ConnectionInfo`` objects are also not static: the
Tub may keep mutating a given object (and returning the same object to
``getConnectionInfo() calls``) until it needs to replace it.
``getConnectionInfo() calls``) until it needs to replace it. Application code
should obtain the ``ConnectionInfo`` object, read the necessary attributes,
render them to a status display, then drop the object reference.


Reconnector Status
Expand All @@ -1169,16 +1170,17 @@ started.
The ``ReconnectionInfo`` object can be obtained by calling
``reconnector.getReconnectionInfo()``. It provides the following API:

* ``ri.getState()``: returns a string: "unstarted", "connecting",
"connected", or "waiting"
* ``ri.getConnectionInfo()``: return an updated ``ConnectionInfo`` object,
which describes the most recent connection attempt or establishment.
Returns None if the Reconnector is unstarted
* ``ri.lastAttempt()``: returns the time (as seconds since epoch) of the
start of the most recent connection attempt, specifically the timestamp of
the last transition to "connecting".
* ``ri.nextAttempt()``: returns the time of the next scheduled connection
establishment attempt (as seconds since epoch). Returns None if the
* ``ri.state``: a string: "unstarted", "connecting", "connected", or
"waiting"
* ``ri.connectionInfo``: provides the current ``ConnectionInfo`` object,
which describes the most recent connection attempt or establishment. This
will be None if the Reconnector is unstarted.
* ``ri.lastAttempt``: provides the time (as seconds since epoch) of the start
of the most recent connection attempt, specifically the timestamp of the
last transition to "connecting". This will be None if the Reconnector is in
the "unstarted" state.
* ``ri.nextAttempt``: provides the time of the next scheduled connection
establishment attempt (as seconds since epoch). This will be None if the
Reconnector is not in the "waiting" state.


Expand Down
49 changes: 15 additions & 34 deletions src/foolscap/info.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,28 @@

class ConnectionInfo:
def __init__(self):
self._connected = False
self._connector_statuses = {}
self._handler_descriptions = {}
self._listener_description = None
self._listener_status = None
self._winning_hint = None
self._established_at = None
self._lost_at = None
self.connected = False
self.connectorStatuses = {}
self.connectionHandlers = {}
self.listenerStatus = (None, None)
self.winningHint = None
self.establishedAt = None
self.lostAt = None

def _set_connected(self, connected):
self._connected = connected
self.connected = connected

def _set_connection_status(self, location, status):
self._connector_statuses[location] = status
self.connectorStatuses[location] = status
def _describe_connection_handler(self, location, description):
self._handler_descriptions[location] = description
self.connectionHandlers[location] = description
def _set_established_at(self, when):
self._established_at = when
self.establishedAt = when
def _set_winning_hint(self, location):
self._winning_hint = location
self.winningHint = location
def _set_listener_description(self, description):
self._listener_description = description
self.listenerStatus = (description, self.listenerStatus[1])
def _set_listener_status(self, status):
self._listener_status = status
self.listenerStatus = (self.listenerStatus[0], status)
def _set_lost_at(self, when):
self._lost_at = when

def connected(self):
return self._connected

def connectorStatuses(self):
return self._connector_statuses
def connectionHandlers(self):
return self._handler_descriptions

def connectionEstablishedAt(self):
return self._established_at
def winningHint(self):
return self._winning_hint
def listenerStatus(self):
return (self._listener_description, self._listener_status)

def connectionLostAt(self):
return self._lost_at
self.lostAt = when
25 changes: 8 additions & 17 deletions src/foolscap/reconnector.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,19 @@

class ReconnectionInfo:
def __init__(self):
self._state = "unstarted"
self._connectionInfo = None
self._last_attempt = None
self._next_attempt = None
self.state = "unstarted"
self.connectionInfo = None
self.lastAttempt = None
self.nextAttempt = None

def _set_state(self, state):
self._state = state # unstarted, connecting, connected, waiting
self.state = state # unstarted, connecting, connected, waiting
def _set_connection_info(self, connectionInfo):
self._connectionInfo = connectionInfo
self.connectionInfo = connectionInfo
def _set_last_attempt(self, when):
self._last_attempt = when
self.lastAttempt = when
def _set_next_attempt(self, when):
self._next_attempt = when

def getState(self):
return self._state
def getConnectionInfo(self):
return self._connectionInfo
def lastAttempt(self):
return self._last_attempt
def nextAttempt(self):
return self._next_attempt
self.nextAttempt = when


class Reconnector(object):
Expand Down
82 changes: 41 additions & 41 deletions src/foolscap/test/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,37 @@ class Info(unittest.TestCase):
def test_stages(self):
ci = info.ConnectionInfo()

self.assertEqual(ci.connected(), False)
self.assertEqual(ci.connectorStatuses(), {})
self.assertEqual(ci.connectionHandlers(), {})
self.assertEqual(ci.connectionEstablishedAt(), None)
self.assertEqual(ci.winningHint(), None)
self.assertEqual(ci.listenerStatus(), (None, None))
self.assertEqual(ci.connectionLostAt(), None)
self.assertEqual(ci.connected, False)
self.assertEqual(ci.connectorStatuses, {})
self.assertEqual(ci.connectionHandlers, {})
self.assertEqual(ci.establishedAt, None)
self.assertEqual(ci.winningHint, None)
self.assertEqual(ci.listenerStatus, (None, None))
self.assertEqual(ci.lostAt, None)

ci._describe_connection_handler("hint1", "tcp")
ci._set_connection_status("hint1", "working")
self.assertEqual(ci.connectorStatuses(), {"hint1": "working"})
self.assertEqual(ci.connectionHandlers(), {"hint1": "tcp"})
self.assertEqual(ci.connectorStatuses, {"hint1": "working"})
self.assertEqual(ci.connectionHandlers, {"hint1": "tcp"})

ci._set_connection_status("hint1", "successful")
ci._set_winning_hint("hint1")
ci._set_established_at(10.0)
ci._set_connected(True)

self.assertEqual(ci.connected(), True)
self.assertEqual(ci.connectorStatuses(), {"hint1": "successful"})
self.assertEqual(ci.connectionHandlers(), {"hint1": "tcp"})
self.assertEqual(ci.connectionEstablishedAt(), 10.0)
self.assertEqual(ci.winningHint(), "hint1")
self.assertEqual(ci.listenerStatus(), (None, None))
self.assertEqual(ci.connectionLostAt(), None)
self.assertEqual(ci.connected, True)
self.assertEqual(ci.connectorStatuses, {"hint1": "successful"})
self.assertEqual(ci.connectionHandlers, {"hint1": "tcp"})
self.assertEqual(ci.establishedAt, 10.0)
self.assertEqual(ci.winningHint, "hint1")
self.assertEqual(ci.listenerStatus, (None, None))
self.assertEqual(ci.lostAt, None)

ci._set_connected(False)
ci._set_lost_at(15.0)

self.assertEqual(ci.connected(), False)
self.assertEqual(ci.connectionLostAt(), 15.0)
self.assertEqual(ci.connected, False)
self.assertEqual(ci.lostAt, 15.0)

@implementer(ipb.IConnectionHintHandler)
class Handler:
Expand Down Expand Up @@ -95,7 +95,7 @@ def makeTub(self, hint_type, listener_test_options={},
def testInfo(self):
def tubA_sendHello_pause(d2):
ci = tubB.getConnectionInfoForFURL(furl)
self.assertEqual(ci.connectorStatuses(), {hint: "negotiating"})
self.assertEqual(ci.connectorStatuses, {hint: "negotiating"})
d2.callback(None)
test_options = {
"debug_pause_sendHello": tubA_sendHello_pause,
Expand All @@ -106,17 +106,17 @@ def tubA_sendHello_pause(d2):
tubB.addConnectionHintHandler("tcp", h)
d = tubB.getReference(furl)
ci = tubB.getConnectionInfoForFURL(furl)
self.assertEqual(ci.connectorStatuses(), {hint: "resolving hint"})
self.assertEqual(ci.connectorStatuses, {hint: "resolving hint"})
h._d.callback(tcp.DefaultTCP().hint_to_endpoint(hint, reactor,
discard_status))
ci = tubB.getConnectionInfoForFURL(furl)
self.assertEqual(ci.connectorStatuses(), {hint: "connecting"})
self.assertEqual(ci.connectorStatuses, {hint: "connecting"})
# we use debug_pause_sendHello to catch "negotiating" here, then wait
rref = yield d
self.failUnlessEqual(h.asked, 1)
self.failUnlessEqual(h.accepted, 1)
ci = tubB.getConnectionInfoForFURL(furl)
self.assertEqual(ci.connectorStatuses(), {hint: "successful"})
self.assertEqual(ci.connectorStatuses, {hint: "successful"})
del rref

def testNoHandler(self):
Expand All @@ -128,12 +128,12 @@ def testNoHandler(self):
d = tubB.getReference(furl)
del d # XXX
ci = tubB.getConnectionInfoForFURL(furl)
cs = ci.connectorStatuses()
cs = ci.connectorStatuses
self.assertEqual(cs["slow:foo"], "resolving hint")
self.assertEqual(cs[missing_hint], "bad hint: no handler registered")
h._update_status("phase2")
ci = tubB.getConnectionInfoForFURL(furl)
cs = ci.connectorStatuses()
cs = ci.connectorStatuses
self.assertEqual(cs["slow:foo"], "phase2")

@defer.inlineCallbacks
Expand All @@ -143,8 +143,8 @@ def testListener(self):
yield rref1.callRemote("free", Target())
rref2 = self._target.calls[0][0][0]
ci = rref2.getConnectionInfo()
self.assertEqual(ci.connectorStatuses(), {})
(listener, status) = ci.listenerStatus()
self.assertEqual(ci.connectorStatuses, {})
(listener, status) = ci.listenerStatus
self.assertEqual(status, "successful")
self.assertEqual(listener,
"Listener on IPv4Address(TCP, '127.0.0.1', %d)"
Expand All @@ -155,38 +155,38 @@ def testLoopback(self):
furl, tubB, hint = self.makeTub("tcp")
rref1 = yield self._tubA.getReference(furl)
ci = rref1.getConnectionInfo()
self.assertEqual(ci.connectorStatuses(), {"loopback": "connected"})
self.assertEqual(ci.listenerStatus(), (None, None))
self.assertEqual(ci.connectorStatuses, {"loopback": "connected"})
self.assertEqual(ci.listenerStatus, (None, None))


class Reconnection(unittest.TestCase):
def test_stages(self):
ri = reconnector.ReconnectionInfo()

self.assertEqual(ri.getState(), "unstarted")
self.assertEqual(ri.getConnectionInfo(), None)
self.assertEqual(ri.lastAttempt(), None)
self.assertEqual(ri.nextAttempt(), None)
self.assertEqual(ri.state, "unstarted")
self.assertEqual(ri.connectionInfo, None)
self.assertEqual(ri.lastAttempt, None)
self.assertEqual(ri.nextAttempt, None)

ci = object()
ri._set_state("connecting")
ri._set_connection_info(ci)
ri._set_last_attempt(10.0)

self.assertEqual(ri.getState(), "connecting")
self.assertEqual(ri.getConnectionInfo(), ci)
self.assertEqual(ri.lastAttempt(), 10.0)
self.assertEqual(ri.nextAttempt(), None)
self.assertEqual(ri.state, "connecting")
self.assertEqual(ri.connectionInfo, ci)
self.assertEqual(ri.lastAttempt, 10.0)
self.assertEqual(ri.nextAttempt, None)

ri._set_state("connected")

self.assertEqual(ri.getState(), "connected")
self.assertEqual(ri.state, "connected")

ri._set_state("waiting")
ri._set_connection_info(None)
ri._set_next_attempt(20.0)

self.assertEqual(ri.getState(), "waiting")
self.assertEqual(ri.getConnectionInfo(), None)
self.assertEqual(ri.lastAttempt(), 10.0)
self.assertEqual(ri.nextAttempt(), 20.0)
self.assertEqual(ri.state, "waiting")
self.assertEqual(ri.connectionInfo, None)
self.assertEqual(ri.lastAttempt, 10.0)
self.assertEqual(ri.nextAttempt, 20.0)
Loading

0 comments on commit 47351c7

Please sign in to comment.