summaryrefslogtreecommitdiffhomepage
path: root/util/ctxlock/doc_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'util/ctxlock/doc_test.go')
-rw-r--r--util/ctxlock/doc_test.go272
1 files changed, 212 insertions, 60 deletions
diff --git a/util/ctxlock/doc_test.go b/util/ctxlock/doc_test.go
index a6b0de407..d1c32f9ba 100644
--- a/util/ctxlock/doc_test.go
+++ b/util/ctxlock/doc_test.go
@@ -6,75 +6,130 @@ package ctxlock_test
import (
"context"
"fmt"
- "sync"
+ "strings"
"testing"
"tailscale.com/util/ctxlock"
)
-type Resource struct {
- mu sync.Mutex
- foo, bar string
-}
+func ExampleMutex_reentrant() {
+ var mu ctxlock.ReentrantMutex // shorthand for ctxlock.Mutex[ctxlock.Reentrant]
-func (r *Resource) GetFoo(ctx ctxlock.State) string {
- // Lock the mutex if not already held.
- defer ctxlock.Lock(ctx, &r.mu).Unlock()
- return r.foo
-}
+ // The mutex is reentrant, so foo can be called with or without holding the mu.
+ // If mu is not already held, it will be locked on entry and unlocked on exit.
+ // The [ctxlock.State] parameter carries the current lock state.
+ foo := func(ctx ctxlock.State, msg string) {
+ lock := ctxlock.Lock(ctx, &mu)
+ defer lock.Unlock()
+ fmt.Println(msg)
+ }
-func (r *Resource) SetFoo(ctx ctxlock.State, foo string) {
- // You can do it this way, if you prefer
- // or if you need to pass the state to another function.
- ctx = ctxlock.Lock(ctx, &r.mu)
- defer ctx.Unlock()
- r.foo = foo
-}
+ // Calling foo without holding the lock.
+ foo(ctxlock.None(), "no lock")
-func (r *Resource) GetBar(ctx ctxlock.State) string {
- defer ctxlock.Lock(ctx, &r.mu).Unlock()
- return r.bar
-}
+ // Locking the mutex and calling foo again.
+ lock := ctxlock.Lock(ctxlock.None(), &mu)
+ foo(lock.State(), "with lock")
+ defer lock.Unlock()
-func (r *Resource) SetBar(ctx ctxlock.State, bar string) {
- defer ctxlock.Lock(ctx, &r.mu).Unlock()
- r.bar = bar
+ // Output:
+ // no lock
+ // with lock
}
-func (r *Resource) WithLock(ctx ctxlock.State, f func(ctx ctxlock.State)) {
- // Lock the mutex if not already held, and get a new state.
- ctx = ctxlock.Lock(ctx, &r.mu)
- defer ctx.Unlock()
- f(ctx) // Call the callback with the new lock state.
+func ExampleMutex_nonReentrant() {
+ var mu ctxlock.Mutex[ctxlock.NonReentrant]
+
+ // The mutex is non-reentrant, so foo must only be called without holding the mu.
+ // If mu is already held, it will panic attempting to lock it again.
+ foo := func(ctx ctxlock.State, msg string) {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("panic:", trimPanicMessage(r))
+ }
+ }()
+
+ lock := ctxlock.Lock(ctx, &mu)
+ defer lock.Unlock()
+ fmt.Println(msg)
+ }
+
+ // Calling foo without holding the lock.
+ foo(ctxlock.None(), "no lock")
+
+ // Locking the mutex and calling foo again.
+ // This will panic because the mutex is non-reentrant.
+ lock := ctxlock.Lock(ctxlock.None(), &mu)
+ foo(lock.State(), "with lock")
+ defer lock.Unlock()
+
+ // Output:
+ // no lock
+ // panic: non-reentrant mutex already locked
}
-func (r *Resource) HandleRequest(ctx context.Context, foo, bar string, f func(ls ctxlock.State) string) string {
- // Same, but with a standard [context.Context] instead of [ctxlock.State].
- // [ctxlock.Lock] is generic and works with both without allocating.
- // The ctx can be used for cancellation, etc.
- mu := ctxlock.Lock(ctx, &r.mu)
- defer mu.Unlock()
- r.foo = foo
- r.bar = bar
- return f(mu)
+func ExampleRank() {
+ var mu1 ctxlock.Mutex[rank1] // cannot be locked after mu2 or mu3
+ var mu2 ctxlock.Mutex[rank2] // cannot be locked after mu3
+ var mu3 ctxlock.Mutex[rank3]
+
+ lock := ctxlock.Lock(ctxlock.None(), &mu1)
+ defer lock.Unlock()
+ fmt.Println("locked mu1")
+
+ lock = ctxlock.Lock(lock.State(), &mu2)
+ defer lock.Unlock()
+ fmt.Println("locked mu2")
+
+ lock = ctxlock.Lock(lock.State(), &mu3)
+ defer lock.Unlock()
+ fmt.Println("locked mu3")
+
+ // Output:
+ // locked mu1
+ // locked mu2
+ // locked mu3
}
-func (r *Resource) HandleIntRequest(ctx context.Context, foo, bar string, f func(ls ctxlock.State) int) int {
- // Same, but returns an int instead of a string,
- // and must not allocate with the unchecked implementation.
- mu := ctxlock.Lock(ctx, &r.mu)
- defer mu.Unlock()
- r.foo = foo
- r.bar = bar
- return f(mu)
+func ExampleRank_lockOrderViolation() {
+ var mu1 ctxlock.Mutex[rank1] // cannot be locked after mu2 or mu3
+ var mu2 ctxlock.Mutex[rank2] // cannot be locked after mu3
+ var mu3 ctxlock.Mutex[rank3]
+
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("panic:", trimPanicMessage(r))
+ }
+ }()
+
+ // While we can lock mu2 first...
+ lock := ctxlock.Lock(ctxlock.None(), &mu2)
+ defer lock.Unlock()
+ fmt.Println("locked mu2")
+
+ // ...and then mu3...
+ lock = ctxlock.Lock(lock.State(), &mu3)
+ defer lock.Unlock()
+ fmt.Println("locked mu3")
+
+ // It is a lock order violation to lock mu1
+ // after either mu2 or mu3.
+ lock = ctxlock.Lock(lock.State(), &mu1)
+ defer lock.Unlock()
+ fmt.Println("locked mu1")
+
+ // Output:
+ // locked mu2
+ // locked mu3
+ // panic: cannot lock ctxlock_test.rank1 after ctxlock_test.rank3
}
-func ExampleState() {
+func ExampleState_resource() {
var r Resource
r.SetFoo(ctxlock.None(), "foo")
r.SetBar(ctxlock.None(), "bar")
r.WithLock(ctxlock.None(), func(ctx ctxlock.State) {
- // This callback is invoked with r's lock held,
+ // This callback is invoked with r's mutex held,
// and ctx carries the lock state. This means we can safely call
// other methods on r using ctx without causing a deadlock.
r.SetFoo(ctx, r.GetFoo(ctx)+r.GetBar(ctx))
@@ -88,7 +143,7 @@ func ExampleState_twoResources() {
r1.SetFoo(ctxlock.None(), "foo")
r2.SetBar(ctxlock.None(), "bar")
r1.WithLock(ctxlock.None(), func(ctx ctxlock.State) {
- // Here, r1's lock is held, but r2's lock is not.
+ // Here, r1's mutex is held, but r2's mutex is not.
// So r2 will be locked when we call r2.GetBar(ctx).
r1.SetFoo(ctx, r1.GetFoo(ctx)+r2.GetBar(ctx))
})
@@ -96,29 +151,27 @@ func ExampleState_twoResources() {
// Output: foobar
}
-func ExampleState_stdContext() {
+func ExampleState_withStdContext() {
var r Resource
ctx := context.Background()
result := r.HandleRequest(ctx, "foo", "bar", func(ctx ctxlock.State) string {
- // The r's lock is held, and ctx carries the lock state.
+ // The r's mutex is held, and ctx carries the lock state.
return r.GetFoo(ctx) + r.GetBar(ctx)
})
fmt.Println(result)
// Output: foobar
}
-func TestAllocFree(t *testing.T) {
- if ctxlock.Checked {
- t.Skip("Exported implementation is not alloc-free (use --tags=ts_omit_ctxlock_checks)")
+func TestEndToEndAllocFree(t *testing.T) {
+ if ctxlock.IsChecked {
+ t.Skip("Exported implementation is not alloc-free (use --tags=ts_omit_ctxlock_checks).")
}
var r Resource
- ctx := context.Background()
-
- const runs = 1000
- if allocs := testing.AllocsPerRun(runs, func() {
- res := r.HandleIntRequest(ctx, "foo", "bar", func(ctx ctxlock.State) int {
- // The r's lock is held, and ctx carries the lock state.
+ const N = 1000
+ if allocs := testing.AllocsPerRun(N, func() {
+ res := r.HandleIntRequest(context.Background(), "foo", "bar", func(ctx ctxlock.State) int {
+ // The r's mutex is held, and ctx carries the lock state.
return len(r.GetFoo(ctx) + r.GetBar(ctx))
})
if res != 6 {
@@ -128,3 +181,102 @@ func TestAllocFree(t *testing.T) {
t.Errorf("expected 0 allocs, got %f", allocs)
}
}
+
+type (
+ rank1 struct{}
+ rank2 struct{}
+ rank3 struct{}
+)
+
+// CheckLockAfter implements [ctxlock.Rank].
+func (r rank1) CheckLockAfter(r2 ctxlock.Rank) error {
+ switch r2.(type) {
+ case rank2, rank3:
+ return fmt.Errorf("cannot lock %T after %T", r, r2)
+ default:
+ return nil
+ }
+}
+
+// CheckLockAfter implements [ctxlock.Rank].
+func (r rank2) CheckLockAfter(r2 ctxlock.Rank) error {
+ switch r2.(type) {
+ case rank2, rank3:
+ return fmt.Errorf("cannot lock %T after %T", r, r2)
+ default:
+ return nil
+ }
+}
+
+// CheckLockAfter implements [ctxlock.Rank].
+func (a rank3) CheckLockAfter(b ctxlock.Rank) error {
+ return nil
+}
+
+type Resource struct {
+ mu ctxlock.ReentrantMutex
+ foo, bar string
+}
+
+func (r *Resource) GetFoo(ctx ctxlock.State) string {
+ // Lock the mutex if not already held,
+ // and unlock it when the function returns.
+ defer ctxlock.Lock(ctx, &r.mu).Unlock()
+ return r.foo
+}
+
+func (r *Resource) SetFoo(ctx ctxlock.State, foo string) {
+ // You can do it this way, if you prefer.
+ mu := ctxlock.Lock(ctx, &r.mu)
+ defer mu.Unlock()
+ r.foo = foo
+}
+
+func (r *Resource) GetBar(ctx ctxlock.State) string {
+ mu := ctxlock.Lock(ctx, &r.mu)
+ defer mu.Unlock()
+ return r.bar
+}
+
+func (r *Resource) SetBar(ctx ctxlock.State, bar string) {
+ mu := ctxlock.Lock(ctx, &r.mu)
+ defer mu.Unlock()
+ r.bar = bar
+}
+
+func (r *Resource) WithLock(ctx ctxlock.State, f func(ctx ctxlock.State)) {
+ mu := ctxlock.Lock(ctx, &r.mu)
+ defer mu.Unlock()
+ // Call the callback with the new lock state.
+ f(mu.State())
+}
+
+func (r *Resource) HandleRequest(ctx context.Context, foo, bar string, f func(ls ctxlock.State) string) string {
+ // Same, but with a standard [context.Context] instead of [ctxlock.State].
+ // [ctxlock.Lock] is generic and works with both without allocating.
+ // The ctx can be used for cancellation, etc.
+ mu := ctxlock.Lock(ctx, &r.mu)
+ defer mu.Unlock()
+ r.foo = foo
+ r.bar = bar
+ return f(mu.State())
+}
+
+func (r *Resource) HandleIntRequest(ctx context.Context, foo, bar string, f func(ls ctxlock.State) int) int {
+ // Same, but returns an int instead of a string.
+ // It must not allocate with the checked implementation.
+ mu := ctxlock.Lock(ctx, &r.mu)
+ defer mu.Unlock()
+ r.foo = foo
+ r.bar = bar
+ return f(mu.State())
+}
+
+func trimPanicMessage(r any) string {
+ msg := fmt.Sprintf("%v", r)
+ msg = strings.TrimSpace(msg)
+ if i := strings.IndexByte(msg, '\n'); i >= 0 {
+ return msg[:i]
+ }
+ return msg
+}