2025-04-29 23:35:07 -05:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
package ctxlock_test
|
|
|
|
|
|
|
|
import (
|
2025-05-02 13:24:38 -05:00
|
|
|
"context"
|
2025-04-29 23:35:07 -05:00
|
|
|
"fmt"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"tailscale.com/util/ctxlock"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Resource struct {
|
|
|
|
mu sync.Mutex
|
|
|
|
foo, bar string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) GetFoo(ctx ctxlock.Context) string {
|
|
|
|
defer ctxlock.Lock(ctx, &r.mu).Unlock() // Lock the mutex if not already held.
|
|
|
|
return r.foo
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) SetFoo(ctx ctxlock.Context, foo string) {
|
|
|
|
defer ctxlock.Lock(ctx, &r.mu).Unlock()
|
|
|
|
r.foo = foo
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) GetBar(ctx ctxlock.Context) string {
|
2025-05-02 13:24:38 -05:00
|
|
|
ctx = ctxlock.Lock(ctx, &r.mu)
|
|
|
|
defer ctx.Unlock() // If you prefer it this way.
|
2025-04-29 23:35:07 -05:00
|
|
|
return r.bar
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) SetBar(ctx ctxlock.Context, bar string) {
|
|
|
|
defer ctxlock.Lock(ctx, &r.mu).Unlock()
|
|
|
|
r.bar = bar
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resource) WithLock(ctx ctxlock.Context, f func(ctx ctxlock.Context)) {
|
|
|
|
// Lock the mutex if not already held, and get a new context.
|
|
|
|
ctx = ctxlock.Lock(ctx, &r.mu)
|
|
|
|
defer ctx.Unlock()
|
|
|
|
f(ctx) // Call the callback with the new context.
|
|
|
|
}
|
|
|
|
|
2025-05-02 13:24:38 -05:00
|
|
|
func (r *Resource) HandleRequest(ctx context.Context, foo, bar string, f func(ctx ctxlock.Context) string) string {
|
|
|
|
// Same, but with a standard [context.Context] instead of [ctxlock.Context].
|
|
|
|
// [ctxlock.Lock] is generic and works with both without allocating.
|
|
|
|
// The provided context can be used for cancellation, etc.
|
|
|
|
muCtx := ctxlock.Lock(ctx, &r.mu)
|
|
|
|
defer muCtx.Unlock()
|
|
|
|
r.foo = foo
|
|
|
|
r.bar = bar
|
|
|
|
return f(muCtx)
|
|
|
|
}
|
|
|
|
|
2025-04-29 23:35:07 -05:00
|
|
|
func ExampleContext() {
|
|
|
|
var r Resource
|
|
|
|
r.SetFoo(ctxlock.None(), "foo")
|
|
|
|
r.SetBar(ctxlock.None(), "bar")
|
|
|
|
r.WithLock(ctxlock.None(), func(ctx ctxlock.Context) {
|
|
|
|
// This callback is invoked with r's lock 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))
|
|
|
|
})
|
|
|
|
fmt.Println(r.GetFoo(ctxlock.None()))
|
|
|
|
// Output: foobar
|
|
|
|
}
|
|
|
|
|
|
|
|
func ExampleContext_twoResources() {
|
|
|
|
var r1, r2 Resource
|
|
|
|
r1.SetFoo(ctxlock.None(), "foo")
|
|
|
|
r2.SetBar(ctxlock.None(), "bar")
|
|
|
|
r1.WithLock(ctxlock.None(), func(ctx ctxlock.Context) {
|
|
|
|
// Here, r1's lock is held, but r2's lock is not.
|
|
|
|
// So r2 will be locked when we call r2.GetBar(ctx).
|
|
|
|
r1.SetFoo(ctx, r1.GetFoo(ctx)+r2.GetBar(ctx))
|
|
|
|
})
|
|
|
|
fmt.Println(r1.GetFoo(ctxlock.None()))
|
|
|
|
// Output: foobar
|
|
|
|
}
|
2025-05-01 15:29:08 -05:00
|
|
|
|
|
|
|
func ExampleContext_zeroValue() {
|
|
|
|
var r1, r2 Resource
|
|
|
|
r1.SetFoo(ctxlock.Context{}, "foo")
|
|
|
|
r2.SetBar(ctxlock.Context{}, "bar")
|
|
|
|
r1.WithLock(ctxlock.Context{}, func(ctx ctxlock.Context) {
|
|
|
|
// Here, r1's lock is held, but r2's lock is not.
|
|
|
|
// So r2 will be locked when we call r2.GetBar(ctx).
|
|
|
|
r1.SetFoo(ctx, r1.GetFoo(ctx)+r2.GetBar(ctx))
|
|
|
|
})
|
|
|
|
fmt.Println(r1.GetFoo(ctxlock.Context{}))
|
|
|
|
// Output: foobar
|
|
|
|
}
|
2025-05-02 13:24:38 -05:00
|
|
|
|
|
|
|
func ExampleContext_stdContext() {
|
|
|
|
var r Resource
|
|
|
|
ctx := context.Background()
|
|
|
|
result := r.HandleRequest(ctx, "foo", "bar", func(ctx ctxlock.Context) string {
|
|
|
|
// The r's lock is held, and ctx carries the lock state.
|
|
|
|
return r.GetFoo(ctx) + r.GetBar(ctx)
|
|
|
|
})
|
|
|
|
fmt.Println(result)
|
|
|
|
// Output: foobar
|
|
|
|
}
|