Option validation and the fail-closed server-precondition gate.
Two clearly separated responsibilities:
validate/1is pure — it normalises the user'sstart_linkoptions and refuses a mis-shaped or unsafe configuration before any socket is opened.check_preconditions/1issues oneCOM_QUERYon an already-authenticated socket and refuses to start unless the source's binlog is configured for lossless row-based CDC.
Server preconditions (ADR-0002)
check_preconditions/1 reads five global variables in a single query and refuses
with a DISTINCT reason per violation — degraded row decoding silently guesses
column identity, so the gate fails closed rather than proceed:
binlog_formatmust beROW— else:binlog_format_not_rowbinlog_row_imagemust beFULL— else:binlog_row_image_not_fullbinlog_row_metadatamust beFULL— else:binlog_row_metadata_not_fullbinlog_row_value_optionsmust be empty (""= full JSON, notPARTIAL_JSON) — else:binlog_row_value_options_not_emptygtid_modemust beON— else:gtid_mode_not_on
MySQL simple-query results are all text strings, so every value is compared as
text against the expected literal and never coerced to a typed term — an empty
binlog_row_value_options arrives as "", not nil or 0.
TLS verification posture (ADR-0002)
ssl defaults true. Peer verification is an explicit operator choice, never a
silent default: with TLS on, ssl_opts must carry EITHER a cacertfile/cacerts
OR an explicit verify:. Given neither, validate/1 fails closed with
:tls_verification_unspecified rather than let OTP's verify_peer default select
a posture nobody chose. This mirrors the guard Capstan.Protocol.Handshake applies
at connect time, moved to config time so a bad TLS configuration is refused before
any socket is opened.
Authenticated TLS against MySQL's auto-generated certificate. A cacertfile
alone drives verify: :verify_peer, but MySQL's auto-generated server certificate
is self-signed with CN …Auto_Generated_Server_Certificate and no SAN, so
verify_peer's hostname check fails against an address such as 127.0.0.1. An
operator taking the authenticated route must ALSO pass
server_name_indication: :disable in ssl_opts — the chain is verified, the
hostname is not (VERIFY_CA semantics). validate/1 does NOT inject this: that would
silently weaken every cacertfile user, so the operator supplies it deliberately.
Summary
Types
A value-free precondition-gate refusal (ADR-0002).
The normalised configuration validate/1 returns on success.
A value-free option-validation refusal.
Functions
Reads the five server preconditions over socket and returns :ok iff all pass.
Validates raw start_link options into a normalised config map, or returns a
value-free error.
Types
@type precondition_error() ::
:binlog_format_not_row
| :binlog_row_image_not_full
| :binlog_row_metadata_not_full
| :binlog_row_value_options_not_empty
| :gtid_mode_not_on
| :precondition_query_failed
A value-free precondition-gate refusal (ADR-0002).
@type t() :: %{ connection: keyword(), server_id: pos_integer(), max_command_retries: non_neg_integer() }
The normalised configuration validate/1 returns on success.
@type validation_error() ::
:config_invalid | :server_id_required | :tls_verification_unspecified
A value-free option-validation refusal.
Functions
@spec check_preconditions(Capstan.Protocol.Packet.socket()) :: :ok | {:error, precondition_error() | {:query_error, non_neg_integer()} | {:transport, term()}}
Reads the five server preconditions over socket and returns :ok iff all pass.
Issues ONE COM_QUERY on the already-authenticated socket and compares each value
as text (ADR-0002). A wrong variable refuses with its distinct reason; a server or
transport error is surfaced fail-closed, never swallowed into a spurious :ok.
@spec validate(keyword()) :: {:ok, t()} | {:error, validation_error()}
Validates raw start_link options into a normalised config map, or returns a
value-free error.
Refuses: :server_id_required (missing or non-positive server_id),
:tls_verification_unspecified (ssl: true with no CA source and no explicit
verify:), and :config_invalid (any other missing or mis-shaped option). Defaults
applied: ssl true, ssl_opts [], auth_plugins [:caching_sha2_password],
password "", database nil, max_command_retries 5.