mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-22 11:17:35 -05:00
In "package tofu" today we try to do everything using a generic acyclic graph model and generic graph walk, which _works_ but tends to make every other part of the problem very hard to follow because we rely a lot on sidecar shared mutable data structures to propagate results between the isolated operations. This is the beginning of an experimental new way to do it where the "graph" is implied by a model that more closely represents how the language itself works, with explicit modelling of the relationships between different types of objects and letting results flow directly from one object to another without any big shared mutable state. There's still a lot to do before this is actually complete enough to evaluate whether it's a viable new design, but I'm considering this a good starting checkpoint since there's enough here to run a simple test of propagating data all the way from input variables to output values via intermediate local values. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
90 lines
3.3 KiB
Go
90 lines
3.3 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package grapheval
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/apparentlymart/go-workgraph/workgraph"
|
|
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// Once is a similar principle to the Go standard library's [sync.Once], but
|
|
// with some additions tailored for use in OpenTofu:
|
|
//
|
|
// - The "Do" method returns a result of type T and diagnostics.
|
|
// - If two Once instances attempt to depend on each other for resolution then
|
|
// both will immediately fail with error diagnostics, rather than deadlocking.
|
|
//
|
|
// The typical way to use this type is to use is as a field of a type
|
|
// representing whatever object the operation conceptually belongs to, and
|
|
// then offer a method of that type which wraps the call to [Once.Do], thus
|
|
// ensuring that the first call to that method will cause the one-time operation
|
|
// to start and then subsequent calls will return the same result as that
|
|
// first call, without directly exposing this type in the public-facing
|
|
// signature.
|
|
type Once[T any] struct {
|
|
mu sync.Mutex
|
|
promise *workgraph.Promise[withDiagnostics[T]]
|
|
requestID workgraph.RequestID
|
|
}
|
|
|
|
// The first time Do is called it runs the given function and returns its result
|
|
// once complete. Subsequent calls then just wait for the result of the function
|
|
// passed in the first call and return its result.
|
|
//
|
|
// The given context MUST have an associated [workgraph.Worker]. Typically
|
|
// the first worker should be established on entry to an internal callgraph
|
|
// that relies on this package, by calling [ContextWithNewWorker].
|
|
//
|
|
// The automatic deadlock detection relies on consistent use of
|
|
// [context.Context] values: any other calls to [once.Do]
|
|
// made directly or indirectly from the given callback function on any
|
|
// Once object in the program MUST pass a context derived from the one passed
|
|
// into the callback function, because it includes internal tracking
|
|
// information.
|
|
func (o *Once[T]) Do(ctx context.Context, f func(ctx context.Context) (T, tfdiags.Diagnostics)) (T, tfdiags.Diagnostics) {
|
|
worker := WorkerFromContext(ctx)
|
|
o.mu.Lock()
|
|
if o.promise == nil {
|
|
// This is the first call, so we'll establish the inner request and
|
|
// start executing the function in a separate goroutine.
|
|
resolver, promise := workgraph.NewRequest[withDiagnostics[T]](worker)
|
|
o.promise = &promise
|
|
o.requestID = resolver.RequestID()
|
|
workgraph.WithNewAsyncWorker(func(w *workgraph.Worker) {
|
|
ctx := ContextWithWorker(ctx, w)
|
|
ret, diags := f(ctx)
|
|
resolver.Report(w, withDiagnostics[T]{ret, diags}, nil)
|
|
}, resolver)
|
|
}
|
|
o.mu.Unlock()
|
|
|
|
withDiags, err := o.promise.Await(worker)
|
|
if err != nil {
|
|
// We return our own errors only as diagnostics, so any error here
|
|
// must be one generated by the workgraph package itself in response
|
|
// to a problem such as self-reference or a failure to resolve some
|
|
// other request.
|
|
var zero T
|
|
return zero, DiagnosticsForWorkgraphError(ctx, err)
|
|
}
|
|
return withDiags.value, withDiags.diags
|
|
}
|
|
|
|
// RequestID returns the [workgraph.RequestID] associated with the inner
|
|
// request, or [workgraph.NoRequest] if Do has not been called yet.
|
|
func (o *Once[T]) RequestID() workgraph.RequestID {
|
|
return o.requestID
|
|
}
|
|
|
|
type withDiagnostics[T any] struct {
|
|
value T
|
|
diags tfdiags.Diagnostics
|
|
}
|