Skip to content

Commit 8dc2881

Browse files
Benehikoclaude
andcommitted
docs(posixage/flock): clarify UnlockFunc lifecycle contract
Document that the returned UnlockFunc owns both the file descriptor and the background heartbeat goroutine, and that callers must invoke it exactly once (typically via defer) to avoid leaking either. Add idiomatic examples on TryLock and TryRLock, and note the heartbeat-stop-before- close ordering on UnlockFunc itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7d5a151 commit 8dc2881

1 file changed

Lines changed: 41 additions & 4 deletions

File tree

store/posixage/internal/flock/flock.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,21 @@ const (
5151
// Exposed as a var rather than a const so tests can shorten it.
5252
var 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.
5669
type 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.
245270
func 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.
263300
func TryRLock(ctx context.Context, root *os.Root) (UnlockFunc, error) {
264301
return tryLock(ctx, root, false)
265302
}

0 commit comments

Comments
 (0)