master
go 190 lines 5.9 KB
Raw
1 package core
2
3 import (
4 "context"
5 "fmt"
6 "reflect"
7 "sync"
8 "time"
9
10 "github.com/ipfs/boxo/bootstrap"
11 "github.com/ipfs/kubo/core/node"
12
13 "github.com/ipfs/go-metrics-interface"
14 "go.uber.org/dig"
15 "go.uber.org/fx"
16 )
17
18 // FXNodeInfo contains information useful for adding fx options.
19 // This is the extension point for providing more info/context to fx plugins
20 // to make decisions about what options to include.
21 type FXNodeInfo struct {
22 FXOptions []fx.Option
23 }
24
25 // fxOptFunc takes in some info about the IPFS node and returns the full set of fx opts to use.
26 type fxOptFunc func(FXNodeInfo) ([]fx.Option, error)
27
28 var fxOptionFuncs []fxOptFunc
29
30 // RegisterFXOptionFunc registers a function that is run before the fx app is initialized.
31 // Functions are invoked in the order they are registered,
32 // and the resulting options are passed into the next function's FXNodeInfo.
33 //
34 // Note that these are applied globally, by all invocations of NewNode.
35 // There are multiple places in Kubo that construct nodes, such as:
36 // - Repo initialization
37 // - Daemon initialization
38 // - When running migrations
39 // - etc.
40 //
41 // If your fx options are doing anything sophisticated, you should keep this in mind.
42 //
43 // For example, if you plug in a blockservice that disallows non-allowlisted CIDs,
44 // this may break migrations that fetch migration code over IPFS.
45 func RegisterFXOptionFunc(optFunc fxOptFunc) {
46 fxOptionFuncs = append(fxOptionFuncs, optFunc)
47 }
48
49 // from https://stackoverflow.com/a/59348871
50 type valueContext struct {
51 context.Context
52 }
53
54 func (valueContext) Deadline() (deadline time.Time, ok bool) { return }
55 func (valueContext) Done() <-chan struct{} { return nil }
56 func (valueContext) Err() error { return nil }
57
58 type BuildCfg = node.BuildCfg // Alias for compatibility until we properly refactor the constructor interface
59
60 // NewNode constructs and returns an IpfsNode using the given cfg.
61 func NewNode(ctx context.Context, cfg *BuildCfg) (*IpfsNode, error) {
62 // save this context as the "lifetime" ctx.
63 lctx := ctx
64
65 // derive a new context that ignores cancellations from the lifetime ctx.
66 ctx, cancel := context.WithCancel(valueContext{ctx})
67
68 // add a metrics scope.
69 ctx = metrics.CtxScope(ctx, "ipfs")
70
71 n := &IpfsNode{
72 ctx: ctx,
73 }
74
75 opts := []fx.Option{
76 node.IPFS(ctx, cfg),
77 fx.NopLogger,
78 }
79 for _, optFunc := range fxOptionFuncs {
80 var err error
81 opts, err = optFunc(FXNodeInfo{FXOptions: opts})
82 if err != nil {
83 cancel()
84 return nil, fmt.Errorf("building fx opts: %w", err)
85 }
86 }
87 //nolint:staticcheck // https://github.com/ipfs/kubo/pull/9423#issuecomment-1341038770
88 opts = append(opts, fx.Extract(n))
89
90 app := fx.New(opts...)
91
92 var once sync.Once
93 var stopErr error
94 n.stop = func() error {
95 once.Do(func() {
96 // Bound app.Stop with a deadline so an FX OnStop hook that
97 // never returns cannot hang the daemon. ShutdownTimeout==0
98 // opts out of the cap entirely and restores the legacy
99 // behavior of waiting forever for hooks to complete. The
100 // daemon's watchdog in cmd/ipfs/kubo/daemon.go fires at the
101 // same deadline and is the unconditional os.Exit fallback.
102 stopCtx := context.Background()
103 if cfg.ShutdownTimeout > 0 {
104 var stopCancel context.CancelFunc
105 stopCtx, stopCancel = context.WithTimeout(stopCtx, cfg.ShutdownTimeout)
106 defer stopCancel()
107 }
108 stopErr = app.Stop(stopCtx)
109 if stopErr != nil {
110 log.Errorf("failure on stop: %v", stopErr)
111 }
112 // Cancel the context _after_ the app has stopped.
113 cancel()
114 })
115 return stopErr
116 }
117 n.IsOnline = cfg.Online
118
119 go func() {
120 // Shut down the application if the lifetime context is canceled.
121 // NOTE: we _should_ stop the application by calling `Close()`
122 // on the process. But we currently manage everything with contexts.
123 select {
124 case <-lctx.Done():
125 err := n.stop()
126 if err != nil {
127 log.Error("failure on stop: ", err)
128 }
129 case <-ctx.Done():
130 }
131 }()
132
133 if app.Err() != nil {
134 return nil, logAndUnwrapFxError(app.Err())
135 }
136
137 if err := app.Start(ctx); err != nil {
138 return nil, logAndUnwrapFxError(err)
139 }
140
141 // TODO: How soon will bootstrap move to libp2p?
142 if !cfg.Online {
143 return n, nil
144 }
145
146 return n, n.Bootstrap(bootstrap.DefaultBootstrapConfig)
147 }
148
149 // Log the entire `app.Err()` but return only the innermost one to the user
150 // given the full error can be very long (as it can expose the entire build
151 // graph in a single string).
152 //
153 // The fx.App error exposed through `app.Err()` normally contains un-exported
154 // errors from its low-level `dig` package:
155 // * https://github.com/uber-go/dig/blob/5e5a20d/error.go#L82
156 // These usually wrap themselves in many layers to expose where in the build
157 // chain did the error happen. Although useful for a developer that needs to
158 // debug it, it can be very confusing for a user that just wants the IPFS error
159 // that he can probably fix without being aware of the entire chain.
160 // Unwrapping everything is not the best solution as there can be useful
161 // information in the intermediate errors, mainly in the next to last error
162 // that locates which component is the build error coming from, but it's the
163 // best we can do at the moment given all errors in dig are private and we
164 // just have the generic `RootCause` API.
165 func logAndUnwrapFxError(fxAppErr error) error {
166 if fxAppErr == nil {
167 return nil
168 }
169
170 log.Error("constructing the node: ", fxAppErr)
171
172 err := fxAppErr
173 for {
174 extractedErr := dig.RootCause(err)
175 // Note that the `RootCause` name is misleading as it just unwraps only
176 // *one* error layer at a time, so we need to continuously call it.
177 if !reflect.TypeOf(extractedErr).Comparable() {
178 // Some internal errors are not comparable (e.g., `dig.errMissingTypes`
179 // which is a slice) and we can't go further.
180 break
181 }
182 if extractedErr == err {
183 // We didn't unwrap any new error in the last call, reached the innermost one.
184 break
185 }
186 err = extractedErr
187 }
188
189 return fmt.Errorf("constructing the node (see log for full detail): %w", err)
190 }