1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-05-17 23:16:11 +08:00
Files
kubo/core/builder.go
2023-10-31 18:25:14 +01:00

179 lines
5.3 KiB
Go

package core
import (
"context"
"fmt"
"reflect"
"sync"
"time"
"github.com/ipfs/boxo/bootstrap"
"github.com/ipfs/kubo/core/node"
"github.com/ipfs/go-metrics-interface"
"go.uber.org/dig"
"go.uber.org/fx"
)
// FXNodeInfo contains information useful for adding fx options.
// This is the extension point for providing more info/context to fx plugins
// to make decisions about what options to include.
type FXNodeInfo struct {
FXOptions []fx.Option
}
// fxOptFunc takes in some info about the IPFS node and returns the full set of fx opts to use.
type fxOptFunc func(FXNodeInfo) ([]fx.Option, error)
var fxOptionFuncs []fxOptFunc
// RegisterFXOptionFunc registers a function that is run before the fx app is initialized.
// Functions are invoked in the order they are registered,
// and the resulting options are passed into the next function's FXNodeInfo.
//
// Note that these are applied globally, by all invocations of NewNode.
// There are multiple places in Kubo that construct nodes, such as:
// - Repo initialization
// - Daemon initialization
// - When running migrations
// - etc.
//
// If your fx options are doing anything sophisticated, you should keep this in mind.
//
// For example, if you plug in a blockservice that disallows non-allowlisted CIDs,
// this may break migrations that fetch migration code over IPFS.
func RegisterFXOptionFunc(optFunc fxOptFunc) {
fxOptionFuncs = append(fxOptionFuncs, optFunc)
}
// from https://stackoverflow.com/a/59348871
type valueContext struct {
context.Context
}
func (valueContext) Deadline() (deadline time.Time, ok bool) { return }
func (valueContext) Done() <-chan struct{} { return nil }
func (valueContext) Err() error { return nil }
type BuildCfg = node.BuildCfg // Alias for compatibility until we properly refactor the constructor interface
// NewNode constructs and returns an IpfsNode using the given cfg.
func NewNode(ctx context.Context, cfg *BuildCfg) (*IpfsNode, error) {
// save this context as the "lifetime" ctx.
lctx := ctx
// derive a new context that ignores cancellations from the lifetime ctx.
ctx, cancel := context.WithCancel(valueContext{ctx})
// add a metrics scope.
ctx = metrics.CtxScope(ctx, "ipfs")
n := &IpfsNode{
ctx: ctx,
}
opts := []fx.Option{
node.IPFS(ctx, cfg),
fx.NopLogger,
}
for _, optFunc := range fxOptionFuncs {
var err error
opts, err = optFunc(FXNodeInfo{FXOptions: opts})
if err != nil {
cancel()
return nil, fmt.Errorf("building fx opts: %w", err)
}
}
//nolint:staticcheck // https://github.com/ipfs/kubo/pull/9423#issuecomment-1341038770
opts = append(opts, fx.Extract(n))
app := fx.New(opts...)
var once sync.Once
var stopErr error
n.stop = func() error {
once.Do(func() {
stopErr = app.Stop(context.Background())
if stopErr != nil {
log.Error("failure on stop: ", stopErr)
}
// Cancel the context _after_ the app has stopped.
cancel()
})
return stopErr
}
n.IsOnline = cfg.Online
go func() {
// Shut down the application if the lifetime context is canceled.
// NOTE: we _should_ stop the application by calling `Close()`
// on the process. But we currently manage everything with contexts.
select {
case <-lctx.Done():
err := n.stop()
if err != nil {
log.Error("failure on stop: ", err)
}
case <-ctx.Done():
}
}()
if app.Err() != nil {
return nil, logAndUnwrapFxError(app.Err())
}
if err := app.Start(ctx); err != nil {
return nil, logAndUnwrapFxError(err)
}
// TODO: How soon will bootstrap move to libp2p?
if !cfg.Online {
return n, nil
}
return n, n.Bootstrap(bootstrap.DefaultBootstrapConfig)
}
// Log the entire `app.Err()` but return only the innermost one to the user
// given the full error can be very long (as it can expose the entire build
// graph in a single string).
//
// The fx.App error exposed through `app.Err()` normally contains un-exported
// errors from its low-level `dig` package:
// * https://github.com/uber-go/dig/blob/5e5a20d/error.go#L82
// These usually wrap themselves in many layers to expose where in the build
// chain did the error happen. Although useful for a developer that needs to
// debug it, it can be very confusing for a user that just wants the IPFS error
// that he can probably fix without being aware of the entire chain.
// Unwrapping everything is not the best solution as there can be useful
// information in the intermediate errors, mainly in the next to last error
// that locates which component is the build error coming from, but it's the
// best we can do at the moment given all errors in dig are private and we
// just have the generic `RootCause` API.
func logAndUnwrapFxError(fxAppErr error) error {
if fxAppErr == nil {
return nil
}
log.Error("constructing the node: ", fxAppErr)
err := fxAppErr
for {
extractedErr := dig.RootCause(err)
// Note that the `RootCause` name is misleading as it just unwraps only
// *one* error layer at a time, so we need to continuously call it.
if !reflect.TypeOf(extractedErr).Comparable() {
// Some internal errors are not comparable (e.g., `dig.errMissingTypes`
// which is a slice) and we can't go further.
break
}
if extractedErr == err {
// We didn't unwrap any new error in the last call, reached the innermost one.
break
}
err = extractedErr
}
return fmt.Errorf("constructing the node (see log for full detail): %w", err)
}