Skip to content

Phase 1: read a whole message off a socket - #3

Merged
m13m merged 2 commits into
masterfrom
phase-1-read-loop
Oct 28, 2026
Merged

m13m merged 2 commits into
masterfrom
phase-1-read-loop

Conversation

@m13m

@m13m m13m commented Oct 28, 2026

Copy link
Copy Markdown
Member

Stacked on #1, which it needs for Mysqlex.Packet. GitHub will retarget this at master once #1 merges.

The socket half of framing. decode_message/1 returns :more when the buffer holds less than a message; this reads again until it doesn't, so no parser above this layer ever sees a partial read.

What is here

  • Mysqlex.Transport — the behaviour the client reads through. One callback so far, recv/3; more get added as the client needs them.
  • Mysqlex.Tcp — now declares the behaviour.
  • Mysqlex.Client.recv_message/4 — the read loop.
  • Mysqlex.FakeTransport in test/support/ — replays a scripted list of byte chunks.

mix.exs gains elixirc_paths so test/support compiles in the test environment.

Why the behaviour

This is the seam that makes the rest of the protocol testable. The test that matters:

socket = FakeTransport.script([<<5::little-24>>, <<0, "hel">>, "lo"])
assert {:ok, 0, "hello", ""} = Client.recv_message(FakeTransport, socket, "", 1_000)

A message split across three reads, broken mid-header — the length in one read, the sequence id and part of the payload in the next, the remainder in a third. That is what a real server produces under load, and it is the case the 2018 code could not exercise at all, because the parser owned the socket.

The same seam is what will let the connection phase be tested without a database: capability negotiation, auth switches, the RSA path and TLS upgrade decisions are all branches that otherwise need specially configured live servers, which in practice means they never get tested.

Note on the buffer

recv_message/4 takes a buffer in and hands rest back out, so the caller carries leftover bytes between messages rather than the transport holding hidden state. Appending with buffer <> data relies on the VM's binary append optimisation rather than an explicit iodata accumulator — worth revisiting if profiling ever says so.

m13m added 2 commits October 28, 2026 22:01
The socket half of framing. decode_message/1 says :more when the
buffer holds less than a message; this reads again until it does not,
so nothing above this layer sees a partial read.

The socket is reached through a Mysqlex.Transport behaviour rather than
:gen_tcp directly. That is what makes the read loop testable: the test
scripts a transport that delivers "hello" across three reads, splitting
mid-header, which is exactly the case a real server produces under load
and the case the 2018 code could not be made to exercise at all.

Callbacks get added to the behaviour as the client needs them — recv/3
is all this slice uses.
A server that disconnects mid-message must not leave the read loop
spinning. The error comes back to the caller, who decides whether the
connection is still usable.
@m13m
m13m changed the base branch from phase-1-foundation to master October 28, 2026 18:09
@m13m
m13m merged commit 893dab9 into master Oct 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant