-
Notifications
You must be signed in to change notification settings - Fork 0
Client Session
The Client Session represents a connection from a PostgreSQL client (e.g., psql, application) to the Springtail proxy. It manages the client's state, handles incoming queries, coordinates with server sessions, and ensures proper query routing and response handling.
The Client Session builds upon a base Session class which provides:
- Basic connection management (socket, read/write buffers)
- Message parsing and framing
- Asynchronous I/O handling
- Connection state tracking
- Query Reception: Receive and parse PostgreSQL protocol messages from client
- Query Routing: Determine which server session should execute the query
- State Management: Track transaction state, prepared statements, portals, cursors
- Response Coordination: Collect responses from server sessions and forward to client
- Session Replay: Ensure new server sessions have correct session state
- Error Handling: Manage error conditions and recovery
┌─────────────┐
│ STARTUP │ Initial state, handling authentication
└──────┬──────┘
│
↓
┌─────────────┐
│ AUTH_SERVER │ Waiting for server authentication
└──────┬──────┘
│
↓
┌─────────────┐
│ READY │ Idle, waiting for next query
└──────┬──────┘
│
↓
┌─────────────┐
│ QUERY │ Processing query (delegated to server session)
└──────┬──────┘
│
↓
┌─────────────┐
│ ERROR │ Fatal error, connection closing
└─────────────┘
- Trigger: Client authentication completes successfully
-
Actions:
- Extract database and user credentials
- Create primary server session
- Initiate server authentication
- Trigger: Server authentication completes successfully
-
Actions:
- Send authentication success to client
- Set transaction status to idle
- Prepare for query processing
- Trigger: Client sends query message (Query, Parse, Bind, Execute, etc.)
-
Actions:
- Validate query message
- Determine target server session (primary vs replica)
- Queue message for server session
- Server session transitions to QUERY or DEPENDENCIES state
- Trigger: Server sends ReadyForQuery
-
Actions:
- Update transaction status (idle, in-transaction, error)
- Client session remains in READY state
- Process next queued message if available
- Trigger: Fatal error, client disconnect, or Terminate message
-
Actions:
- Close connection
- Clean up resources
- Notify server sessions to shut down
The client session receives PostgreSQL protocol messages from the client:
Message Types Handled:
- Query (Simple Protocol): Multiple semicolon-separated SQL statements
- Parse: Prepare a named or unnamed statement
- Bind: Bind parameters to a prepared statement creating a portal
- Execute: Execute a bound portal
- Describe: Request description of a statement or portal
- Close: Close a statement or portal
- Sync: Synchronization point in extended protocol
- Flush: Force pending responses to be sent
For each incoming query, the client session:
- Parse Query Text: Extract SQL statements from message
- Classify Statements: Determine query types (SELECT, INSERT, UPDATE, etc.)
-
Analyze Dependencies: Identify state-changing operations:
- SET statements (session variables)
- PREPARE statements (prepared statements)
- DECLARE statements (cursors)
- Transaction control (BEGIN, COMMIT, ROLLBACK, SAVEPOINT)
- Determine Read Safety: Check if query modifies data or accesses replicated tables
-
Determine Routing:
- Read-only queries → replica (if available and not in transaction)
- Write queries → primary
- Queries in transaction → same server as transaction
The routing logic follows these rules:
In Transaction:
- All queries route to the server that started the transaction
- Typically this is the primary server
- Ensures transaction isolation and consistency
Outside Transaction:
- Write Queries: Always route to primary
-
Read-Only Queries:
- Route to replica if available and database is ready
- Fall back to primary if no replica available
- Fall back to primary if in primary-only mode
Shadow Mode:
- Read-only queries sent to both primary and replica
- Primary results returned to client
- Replica results discarded (for testing/validation)
When sending a query to a server session that hasn't processed queries from this client before, or when switching servers, the client session ensures the server has the correct session state through a process called "session replay."
Session State Includes:
- Session variables (SET statements like
work_mem,application_name) - Prepared statements (PREPARE statements)
- Cursors with hold (DECLARE WITH HOLD statements)
Replay Process:
Client Session State:
- work_mem = '64MB'
- application_name = 'myapp'
- Prepared statement 'stmt1'
First Query to New Server:
↓
1. Client session queries statement cache for replay history
2. Generates dependency messages containing:
- SET work_mem = '64MB'
- SET application_name = 'myapp'
- PREPARE stmt1 AS ...
3. Server session receives dependencies first
4. Server session executes dependencies (server enters DEPENDENCIES state)
5. Server session transitions to QUERY state
6. Server session executes actual query
Transaction Replay:
When switching from replica to primary mid-transaction (rare case):
- Transaction-level statements are also replayed
- Ensures transaction state is consistent on new server
Server sessions send responses back to the client session via callbacks:
Response Types:
- ParseComplete: Parse succeeded
- BindComplete: Bind succeeded
- CommandComplete: Statement executed with result summary
- RowDescription: Column metadata for query results
- DataRow: Individual result row data
- ReadyForQuery: Server ready for next command (includes transaction status)
- ErrorResponse: Query failed with error details
The client session forwards these responses directly to the client, maintaining protocol transparency.
Server sessions invoke callbacks on the client session to report completion and status:
Called when a message completes execution:
For Each Message:
- Server session executes all statements in the message
- Sends completion notification to client session
- Client session updates statement cache
- Records success or failure status
Statement Cache Updates:
- Add prepared statements to cache
- Add cursors to cache
- Track transaction state changes
- Remove closed statements/portals
Called when the server sends ReadyForQuery:
Transaction Status Update:
- Update transaction status (I/T/E)
- Clear associated session if no longer in transaction
- Perform transaction commit/rollback in cache
State Synchronization:
- Replay pending state to other server sessions
- Ensure all server sessions have consistent session state
- Only done when outside transactions
The client session maintains a History Cache that tracks query statements and their properties:
Long-lived state that persists across transactions:
- SET statements: Session variables (e.g., work_mem, search_path)
- PREPARE statements: Named prepared statements
- DECLARE WITH HOLD: Cursors that survive transaction end
- LISTEN statements: Notification channels
Transaction-scoped state that exists only within a transaction:
- All statements executed in the current transaction
- Rolled back on ROLLBACK or error
- Merged to session history on successful COMMIT
- Organized by savepoint levels
Adding Statements:
- Parse each query to determine type and properties
- Track read-safety, dependencies, and side effects
- Associate with current transaction or savepoint level
- Store metadata for replay purposes
Replay for Server Sessions:
- Query cache for statements needed by a server session
- Filter by session vs transaction scope
- Filter by read-only vs read-write
- Generate dependency messages in correct order
Transaction Operations:
- Commit: Merge transaction history to session history
- Rollback: Discard entire transaction history
- Savepoint: Create nested scope level
- Rollback to Savepoint: Discard statements after savepoint
The Client Session operates within a single-threaded asynchronous I/O model:
- Each client session runs on a single I/O thread
- No locking required within a client session
- Server session callbacks execute on the same thread
- Message queues use locks for cross-thread notifications
- Failover notifications use mutex-protected queue
Multiple messages are grouped together to reduce round trips between the proxy and PostgreSQL server. This is especially beneficial for extended protocol sequences (Parse/Bind/Execute/Sync).
Read-only queries are offloaded to replica servers, reducing load on the primary and improving overall throughput. This works transparently as long as the client is not in a transaction.
Server sessions can be pooled and reused across different client sessions, avoiding the overhead of establishing new connections and re-authenticating.
The statement cache minimizes the overhead of session replay by:
- Only replaying statements not yet seen by a server
- Tracking which servers have which state
- Compacting history on transaction commit
- Removing redundant statements
Server sessions pipeline messages to PostgreSQL, allowing multiple messages to be sent before waiting for responses. This reduces latency and improves throughput.