Client State Machine

The Client class uses a state machine to manage its internal state, and wherever possible, prefers callbacks to asyncio tasks. While this makes the code a bit more complex, it is easier to reason about correctness because the state is represented explicitly (as an enum) rather than by the position of the instruction pointer within a coroutine (or several).

The following states are used

CONNECTING

We’re establishing the TCP connection.

NEGOTIATING

We’ve established the TCP connection and are waiting for #katcp-protocol.

CONNECTED

We’re fully connected.

DISCONNECTING

We have started disconnecting from our side, but the connection still exists.

SLEEPING

We’re sleeping between reconnection attempts.

CLOSED

There is no connection and we are not going to try again (either because Client.close() was called or because auto-reconnect is disabled).

The possible transitions between these states are depicted below. To keep the picture readable, the SLEEPING and CLOSED states are represented together. Transitions to these states will transition to SLEEPING if auto-reconnection is enabled and CLOSED if not (calling Client.close internally disables auto-reconnection). There are no transitions from CLOSED.

Figure made with TikZ

Transitions marked in red above will call the failed connection callbacks, provided that there is an exception to report. Transitions to CONNECTED trigger connected callbacks, while transitions from CONNECTED trigger disconnected callbacks.

Invariants

The following invariants are maintained at any time the event loop runs or when calling user callbacks. Most of the work of ensuring this occurs in Client._set_state(). These invariants are checked by ClientStateMachine in tests/test_client.py.

  • Client.is_connected is true iff the state is CONNECTED.

  • Client._closed_event is set iff the state is CLOSED.

  • Client._connection is None iff the state is CONNECTING, SLEEPING or CLOSED.

  • Client._disconnected_event is set iff Client._connection is None.

  • Client._connect_task is set iff the state is CONNECTING.

  • Client._sleep_handle is set iff the state is SLEEPING.

  • If the state is DISCONNECTING, then Client._connection.is_closing() is true (the reverse is not true: a fatal I/O error on the connection will schedule a asyncio.BaseProtocol.connection_lost() call for the next event loop iteration).

  • In states DISCONNECTING, CLOSED and SLEEPING, Client.last_exc will be set.

  • In state CONNECTED, Client.last_exc will be None.