A Go package for resolving values with retries, caching, and concurrency safety.
A "resolvable" takes the form of func[T any](context.Context) (T, error). This package provides a few composable resolvables that allow you mix-and-match different behaviors as needed.
Use New(...) to create a resolvable specifying the desired behaviors as options:
op := func(ctx context.Context) ([]byte, error) {
r, err := http.Get("api call")
if err != nil {
return nil, err
}
defer r.Body.Close()
return ioutil.ReadAll(r.Body)
}
res := resolvable.New(op,
resolvable.WithSafe(), // add concurrency-safety
resolvable.WithCacheTTL(time.Minute), // cache results for a minute
resolvable.WithRetry(), // do not cache errored results
resolvable.WithGraceful(), // return the last known good value on error
)New(...) sets WithSafe() by default for concurrency safety. You may disable it by passing the WithUnsafe() option.
Composables can also be used directly without New().
Resolve a value once and cache the results forever.
getRandomNumber := resolvable.Once(func(context.Context) (int, error) {
return rand.IntN(100), nil
}).WithBackgroundContext()
// the first call will generate a random number
num1, err := getRandomNumber() // -> 42, nil
// subsequent calls will return the exact same result
num2, err := getRandomNumber() // -> 42, nil
num3, err := getRandomNumber() // -> 42, nilResolve a value and cache it forever only if it was successful.
bottleRubs := 0
rubGenieBottle := resolvable.Once(func(context.Context) (*Genie, error) {
bottleRubs++
if bottleRubs < 3 {
return nil, errors.New("rub again")
}
return &Genie{}, nil
}).WithBackgroundContext()
genie, err := rubGenieBottle() // -> nil, error
genie, err := rubGenieBottle() // -> nil, error
genie, err := rubGenieBottle() // -> &Genie{}, nil
// subsequent calls will return the same &Genie{} instance.Resolve a value and cache for a specific period of time. Wrap it with Safe to add concurrency safety.
getRandomNumber := resolvable.Cache(
func(context.Context) (int, error) {
return rand.IntN(100), nil
},
resolvable.CacheOpts{
Expiry: time.Minute,
},
).WithBackgroundContext()
// the first call will generate a random number and cache it for one minute
num1, err := getRandomNumber() // -> 42, nil
num2, err := getRandomNumber() // -> 42, nil
// one minute later...
num3, err := getRandomNumber() // -> 7, nil
// guard it with a mutex
concurrencySafe := resolvable.Safe(getRandomNumber)Returns the last known good value on error.
graceful := resolvable.Graceful(func(ctx context.Context) ([]byte, error) {
// flakey network call
r, err := http.Get("...")
if err != nil {
return nil, err
}
defer r.Body.Close()
return io.ReadAll(r.Body)
})
res1 := graceful(ctx) // success -> []byte{...}, nil
res2 := graceful(ctx) // http error -> []byte{cached res1 value}, error
res3 := graceful(ctx) // success -> []byte{fresh value}, nilGuard a resovable with a mutex ensuring concurrency safety.
safe := resolvable.Safe(func(context.Context) (any, error) {
// code that interacts with a shared resource.
})
// safe() can be safely called concurrently.A helper that returns a static value.
a := resolvable.Static(123)
// syntactic sugar for:
a := func(context.Context) (int, error) {
return 123, nil
}