Skip to content

Writing a client

An official client is available for Go. Python, Node.js, and Rust clients are planned. Until then, or for a different language, you can write your own client. The wire protocol is intentionally small. A correct client is a few hundred lines. This page is the specification-as-checklist to write one.

connect(address) # TCP, or TLS with a client certificate
send ACQUIRE(version=1, lease_ms, name)
loop:
msg = read() # a PERMANENT reader — see rule 1
if msg == WAITING: still queued; keep heartbeating
if msg == ACQUIRED: own the lock; remember token; keep heartbeating
if msg == ERROR: classify by code range and stop or retry
on any doubt: # timeout, EOF, garbage bytes
close the connection and treat the lock as lost

The protocol reference shows the four messages byte-for-byte. The rules below change “can speak the bytes” into “is safe to build on”.

Promotion is a push. When you are the next waiter, the server sends ACQUIRED on its own initiative, not as a reply to a message. A client that reads only after it sends does not see the grant. The lock is then granted, and the client does not know it. Run a read loop for the full life of the connection.

TCP is a byte stream. Read exactly the frame that the length fields describe (read_full semantics). Do not assume that one read() call returns one message. Do not assume that a message arrives in one piece.

3. Derive the heartbeat schedule from the lease

Section titled “3. Derive the heartbeat schedule from the lease”

The heartbeat rules are normative:

  • the base interval is lease / 4;
  • decrease the interval by two times the round-trip time (RTT), smoothed with an exponentially weighted moving average (EWMA, alpha 0.2); do not go below a floor (the Go client uses 100ms); do not go above the base interval;
  • a maximum of one heartbeat in flight — send the subsequent heartbeat only after the reply;
  • stop your claim at 0.8 × lease without a confirmation. Thus you always stop before the server removes you.

WAITING and ACQUIRED are also the heartbeat acks. There is no PONG. Each frame from the server shows that the session is alive and tells you your current state. There are no sequence numbers to match, because rule 3 guarantees that a maximum of one heartbeat is unanswered.

Deliver the token from ACQUIRED to the code that does the guarded work. Make the transfer of the token easy. The token is the only defense of the resource against a stale holder (why). A client API that hides the token makes unsafe usage more probable.

Obey the retry contract. Codes below 0x10 are server conditions: connect again (immediately after 0x01, with backoff in the other cases). Codes from 0x10 and above are client errors: show the error and stop. Classify by range, not by a list of known codes. Then your client processes future codes correctly. Branch on the code, never on the reason text.

7. Release by closing, and only by closing

Section titled “7. Release by closing, and only by closing”

There is no RELEASE message. Close the connection when the work is complete. Also close the connection when there is a doubt: a timeout, an unexpected byte, a failed write. Close-on-doubt is always safe. The server promotes the next waiter, and the only result for you is the loss of the lock. Continuation in an uncertain state is never safe.

The session can stop because of a heartbeat timeout, an EOF, or an ERROR. Then the work that the lock guarded must get a stop signal (cancel a context, set a flag, kill a task). A new acquisition is a new ACQUIRE on a new connection, at the back of the first-in, first-out (FIFO) queue. There is no silent re-acquisition.

A local server is one command:

Terminal window
docker run -p 7070:7070 ghcr.io/monolock-dev/monolock

Script these scenarios. The sequence is approximately the frequency with which they find bugs:

  1. Two clients, one lock. The second client gets WAITING, then ACQUIRED when the first client disconnects — without a sent message. This tests rule 1.

  2. Hold a lock for more than several heartbeat intervals. This tests the schedule of rule 3.

  3. Stop the network while the client holds the lock (a firewall rule, a proxy that you kill). The client must stop its claim before the lease and stop the work. This tests rules 3 and 8.

  4. Restart the server while the client is in the queue. Expect ERROR 0x01, and connect again immediately. This tests rule 6.

  5. Send a 300-byte name. Expect ERROR 0x15 and no retry. This tests rule 6.

If you write a client, tell us. The plan is to show a list of community clients on this page.