Back to Proxysql

ProxySQL MySQL X Protocol Plugin — Architecture & Design (v2)

doc/mysqlx/ARCHITECTURE.md

4.0.1053.6 KB
Original Source

ProxySQL MySQL X Protocol Plugin — Architecture & Design (v2)

1. Executive Summary

The mysqlx plugin is ProxySQL's first dynamically loaded plugin. It adds MySQL X Protocol support without modifying the core proxy engine. The plugin uses the ProxySQL Plugin ABI (currently version 3 — descriptor ABI 2 four-phase lifecycle plus the ABI-3 services.register_runtime_view callback for declaring admin-side projections of module state) to register admin tables, commands, runtime-view refreshes, and manage its own listener sockets and worker threads.

v2 introduces an event-driven architecture replacing the Phase 1 single-threaded blocking model. Instead of one thread handling one session at a time with blocking I/O, v2 uses N configurable threads each running an independent poll() event loop, cooperatively multiplexing thousands of concurrent X Protocol sessions — matching ProxySQL's core MySQL_Thread / PgSQL_Thread design.

Traditional MySQL connections use the classic wire protocol (port 3306). MySQL 8.x introduced the X Protocol (default port 33060), a document-oriented, protobuf-based protocol that supports CRUD operations, prepared statements, and pipelined commands. The mysqlx plugin makes ProxySQL a transparent proxy for X Protocol clients, handling authentication, connection routing, protocol-aware frame forwarding for all 23 client message types, connection pooling, and bidirectional relay while reusing ProxySQL's admin SQLite infrastructure for configuration.

2. Architecture Diagram

                               ┌─────────────────────────────────────┐
                               │         ProxySQL Core               │
                               │                                     │
                               │  ┌──────────┐  ┌───────────────┐  │
                               │  │ Admin    │  │ Plugin        │  │
                               │  │ Handler  │  │ Manager       │  │
                               │  │          │  │               │  │
                               │  │ MYSQLX   │  │ dlopen/dlsym  │  │
                               │  │ aliases  │  │ register_table│  │
                               │  │    ↓     │  │ register_cmd  │  │
                               │  │ dispatch │  │ dispatch_cmd  │  │
                               │  └────┬─────┘  └───────┬───────┘  │
                               │       │                │           │
                               └───────┼────────────────┼───────────┘
                                       │                │
                     ┌─────────────────┼────────────────┼──────────────┐
                     │  ProxySQL_MySQLX_Plugin.so       │              │
                     │                                  │              │
                     │  ┌───────────────────┐  ┌───────┴────────┐    │
                     │  │  Mysqlx_Thread    │  │ mysqlx_admin_   │    │
                     │  │  (x N threads)    │  │ schema          │    │
                     │  │                   │  │                  │    │
                     │  │  poll() loop      │  │ Table DDL        │    │
                     │  │  Accept (builtin) │  │ LOAD/SAVE cmds   │    │
                     │  │  Session dispatch │  │ Variables        │    │
                     │  └────────┬──────────┘  └────────────────┘    │
                     │           │                                      │
                     │  ┌────────┴──────────┐                         │
                     │  │ MysqlxSession     │                         │
                     │  │ (per connection)  │                         │
                     │  │                   │                         │
                     │  │ 22 states         │                         │
                     │  │  Auth (MYSQL41)   │                         │
                     │  │  Message dispatch │                         │
                     │  │  Frame forwarding │                         │
                     │  └────┬─────────┬────┘                         │
                     │       │         │                              │
                     │  ┌────┴───┐ ┌───┴──────────┐                  │
                     │  │client  │ │server         │                  │
                     │  │DS      │ │DS             │                  │
                     │  │        │ │               │                  │
                     │  │Frame   │ │Frame          │                  │
                     │  │parse   │ │forward        │                  │
                     │  └────────┘ └──────┬────────┘                  │
                     │                    │                            │
                     │  ┌─────────────────┴──────────┐                │
                     │  │ MysqlxConnection (pooled)   │                │
                     │  │                              │                │
                     │  │ Per-thread cache + global    │                │
                     │  │ Match by hostgroup/user/schema│               │
                     │  └──────────────────────────────┘                │
                     │                                                  │
                     │  ┌──────────────────┐   ┌────────────────┐     │
                     │  │ mysqlx_config_   │   │ mysqlx_stats   │     │
                     │  │ store            │   │                │     │
                     │  │                  │   │ Atomic counters│     │
                     │  │ Identity cache   │   │ SQLite flush   │     │
                     │  │ Route cache      │   │                │     │
                     │  │ Endpoint cache   │   │                │     │
                     │  │ TLS config       │   │                │     │
                     │  └──────────────────┘   └────────────────┘     │
                     │                                                  │
                     └──────────────────────────────────────────────────┘
                                               │
                                               ▼
                                     ┌──────────────────┐
                                     │  MySQL 8.x       │
                                     │  (X Protocol     │
                                     │   port 33060)    │
                                     └──────────────────┘

3. Component Descriptions

3.1 Mysqlx_Thread — Event Loop

Headerplugins/mysqlx/include/mysqlx_thread.h
Sourceplugins/mysqlx/src/mysqlx_thread.cpp
ResponsibilityCore event loop. Each thread runs an independent poll() loop, accepting new connections (listeners are built into the poll set), processing session state machines, and managing a per-thread connection cache. Multiple threads run in parallel (configurable via mysqlx_thread_pool_size, default 4, max 64).
Key methodsinit(thread_index) — create signal pipe, initialize poll set. start() / stop() — thread lifecycle. run() — main poll loop: poll()process_ready_fds()process_all_sessions(). add_listener(addr, port) — add a listener socket to the poll set as XDS_LISTENER. get_connection_from_cache(hostgroup, user, schema) — per-thread pool lookup. return_connection_to_cache(conn) — return a reusable connection.
Thread safetyEach Mysqlx_Thread is independent — no shared state between threads. The per-thread connection cache uses a std::mutex for safety. Signal pipe for wakeup on shutdown.

3.2 MysqlxSession — State Machine

Headerplugins/mysqlx/include/mysqlx_session.h
Sourceplugins/mysqlx/src/mysqlx_session.cpp
ResponsibilityX Protocol session state machine with 22 states. Handles the full client connection lifecycle cooperatively: capabilities exchange → TLS negotiation → MYSQL41/PLAIN auth → identity resolution → backend connection (pool-first) → message dispatch → frame forwarding → session close. Each handler returns control immediately when waiting for I/O.
Key statesCONNECTING_CLIENTX_CAPABILITIES_GETX_CAPABILITIES_SETX_TLS_ACCEPT_* (optional) → X_AUTH_STARTX_AUTH_CHALLENGE_SENTX_AUTH_OK_SENTWAITING_CLIENT_XMSGCONNECTING_SERVERWAITING_SERVER_XMSGX_FAST_FORWARDX_SESSION_CLOSING
Key methodshandler() — top-level dispatcher using switch(status) with to_process flag. handler_connecting_client() — wait for first frame, detect message type. handler_capabilities_get() — build and send ServerCapabilities. handler_capabilities_set() — process TLS request, send OK. handler_auth_start() / handler_auth_challenge_response() — MYSQL41 challenge-response. dispatch_client_message(msg_type) — protocol-aware dispatch for all 23 client message types. handler_waiting_server_msg() — forward backend frames to client. handler_fast_forward() — bidirectional frame relay.
Thread safetyEach session is owned by exactly one Mysqlx_Thread. No cross-thread access. The session reads from MysqlxConfigStore using shared (reader) locks.

3.3 MysqlxDataStream — Non-Blocking Frame I/O

Headerplugins/mysqlx/include/mysqlx_data_stream.h
Sourceplugins/mysqlx/src/mysqlx_data_stream.cpp
ResponsibilityNon-blocking X Protocol frame parsing from raw TCP byte streams. Handles partial reads: accumulates bytes in a read buffer, parses complete frames (5-byte header + protobuf body), and queues them for the session. Writes are buffered and flushed when POLLOUT is available. Supports TLS mode via OpenSSL (encrypted flag).
Key methodsinit(type, fd) — set type (FRONTEND/BACKEND/LISTENER) and non-blocking fd. feed_bytes(data, len) — append bytes, trigger frame parsing. has_complete_frame() / front_frame() / pop_frame() — frame queue access. enqueue_frame(msg_type, body, len) — buffer an X Protocol frame for writing. read_from_net()recv()feed_bytes(). write_to_net() — flush write buffer via send().
Thread safetyEach data stream is owned by exactly one session, which is owned by one thread. No concurrent access.
ConstantsX_FRAME_HEADER_SIZE = 5 (4 bytes LE payload size + 1 byte message type). X_MAX_PAYLOAD_SIZE = 16MB.

3.4 MysqlxConnection — Pooled Backend Connection

Headerplugins/mysqlx/include/mysqlx_connection.h
Sourceplugins/mysqlx/src/mysqlx_connection.cpp
ResponsibilityBackend connection object for connection pooling. Tracks connection state (CREATED → CONNECTING → AUTHENTICATING → IDLE → IN_USE), multiplexing eligibility (transactions and prepared statements disable reuse), and connection metadata (hostgroup, user, schema, address, port). Supports non-blocking connect() with EINPROGRESS.
Key methodsis_reusable() — true only if state is IDLE, not in transaction, no prepared statements. reset() — clear transaction/stmt flags, mark reusable. start_connect(host, port) — non-blocking socket(SOCK_NONBLOCK) + connect(), returns 0 (immediate), 1 (EINPROGRESS), or -1 (error). check_connect() — verify SO_ERROR == 0 after poll returns.
Thread safetyOwned by one thread at a time. When in the per-thread cache, protected by the thread's conn_cache_mutex_.

3.5 mysqlx_plugin (Entry Point)

Headerplugins/mysqlx/include/mysqlx_plugin.h
Sourceplugins/mysqlx/src/mysqlx_plugin.cpp
ResponsibilityPlugin entry point. Exports proxysql_plugin_descriptor_v1 — the single symbol the PluginManager resolves. Owns the plugin context: a MysqlxConfigStore, the cached ProxySQL_PluginServices*, the Mysqlx_Thread pool, and the route_to_thread mapping. Creates N Mysqlx_Thread instances on start, joins them on stop.
Key methodsmysqlx_init(services) — create config/stats stores, register admin tables and commands via the services pointer. mysqlx_start() — load config from runtime SQLite, create thread pool, add listeners to threads, start all threads. mysqlx_stop() — stop and join all threads, free resources. mysqlx_status_json() — return a JSON string for monitoring.
Thread safetyThe descriptor functions are called sequentially by the PluginManager during startup/shutdown. No concurrent access to the plugin context during init/start/stop.

3.6 mysqlx_config_store

Headerplugins/mysqlx/include/mysqlx_config_store.h
Sourceplugins/mysqlx/src/mysqlx_config_store.cpp
ResponsibilityAuthoritative in-memory store of mysqlx configuration. This is the canonical state — the editable mysqlx_* admin tables are persistent input, the runtime_mysqlx_* tables are admin-side projections of this store (see §10.2). Maintains: identities (user → hostgroup + credentials), routes (name → listener bind + destination hostgroup), endpoint overrides (per-host X-Protocol port), per-hostgroup endpoint lists rebuilt from runtime_mysql_servers, topology generation tracking, TLS configuration, and connection pool settings.
Key methodsPer-entity install (LOAD path): install_users_from_admin(db, err), install_routes_from_admin(db, err), install_endpoints_from_admin(db, err), install_variables_from_admin(db, err) — each SELECTs the editable mysqlx_<X> table (plus, for users, runtime_mysql_users for canonical identity, and for endpoints, runtime_mysql_servers for the hostgroup → host topology), builds a fresh local map, and atomically swaps it under the exclusive lock. Per-entity save (SAVE path): save_users_to_admin_table(db), save_routes_to_admin_table(db), save_endpoints_to_admin_table(db), save_variables_to_admin_table(db) — dump current store state into the editable mysqlx_<X> table (UPDATE active=0, then REPLACE INTO with active=1). Per-entity projection (runtime-view refresh): project_users_to_runtime_view(db) etc. — DELETE FROM runtime_mysqlx_<X>; INSERT ... <store dump>. Lookups: resolve_identity(username), pick_endpoint(route_name), route_exists(route_name).
Thread safetystd::shared_mutex — shared (reader) lock for lookups and projections, exclusive (writer) lock for the install/save paths.

3.7 mysqlx_stats

Headerplugins/mysqlx/include/mysqlx_stats.h
Sourceplugins/mysqlx/src/mysqlx_stats.cpp
ResponsibilityPer-route atomic counters tracking connections, queries, errors, and bytes transferred. Periodically flushes aggregated statistics to the stats_mysqlx_routes SQLite table.
Thread safetyAll counters are std::atomic<uint64_t>. Lock-free per-counter increments.

3.8 mysqlx_admin_schema

Headerplugins/mysqlx/include/mysqlx_admin_schema.h
Sourceplugins/mysqlx/src/mysqlx_admin_schema.cpp
ResponsibilityAdmin table DDL definitions, variable definitions, LOAD/SAVE command handlers, and runtime-view refresh callbacks. Defines the schema for the editable tables (mysqlx_users, mysqlx_routes, mysqlx_backend_endpoints, mysqlx_variables) and the projected views (runtime_mysqlx_*). LOAD/SAVE callbacks interact with MysqlxConfigStore directly (via install_*_from_admin / save_*_to_admin_table) — they do NOT do SQLite-to-SQLite copies between editable and runtime tables. The four refresh_<X>_runtime_view free functions are registered with the chassis via services.register_runtime_view() during schema registration; they fire when admin executes a SELECT against a runtime_mysqlx_* table and call MysqlxConfigStore::project_<X>_to_runtime_view.
New v2 variablesmysqlx_thread_pool_size (default 4), mysqlx_connect_timeout (default 10000ms), mysqlx_tls_mode (default DISABLED), mysqlx_tls_cert, mysqlx_tls_key, mysqlx_tls_ca, mysqlx_tls_backend_mode (default DISABLED), mysqlx_max_cached_connections_per_thread (default 100).

4. Thread Pool Architecture

4.1 Thread Pool Diagram

                    mysqlx_start()
                         │
           ┌─────────────┼─────────────┐
           │             │             │
    ┌──────┴──────┐ ┌────┴─────┐ ┌────┴──────┐
    │Mysqlx_Thread│ │Mysqlx_   │ │Mysqlx_    │
    │     #0      │ │Thread #1 │ │Thread #N  │
    │             │ │          │ │           │
    │ poll() {    │ │ poll() { │ │ poll() {  │
    │   listener  │ │  ...     │ │  ...      │
    │   sess[0]   │ │          │ │           │
    │   sess[1]   │ │          │ │           │
    │   ...       │ │          │ │           │
    │   sess[K]   │ │          │ │           │
    │   signal_fd │ │          │ │           │
    │ }           │ │ }        │ │ }         │
    │             │ │          │ │           │
    │ conn_cache  │ │conn_cache│ │conn_cache │
    └─────────────┘ └──────────┘ └───────────┘

    Each thread:
    - Independent poll() loop (200ms timeout)
    - Listeners baked into poll set (no separate accept thread)
    - Thousands of concurrent sessions
    - Per-thread connection cache (no lock contention)

4.2 Cooperative Multitasking Model

Sessions never block. Each session's handler() method:

  1. Reads available bytes from the client data stream
  2. Dispatches based on the current session state
  3. Processes any complete frames
  4. Flushes pending writes
  5. Returns control to the thread

If a session needs more data (e.g., waiting for a client response), it returns immediately. The thread's poll() call will wake up when data arrives and call the session's handler again.

  Thread run() loop:
  ┌────────────────────────────────────────┐
  │ while (running):                       │
  │   housekeeping()                       │
  │   nfds = poll(poll_fds, timeout=200ms) │
  │   process_ready_fds(nfds)              │
  │     → accept new connections           │
  │     → mark sessions with pending data  │
  │   process_all_sessions()               │
  │     → for each session:                │
  │       sess.handler()                   │
  │       update poll events               │
  │       cleanup if unhealthy             │
  └────────────────────────────────────────┘

5. Session State Machine

5.1 State Diagram

                    ┌─────────────────┐
                    │  NONE           │
                    └────────┬────────┘
                             │ init(fd)
                    ┌────────▼────────┐
                    │ CONNECTING_     │──── wait for first frame
                    │ CLIENT          │
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼──────┐      │      ┌───────▼────────┐
     │ X_CAPABILITIES│      │      │ X_AUTH_START   │
     │ _GET          │      │      │ (skip caps)    │
     └────────┬──────┘      │      └───────┬────────┘
              │              │              │
     ┌────────▼──────┐      │              │
     │ X_CAPABILITIES│      │              │
     │ _SET          │      │              │
     └────────┬──────┘      │              │
              │              │              │
     ┌────────▼──────┐      │              │
     │ X_TLS_ACCEPT_ │──────┤              │
     │ INIT/CONT/DONE│      │              │
     │ (if TLS)      │      │              │
     └────────┬──────┘      │              │
              │              │              │
              └──────────────┼──────────────┘
                             │
                    ┌────────▼────────┐
                    │ X_AUTH_START    │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │ X_AUTH_CHALLENGE│
                    │ _SENT           │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │ X_AUTH_OK_SENT  │──── or X_AUTH_FAILED → closing
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │ WAITING_CLIENT_ │◄────────────────────────┐
                    │ XMSG            │                         │
                    └────────┬────────┘                         │
                             │ dispatch_client_message()        │
              ┌──────────────┼──────────────┐                   │
              │              │              │                   │
     ┌────────▼──────┐      │      ┌───────▼────────┐          │
     │ CONNECTING_   │      │      │ forward to     │          │
     │ SERVER        │      │      │ backend        │          │
     └────────┬──────┘      │      └───────┬────────┘          │
              │              │              │                   │
              │              │      ┌───────▼────────┐          │
              │              │      │ WAITING_SERVER_│          │
              │              │      │ XMSG           │          │
              │              │      └───────┬────────┘          │
              │              │              │                   │
              │              │      ┌───────▼────────┐          │
              │              │      │ X_FAST_FORWARD │──────────┘
              │              │      └────────────────┘
              │              │
     ┌────────▼──────────────▼───────┐
     │ X_SESSION_CLOSING / X_SESSION │
     │ _CLOSED                       │
     └───────────────────────────────┘

5.2 State Reference (22 States)

StateDescription
NONEInitial state before init()
CONNECTING_CLIENTWaiting for first client frame
X_CAPABILITIES_GETProcessing CapabilitiesGet
X_CAPABILITIES_SETProcessing CapabilitiesSet (may trigger TLS)
X_AUTH_STARTAuthentication initiated
X_AUTH_CHALLENGE_SENTMYSQL41 challenge sent, waiting for response
X_AUTH_OK_SENTAuthentication successful
X_AUTH_FAILEDAuthentication failed
WAITING_CLIENT_XMSGMain loop: waiting for client message (dispatches all 23 types)
PROCESSING_X_QUERYProcessing a SQL statement
CONNECTING_SERVEREstablishing backend connection (pool-first, then async connect)
WAITING_SERVER_XMSGWaiting for backend response frames
X_FAST_FORWARDBidirectional frame relay
X_TLS_ACCEPT_INITStarting TLS handshake (server-side, client connection)
X_TLS_ACCEPT_CONTTLS handshake in progress
X_TLS_ACCEPT_DONETLS handshake complete
X_TLS_CONNECT_INITStarting TLS handshake (client-side, backend connection)
X_TLS_CONNECT_CONTBackend TLS in progress
X_TLS_CONNECT_DONEBackend TLS complete
X_SESSION_CLOSINGSession shutting down
X_SESSION_CLOSEDSession terminated
X_SESSION_RESET_WAITINGWaiting for backend response to SESS_RESET

6. Data Stream I/O — Non-Blocking Frame Parsing

6.1 Frame Format

X Protocol frames consist of a 5-byte header followed by a protobuf body:

┌───────────────────────────────────────────────┐
│ payload_size (4 bytes, little-endian)          │  ← includes msg_type byte
│ msg_type (1 byte)                              │
│ protobuf body (payload_size - 1 bytes)         │
└───────────────────────────────────────────────┘

6.2 Partial Frame Buffering

MysqlxDataStream handles partial reads from non-blocking sockets:

  recv() returns N bytes (may be less than a full frame)
       │
       ▼
  feed_bytes(data, N)
       │
       ▼
  read_buf_ += data
       │
       ▼
  try_parse_frame()
       │
       ├── available < 5 bytes? → return false (need header)
       │
       ├── decode payload_size from header
       │   payload_size < 1 or > 16MB? → parse error
       │
       ├── available < frame_total? → return false (need body)
       │
       └── copy frame → complete_frames_.push()
           advance read_offset_
           try again (may be more frames in buffer)

6.3 Write Buffering

  enqueue_frame(msg_type, body, body_len)
       │
       ▼
  write_buf_ += [4-byte payload_size][msg_type][body]
       │
       ▼
  Thread sets POLLOUT on fd when write_buf_ is non-empty
       │
       ▼
  write_to_net() → send() → advance write_offset_
       │
       ▼
  write_buf_.clear() when fully flushed

7. Connection Pool

7.1 Two-Tier Pool Model

  Session needs backend connection
       │
       ▼
  ┌─────────────────────────────────┐
  │ Tier 1: Per-thread cache        │
  │ (no lock contention)            │
  │                                 │
  │ Search: hostgroup + user +      │
  │ schema match, reusable, no      │
  │ active transaction/stmt         │
  │                                 │
  │ Found? → attach, set IN_USE     │
  │ Not found? → ↓                  │
  └─────────────┬───────────────────┘
                │
                ▼
  ┌─────────────────────────────────┐
  │ Create new connection           │
  │                                 │
  │ Non-blocking connect()          │
  │ Authenticate via MYSQL41        │
  │ Set IDLE + reusable             │
  └─────────────────────────────────┘

  When query completes:
       │
       ▼
  return_connection_to_cache()
       │
       ├── reset() — clear tx/stmt flags
       ├── set_state(IDLE)
       └── push to thread's conn_cache_
              │
              ├── cache size > max_cached_?
              │   → evict oldest (FIFO)
              └── done

7.2 Pool Matching Rules

A cached connection is returned only if ALL match:

  • Same hostgroup
  • Same user
  • Same schema
  • is_reusable() == true (state is IDLE, no transaction, no prepared statement)

7.3 Pool Configuration

VariableDefaultDescription
mysqlx_max_cached_connections_per_thread100Max cached connections per thread

8. Frame Forwarding — Protocol-Aware Dispatch

8.1 All 23 Client Message Types

The session's dispatch_client_message() handles every X Protocol client message type:

MessageActionCategory
CON_CAPABILITIES_GETHandle locally (send capabilities)Connection
CON_CAPABILITIES_SETHandle locally (process TLS request)Connection
CON_CLOSEHandle locally (close session)Connection
SESS_AUTHENTICATE_STARTHandle locally (start auth)Session
SESS_AUTHENTICATE_CONTINUEHandle locally (continue auth)Session
SESS_RESETForward to backendSession
SESS_CLOSEHandle locally (close session)Session
SQL_STMT_EXECUTEForward to backendSQL
CRUD_FINDForward to backendCRUD
CRUD_INSERTForward to backendCRUD
CRUD_UPDATEForward to backendCRUD
CRUD_DELETEForward to backendCRUD
EXPECT_OPENForward to backendExpect
EXPECT_CLOSEForward to backendExpect
CRUD_CREATE_VIEWForward to backendView
CRUD_MODIFY_VIEWForward to backendView
CRUD_DROP_VIEWForward to backendView
PREPARE_PREPAREForward to backend (mark conn has stmt)Prepared Stmt
PREPARE_EXECUTEForward to backendPrepared Stmt
PREPARE_DEALLOCATEForward to backendPrepared Stmt
CURSOR_OPENForward to backendCursor
CURSOR_CLOSEForward to backendCursor
CURSOR_FETCHForward to backendCursor

Unknown message types receive ER_X_BAD_MESSAGE error and session closure.

8.2 Backend Response Forwarding

Multi-frame responses from the backend (e.g., column metadata → rows → fetch done) are forwarded frame-by-frame to the client. The session reads all available frames from the server data stream and enqueues each one on the client data stream.

9. TLS Architecture

9.1 TLS Modes

ModeFrontend BehaviorBackend Behavior
DISABLEDNo tls capability advertised. Plaintext only.Plaintext only.
PREFERREDtls capability advertised. Client chooses.TLS if backend supports.
REQUIREDtls capability advertised. Reject client that doesn't upgrade.TLS required.

Default: DISABLED. Frontend TLS uses OpenSSL Memory BIO pattern (SSL_new, BIO_new_mem_buf, SSL_set_accept_state). Backend TLS negotiates via CapabilitiesSet.

9.2 TLS State Machine

TLS negotiation integrates into the session state machine:

  X_CAPABILITIES_SET
       │
       ├── client requests TLS? AND tls_mode != DISABLED
       │         │
       │         ▼
       │   X_TLS_ACCEPT_INIT
       │         │
       │         ▼
       │   X_TLS_ACCEPT_CONT (SSL_accept loop)
       │         │
       │         ▼
       │   X_TLS_ACCEPT_DONE
       │         │
       │         ▼
       │   X_AUTH_START
       │
       └── no TLS requested
              │
              ▼
         X_AUTH_START

9.3 OpenSSL Memory BIO Pattern

Following the same pattern as MySQL_Data_Stream:

  Read path:
    recv(fd) → BIO_write(rbio_ssl) → SSL_read(ssl) → feed_bytes()

  Write path:
    SSL_write(ssl, data) → BIO_read(wbio_ssl) → send(fd)

Each MysqlxDataStream has encrypted_ flag, SSL*, rbio_ssl, wbio_ssl members.

10. Data Flow Diagrams

10.1 Client Connection Flow (v2)

Client         Mysqlx_Thread      MysqlxSession     ConfigStore      MysqlxConnection   MySQL
  │                  │                  │                 │                  │              │
  │── TCP connect ──→│                  │                 │                  │              │
  │                  │── accept ──────→ │                 │                  │              │
  │                  │   (in poll set)  │                 │                  │              │
  │←── Capabilities ──────────────────│                 │                  │              │
  │── CapSet ────────────────────────→│                 │                  │              │
  │                  │                  │ (optional TLS)  │                  │              │
  │←── AuthStart ─────────────────────│                 │                  │              │
  │                  │                  │── resolve ────→│                  │              │
  │── AuthContinue ──────────────────→│                 │                  │              │
  │←── AuthOk ────────────────────────│                 │                  │              │
  │                  │                  │── pool lookup ──────────────────→│              │
  │                  │                  │   (miss)        │                  │              │
  │                  │                  │── async connect ─────────────────────────────→  │
  │── SQL/CRUD ─────────────────────→│                 │                  │── forward ──→│
  │                  │                  │                 │                  │←── result ───│
  │←── result ────────────────────────│                 │                  │              │

10.2 Admin Command Flow

The mysqlx plugin follows the canonical Admin/module separation of duties used by core's mysql_users / GloMyAuth / runtime_mysql_users triplet (see lib/ProxySQL_Admin.cpp::save_mysql_users_runtime_to_database):

  • Admin owns the editable tables (mysqlx_users, mysqlx_routes, mysqlx_backend_endpoints, mysqlx_variables) and the chassis dispatch glue.
  • MysqlxConfigStore owns the authoritative runtime state in process memory.
  • runtime_mysqlx_* are not persistent storage. They are admin-side views of the store, rebuilt on demand by a refresh callback that the plugin registered via services.register_runtime_view().

10.2.1 LOAD path — editable table → module

LOAD MYSQLX USERS TO RUNTIME does not copy mysqlx_users into runtime_mysqlx_users. The runtime table is never written by this path.

Admin Client       Admin Handler   PluginManager   mysqlx_admin_schema           MysqlxConfigStore
     │                  │                │                  │                            │
     │── LOAD MYSQLX ──→│                │                  │                            │
     │   USERS TO       │                │                  │                            │
     │   RUNTIME        │                │                  │                            │
     │                  │── alias ─────→│                  │                            │
     │                  │   normalize    │── dispatch ────→│                            │
     │                  │                │                  │── install_users_from_admin →│
     │                  │                │                  │   reads:                   │   SELECT runtime_mysql_users
     │                  │                │                  │   - runtime_mysql_users    │   SELECT mysqlx_users
     │                  │                │                  │     (canonical identity)   │     WHERE active=1
     │                  │                │                  │   - mysqlx_users           │
     │                  │                │                  │     (X-side overlay)       │   build new unordered_map,
     │                  │                │                  │                            │   atomic swap under
     │                  │                │                  │                            │   shared_mutex.
     │←── OK ──────────│←──────────────│←────────────────│←──────────────────────────│

Same shape for LOAD MYSQLX ROUTES, LOAD MYSQLX BACKEND ENDPOINTS, and LOAD MYSQLX VARIABLES TO RUNTIME — each calls its own install_<X>_from_admin. None of them touch runtime_mysqlx_<X>.

10.2.2 Runtime-view refresh — module → projected view (on SELECT)

When an operator runs SELECT * FROM runtime_mysqlx_users, Admin's pre-SELECT hook detects the table name and walks the chassis registry of runtime views (see proxysql_refresh_configured_plugin_runtime_views in lib/ProxySQL_PluginManager.cpp). For each registered view whose table_name appears as a whole identifier in the query, it invokes the plugin-supplied refresh callback before the SELECT runs.

Admin Client       Admin pre-SELECT hook        Chassis dispatch         mysqlx refresh cb        MysqlxConfigStore
     │                    │                            │                         │                         │
     │── SELECT * ───────→│                            │                         │                         │
     │   FROM runtime_    │── refresh views? ────────→│                         │                         │
     │   mysqlx_users     │                            │── matches view ───────→│                         │
     │                    │                            │   "runtime_mysqlx_     │── project_users_to_     →│
     │                    │                            │    users"              │   runtime_view(admindb)  │
     │                    │                            │                         │                         │   under shared_mutex:
     │                    │                            │                         │                         │   DELETE FROM
     │                    │                            │                         │                         │     runtime_mysqlx_users;
     │                    │                            │                         │                         │   INSERT INTO
     │                    │                            │                         │                         │     runtime_mysqlx_users
     │                    │                            │                         │                         │     <dump store state>
     │                    │── now run SELECT ─────────────────────────────────────────────────────────────→│
     │←── rows ───────────│                                                                                  │

The view is wiped and refilled on every qualifying SELECT, so deletions in the store always propagate. There is brief duplication (data lives in the module and in the projected admin table during the query) — that is by design, mirroring the canonical core pattern.

10.2.3 SAVE path — module → editable table

SAVE MYSQLX USERS FROM RUNTIME TO MEMORY does not read runtime_mysqlx_users. It dumps the store directly into mysqlx_users (UPDATE active=0, then REPLACE INTO with active=1, mirror of the canonical pattern).

Admin Client       Admin Handler   PluginManager   mysqlx_admin_schema           MysqlxConfigStore
     │                  │                │                  │                            │
     │── SAVE MYSQLX ──→│                │                  │                            │
     │   USERS FROM     │                │                  │                            │
     │   RUNTIME TO     │── alias ─────→│                  │                            │
     │   MEMORY         │   normalize    │── dispatch ────→│                            │
     │                  │                │                  │── save_users_to_admin_ ───→│
     │                  │                │                  │   table(admindb)           │
     │                  │                │                  │                            │   under shared_mutex:
     │                  │                │                  │   writes:                  │   UPDATE mysqlx_users
     │                  │                │                  │   - mysqlx_users           │     SET active=0;
     │                  │                │                  │     (editable, persistent) │   REPLACE INTO
     │                  │                │                  │                            │     mysqlx_users
     │                  │                │                  │                            │     <dump store state,
     │                  │                │                  │                            │      active=1>
     │←── OK ──────────│←──────────────│←────────────────│←──────────────────────────│

10.2.4 Disk tier

LOAD MYSQLX <X> FROM DISK and SAVE MYSQLX <X> TO DISK are unchanged — those copy between disk-backed and in-memory editable tables and have no module involvement. They remain plain BEGIN; DELETE; INSERT; COMMIT admin-tier persistence.

10.2.5 What changed from earlier versions

Earlier revisions of this plugin treated runtime_mysqlx_users (and the other runtime_mysqlx_* tables) as authoritative SQLite-backed mirrors populated by LOAD ... TO RUNTIME via INSERT INTO runtime_mysqlx_users SELECT * FROM mysqlx_users. MysqlxConfigStore had a single load_from_runtime() that read those mirrors back into memory, and mysqlx_admin_schema did a copy_table SQLite-to-SQLite copy on every LOAD. Operators relying on that mental model need to unlearn it: the runtime tables now hold no persistent rows of their own, the LOAD path bypasses them entirely, and only the on-demand projection callback ever touches them.

11. Thread Model

Main Thread
  ├── ProxySQL_PluginManager::load()        (dlopen, dlsym)
  ├── ProxySQL_PluginManager::init_all()    (register tables/commands)
  ├── ProxySQL_PluginManager::start_all()   (start thread pool)
  │     └── mysqlx_start()
  │           ├── sync_disk_to_memory()              (disk → editable mysqlx_* tables)
  │           ├── config_store->install_users_from_admin()       (mysqlx_users → store)
  │           ├── config_store->install_routes_from_admin()      (mysqlx_routes → store)
  │           ├── config_store->install_endpoints_from_admin()   (mysqlx_backend_endpoints → store)
  │           ├── config_store->install_variables_from_admin()   (mysqlx_variables → store)
  │           └── Create N Mysqlx_Thread instances
  │                 ├── Mysqlx_Thread #0 (poll loop, listeners, sessions, conn cache)
  │                 ├── Mysqlx_Thread #1 (poll loop, listeners, sessions, conn cache)
  │                 └── Mysqlx_Thread #N ...
  └── ProxySQL_PluginManager::stop_all()
        └── mysqlx_stop()
              └── Stop and join all Mysqlx_Thread instances

Thread Lifecycle

  1. Startup (Main Thread): The main thread calls PluginManager::load() which dlopens the plugin shared library. Then init_all() invokes the plugin's init() callback, which registers admin tables, commands, and the four runtime-view refresh callbacks via services.register_runtime_view(). Then start_all() invokes mysqlx_start(), which (1) syncs the on-disk admin tables into the in-memory editable tables, (2) drives the four install_<X>_from_admin calls to populate MysqlxConfigStore from those editable tables (plus runtime_mysql_users for canonical identity), (3) creates N Mysqlx_Thread instances, (4) adds listeners based on configured routes, and (5) starts all threads.

  2. Mysqlx_Thread: Each thread runs an independent poll() loop. Listener sockets are baked into the poll set (no separate accept thread). When POLLIN fires on a listener fd, the thread calls accept(), creates a MysqlxSession, and adds the client data stream to the poll set. Sessions are processed cooperatively — each session's handler() returns immediately when waiting for I/O.

  3. Shutdown (Main Thread): The main thread calls PluginManager::stop_all(), which invokes mysqlx_stop(). This sets the running flag to false on each thread, writes to the signal pipe to wake up poll(), and joins all threads.

Thread Safety Summary

ResourceMechanismScope
MysqlxConfigStore cachesstd::shared_mutexShared for reads (resolve_identity, pick_endpoint, project_<X>_to_runtime_view); exclusive for the install / save paths
MysqlxStatsStore countersstd::atomic<uint64_t>Lock-free per-counter increments
ProxySQL_PluginManager dispatchstd::mutexGuards command dispatch
Per-thread connection cachestd::mutexPer-thread, no inter-thread contention
MysqlxSession / MysqlxDataStreamNone neededSingle-owner (one thread)

12. Build System

12.1 File Structure (v2)

plugins/mysqlx/
  ├── Makefile
  ├── mysqlx_plugin.cpp/.h             # Entry point (rewritten for v2)
  ├── mysqlx_thread.cpp/.h             # Event loop (NEW)
  ├── mysqlx_session.cpp/.h            # State machine (NEW)
  ├── mysqlx_data_stream.cpp/.h        # Non-blocking frame I/O (NEW)
  ├── mysqlx_connection.cpp/.h         # Pooled backend connection (NEW)
  ├── mysqlx_protocol.cpp/.h           # Frame encode/decode helpers
  ├── mysqlx_config_store.cpp/.h       # Config cache (modified for v2)
  ├── mysqlx_stats.cpp/.h              # Stats counters
  ├── mysqlx_admin_schema.cpp/.h       # Admin tables/commands (new variables)
  └── protobuf/                        # Pre-generated protobuf sources

12.2 Build Commands

bash
# Build the plugin (from project root or plugins/mysqlx/)
make -C plugins/mysqlx

# Output: plugins/mysqlx/mysqlx_plugin.so

12.3 Dependencies

DependencyPurpose
protobufX Protocol message serialization/deserialization
OpenSSLSHA1 hash for MYSQL41 auth, TLS (Memory BIOs), CRYPTO_memcmp
SQLite3Admin/runtime database access (via PluginServices pointers)

13. Comparison: Phase 1 vs Phase 2 vs MySQL Router

AspectPhase 1Phase 2 (v2)MySQL Router
Threading1 thread, 1 session, blockingN threads, thousands of sessions per threadEvent-driven
I/O modelBlocking read()/write()Non-blocking poll()Non-blocking I/O
Frame forwardingBlind TCP relayProtocol-aware: all 23 message typesProtocol-aware
TLSNot implemented3 modes (Disabled/Preferred/Required)3 modes + passthrough
Connection poolingNone (1:1 pinned)Two-tier: per-thread + globalNone
Backend connectBlocking connect()Non-blocking connect() with EINPROGRESSNon-blocking
Error handlingBasic string errorsFull X Protocol error framesFull error frames
Message awareness5 types (handshake)All 23 client + server typesAll types
RoutingStatic route → single endpointHostgroup-based with poolStatic routing
AuthMYSQL41/PLAIN verifyMYSQL41/PLAIN + credential mappingPassthrough
Admin interfaceLOAD/SAVE MYSQLX tablesSame + new variablesConfig file only
Concurrency1 session at a timeThousands per threadConcurrent

14. MYSQL41 Authentication Internals

X Protocol supports multiple authentication mechanisms. The mysqlx plugin implements MYSQL41, which is the native MySQL authentication adapted for the X Protocol. It uses a challenge-response mechanism based on double-SHA1 hashing.

14.1 Double-SHA1 Algorithm

hash_stage1 = SHA1(password)
hash_stage2 = SHA1(hash_stage1)
scramble    = XOR(hash_stage1, SHA1(challenge + hash_stage2))

14.2 Server-Side Verification

received_scramble = auth_data from client
hash_stage2       = SHA1(stored_hash)
candidate         = SHA1(challenge || hash_stage2)
hash_stage1       = XOR(received_scramble, candidate)
computed_stage2   = SHA1(hash_stage1)

verified = CRYPTO_memcmp(computed_stage2, hash_stage2, 20) == 0

14.3 Authentication Flow Diagram

Client                          ProxySQL (Frontend)               MySQL (Backend)
  │                                    │                               │
  │── AuthenticateStart ──────────────→│                               │
  │   (mechanism: MYSQL41)             │                               │
  │←── AuthenticateContinue ───────────│                               │
  │   (challenge: 20-byte nonce)       │                               │
  │                                    │                               │
  │── AuthContinue ───────────────────→│                               │
  │   (auth_data: 20-byte scramble)    │                               │
  │                                    │── verify scramble ──→         │
  │                                    │   (if proxying auth)          │
  │                                    │                               │
  │                                    │── connect to backend ────────→│
  │                                    │── AuthenticateStart ─────────→│
  │                                    │←── AuthenticateContinue ─────│
  │                                    │── AuthContinue ──────────────→│
  │                                    │←── AuthenticateOk ───────────│
  │                                    │                               │
  │←── AuthenticateOk ────────────────│                               │
  │                                    │                               │

14.4 Security Considerations

  • No password storage in plugin: The plugin stores the double-SHA1 hash, not the plaintext password.
  • Constant-time comparison: All hash comparisons use CRYPTO_memcmp to prevent timing attacks.
  • Challenge freshness: The 20-byte challenge is generated per-connection using OpenSSL's RAND_bytes.
  • TLS recommended: PLAIN authentication should only be enabled when the client connection uses TLS.