master
go 1,496 lines 54 KB
Raw
1 package node
2
3 import (
4 "context"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "time"
11
12 "github.com/ipfs/boxo/blockstore"
13 "github.com/ipfs/boxo/dag/walker"
14 "github.com/ipfs/boxo/fetcher"
15 "github.com/ipfs/boxo/mfs"
16 pin "github.com/ipfs/boxo/pinning/pinner"
17 "github.com/ipfs/boxo/pinning/pinner/dspinner"
18 "github.com/ipfs/boxo/provider"
19 "github.com/ipfs/go-cid"
20 "github.com/ipfs/go-datastore"
21 "github.com/ipfs/go-datastore/mount"
22 "github.com/ipfs/go-datastore/namespace"
23 "github.com/ipfs/go-datastore/query"
24 log "github.com/ipfs/go-log/v2"
25 "github.com/ipfs/kubo/config"
26 "github.com/ipfs/kubo/core/shutdown"
27 "github.com/ipfs/kubo/repo"
28 "github.com/ipfs/kubo/repo/fsrepo"
29 irouting "github.com/ipfs/kubo/routing"
30 dht "github.com/libp2p/go-libp2p-kad-dht"
31 "github.com/libp2p/go-libp2p-kad-dht/amino"
32 "github.com/libp2p/go-libp2p-kad-dht/dual"
33 "github.com/libp2p/go-libp2p-kad-dht/fullrt"
34 dht_pb "github.com/libp2p/go-libp2p-kad-dht/pb"
35 dhtprovider "github.com/libp2p/go-libp2p-kad-dht/provider"
36 "github.com/libp2p/go-libp2p-kad-dht/provider/buffered"
37 ddhtprovider "github.com/libp2p/go-libp2p-kad-dht/provider/dual"
38 "github.com/libp2p/go-libp2p-kad-dht/provider/keystore"
39 routinghelpers "github.com/libp2p/go-libp2p-routing-helpers"
40 "github.com/libp2p/go-libp2p/core/host"
41 peer "github.com/libp2p/go-libp2p/core/peer"
42 "github.com/libp2p/go-libp2p/core/routing"
43 ma "github.com/multiformats/go-multiaddr"
44 mh "github.com/multiformats/go-multihash"
45 "go.uber.org/fx"
46 )
47
48 const (
49 // The size of a batch that will be used for calculating average announcement
50 // time per CID, inside of boxo/provider.ThroughputReport
51 // and in 'ipfs stats provide' report.
52 // Used when Provide.DHT.SweepEnabled=false
53 sampledBatchSize = 1000
54
55 // Datastore key used to store previous reprovide strategy.
56 reprovideStrategyKey = "/reprovideStrategy"
57
58 // KeystoreDatastorePath is the base directory for the provider keystore datastores.
59 KeystoreDatastorePath = "provider-keystore"
60
61 // reprovideLastUniqueCountKey stores the unique CID count from
62 // the last +unique reprovide cycle, used to size the next cycle's
63 // bloom filter.
64 reprovideLastUniqueCountKey = "/reprovideLastUniqueCount"
65 )
66
67 var (
68 // Datastore namespace key for provider data.
69 providerDatastoreKey = datastore.NewKey("provider")
70 // Datastore namespace key for provider keystore data.
71 keystoreDatastoreKey = datastore.NewKey("keystore")
72 )
73
74 // providerLog is the go-log subsystem used for provide/reprovide-related
75 // messages emitted from kubo's own orchestration code. It shares the
76 // "provider" subsystem name with boxo's provider package so users can set
77 // GOLOG_LOG_LEVEL=provider=<level> to control both layers at once. See
78 // docs/debug-guide.md for the full list of provide-related subsystems.
79 var providerLog = log.Logger("provider")
80
81 var errAcceleratedDHTNotReady = errors.New("AcceleratedDHTClient: routing table not ready")
82
83 // validateKeystoreSuffix rejects any suffix other than "0" or "1".
84 // The upstream library uses these two values as alternating namespace
85 // identifiers. Validating here prevents accidental deletion of unrelated
86 // directories via os.RemoveAll if the upstream ever changes its scheme.
87 func validateKeystoreSuffix(suffix string) error {
88 if suffix != "0" && suffix != "1" {
89 return fmt.Errorf("unexpected keystore suffix %q, expected \"0\" or \"1\"", suffix)
90 }
91 return nil
92 }
93
94 // Interval between reprovide queue monitoring checks for slow reprovide alerts.
95 // Used when Provide.DHT.SweepEnabled=true
96 const reprovideAlertPollInterval = 15 * time.Minute
97
98 // Number of consecutive polling intervals with sustained queue growth before
99 // triggering a slow reprovide alert (3 intervals = 45 minutes).
100 // Used when Provide.DHT.SweepEnabled=true
101 const consecutiveAlertsThreshold = 3
102
103 // DHTProvider is an interface for providing keys to a DHT swarm. It holds a
104 // state of keys to be advertised, and is responsible for periodically
105 // publishing provider records for these keys to the DHT swarm before the
106 // records expire.
107 type DHTProvider interface {
108 // StartProviding ensures keys are periodically advertised to the DHT swarm.
109 //
110 // If the `keys` aren't currently being reprovided, they are added to the
111 // queue to be provided to the DHT swarm as soon as possible, and scheduled
112 // to be reprovided periodically. If `force` is set to true, all keys are
113 // provided to the DHT swarm, regardless of whether they were already being
114 // reprovided in the past. `keys` keep being reprovided until `StopProviding`
115 // is called.
116 //
117 // This operation is asynchronous, it returns as soon as the `keys` are added
118 // to the provide queue, and provides happens asynchronously.
119 //
120 // Returns an error if the keys couldn't be added to the provide queue. This
121 // can happen if the provider is closed or if the node is currently Offline
122 // (either never bootstrapped, or disconnected since more than `OfflineDelay`).
123 // The schedule and provide queue depend on the network size, hence recent
124 // network connectivity is essential.
125 StartProviding(force bool, keys ...mh.Multihash) error
126 // ProvideOnce sends provider records for the specified keys to the DHT swarm
127 // only once. It does not automatically reprovide those keys afterward.
128 //
129 // Add the supplied multihashes to the provide queue, and return immediately.
130 // The provide operation happens asynchronously.
131 //
132 // Returns an error if the keys couldn't be added to the provide queue. This
133 // can happen if the provider is closed or if the node is currently Offline
134 // (either never bootstrapped, or disconnected since more than `OfflineDelay`).
135 // The schedule and provide queue depend on the network size, hence recent
136 // network connectivity is essential.
137 ProvideOnce(keys ...mh.Multihash) error
138 // Clear clears the all the keys from the provide queue and returns the number
139 // of keys that were cleared.
140 //
141 // The keys are not deleted from the keystore, so they will continue to be
142 // reprovided as scheduled.
143 Clear() int
144 // RefreshSchedule scans the Keystore for any keys that are not currently
145 // scheduled for reproviding. If such keys are found, it schedules their
146 // associated keyspace region to be reprovided.
147 //
148 // This function doesn't remove prefixes that have no keys from the schedule.
149 // This is done automatically during the reprovide operation if a region has no
150 // keys.
151 //
152 // Returns an error if the provider is closed or if the node is currently
153 // Offline (either never bootstrapped, or disconnected since more than
154 // `OfflineDelay`). The schedule depends on the network size, hence recent
155 // network connectivity is essential.
156 RefreshSchedule() error
157 Close() error
158 }
159
160 var (
161 _ DHTProvider = &ddhtprovider.SweepingProvider{}
162 _ DHTProvider = &dhtprovider.SweepingProvider{}
163 _ DHTProvider = &NoopProvider{}
164 _ DHTProvider = &LegacyProvider{}
165 )
166
167 // NoopProvider is a no-operation provider implementation that does nothing.
168 // It is used when providing is disabled or when no DHT is available.
169 // All methods return successfully without performing any actual operations.
170 type NoopProvider struct{}
171
172 func (r *NoopProvider) StartProviding(bool, ...mh.Multihash) error { return nil }
173 func (r *NoopProvider) ProvideOnce(...mh.Multihash) error { return nil }
174 func (r *NoopProvider) Clear() int { return 0 }
175 func (r *NoopProvider) RefreshSchedule() error { return nil }
176 func (r *NoopProvider) Close() error { return nil }
177
178 // LegacyProvider is a wrapper around the boxo/provider.System that implements
179 // the DHTProvider interface. This provider manages reprovides using a burst
180 // strategy where it sequentially reprovides all keys at once during each
181 // reprovide interval, rather than spreading the load over time.
182 //
183 // This is the legacy provider implementation that can cause resource spikes
184 // during reprovide operations. For more efficient providing, consider using
185 // the SweepingProvider which spreads the load over the reprovide interval.
186 type LegacyProvider struct {
187 provider.System
188 }
189
190 func (r *LegacyProvider) StartProviding(force bool, keys ...mh.Multihash) error {
191 return r.ProvideOnce(keys...)
192 }
193
194 func (r *LegacyProvider) ProvideOnce(keys ...mh.Multihash) error {
195 if many, ok := r.System.(routinghelpers.ProvideManyRouter); ok {
196 return many.ProvideMany(context.Background(), keys)
197 }
198
199 for _, k := range keys {
200 if err := r.Provide(context.Background(), cid.NewCidV1(cid.Raw, k), true); err != nil {
201 return err
202 }
203 }
204 return nil
205 }
206
207 func (r *LegacyProvider) Clear() int {
208 return r.System.Clear()
209 }
210
211 func (r *LegacyProvider) RefreshSchedule() error { return nil }
212
213 // LegacyProviderOpt creates a LegacyProvider to be used as provider in the
214 // IpfsNode
215 func LegacyProviderOpt(reprovideInterval time.Duration, strategy string, acceleratedDHTClient bool, provideWorkerCount int) fx.Option {
216 system := fx.Provide(
217 fx.Annotate(func(lc fx.Lifecycle, cr irouting.ProvideManyRouter, repo repo.Repo) (*LegacyProvider, error) {
218 // Initialize provider.System first, before pinner/blockstore/etc.
219 // The KeyChanFunc will be set later via SetKeyProvider() once we have
220 // created the pinner, blockstore and other dependencies.
221 opts := []provider.Option{
222 provider.Online(cr),
223 provider.ReproviderInterval(reprovideInterval),
224 provider.ProvideWorkerCount(provideWorkerCount),
225 }
226 if !acceleratedDHTClient && reprovideInterval > 0 {
227 // The estimation kinda suck if you are running with accelerated DHT client,
228 // given this message is just trying to push people to use the acceleratedDHTClient
229 // let's not report on through if it's in use
230 opts = append(opts,
231 provider.ThroughputReport(func(reprovide bool, complete bool, keysProvided uint, duration time.Duration) bool {
232 avgProvideSpeed := duration / time.Duration(keysProvided)
233 count := uint64(keysProvided)
234
235 if !reprovide || !complete {
236 // We don't know how many CIDs we have to provide, try to fetch it from the blockstore.
237 // But don't try for too long as this might be very expensive if you have a huge datastore.
238 ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
239 defer cancel()
240
241 // FIXME: I want a running counter of blocks so size of blockstore can be an O(1) lookup.
242 // Note: talk to datastore directly, as to not depend on Blockstore here.
243 qr, err := repo.Datastore().Query(ctx, query.Query{
244 Prefix: blockstore.BlockPrefix.String(),
245 KeysOnly: true,
246 })
247 if err != nil {
248 providerLog.Errorf("fetching AllKeysChain in provider ThroughputReport: %v", err)
249 return false
250 }
251 defer qr.Close()
252 count = 0
253 countLoop:
254 for {
255 select {
256 case _, ok := <-qr.Next():
257 if !ok {
258 break countLoop
259 }
260 count++
261 case <-ctx.Done():
262 // really big blockstore mode
263
264 // how many blocks would be in a 10TiB blockstore with 128KiB blocks.
265 const probableBigBlockstore = (10 * 1024 * 1024 * 1024 * 1024) / (128 * 1024)
266 // How long per block that lasts us.
267 expectedProvideSpeed := reprovideInterval / probableBigBlockstore
268 if avgProvideSpeed > expectedProvideSpeed {
269 providerLog.Errorf(`
270 🔔🔔🔔 Reprovide Operations Too Slow 🔔🔔🔔
271
272 Your node may be falling behind on DHT reprovides, which could affect content availability.
273
274 Observed: %d keys at %v per key
275 Estimated: Assuming 10TiB blockstore, would take %v to complete
276 ⏰ Must finish within %v (Provide.DHT.Interval)
277
278 Solutions (try in order):
279 1. Enable Provide.DHT.SweepEnabled=true (recommended)
280 2. Increase Provide.DHT.MaxWorkers if needed
281 3. Enable Routing.AcceleratedDHTClient=true (last resort, resource intensive)
282
283 Learn more: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide`,
284 keysProvided, avgProvideSpeed, avgProvideSpeed*probableBigBlockstore, reprovideInterval)
285 return false
286 }
287 }
288 }
289 }
290
291 // How long per block that lasts us.
292 expectedProvideSpeed := reprovideInterval
293 if count > 0 {
294 expectedProvideSpeed = reprovideInterval / time.Duration(count)
295 }
296
297 if avgProvideSpeed > expectedProvideSpeed {
298 providerLog.Errorf(`
299 🔔🔔🔔 Reprovide Operations Too Slow 🔔🔔🔔
300
301 Your node is falling behind on DHT reprovides, which will affect content availability.
302
303 Observed: %d keys at %v per key
304 Confirmed: ~%d total CIDs requiring %v to complete
305 ⏰ Must finish within %v (Provide.DHT.Interval)
306
307 Solutions (try in order):
308 1. Enable Provide.DHT.SweepEnabled=true (recommended)
309 2. Increase Provide.DHT.MaxWorkers if needed
310 3. Enable Routing.AcceleratedDHTClient=true (last resort, resource intensive)
311
312 Learn more: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide`,
313 keysProvided, avgProvideSpeed, count, avgProvideSpeed*time.Duration(count), reprovideInterval)
314 }
315 return false
316 }, sampledBatchSize))
317 }
318
319 sys, err := provider.New(repo.Datastore(), opts...)
320 if err != nil {
321 return nil, err
322 }
323 lc.Append(fx.Hook{
324 OnStop: func(ctx context.Context) error {
325 return shutdown.CloseWithCtx(ctx, "legacy-provider", sys.Close)
326 },
327 })
328
329 prov := &LegacyProvider{sys}
330 handleStrategyChange(strategy, prov, repo.Datastore())
331
332 return prov, nil
333 },
334 fx.As(new(provider.System)),
335 fx.As(new(DHTProvider)),
336 ),
337 )
338 setKeyProvider := fx.Invoke(func(lc fx.Lifecycle, system provider.System, keyProvider provider.KeyChanFunc) {
339 lc.Append(fx.Hook{
340 OnStart: func(ctx context.Context) error {
341 // SetKeyProvider breaks the circular dependency between provider, blockstore, and pinner.
342 // We cannot create the blockstore without the provider (it needs to provide blocks),
343 // and we cannot determine the reproviding strategy without the pinner/blockstore.
344 // This deferred initialization allows us to create provider.System first,
345 // then set the actual key provider function after all dependencies are ready.
346 system.SetKeyProvider(keyProvider)
347 return nil
348 },
349 })
350 })
351 return fx.Options(
352 system,
353 setKeyProvider,
354 )
355 }
356
357 type dhtImpl interface {
358 routing.Routing
359 GetClosestPeers(context.Context, string) ([]peer.ID, error)
360 Host() host.Host
361 MessageSender() dht_pb.MessageSender
362 }
363
364 type fullrtRouter struct {
365 *fullrt.FullRT
366 ready bool
367 logger *log.ZapEventLogger
368 }
369
370 func newFullRTRouter(fr *fullrt.FullRT, loggerName string) *fullrtRouter {
371 return &fullrtRouter{
372 FullRT: fr,
373 ready: true,
374 logger: log.Logger(loggerName),
375 }
376 }
377
378 // GetClosestPeers overrides fullrt.FullRT's GetClosestPeers and returns an
379 // error if the fullrt's initial network crawl isn't complete yet.
380 func (fr *fullrtRouter) GetClosestPeers(ctx context.Context, key string) ([]peer.ID, error) {
381 if fr.ready {
382 if !fr.Ready() {
383 fr.ready = false
384 fr.logger.Info("AcceleratedDHTClient: waiting for routing table initialization (5-10 min, depends on DHT size and network) to complete before providing")
385 return nil, errAcceleratedDHTNotReady
386 }
387 } else {
388 if fr.Ready() {
389 fr.ready = true
390 fr.logger.Info("AcceleratedDHTClient: routing table ready, providing can begin")
391 } else {
392 return nil, errAcceleratedDHTNotReady
393 }
394 }
395 return fr.FullRT.GetClosestPeers(ctx, key)
396 }
397
398 var (
399 _ dhtImpl = &dht.IpfsDHT{}
400 _ dhtImpl = &fullrtRouter{}
401 )
402
403 type addrsFilter interface {
404 FilteredAddrs() []ma.Multiaddr
405 }
406
407 // findRootDatastoreSpec extracts the leaf datastore spec for the root ("/")
408 // mount from the repo's Datastore.Spec config. It unwraps mount (picks the "/"
409 // mountpoint), measure, and log wrappers to find the actual backend spec
410 // (e.g., levelds, pebbleds).
411 func findRootDatastoreSpec(spec map[string]any) map[string]any {
412 if spec == nil {
413 return nil
414 }
415 switch spec["type"] {
416 case "mount":
417 mounts, ok := spec["mounts"].([]any)
418 if !ok {
419 return spec
420 }
421 for _, m := range mounts {
422 mnt, ok := m.(map[string]any)
423 if !ok {
424 continue
425 }
426 if mnt["mountpoint"] == "/" {
427 return findRootDatastoreSpec(mnt)
428 }
429 }
430 // No root mount found; return nil so callers fall back gracefully
431 // (in-memory datastore or skip mounting) rather than passing a
432 // mount-type spec to openDatastoreAt which expects a leaf backend.
433 return nil
434 case "measure", "log":
435 if child, ok := spec["child"].(map[string]any); ok {
436 return findRootDatastoreSpec(child)
437 }
438 return spec
439 default:
440 if _, hasChild := spec["child"]; hasChild {
441 providerLog.Warnw("unrecognized datastore wrapper type, using as-is",
442 "type", spec["type"])
443 }
444 return spec
445 }
446 }
447
448 // MountKeystoreDatastores opens any provider keystore datastores that exist on
449 // disk and returns them as mount.Mount entries ready to be combined with the
450 // main repo datastore. The caller must call the returned cleanup function when
451 // done. Returns nil mounts and a no-op closer if no keystores exist.
452 func MountKeystoreDatastores(repo repo.Repo) ([]mount.Mount, func(), error) {
453 cfg, err := repo.Config()
454 if err != nil {
455 return nil, nil, fmt.Errorf("reading repo config: %w", err)
456 }
457
458 rootSpec := findRootDatastoreSpec(cfg.Datastore.Spec)
459 if rootSpec == nil {
460 return nil, func() {}, nil
461 }
462
463 keystoreBasePath := filepath.Join(repo.Path(), KeystoreDatastorePath)
464 var mounts []mount.Mount
465 var closers []func()
466
467 for _, suffix := range []string{"0", "1"} {
468 dir := filepath.Join(keystoreBasePath, suffix)
469 if _, err := os.Stat(dir); err != nil {
470 continue
471 }
472 ds, err := openDatastoreAt(rootSpec, dir)
473 if err != nil {
474 for _, c := range closers {
475 c()
476 }
477 return nil, nil, err
478 }
479 prefix := providerDatastoreKey.Child(keystoreDatastoreKey).ChildString(suffix)
480 mounts = append(mounts, mount.Mount{Prefix: prefix, Datastore: ds})
481 closers = append(closers, func() { ds.Close() })
482 }
483
484 closer := func() {
485 for _, c := range closers {
486 c()
487 }
488 }
489 return mounts, closer, nil
490 }
491
492 // openDatastoreAt opens a datastore using the given spec at the specified path.
493 // It deep-copies the spec to avoid mutating the original.
494 func openDatastoreAt(rootSpec map[string]any, path string) (datastore.Batching, error) {
495 spec := copySpec(rootSpec)
496 spec["path"] = path
497 dsc, err := fsrepo.AnyDatastoreConfig(spec)
498 if err != nil {
499 return nil, fmt.Errorf("creating datastore config for %s: %w", path, err)
500 }
501 return dsc.Create("")
502 }
503
504 // copySpec deep-copies a datastore spec map so modifications (e.g., changing
505 // the path) don't affect the original.
506 func copySpec(spec map[string]any) map[string]any {
507 if spec == nil {
508 return nil
509 }
510 cp := make(map[string]any, len(spec))
511 for k, v := range spec {
512 switch val := v.(type) {
513 case map[string]any:
514 cp[k] = copySpec(val)
515 case []any:
516 s := make([]any, len(val))
517 for i, elem := range val {
518 if m, ok := elem.(map[string]any); ok {
519 s[i] = copySpec(m)
520 } else {
521 s[i] = elem
522 }
523 }
524 cp[k] = s
525 default:
526 cp[k] = v
527 }
528 }
529 return cp
530 }
531
532 // purgeBatchSize is the number of keys deleted per batch commit during
533 // orphaned keystore cleanup. Each commit is a cancellation checkpoint.
534 const purgeBatchSize = 1 << 12 // 4096
535
536 // purgeOrphanedKeystoreData deletes all keys under /provider/keystore/ from the
537 // shared repo datastore. These were written by older Kubo versions that stored
538 // provider keystore data inline in the shared datastore. The new code uses
539 // separate filesystem datastores under <repo>/{KeystoreDatastorePath}/ instead.
540 //
541 // The operation is idempotent and safe to interrupt: partial completion is
542 // fine because already-deleted keys are no-ops on re-run.
543 func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error {
544 orphanedPrefix := providerDatastoreKey.Child(keystoreDatastoreKey).String()
545 syncKey := datastore.NewKey(orphanedPrefix)
546
547 results, err := ds.Query(ctx, query.Query{
548 Prefix: orphanedPrefix,
549 KeysOnly: true,
550 })
551 if err != nil {
552 return fmt.Errorf("querying orphaned keystore data: %w", err)
553 }
554 defer results.Close()
555
556 var batch datastore.Batch
557 var count, pending int
558 for result := range results.Next() {
559 if ctx.Err() != nil {
560 return ctx.Err()
561 }
562 if result.Error != nil {
563 return fmt.Errorf("iterating orphaned keystore data: %w", result.Error)
564 }
565 if batch == nil {
566 batch, err = ds.Batch(ctx)
567 if err != nil {
568 return fmt.Errorf("creating batch for orphaned keystore cleanup: %w", err)
569 }
570 }
571 if err := batch.Delete(ctx, datastore.NewKey(result.Key)); err != nil {
572 return fmt.Errorf("batch deleting orphaned key %s: %w", result.Key, err)
573 }
574 count++
575 pending++
576 if pending >= purgeBatchSize {
577 if err := batch.Commit(ctx); err != nil {
578 return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err)
579 }
580 if err := ds.Sync(ctx, syncKey); err != nil {
581 return fmt.Errorf("syncing orphaned keystore cleanup: %w", err)
582 }
583 batch = nil
584 pending = 0
585 }
586 }
587 if pending > 0 {
588 if err := batch.Commit(ctx); err != nil {
589 return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err)
590 }
591 if err := ds.Sync(ctx, syncKey); err != nil {
592 return fmt.Errorf("syncing orphaned keystore cleanup: %w", err)
593 }
594 }
595 if count > 0 {
596 providerLog.Infow("purged orphaned provider keystore data from shared datastore", "keys", count)
597 }
598 return nil
599 }
600
601 func SweepingProviderOpt(cfg *config.Config) fx.Option {
602 reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval)
603 // noScheduleMode is true when the user disabled the periodic reprovide
604 // schedule (Provide.DHT.Interval=0). In this mode the keystore is
605 // inert: kad-dht's burst-only path (ProvideOnce, StartProviding) does
606 // not Put or Delete keys, and no reprovide loop runs to read them.
607 noScheduleMode := reprovideInterval == 0
608 type providerInput struct {
609 fx.In
610 DHT routing.Routing `name:"dhtc"`
611 Repo repo.Repo
612 Lc fx.Lifecycle
613 }
614 sweepingReprovider := fx.Provide(func(in providerInput) (DHTProvider, *keystore.ResettableKeystore, error) {
615 ds := namespace.Wrap(in.Repo.Datastore(), providerDatastoreKey)
616
617 // Get repo path and config to determine datastore type
618 repoPath := in.Repo.Path()
619 repoCfg, err := in.Repo.Config()
620 if err != nil {
621 return nil, nil, fmt.Errorf("getting repo config: %w", err)
622 }
623
624 // Find the root datastore type (levelds, pebbleds, etc.)
625 rootSpec := findRootDatastoreSpec(repoCfg.Datastore.Spec)
626
627 // Keystore datastores live at <repo>/{KeystoreDatastorePath}/<suffix>
628 keystoreBasePath := filepath.Join(repoPath, KeystoreDatastorePath)
629
630 createDs := func(suffix string) (datastore.Batching, error) {
631 if err := validateKeystoreSuffix(suffix); err != nil {
632 return nil, err
633 }
634 // In-memory datastore in no-schedule mode (keystore is inert)
635 // or when no datastore spec is configured (test/mock repos).
636 if noScheduleMode || rootSpec == nil {
637 return datastore.NewMapDatastore(), nil
638 }
639 if err := os.MkdirAll(keystoreBasePath, 0o755); err != nil {
640 return nil, fmt.Errorf("creating keystore base directory: %w", err)
641 }
642 ds, err := openDatastoreAt(rootSpec, filepath.Join(keystoreBasePath, suffix))
643 if err != nil {
644 return nil, err
645 }
646 providerLog.Infow("provider keystore: opened datastore", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix))
647 return ds, nil
648 }
649
650 destroyDs := func(suffix string) error {
651 if err := validateKeystoreSuffix(suffix); err != nil {
652 return err
653 }
654 if noScheduleMode {
655 return nil
656 }
657 providerLog.Infow("provider keystore: removing datastore from disk", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix))
658 return os.RemoveAll(filepath.Join(keystoreBasePath, suffix))
659 }
660
661 // In no-schedule mode the on-disk keystore is never used. If a
662 // previous run was in schedule mode it may have left data behind;
663 // purge it once on startup to free disk.
664 if noScheduleMode {
665 if _, statErr := os.Stat(keystoreBasePath); statErr == nil {
666 providerLog.Infow("provider keystore: purging on-disk data (Provide.DHT.Interval=0)", "path", keystoreBasePath)
667 if rmErr := os.RemoveAll(keystoreBasePath); rmErr != nil {
668 providerLog.Warnw("provider keystore: purge failed", "path", keystoreBasePath, "err", rmErr)
669 }
670 }
671 }
672
673 // One-time cleanup of stale keystore data left by older Kubo in the
674 // shared repo datastore under /provider/keystore/. New code stores
675 // bulk key data in separate filesystem datastores under
676 // <repo>/{KeystoreDatastorePath}/ while still using the same
677 // /provider/keystore/ namespace in the shared datastore for metadata.
678 //
679 // The absence of the keystoreBasePath directory signals a first run
680 // after upgrade: the directory is created later by createDs on first
681 // use, so it doubles as a "cleanup done" flag. If the process dies
682 // mid-purge the directory still won't exist and the cleanup re-runs
683 // on next start (it is idempotent). Must run synchronously before
684 // NewResettableKeystore to avoid racing with reads on the same
685 // namespace.
686 if _, statErr := os.Stat(keystoreBasePath); os.IsNotExist(statErr) {
687 providerLog.Infow("migrating provider keystore data from shared datastore to separate filesystem datastores", "path", keystoreBasePath)
688 // Create a cancellable context for the purge. The OnStop hook
689 // below calls purgeCancel when the node receives a shutdown
690 // signal (e.g., SIGINT), which interrupts the purge loop
691 // instead of blocking indefinitely.
692 purgeCtx, purgeCancel := context.WithCancel(context.Background())
693 in.Lc.Append(fx.Hook{
694 OnStop: func(_ context.Context) error {
695 purgeCancel()
696 return nil
697 },
698 })
699 if purgeErr := purgeOrphanedKeystoreData(purgeCtx, in.Repo.Datastore()); purgeErr != nil {
700 if purgeCtx.Err() != nil {
701 providerLog.Infow("provider keystore migration interrupted by shutdown, will resume on next start")
702 } else {
703 providerLog.Warnw("provider keystore migration failed, will retry on next start", "error", purgeErr)
704 }
705 } else {
706 providerLog.Infow("provider keystore migration completed")
707 }
708 purgeCancel()
709 }
710
711 keystoreDs := namespace.Wrap(ds, keystoreDatastoreKey)
712 ks, err := keystore.NewResettableKeystore(keystoreDs,
713 keystore.WithDatastoreFactory(createDs, destroyDs),
714 keystore.KeystoreOption(
715 keystore.WithPrefixBits(16),
716 keystore.WithBatchSize(int(cfg.Provide.DHT.KeystoreBatchSize.WithDefault(config.DefaultProvideDHTKeystoreBatchSize))),
717 ),
718 )
719 if err != nil {
720 return nil, nil, err
721 }
722 // Constants for buffered provider configuration
723 // These values match the upstream defaults from go-libp2p-kad-dht and have been battle-tested
724 const (
725 // bufferedDsName is the datastore namespace used by the buffered provider.
726 // The dsqueue persists operations here to handle large data additions without
727 // being memory-bound, allowing operations on hardware with limited RAM and
728 // enabling core operations to return instantly while processing happens async.
729 bufferedDsName = "bprov"
730
731 // bufferedBatchSize controls how many operations are dequeued and processed
732 // together from the datastore queue. The worker processes up to this many
733 // operations at once, grouping them by type for efficiency.
734 bufferedBatchSize = 1 << 10 // 1024 items
735
736 // bufferedIdleWriteTime is an implementation detail of go-dsqueue that controls
737 // how long the datastore buffer waits for new multihashes to arrive before
738 // flushing in-memory items to the datastore. This does NOT affect providing speed -
739 // provides happen as fast as possible via a dedicated worker that continuously
740 // processes the queue regardless of this timing.
741 bufferedIdleWriteTime = time.Minute
742
743 // loggerName is the name of the go-log logger used by the provider.
744 loggerName = dhtprovider.DefaultLoggerName
745 )
746
747 bufferedProviderOpts := []buffered.Option{
748 buffered.WithBatchSize(bufferedBatchSize),
749 buffered.WithDsName(bufferedDsName),
750 buffered.WithIdleWriteTime(bufferedIdleWriteTime),
751 }
752 var impl dhtImpl
753 switch inDht := in.DHT.(type) {
754 case *dht.IpfsDHT:
755 if inDht != nil {
756 impl = inDht
757 }
758 case *dual.DHT:
759 if inDht != nil {
760 prov, err := ddhtprovider.New(inDht,
761 ddhtprovider.WithKeystore(ks),
762 ddhtprovider.WithDatastore(ds),
763 ddhtprovider.WithResumeCycle(cfg.Provide.DHT.ResumeEnabled.WithDefault(config.DefaultProvideDHTResumeEnabled)),
764
765 ddhtprovider.WithReprovideInterval(reprovideInterval),
766 ddhtprovider.WithMaxReprovideDelay(time.Hour),
767 ddhtprovider.WithOfflineDelay(cfg.Provide.DHT.OfflineDelay.WithDefault(config.DefaultProvideDHTOfflineDelay)),
768 ddhtprovider.WithConnectivityCheckOnlineInterval(1*time.Minute),
769 ddhtprovider.WithSendProviderRecordTimeout(cfg.Provide.DHT.SendProviderRecordTimeout.WithDefault(config.DefaultProvideDHTSendProviderRecordTimeout)),
770
771 ddhtprovider.WithMaxWorkers(int(cfg.Provide.DHT.MaxWorkers.WithDefault(config.DefaultProvideDHTMaxWorkers))),
772 ddhtprovider.WithDedicatedPeriodicWorkers(int(cfg.Provide.DHT.DedicatedPeriodicWorkers.WithDefault(config.DefaultProvideDHTDedicatedPeriodicWorkers))),
773 ddhtprovider.WithDedicatedBurstWorkers(int(cfg.Provide.DHT.DedicatedBurstWorkers.WithDefault(config.DefaultProvideDHTDedicatedBurstWorkers))),
774 ddhtprovider.WithMaxProvideConnsPerWorker(int(cfg.Provide.DHT.MaxProvideConnsPerWorker.WithDefault(config.DefaultProvideDHTMaxProvideConnsPerWorker))),
775
776 ddhtprovider.WithLoggerName(loggerName),
777 )
778 if err != nil {
779 return nil, nil, err
780 }
781 return buffered.New(prov, ds, bufferedProviderOpts...), ks, nil
782 }
783 case *fullrt.FullRT:
784 if inDht != nil {
785 impl = newFullRTRouter(inDht, loggerName)
786 }
787 }
788 if impl == nil {
789 return &NoopProvider{}, nil, nil
790 }
791
792 var selfAddrsFunc func() []ma.Multiaddr
793 if imlpFilter, ok := impl.(addrsFilter); ok {
794 selfAddrsFunc = imlpFilter.FilteredAddrs
795 } else {
796 selfAddrsFunc = func() []ma.Multiaddr { return impl.Host().Addrs() }
797 }
798 opts := []dhtprovider.Option{
799 dhtprovider.WithKeystore(ks),
800 dhtprovider.WithDatastore(ds),
801 dhtprovider.WithResumeCycle(cfg.Provide.DHT.ResumeEnabled.WithDefault(config.DefaultProvideDHTResumeEnabled)),
802 dhtprovider.WithHost(impl.Host()),
803 dhtprovider.WithRouter(impl),
804 dhtprovider.WithMessageSender(impl.MessageSender()),
805 dhtprovider.WithSelfAddrs(selfAddrsFunc),
806 dhtprovider.WithAddLocalRecord(func(h mh.Multihash) error {
807 return impl.Provide(context.Background(), cid.NewCidV1(cid.Raw, h), false)
808 }),
809
810 dhtprovider.WithReplicationFactor(amino.DefaultBucketSize),
811 dhtprovider.WithReprovideInterval(reprovideInterval),
812 dhtprovider.WithMaxReprovideDelay(time.Hour),
813 dhtprovider.WithOfflineDelay(cfg.Provide.DHT.OfflineDelay.WithDefault(config.DefaultProvideDHTOfflineDelay)),
814 dhtprovider.WithConnectivityCheckOnlineInterval(1 * time.Minute),
815 dhtprovider.WithSendProviderRecordTimeout(cfg.Provide.DHT.SendProviderRecordTimeout.WithDefault(config.DefaultProvideDHTSendProviderRecordTimeout)),
816
817 dhtprovider.WithMaxWorkers(int(cfg.Provide.DHT.MaxWorkers.WithDefault(config.DefaultProvideDHTMaxWorkers))),
818 dhtprovider.WithDedicatedPeriodicWorkers(int(cfg.Provide.DHT.DedicatedPeriodicWorkers.WithDefault(config.DefaultProvideDHTDedicatedPeriodicWorkers))),
819 dhtprovider.WithDedicatedBurstWorkers(int(cfg.Provide.DHT.DedicatedBurstWorkers.WithDefault(config.DefaultProvideDHTDedicatedBurstWorkers))),
820 dhtprovider.WithMaxProvideConnsPerWorker(int(cfg.Provide.DHT.MaxProvideConnsPerWorker.WithDefault(config.DefaultProvideDHTMaxProvideConnsPerWorker))),
821
822 dhtprovider.WithLoggerName(loggerName),
823 }
824
825 prov, err := dhtprovider.New(opts...)
826 if err != nil {
827 return nil, nil, err
828 }
829 return buffered.New(prov, ds, bufferedProviderOpts...), ks, nil
830 })
831
832 type keystoreInput struct {
833 fx.In
834 Provider DHTProvider
835 Keystore *keystore.ResettableKeystore
836 KeyProvider provider.KeyChanFunc
837 }
838 initKeystore := fx.Invoke(func(lc fx.Lifecycle, in keystoreInput) {
839 // Skip keystore initialization for NoopProvider
840 if _, ok := in.Provider.(*NoopProvider); ok {
841 return
842 }
843 // In no-schedule mode no reprovide loop runs, so there is no
844 // reader for the keystore and no need to sync it. The zero
845 // interval would also panic the periodic sync ticker.
846 if noScheduleMode {
847 return
848 }
849
850 var (
851 cancel context.CancelFunc
852 done = make(chan struct{})
853 )
854
855 syncKeystore := func(ctx context.Context) error {
856 kcf, err := in.KeyProvider(ctx)
857 if err != nil {
858 return err
859 }
860 if err := in.Keystore.ResetCids(ctx, kcf); err != nil {
861 return err
862 }
863 if err := in.Provider.RefreshSchedule(); err != nil {
864 providerLog.Infow("refreshing provider schedule", "err", err)
865 }
866 return nil
867 }
868
869 lc.Append(fx.Hook{
870 OnStart: func(ctx context.Context) error {
871 // Set the KeyProvider as a garbage collection function for the
872 // keystore. Periodically purge the Keystore from all its keys and
873 // replace them with the keys that needs to be reprovided, coming from
874 // the KeyChanFunc. So far, this is the less worse way to remove CIDs
875 // that shouldn't be reprovided from the provider's state.
876 go func() {
877 // Sync the keystore once at startup. This operation is async since
878 // we need to walk the DAG of objects matching the provide strategy,
879 // which can take a while.
880 strategy := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
881 providerLog.Infow("provider keystore sync started", "strategy", strategy)
882 if err := syncKeystore(ctx); err != nil {
883 // Shutdown can race ahead of ctx.Err() becoming
884 // visible here: ResetCids returns ctx.Err()
885 // straight from its own ctx-done select, and the
886 // keystore can also close mid-sync (ErrClosed)
887 // before the OnStart ctx is cancelled. Classify
888 // both as shutdown.
889 if ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, keystore.ErrClosed) {
890 providerLog.Debugw("provider keystore sync interrupted by shutdown", "err", err, "strategy", strategy)
891 } else {
892 providerLog.Errorw("provider keystore sync failed", "err", err, "strategy", strategy)
893 }
894 return
895 }
896 providerLog.Infow("provider keystore sync completed", "strategy", strategy)
897 }()
898
899 gcCtx, c := context.WithCancel(context.Background())
900 cancel = c
901
902 go func() { // garbage collection loop for cids to reprovide
903 defer close(done)
904 ticker := time.NewTicker(reprovideInterval)
905 defer ticker.Stop()
906
907 for {
908 select {
909 case <-gcCtx.Done():
910 return
911 case <-ticker.C:
912 if err := syncKeystore(gcCtx); err != nil {
913 // See classifier note on the startup-sync
914 // branch above: context.Canceled can
915 // arrive ahead of gcCtx.Err() becoming
916 // visible to this goroutine.
917 if gcCtx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, keystore.ErrClosed) {
918 providerLog.Debugw("provider keystore sync interrupted by shutdown", "err", err)
919 } else {
920 providerLog.Errorw("provider keystore sync failed", "err", err)
921 }
922 }
923 }
924 }
925 }()
926 return nil
927 },
928 OnStop: func(ctx context.Context) error {
929 if cancel != nil {
930 cancel()
931 }
932 select {
933 case <-done:
934 case <-ctx.Done():
935 return ctx.Err()
936 }
937 // Keystore will be closed by ensureProviderClosesBeforeKeystore hook
938 // to guarantee provider closes before keystore.
939 return nil
940 },
941 })
942 })
943
944 // ensureProviderClosesBeforeKeystore manages the shutdown order between
945 // provider and keystore to prevent race conditions.
946 //
947 // The provider's worker goroutines may call keystore methods during their
948 // operation. If keystore closes while these operations are in-flight, we get
949 // "keystore is closed" errors. By closing the provider first, we ensure all
950 // worker goroutines exit and complete any pending keystore operations before
951 // the keystore itself closes.
952 type providerKeystoreShutdownInput struct {
953 fx.In
954 Provider DHTProvider
955 Keystore *keystore.ResettableKeystore
956 }
957 ensureProviderClosesBeforeKeystore := fx.Invoke(func(lc fx.Lifecycle, in providerKeystoreShutdownInput) {
958 // Skip for NoopProvider
959 if _, ok := in.Provider.(*NoopProvider); ok {
960 return
961 }
962
963 lc.Append(fx.Hook{
964 OnStop: func(ctx context.Context) error {
965 // Close provider first; waits for all worker goroutines
966 // to exit so nothing can access the keystore after this
967 // returns. If ctx fires before provider drains, the
968 // keystore close below sees an expired ctx and returns
969 // immediately; the watchdog is the ultimate backstop.
970 if err := shutdown.CloseWithCtx(ctx, "dht-provider", in.Provider.Close); err != nil {
971 providerLog.Errorw("error closing provider during shutdown", "error", err)
972 }
973 return shutdown.CloseWithCtx(ctx, "keystore", in.Keystore.Close)
974 },
975 })
976 })
977
978 // extractSweepingProvider extracts a SweepingProvider from the given provider interface.
979 // It handles unwrapping buffered and dual providers, always selecting WAN for dual DHT.
980 // Returns nil if the provider is not a sweeping provider type.
981 var extractSweepingProvider func(prov any) *dhtprovider.SweepingProvider
982 extractSweepingProvider = func(prov any) *dhtprovider.SweepingProvider {
983 switch p := prov.(type) {
984 case *dhtprovider.SweepingProvider:
985 return p
986 case *ddhtprovider.SweepingProvider:
987 return p.WAN
988 case *buffered.SweepingProvider:
989 // Recursively extract from the inner provider
990 return extractSweepingProvider(p.Provider)
991 default:
992 return nil
993 }
994 }
995
996 type alertInput struct {
997 fx.In
998 Provider DHTProvider
999 }
1000 reprovideAlert := fx.Invoke(func(lc fx.Lifecycle, in alertInput) {
1001 prov := extractSweepingProvider(in.Provider)
1002 if prov == nil {
1003 return
1004 }
1005
1006 var (
1007 cancel context.CancelFunc
1008 done = make(chan struct{})
1009 )
1010
1011 lc.Append(fx.Hook{
1012 OnStart: func(ctx context.Context) error {
1013 gcCtx, c := context.WithCancel(context.Background())
1014 cancel = c
1015 go func() {
1016 defer close(done)
1017
1018 ticker := time.NewTicker(reprovideAlertPollInterval)
1019 defer ticker.Stop()
1020
1021 var (
1022 queueSize, prevQueueSize int64
1023 queuedWorkers, prevQueuedWorkers bool
1024 count int
1025 )
1026
1027 for {
1028 select {
1029 case <-gcCtx.Done():
1030 return
1031 case <-ticker.C:
1032 }
1033
1034 statsCtx, statsCancel := context.WithTimeout(gcCtx, time.Minute)
1035 stats, err := prov.Stats(statsCtx)
1036 statsCancel()
1037 if err != nil {
1038 if gcCtx.Err() != nil {
1039 return
1040 }
1041 providerLog.Debugw("provider stats unavailable for reprovide alert", "err", err)
1042 continue
1043 }
1044 queuedWorkers = stats.Workers.QueuedPeriodic > 0
1045 queueSize = int64(stats.Queues.PendingRegionReprovides)
1046
1047 // Alert if reprovide queue keeps growing and all periodic workers are busy.
1048 // Requires consecutiveAlertsThreshold intervals of sustained growth.
1049 if prevQueuedWorkers && queuedWorkers && queueSize > prevQueueSize {
1050 count++
1051 if count >= consecutiveAlertsThreshold {
1052 providerLog.Errorf(`
1053 🔔🔔🔔 Reprovide Operations Too Slow 🔔🔔🔔
1054
1055 Your node is falling behind on DHT reprovides, which will affect content availability.
1056
1057 Keyspace regions enqueued for reprovide:
1058 %s ago:\t%d
1059 Now:\t%d
1060
1061 All periodic workers are busy!
1062 Active workers:\t%d / %d (max)
1063 Active workers types:\t%d periodic, %d burst
1064 Dedicated workers:\t%d periodic, %d burst
1065
1066 Solutions (try in order):
1067 1. Increase Provide.DHT.MaxWorkers (current %d)
1068 2. Increase Provide.DHT.DedicatedPeriodicWorkers (current %d)
1069 3. Set Provide.DHT.SweepEnabled=false and Routing.AcceleratedDHTClient=true (last resort, not recommended)
1070
1071 See how the reprovide queue is processed in real-time with 'watch ipfs provide stat --all --compact'
1072
1073 See docs: https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtmaxworkers`,
1074 reprovideAlertPollInterval.Truncate(time.Minute).String(), prevQueueSize, queueSize,
1075 stats.Workers.Active, stats.Workers.Max,
1076 stats.Workers.ActivePeriodic, stats.Workers.ActiveBurst,
1077 stats.Workers.DedicatedPeriodic, stats.Workers.DedicatedBurst,
1078 stats.Workers.Max, stats.Workers.DedicatedPeriodic)
1079 }
1080 } else if !queuedWorkers {
1081 count = 0
1082 }
1083
1084 prevQueueSize, prevQueuedWorkers = queueSize, queuedWorkers
1085 }
1086 }()
1087 return nil
1088 },
1089 OnStop: func(ctx context.Context) error {
1090 // Cancel the alert loop
1091 if cancel != nil {
1092 cancel()
1093 }
1094 select {
1095 case <-done:
1096 case <-ctx.Done():
1097 return ctx.Err()
1098 }
1099 return nil
1100 },
1101 })
1102 })
1103
1104 return fx.Options(
1105 sweepingReprovider,
1106 initKeystore,
1107 ensureProviderClosesBeforeKeystore,
1108 reprovideAlert,
1109 )
1110 }
1111
1112 // ONLINE/OFFLINE
1113
1114 // hasDHTRouting checks if the routing configuration includes a DHT component.
1115 // Returns false for HTTP-only custom routing configurations (e.g., Routing.Type="custom"
1116 // with only HTTP routers). This is used to determine whether SweepingProviderOpt
1117 // can be used, since it requires a DHT client.
1118 func hasDHTRouting(cfg *config.Config) bool {
1119 routingType := cfg.Routing.Type.WithDefault(config.DefaultRoutingType)
1120 switch routingType {
1121 case "auto", "autoclient", "dht", "dhtclient", "dhtserver":
1122 return true
1123 case "custom":
1124 // Check if any router in custom config is DHT-based
1125 for _, router := range cfg.Routing.Routers {
1126 if routerIncludesDHT(router, cfg) {
1127 return true
1128 }
1129 }
1130 return false
1131 default: // "none", "delegated"
1132 return false
1133 }
1134 }
1135
1136 // routerIncludesDHT recursively checks if a router configuration includes DHT.
1137 // Handles parallel and sequential composite routers by checking their children.
1138 func routerIncludesDHT(rp config.RouterParser, cfg *config.Config) bool {
1139 switch rp.Type {
1140 case config.RouterTypeDHT:
1141 return true
1142 case config.RouterTypeParallel, config.RouterTypeSequential:
1143 if children, ok := rp.Parameters.(*config.ComposableRouterParams); ok {
1144 for _, child := range children.Routers {
1145 if childRouter, exists := cfg.Routing.Routers[child.RouterName]; exists {
1146 if routerIncludesDHT(childRouter, cfg) {
1147 return true
1148 }
1149 }
1150 }
1151 }
1152 }
1153 return false
1154 }
1155
1156 // OnlineProviders groups units managing provide routing records online
1157 func OnlineProviders(provide bool, cfg *config.Config) fx.Option {
1158 if !provide {
1159 return OfflineProviders()
1160 }
1161
1162 providerStrategy := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
1163
1164 if _, err := config.ParseProvideStrategy(providerStrategy); err != nil {
1165 return fx.Error(fmt.Errorf("provider: %w", err))
1166 }
1167
1168 bloomFPRate := uint(cfg.Provide.BloomFPRate.WithDefault(config.DefaultProvideBloomFPRate))
1169
1170 opts := []fx.Option{
1171 fx.Provide(setReproviderKeyProvider(providerStrategy, bloomFPRate)),
1172 }
1173
1174 sweepEnabled := cfg.Provide.DHT.SweepEnabled.WithDefault(config.DefaultProvideDHTSweepEnabled)
1175 dhtAvailable := hasDHTRouting(cfg)
1176
1177 // Use SweepingProvider only when both sweep is enabled AND DHT is available.
1178 // For HTTP-only routing (e.g., Routing.Type="custom" with only HTTP routers),
1179 // fall back to LegacyProvider which works with ProvideManyRouter.
1180 // See https://github.com/ipfs/kubo/issues/11089
1181 if sweepEnabled && dhtAvailable {
1182 opts = append(opts, SweepingProviderOpt(cfg))
1183 } else {
1184 reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval)
1185 acceleratedDHTClient := cfg.Routing.AcceleratedDHTClient.WithDefault(config.DefaultAcceleratedDHTClient)
1186 provideWorkerCount := int(cfg.Provide.DHT.MaxWorkers.WithDefault(config.DefaultProvideDHTMaxWorkers))
1187
1188 opts = append(opts, LegacyProviderOpt(reprovideInterval, providerStrategy, acceleratedDHTClient, provideWorkerCount))
1189 }
1190
1191 return fx.Options(opts...)
1192 }
1193
1194 // OfflineProviders groups units managing provide routing records offline
1195 func OfflineProviders() fx.Option {
1196 return fx.Provide(func() DHTProvider {
1197 return &NoopProvider{}
1198 })
1199 }
1200
1201 func mfsProvider(mfsRoot *mfs.Root, fetcher fetcher.Factory) provider.KeyChanFunc {
1202 return func(ctx context.Context) (<-chan cid.Cid, error) {
1203 err := mfsRoot.FlushMemFree(ctx)
1204 if err != nil {
1205 return nil, fmt.Errorf("provider: error flushing MFS, cannot provide MFS: %w", err)
1206 }
1207 rootNode, err := mfsRoot.GetDirectory().GetNode()
1208 if err != nil {
1209 return nil, fmt.Errorf("provider: error loading MFS root, cannot provide MFS: %w", err)
1210 }
1211
1212 kcf := provider.NewDAGProvider(rootNode.Cid(), fetcher)
1213 return kcf(ctx)
1214 }
1215 }
1216
1217 type provStrategyIn struct {
1218 fx.In
1219 Pinner pin.Pinner
1220 Blockstore blockstore.Blockstore
1221 OfflineIPLDFetcher fetcher.Factory `name:"offlineIpldFetcher"`
1222 OfflineUnixFSFetcher fetcher.Factory `name:"offlineUnixfsFetcher"`
1223 MFSRoot *mfs.Root
1224 Repo repo.Repo
1225 }
1226
1227 type provStrategyOut struct {
1228 fx.Out
1229 ProvidingStrategy config.ProvideStrategy
1230 ProvidingKeyChanFunc provider.KeyChanFunc
1231 }
1232
1233 // readLastUniqueCount reads the persisted unique CID count from the
1234 // previous +unique reprovide cycle. Returns 0 if not found or corrupt.
1235 func readLastUniqueCount(ds datastore.Datastore) uint64 {
1236 val, err := ds.Get(context.Background(), datastore.NewKey(reprovideLastUniqueCountKey))
1237 if err != nil {
1238 return 0
1239 }
1240 if len(val) != 8 {
1241 return 0
1242 }
1243 return binary.BigEndian.Uint64(val)
1244 }
1245
1246 // persistUniqueCount stores the unique CID count for the next cycle.
1247 func persistUniqueCount(ds datastore.Datastore, count uint64) {
1248 buf := make([]byte, 8)
1249 binary.BigEndian.PutUint64(buf, count)
1250 if err := ds.Put(context.Background(), datastore.NewKey(reprovideLastUniqueCountKey), buf); err != nil {
1251 providerLog.Errorf("failed to persist unique count: %s", err)
1252 }
1253 }
1254
1255 // walkFunc abstracts a DAG walk (WalkDAG or WalkEntityRoots) so the
1256 // MFS provider can be parameterized without duplicating the
1257 // flush+walk+channel boilerplate.
1258 type walkFunc func(ctx context.Context, root cid.Cid, emit func(cid.Cid) bool, opts ...walker.Option) error
1259
1260 // uniqueMFSProvider is the +unique counterpart of mfsProvider. It
1261 // flushes the MFS root, then walks the MFS DAG with a shared
1262 // VisitedTracker and a locality check (blockstore.Has) so only
1263 // locally-present blocks are emitted.
1264 func uniqueMFSProvider(mfsRoot *mfs.Root, bs blockstore.Blockstore, tracker walker.VisitedTracker) provider.KeyChanFunc {
1265 walk := func(ctx context.Context, root cid.Cid, emit func(cid.Cid) bool, opts ...walker.Option) error {
1266 return walker.WalkDAG(ctx, root, walker.LinksFetcherFromBlockstore(bs), emit, opts...)
1267 }
1268 return mfsWalkProvider(mfsRoot, bs, tracker, walk)
1269 }
1270
1271 // mfsEntityRootsProvider is the +entities counterpart. It walks with
1272 // WalkEntityRoots, emitting only entity roots and skipping file chunks.
1273 func mfsEntityRootsProvider(mfsRoot *mfs.Root, bs blockstore.Blockstore, tracker walker.VisitedTracker) provider.KeyChanFunc {
1274 walk := func(ctx context.Context, root cid.Cid, emit func(cid.Cid) bool, opts ...walker.Option) error {
1275 return walker.WalkEntityRoots(ctx, root, walker.NodeFetcherFromBlockstore(bs), emit, opts...)
1276 }
1277 return mfsWalkProvider(mfsRoot, bs, tracker, walk)
1278 }
1279
1280 // mfsWalkProvider builds a KeyChanFunc that flushes MFS, then walks
1281 // with the given walkFunc using a shared tracker and locality check.
1282 func mfsWalkProvider(mfsRoot *mfs.Root, bs blockstore.Blockstore, tracker walker.VisitedTracker, walk walkFunc) provider.KeyChanFunc {
1283 return func(ctx context.Context) (<-chan cid.Cid, error) {
1284 if err := mfsRoot.FlushMemFree(ctx); err != nil {
1285 return nil, fmt.Errorf("provider: error flushing MFS: %w", err)
1286 }
1287 rootNode, err := mfsRoot.GetDirectory().GetNode()
1288 if err != nil {
1289 return nil, fmt.Errorf("provider: error loading MFS root: %w", err)
1290 }
1291
1292 ch := make(chan cid.Cid)
1293 go func() {
1294 defer close(ch)
1295 locality := func(ctx context.Context, c cid.Cid) (bool, error) {
1296 return bs.Has(ctx, c)
1297 }
1298 _ = walk(ctx, rootNode.Cid(), func(c cid.Cid) bool {
1299 select {
1300 case ch <- c:
1301 return true
1302 case <-ctx.Done():
1303 return false
1304 }
1305 }, walker.WithVisitedTracker(tracker), walker.WithLocality(locality))
1306 }()
1307 return ch, nil
1308 }
1309 }
1310
1311 // createKeyProvider creates the appropriate KeyChanFunc based on strategy.
1312 // fpRate is the bloom filter target false-positive rate (1/N) used by
1313 // +unique and +entities cycles. Ignored by other strategies.
1314 func createKeyProvider(strategyFlag config.ProvideStrategy, fpRate uint, in provStrategyIn) provider.KeyChanFunc {
1315 // +unique modifier: use bloom filter cross-DAG dedup
1316 useUnique := strategyFlag&config.ProvideStrategyUnique != 0
1317 if useUnique {
1318 basePinned := strategyFlag&config.ProvideStrategyPinned != 0
1319 baseMFS := strategyFlag&config.ProvideStrategyMFS != 0
1320 ds := in.Repo.Datastore()
1321
1322 // return a KeyChanFunc that creates a fresh bloom each cycle
1323 return func(ctx context.Context) (<-chan cid.Cid, error) {
1324 count := readLastUniqueCount(ds)
1325 // size the bloom from the previous cycle's count (with growth
1326 // margin for repo changes between cycles), falling back to
1327 // DefaultBloomInitialCapacity on the very first cycle. The
1328 // bloom chain auto-grows if the repo exceeds this estimate.
1329 expectedItems := max(
1330 uint64(walker.DefaultBloomInitialCapacity),
1331 uint64(float64(count)*walker.BloomGrowthMargin),
1332 )
1333 // the tracker is shared across all sub-walks (MFS, recursive
1334 // pins, direct pins) within a single reprovide cycle. it
1335 // detects duplicate sub-DAG branches across recursive pins
1336 // that share content (e.g. append-only datasets where each
1337 // version differs by a small delta). when a CID is already
1338 // in the bloom, its entire subtree is skipped, reducing
1339 // traversal from O(pins * total_blocks) to O(unique_blocks).
1340 tracker, err := walker.NewBloomTracker(uint(expectedItems), fpRate)
1341 if err != nil {
1342 return nil, fmt.Errorf("bloom tracker: %w", err)
1343 }
1344
1345 useEntities := strategyFlag&config.ProvideStrategyEntities != 0
1346
1347 // select provider functions based on +entities modifier:
1348 // +entities uses WalkEntityRoots (skips file chunks),
1349 // +unique without +entities uses WalkDAG (all blocks).
1350 makePinProv := dspinner.NewUniquePinnedProvider
1351 makeMFSProv := uniqueMFSProvider
1352 if useEntities {
1353 makePinProv = dspinner.NewPinnedEntityRootsProvider
1354 makeMFSProv = mfsEntityRootsProvider
1355 }
1356
1357 var inner provider.KeyChanFunc
1358 switch {
1359 case basePinned && baseMFS:
1360 // MFS first: walk MFS (locality-filtered), then pinned.
1361 // NewConcatProvider (not NewPrioritizedProvider) because
1362 // the shared bloom tracker already guarantees each CID
1363 // is emitted at most once -- no need for a second dedup
1364 // layer. NewBufferedProvider decouples the pinned
1365 // provider so the pinner lock is released promptly.
1366 inner = provider.NewConcatProvider(
1367 makeMFSProv(in.MFSRoot, in.Blockstore, tracker),
1368 provider.NewBufferedProvider(
1369 makePinProv(in.Pinner, in.Blockstore, tracker)),
1370 )
1371 case basePinned:
1372 inner = provider.NewBufferedProvider(
1373 makePinProv(in.Pinner, in.Blockstore, tracker))
1374 case baseMFS:
1375 inner = makeMFSProv(in.MFSRoot, in.Blockstore, tracker)
1376 default:
1377 return nil, fmt.Errorf("provider: +unique requires pinned and/or mfs")
1378 }
1379
1380 // wrap inner channel to persist bloom count on successful close
1381 innerCh, err := inner(ctx)
1382 if err != nil {
1383 return nil, err
1384 }
1385
1386 ch := make(chan cid.Cid)
1387 go func() {
1388 defer func() {
1389 if ctx.Err() == nil {
1390 persistUniqueCount(ds, tracker.Count())
1391 }
1392 providerLog.Infow("unique reprovide cycle finished",
1393 "providedCIDs", tracker.Count(),
1394 "skippedBranches", tracker.Deduplicated())
1395 close(ch)
1396 }()
1397 for c := range innerCh {
1398 select {
1399 case ch <- c:
1400 case <-ctx.Done():
1401 return
1402 }
1403 }
1404 }()
1405
1406 providerLog.Infow("unique reprovide cycle started",
1407 "expectedItems", expectedItems,
1408 "previousCount", count,
1409 )
1410 return ch, nil
1411 }
1412 }
1413
1414 // non-unique strategies (unchanged)
1415 switch strategyFlag {
1416 case config.ProvideStrategyRoots:
1417 return provider.NewBufferedProvider(dspinner.NewPinnedProvider(true, in.Pinner, in.OfflineIPLDFetcher))
1418 case config.ProvideStrategyPinned:
1419 return provider.NewBufferedProvider(dspinner.NewPinnedProvider(false, in.Pinner, in.OfflineIPLDFetcher))
1420 case config.ProvideStrategyPinned | config.ProvideStrategyMFS:
1421 return provider.NewPrioritizedProvider(
1422 provider.NewBufferedProvider(dspinner.NewPinnedProvider(false, in.Pinner, in.OfflineIPLDFetcher)),
1423 mfsProvider(in.MFSRoot, in.OfflineUnixFSFetcher),
1424 )
1425 case config.ProvideStrategyMFS:
1426 return mfsProvider(in.MFSRoot, in.OfflineUnixFSFetcher)
1427 default: // "all", "", "flat" (compat)
1428 return in.Blockstore.AllKeysChan
1429 }
1430 }
1431
1432 // detectStrategyChange checks if the reproviding strategy has changed from what's persisted.
1433 // Returns: (previousStrategy, hasChanged, error)
1434 func detectStrategyChange(ctx context.Context, strategy string, ds datastore.Datastore) (string, bool, error) {
1435 strategyKey := datastore.NewKey(reprovideStrategyKey)
1436
1437 prev, err := ds.Get(ctx, strategyKey)
1438 if err != nil {
1439 if errors.Is(err, datastore.ErrNotFound) {
1440 return "", strategy != "", nil
1441 }
1442 return "", false, err
1443 }
1444
1445 previousStrategy := string(prev)
1446 return previousStrategy, previousStrategy != strategy, nil
1447 }
1448
1449 // persistStrategy saves the current reproviding strategy to the datastore.
1450 // Empty string strategies are deleted rather than stored.
1451 func persistStrategy(ctx context.Context, strategy string, ds datastore.Datastore) error {
1452 strategyKey := datastore.NewKey(reprovideStrategyKey)
1453
1454 if strategy == "" {
1455 return ds.Delete(ctx, strategyKey)
1456 }
1457 return ds.Put(ctx, strategyKey, []byte(strategy))
1458 }
1459
1460 // handleStrategyChange manages strategy change detection and queue clearing.
1461 // Strategy change detection: when the reproviding strategy changes,
1462 // we clear the provide queue to avoid unexpected behavior from mixing
1463 // strategies. This ensures a clean transition between different providing modes.
1464 func handleStrategyChange(strategy string, provider DHTProvider, ds datastore.Datastore) {
1465 ctx := context.Background()
1466
1467 previous, changed, err := detectStrategyChange(ctx, strategy, ds)
1468 if err != nil {
1469 providerLog.Error("cannot read previous reprovide strategy", "err", err)
1470 return
1471 }
1472
1473 if !changed {
1474 return
1475 }
1476
1477 providerLog.Infow("Provide.Strategy changed, clearing provide queue", "previous", previous, "current", strategy)
1478 provider.Clear()
1479
1480 if err := persistStrategy(ctx, strategy, ds); err != nil {
1481 providerLog.Error("cannot update reprovide strategy", "err", err)
1482 }
1483 }
1484
1485 func setReproviderKeyProvider(strategy string, fpRate uint) func(in provStrategyIn) provStrategyOut {
1486 strategyFlag := config.MustParseProvideStrategy(strategy)
1487
1488 return func(in provStrategyIn) provStrategyOut {
1489 // Create the appropriate key provider based on strategy
1490 kcf := createKeyProvider(strategyFlag, fpRate, in)
1491 return provStrategyOut{
1492 ProvidingStrategy: strategyFlag,
1493 ProvidingKeyChanFunc: kcf,
1494 }
1495 }
1496 }