blob: 3d02c0eb426bb6d1fd08bc25dfc984f912425956 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package promising
import (
"context"
"sync"
)
// Once is a higher-level wrapper around promises that is similar to
// the Go standard library's sync.Once but executes the one-time function
// in an asynchronous task and makes all subsequent calls block until the
// asynchronous task has completed, after which all calls will return the
// result of the initial call.
type Once[T any] struct {
get PromiseGet[T]
promiseID PromiseID
mu sync.Mutex
}
// Do makes an asynchronous call to function f only on the first call to
// this method.
//
// The first and all subsequent calls then block on the completion of that
// asynchronous call and all return its single result.
//
// The context used for the call must belong to an active task. The context
// passed to f will belong to a separate asynchronous task and so f can
// create its own promises and create further asynchronous tasks to deal
// with them, as normal.
//
// The typical way to use Do is to have only a single callsite where f is
// set to a function literal whose behavior and results would be equivalent
// regardless of which instance of its closure happens to be the chosen on
// to actually run. All subsequent calls will ignore f entirely, so it is
// incorrect (and useless) to try to vary the effect of f between calls.
//
// This function will return an error of type [ErrSelfDependent] if two
// different Once instances attempt to mutually depend on one another for
// completion. This means that, unlike standard library sync.Once,
// self-dependence cannot cause a deadlock. (Other non-promise-related
// synchronization between calls can still potentially deadlock, though.)
//
// If f panics then that prevents the internal promise from being resolved,
// and so all calls to Do will return [ErrUnresolved]. However, there is
// no built-in facility to catch and recover from such panics since they occur
// in a separate goroutine from all of the waiters.
func (o *Once[T]) Do(ctx context.Context, name string, f func(ctx context.Context) (T, error)) (T, error) {
AssertContextInTask(ctx)
o.mu.Lock()
if o.get == nil {
// We seem to be the first call, so we'll get the asynchronous task
// running and then block on its result.
resolver, get := NewPromise[T](ctx, name)
o.get = get
o.promiseID = resolver.PromiseID()
o.mu.Unlock()
// The responsibility for resolving the promise transfers to the
// asynchronous task, which makes it valid for this main task to
// await it without a self-dependency error.
AsyncTask(
ctx, resolver,
func(ctx context.Context, resolver PromiseResolver[T]) {
v, err := f(ctx)
resolver.Resolve(ctx, v, err)
},
)
} else {
o.mu.Unlock()
}
// Regardless of whether we launched the async task or not, we'll
// wait for it to resolve the promise before we return.
return o.get(ctx)
}
// PromiseID returns the unique identifier for the backing promise of the
// receiver, or [NoPromise] if the once hasn't been started yet.
//
// If PromiseID returns [NoPromise] then that result might be immediately
// invalidated by a concurrent or subsequent call to [Once.Do]. However,
// if PromiseID returns a nonzero promise ID then it's guaranteed to remain
// consistent for the remaining lifetime of the object.
func (o *Once[T]) PromiseID() PromiseID {
o.mu.Lock()
ret := o.promiseID
o.mu.Unlock()
return ret
}