@@ -51,8 +51,21 @@ const (
5151// Exposed as a var rather than a const so tests can shorten it.
5252var heartbeatInterval = 10 * time .Second
5353
54- // UnlockFunc is the callback function returned by [TryLock] and [TryRLock]
55- // it should always be called inside a defer.
54+ // UnlockFunc is the callback returned by [TryLock] and [TryRLock]. It
55+ // releases the advisory lock, closes the underlying file descriptor, and
56+ // stops the background heartbeat goroutine that refreshes the lock
57+ // file's modtime.
58+ //
59+ // Callers MUST invoke this function exactly once, typically via defer
60+ // immediately after a successful lock acquisition. Failing to call it
61+ // leaks both the file descriptor and the heartbeat goroutine for the
62+ // remaining lifetime of the process — the goroutine has no other
63+ // termination signal. Calling it more than once is safe and idempotent;
64+ // only the first call performs the release.
65+ //
66+ // The returned error reflects the unlock/close step only. The heartbeat
67+ // goroutine is always stopped and joined before the unlock is attempted,
68+ // so the file descriptor is never touched after it has been closed.
5669type UnlockFunc func () error
5770
5871// openFile is a helper function for internal use by [tryLock]
@@ -241,7 +254,19 @@ func retryLock(ctx context.Context, root *os.Root, exclusive bool) (*os.File, er
241254// recovery is skipped when ctx has been canceled. If recovery fails,
242255// manual intervention may be required.
243256//
244- // It returns an unlock function that must be called to release the lock.
257+ // On success, the returned [UnlockFunc] MUST be called exactly once to
258+ // release the lock, close the file descriptor, and stop the heartbeat
259+ // goroutine. The idiomatic pattern is to defer it immediately:
260+ //
261+ // unlock, err := flock.TryLock(ctx, root)
262+ // if err != nil {
263+ // return err
264+ // }
265+ // defer unlock()
266+ //
267+ // Failing to call the returned function leaks both the file descriptor
268+ // and the heartbeat goroutine for the remaining lifetime of the process.
269+ // See [UnlockFunc] for details.
245270func TryLock (ctx context.Context , root * os.Root ) (UnlockFunc , error ) {
246271 return tryLock (ctx , root , true )
247272}
@@ -259,7 +284,19 @@ func TryLock(ctx context.Context, root *os.Root) (UnlockFunc, error) {
259284// recovery is skipped when ctx has been canceled. If recovery fails,
260285// manual intervention may be required.
261286//
262- // It returns an unlock function that must be called to release the lock.
287+ // On success, the returned [UnlockFunc] MUST be called exactly once to
288+ // release the lock, close the file descriptor, and stop the heartbeat
289+ // goroutine. The idiomatic pattern is to defer it immediately:
290+ //
291+ // unlock, err := flock.TryRLock(ctx, root)
292+ // if err != nil {
293+ // return err
294+ // }
295+ // defer unlock()
296+ //
297+ // Failing to call the returned function leaks both the file descriptor
298+ // and the heartbeat goroutine for the remaining lifetime of the process.
299+ // See [UnlockFunc] for details.
263300func TryRLock (ctx context.Context , root * os.Root ) (UnlockFunc , error ) {
264301 return tryLock (ctx , root , false )
265302}
0 commit comments