monolock is a lightweight TCP server for named locks — a distributed mutex without the distributed system.
Documentation: monolock-dev.github.io — concepts, operations, wire protocol reference, clients.
- raw TCP, one connection per lock
- single server, no database, no dependencies outside the Go standard library
- named locks, FIFO waiters
- connection-scoped ownership: closing the connection is the release
- client-chosen lease and RTT-aware heartbeats
- instant graceful handover
- fencing tokens: every grant carries a monotonically growing number
- TLS and mTLS with certificate rotation on
SIGHUP - ACL authorization: mTLS identity → lock name glob rules
- JSON audit log of every ownership change
- admin HTTP API: introspection, force-release, kicking waiters
- Prometheus metrics and health endpoints
- connections and waiters limited only by system resources
- simple by design: a single point of coordination — no replication, no quorum, no consensus guarantees
docker run -p 7070:7070 ghcr.io/monolock-dev/monolockOne client owns a named lock and does the work; the others queue up in FIFO order. On graceful shutdown the next client takes over immediately. If a client hangs or the network drops, the lock moves on once the session stays silent for a full lease.
The lease is not a fixed term — it is a sliding window of silence. The server
remembers when it last heard from a session, and every heartbeat resets that
countdown to the full lease. As long as heartbeats keep arriving, a lock can
be held forever; the lease only decides how quickly a dead holder is
detected. Clients heartbeat at lease / 4 (shrunk further on slow links) and
give up on their own at 0.8 × lease — before the server does — so a holder
never believes in a lock the server has already moved on from.
sequenceDiagram
participant A as worker A
participant S as monolock
participant B as worker B
A->>S: ACQUIRE "deploy" lease=2s
S-->>A: ACQUIRED token=41
B->>S: ACQUIRE "deploy" lease=2s
S-->>B: WAITING (queued, FIFO)
par holder works
loop every lease/4
A->>S: HEARTBEAT
S-->>A: ACQUIRED token=41
end
and waiter keeps its place
loop every lease/4
B->>S: HEARTBEAT
S-->>B: WAITING
end
end
A--xS: connection closed
S-->>B: ACQUIRED token=42
There is no config file. Every knob is a flag, and every flag has a matching
environment variable; a flag wins over the environment, which wins over the
default. monolock -h prints the same table.
| Flag | Environment variable | Default | Meaning |
|---|---|---|---|
-listen |
MONOLOCK_LISTEN_ADDRESS |
0.0.0.0:7070 |
address to listen on |
-ops-listen |
MONOLOCK_OPS_LISTEN_ADDRESS |
(empty) | address of the ops HTTP server: /metrics, /healthz, /readyz; empty disables it |
-admin-listen |
MONOLOCK_ADMIN_LISTEN_ADDRESS |
(empty) | address of the admin HTTP API; empty disables it |
-tls-cert |
MONOLOCK_TLS_CERT |
(empty) | PEM server certificate; with -tls-key enables TLS on the protocol port |
-tls-key |
MONOLOCK_TLS_KEY |
(empty) | PEM private key for -tls-cert |
-tls-client-ca |
MONOLOCK_TLS_CLIENT_CA |
(empty) | PEM CA pool for client certificates; enables mTLS |
-acl-file |
MONOLOCK_ACL_FILE |
(empty) | JSON file with identity → lock name glob rules; requires -tls-client-ca, empty disables authorization |
-audit-log |
MONOLOCK_AUDIT_LOG |
(empty) | audit log destination: a file path or - for stdout; empty disables audit |
-io-timeout |
MONOLOCK_IO_TIMEOUT |
5s |
deadline for a single read or write on a connection |
-log-level |
MONOLOCK_LOG_LEVEL |
info |
debug, info, warn or error |
-log-format |
MONOLOCK_LOG_FORMAT |
text |
text or json |
Durations are Go duration strings, e.g. 5s or 250ms. There is no lease
knob and no heartbeat knob: each client picks its own lease in ACQUIRE, and
the server's only I/O policy is io-timeout.
SIGHUP re-reads every external file — the TLS certificate, key and client
CA, the ACL file — and reopens the audit log for logrotate. A failed reload
keeps the previous state.