Go client
monolock-go is the official Go
client. One lock is one TCP connection. Do blocks until this process owns
the named lock. Do then runs your function for exactly the time in which
the server confirms ownership. When the function returns, Do releases the
lock.
Install
Section titled “Install”go get github.com/monolock-dev/monolock-goimport monolock "github.com/monolock-dev/monolock-go"
c := monolock.New(monolock.Config{Address: "127.0.0.1:7070"})
// Blocks until this process owns "nightly-import", runs the function, and// releases the lock when it returns. The lease is this client's// failure-detection window: how long the lock may sit behind a dead holder// before moving on.err := c.Do(ctx, "nightly-import", 2*time.Second, func(ctx context.Context, token uint64) error { // token is the fencing token of this grant. Hand it to the resource // the lock guards with every write — a conditional update, a CAS — // and have the resource reject anything with a smaller token than // the largest it has seen. That fences out a previous holder that // woke up after losing the lock. // // ctx is cancelled the moment ownership stops being confirmed. for { select { case <-ctx.Done(): return nil // the lock is gone; Do reports why case job := <-jobs: if err := process(ctx, job, token); err != nil { return err } } } })The client cancels the function’s context on a heartbeat confirmation
timeout, an EOF, a connection error, a server shutdown, or a cancellation of
Do’s own context. When the context is cancelled, the work that the lock
guarded must stop. Call Do again to go into the queue for a new turn.
There is no silent re-acquisition. A new turn is a new position at the back
of the first-in, first-out (FIFO) queue.
Do makes exactly one attempt. A dial failure, a connection loss in the
queue, or a server rejection returns as an error, and
a new attempt is the decision of the caller. But Do blocks on
the queue itself. A satisfactory connection in the WAITING state waits for
the full time that the context permits.
The client releases the lock each time the function returns — or panics. There is no release call that you can forget. For the server, the closure of the connection is the release. Thus the server immediately promotes the next waiter and does not wait for the end of the lease.
Configuration
Section titled “Configuration”| Field | Default | Meaning |
|---|---|---|
Address |
— | host:port of the server, required |
Dialer |
&net.Dialer{} |
each type that has DialContext: a *tls.Dialer connects to a server that operates with -tls-cert (its client certificate is the client’s identity for the access-control list (ACL) and the audit log of the server); a *net.Dialer adjusts dial timeouts, keep-alives, or the source address |
The client calculates all other values from the lease that you give to
Do. The client sends a heartbeat each lease/4, minus a safety margin of
two times the smoothed round trip. The client does not send heartbeats more
frequently than each 100ms. Silence that is longer than lease * 0.8 makes
the client stop its claim. This rule is applicable in the ACQUIRE handshake
and also while the client holds the lock. The client always stops before the
server stops. See
the heartbeat rules.
Errors
Section titled “Errors”Do returns exactly the error that occurred. Errors are possible before
the function runs. Do returns invalid input as the validation errors of
the protocol (protocol.ErrEmptyName, protocol.ErrNameTooLong,
protocol.ErrInvalidLease). Do returns a dial error or a connection error
without a change. Do returns a server rejection as a *ServerError that
contains the wire code and the reason. A ServerError unwraps to the
canonical protocol error. Thus errors.Is(err, protocol.ErrShuttingDown)
operates correctly. ServerError.Temporary tells you if a new attempt can
help. It obeys the code ranges.
Codes below 0x10 are server conditions (for example, a shutdown). Codes
from 0x10 and above are client errors, and the same bytes cause the same
error again.
DoRetry is Do with an included retry loop. Transient acquisition
failures are a lost dial, a lost connection, or a temporary rejection.
DoRetry tries these again with exponential backoff and jitter, from 100ms
with a doubling to 5s, until the context is cancelled. Invalid input and
permanent rejections return immediately. Each attempt is a new position at
the back of the FIFO queue. After the function has run, DoRetry returns
its result and does not run the function a second time. The question of a
repetition of incomplete work is a property of the work, not of the lock.
After the function runs, Do returns the error of the function. Possibly
the client lost the lock while the function ran. Then Do joins the cause
into the result: ErrHeartbeatTimeout, ErrConnectionClosed,
ErrProtocolViolation, or a *ServerError. This occurs also when the
function returned nil, because the last actions of the function possibly ran
without the lock. A graceful stop through Do’s own context is not a loss
and joins nothing.
Give a *tls.Dialer to connect to a TLS-enabled server.
With mutual TLS (mTLS), the client certificate is also the identity of the
client:
c := monolock.New(monolock.Config{ Address: "locks.internal:7070", Dialer: &tls.Dialer{Config: &tls.Config{ RootCAs: serverCAPool, Certificates: []tls.Certificate{clientCert}, }},})
