Wire protocol
monolock uses a binary protocol, version 1, over raw TCP. One connection is
one claim on one named lock. The connection is the lease. When the
connection closes, the server releases the lock. All multi-byte integers are
big endian. TCP is a byte stream. Thus a client must read fixed-size
fields fully (io.ReadFull in Go). Do not assume that one read returns one
message.
The wire format is in
pkg/protocol.
This is the one package that the server module exports. Clients in each
language implement the same bytes — see
Writing a client.
Messages
Section titled “Messages”Client to server
Section titled “Client to server”ACQUIRE [0x01][version uint8][lease_ms uint32][name_len uint16][name bytes]HEARTBEAT [0x02]This is the ACQUIRE frame, byte by byte:
packet-beta 0-7: "0x01" 8-15: "version = 1" 16-47: "lease_ms (uint32, big endian)" 48-63: "name_len (uint16, big endian)" 64-95: "name (name_len bytes, raw UTF-8)"
ACQUIRE must be the first message on a connection. It must occur exactly
one time. The server answers a second ACQUIRE with error 0x12.
lease_ms is the lease that the client selects for this connection. The
server drops the session when the session is silent for this duration. The
value must be positive; the server answers 0 with error 0x17. The value
stays constant for the life of the connection. There is no upper limit. The
hold time of a lock is unlimited in all cases. The lease only sets the
detection speed for a dead holder. Select the smallest value that is safe
against the network pauses between the client and the server: milliseconds
on the same machine, seconds across an unreliable wide-area network (WAN).
Lock names are raw UTF-8. They are case sensitive. The server does not
normalize them and does not trim them. The limit is 255 bytes. This
limit is a part of the protocol, not a server setting. Thus each server
accepts each name that a client can build. The length field is wider than
the limit. Thus a longer name is possible on the wire, and the server
answers it with error 0x15. An empty name causes error 0x14. Invalid
UTF-8 causes error 0x16.
HEARTBEAT is one byte. See Heartbeats for the necessary
heartbeat schedule of the client.
Server to client
Section titled “Server to client”WAITING [0x11]ACQUIRED [0x12][token uint64]ERROR [0x13][code uint8][reason_len uint8][reason bytes]token is the fencing token of the grant.
Each ACQUIRED message that a session receives contains the token of its
grant. The first grant, a promotion, and a heartbeat acknowledgement all
contain the same token.
WAITING and ACQUIRED are also the heartbeat acknowledgement. There is no
separate PING, PONG, or ACK message. A reply gives four facts: the
heartbeat arrived, the session is registered, the return path operates, and
the client has the reported state. When the server promotes a waiter, the
server sends ACQUIRED on its own initiative. It does not wait for the
subsequent heartbeat. Thus a client needs a permanent reader on the
connection.
For ERROR, the code is the contract. The reason is a
human-readable UTF-8 string for error messages and debugging. It contains
the canonical text of the code, sometimes with more detail — “unknown
message type: 0x7f” — and it can be empty. Clients branch on the code only,
never on the text. The full code table with the retry rules is on
Error codes.
Session state machine
Section titled “Session state machine”stateDiagram-v2
direction LR
[*] --> CONNECTED
CONNECTED --> WAITING: ACQUIRE (lock busy)
CONNECTED --> ACQUIRED: ACQUIRE (lock free)
WAITING --> ACQUIRED: promoted
WAITING --> DISCONNECTED
ACQUIRED --> DISCONNECTED
DISCONNECTED --> [*]
A connection moves through CONNECTED → WAITING → ACQUIRED → DISCONNECTED
or CONNECTED → ACQUIRED → DISCONNECTED. ACQUIRED never goes back to
WAITING. There is no RELEASE command. Close the connection, and the
server immediately promotes the next waiter. It is not necessary to wait for
the end of the lease.
Heartbeats
Section titled “Heartbeats”The server stores lastSeenAt for each session. Each valid ACQUIRE or
HEARTBEAT refreshes this value. The server drops the session when the time
after lastSeenAt becomes more than the lease that the session requested.
Only durations go across the wire, never timestamps. Thus clock
synchronization is not necessary, and the two sides use only monotonic time.
The client calculates all other values from the lease that it selected:
baseInterval = lease / 4clientTimeout = lease * 0.8 // always fires before the server leasesafeRTT = smoothedRTT * 2 // EWMA, alpha = 0.2interval = clamp(baseInterval - safeRTT, minHeartbeatInterval, baseInterval)The round-trip time (RTT) can only decrease the interval. A fast link
never increases it above
baseInterval. If safeRTT is equal to or more than the full base
interval, the client sends the subsequent heartbeat immediately after the
answer to the previous one.
A maximum of one heartbeat is in flight at one time. Thus heartbeats cannot collect in a TCP buffer, a stale message cannot extend a session, and sequence IDs are not necessary. The client measures the interval between sends, not as a pause after the reply.
How it works explains the reasons for these rules. The rules are normative for each client implementation.
Versioning
Section titled “Versioning”Each ACQUIRE contains the protocol version (currently 1). A server that
does not support the requested version answers with error 0x10. There is
no negotiation. A client speaks one version. The error is the signal for the
client to change to a version that the server supports, if the client has
one.

