State Machine¶
The state machine tracks connection phases at the level of detail pygwire needs for correct decoding and useful lifecycle information. It is not intended to enforce every rule of the PostgreSQL protocol.
Phases exist for three reasons:
-
Framing. The PostgreSQL wire protocol uses different message framing depending on the connection phase. During
STARTUPmessages have no identifier byte (just length + payload). DuringSSL_NEGOTIATIONandGSS_NEGOTIATIONthe server responds with a single byte. All other phases use standard framing (identifier byte + length + payload). The state machine is used to determine the framing mode the codec should use. -
Message disambiguation. Some message identifiers are reused across phases. The
'p'byte can meanPasswordMessage,SASLInitialResponse, orSASLResponsedepending on the current authentication phase. The SASL sub-phases (AUTHENTICATING_SASL_INITIAL,AUTHENTICATING_SASL_CONTINUE) exist so the codec can decode the correct message type. -
Lifecycle tracking. Phases like
READY,SIMPLE_QUERY,EXTENDED_QUERY, and theCOPY_*phases let consumers answer questions like "is it safe to send a query?" or "is the server still processing my request?". These phases are not required by the codec itself but are useful for building clients, proxies, and connection pools (etc.) on top of pygwire.
The state machine does not validate message ordering within a phase. For example, it will not reject a DataRow sent before RowDescription during SIMPLE_QUERY, because both are valid message types in that phase. Enforcing that kind of sequencing would require SQL-level knowledge and belongs in a higher-level layer built on top of pygwire.
State machines¶
| State Machine | Role | Use case |
|---|---|---|
FrontendStateMachine |
Client | Track client-side protocol phase |
BackendStateMachine |
Server | Track server-side protocol phase |
Both track a ConnectionPhase and raise StateMachineError if a message type is not valid for the current phase.
FrontendStateMachine¶
Tracks protocol state from the client's perspective.
from pygwire import ConnectionPhase
from pygwire import FrontendStateMachine
sm = FrontendStateMachine(phase=ConnectionPhase.STARTUP)
Parameters:
phase(ConnectionPhase, defaultSTARTUP): Initial connection phase.
Methods¶
send(msg) -> None¶
Record sending a frontend message. Raises StateMachineError if the message is not valid for the current phase.
receive(msg) -> None¶
Record receiving a backend message. Raises StateMachineError if the message is not valid for the current phase.
Properties¶
| Property | Type | Description |
|---|---|---|
phase |
ConnectionPhase |
Current connection phase |
is_ready |
bool |
True if phase is READY |
is_active |
bool |
True if not in TERMINATED or FAILED |
pending_syncs |
int |
Number of pending Sync responses (for pipelined extended queries) |
BackendStateMachine¶
Tracks protocol state from the server's perspective. Same API as FrontendStateMachine.
from pygwire import ConnectionPhase
from pygwire import BackendStateMachine
sm = BackendStateMachine(phase=ConnectionPhase.STARTUP)
Parameters: Same as FrontendStateMachine.
Methods¶
receive(msg) -> None¶
Record receiving a frontend message. Raises StateMachineError if invalid for the current phase.
send(msg) -> None¶
Record sending a backend message. Raises StateMachineError if invalid for the current phase.
Properties¶
Same as FrontendStateMachine.
ConnectionPhase¶
Enum of connection lifecycle phases.
| Phase | Description |
|---|---|
STARTUP |
Initial state, waiting for startup message |
SSL_NEGOTIATION |
SSL/TLS negotiation in progress |
GSS_NEGOTIATION |
GSS encryption negotiation in progress |
AUTHENTICATING |
Authentication exchange active |
AUTHENTICATING_SASL_INITIAL |
SASL authentication initial response |
AUTHENTICATING_SASL_CONTINUE |
SASL authentication continuation |
INITIALIZATION |
Post-auth setup (ParameterStatus, BackendKeyData) |
READY |
Idle, ready for queries |
SIMPLE_QUERY |
Simple query protocol active |
EXTENDED_QUERY |
Extended query protocol active |
COPY_IN |
COPY FROM stdin active |
COPY_OUT |
COPY TO stdout active |
FUNCTION_CALL |
Legacy function call active |
TERMINATED |
Connection closed |
FAILED |
Unrecoverable error |
Typical phase flow¶
STARTUP → AUTHENTICATING → INITIALIZATION → READY
↕
SIMPLE_QUERY
EXTENDED_QUERY
COPY_IN / COPY_OUT
↓
TERMINATED
StateMachineError¶
Raised when an invalid message is sent or received for the current connection phase. Subclass of ProtocolError.
Basic usage¶
"""State Machine: Basic usage."""
from pygwire import FrontendStateMachine, TransactionStatus
from pygwire.messages import (
AuthenticationOk,
BackendKeyData,
ParameterStatus,
ReadyForQuery,
StartupMessage,
)
sm = FrontendStateMachine()
print(sm.phase) # ConnectionPhase.STARTUP
# Record messages as you send/receive them
sm.send(StartupMessage(params={"user": "postgres", "database": "mydb"}))
print(sm.phase) # ConnectionPhase.AUTHENTICATING
sm.receive(AuthenticationOk())
sm.receive(ParameterStatus(name="server_version", value="15.0"))
sm.receive(BackendKeyData(process_id=1234, secret_key=b"\x00\x00\x00\x01"))
sm.receive(ReadyForQuery(status=TransactionStatus.IDLE))
print(sm.phase) # ConnectionPhase.READY
Error handling¶
"""State Machine: Error handling."""
from pygwire import FrontendStateMachine, StateMachineError
from pygwire.messages import Query
sm = FrontendStateMachine()
try:
# Can't send a query before completing startup
sm.send(Query(query_string="SELECT 1"))
except StateMachineError as e:
print(f"Invalid: {e}")
Proxy usage¶
A proxy needs state machines for both sides:
"""State Machine: Proxy usage."""
from pygwire import BackendStateMachine, FrontendStateMachine
frontend_sm = FrontendStateMachine()
backend_sm = BackendStateMachine()
# When a client message arrives:
# frontend_sm.send(client_msg) # Client sent it
# backend_sm.receive(client_msg) # Server received it
# When a server message arrives:
# backend_sm.send(server_msg) # Server sent it
# frontend_sm.receive(server_msg) # Client received it
# Both state machines should stay in the same phase.
# A mismatch indicates a protocol violation.
Both state machines should stay in the same phase. A mismatch indicates a protocol violation.
Connection classes
If you do not need to manage the decoder and state machine separately, use FrontendConnection or BackendConnection from pygwire.connection. They coordinate both automatically. See the Connection reference.