Client State Machine ==================== The :class:`.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 :meth:`.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 :class:`.Client.close` internally disables auto-reconnection). There are no transitions from **CLOSED**. .. tikz:: :libs: graphs,graphdrawing,quotes \tikzset{ >=latex, bend angle=10, } \graph[ simple necklace layout, node distance=8cm, edges={nodes={sloped, font=\scriptsize}}, ] { % Success path CONNECTING ->["connection\_made"] NEGOTIATING; NEGOTIATING ->["\#katcp-protocol received"] CONNECTED; CONNECTED ->["\#disconnect / EOF / close"] DISCONNECTING; DISCONNECTING -> ["connection\_lost"] SC[as={SLEEPING\\ CLOSED}, align=center]; SC ->["backoff timer reached", bend right] CONNECTING; % Other cases CONNECTING ->["connection failed / close", bend right, swap, red] SC; NEGOTIATING ->["connection\_lost", red] SC; NEGOTIATING ->["invalid \#katcp-protocol / \#disconnect / EOF / close", red] { DISCONNECTING }; CONNECTED ->["connection\_lost", near end] SC; SC ->["close", loop right] SC; }; 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 :meth:`.Client._set_state`. These invariants are checked by :class:`!ClientStateMachine` in :file:`tests/test_client.py`. - :attr:`.Client.is_connected` is true iff the state is **CONNECTED**. - :attr:`.Client._closed_event` is set iff the state is **CLOSED**. - :attr:`.Client._connection` is ``None`` iff the state is :attr:`CONNECTING`, :attr:`SLEEPING` or :attr:`CLOSED`. - :attr:`Client._disconnected_event` is set iff :attr:`Client._connection` is ``None``. - :attr:`Client._connect_task` is set iff the state is :attr:`CONNECTING`. - :attr:`Client._sleep_handle` is set iff the state is :attr:`SLEEPING`. - If the state is :attr:`DISCONNECTING`, then :attr:`Client._connection.is_closing()` is true (the reverse is not true: a fatal I/O error on the connection will schedule a :meth:`asyncio.BaseProtocol.connection_lost` call for the next event loop iteration). - In states **DISCONNECTING**, **CLOSED** and **SLEEPING**, :attr:`.Client.last_exc` will be set. - In state **CONNECTED**, :attr:`.Client.last_exc` will be ``None``.