From 7587d00d532267de858da508801cd185d9778315 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 11:37:06 +0200 Subject: [PATCH 01/26] feat: provide stats --- core/commands/provide.go | 196 ++++++++++++++++++++++--- docs/examples/kubo-as-a-library/go.mod | 4 +- docs/examples/kubo-as-a-library/go.sum | 8 +- go.mod | 4 +- go.sum | 8 +- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 +- 7 files changed, 188 insertions(+), 38 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 3cc8b4f3ce7..3c240dbc64c 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -8,15 +8,30 @@ import ( "time" humanize "github.com/dustin/go-humanize" - "github.com/ipfs/boxo/provider" + boxoprovider "github.com/ipfs/boxo/provider" cmds "github.com/ipfs/go-ipfs-cmds" "github.com/ipfs/kubo/core/commands/cmdenv" "github.com/libp2p/go-libp2p-kad-dht/fullrt" + "github.com/libp2p/go-libp2p-kad-dht/provider" + "github.com/libp2p/go-libp2p-kad-dht/provider/buffered" + "github.com/libp2p/go-libp2p-kad-dht/provider/dual" + "github.com/libp2p/go-libp2p-kad-dht/provider/stats" + "github.com/probe-lab/go-libdht/kad/key" "golang.org/x/exp/constraints" ) const ( provideQuietOptionName = "quiet" + provideLanOptionName = "lan" + + provideStatAllOptionName = "all" + provideStatNetworkOptionName = "network" + provideStatConnectivityOptionName = "connectivity" + provideStatOperationsOptionName = "operations" + provideStatTimingsOptionName = "timings" + provideStatScheduleOptionName = "schedule" + provideStatQueuesOptionName = "queues" + provideStatWorkersOptionName = "workers" ) var ProvideCmd = &cmds.Command{ @@ -90,8 +105,9 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy } type provideStats struct { - provider.ReproviderStats - fullRT bool + Sweep *stats.Stats + Legacy *boxoprovider.ReproviderStats + FullRT bool // only used for legacy stats } var provideStatCmd = &cmds.Command{ @@ -108,7 +124,17 @@ This interface is not stable and may change from release to release. `, }, Arguments: []cmds.Argument{}, - Options: []cmds.Option{}, + Options: []cmds.Option{ + cmds.BoolOption(provideLanOptionName, "Show stats for LAN DHT only (for Sweep+Dual DHT only)"), + cmds.BoolOption(provideStatAllOptionName, "a", "Display all provide sweep stats"), + cmds.BoolOption(provideStatConnectivityOptionName, "Display DHT connectivity status"), + cmds.BoolOption(provideStatNetworkOptionName, "Display network stats (peers, reachability, region size)"), + cmds.BoolOption(provideStatScheduleOptionName, "Display reprovide schedule (CIDs/regions scheduled, next reprovide time)"), + cmds.BoolOption(provideStatTimingsOptionName, "Display timing information (uptime, cycle start, reprovide interval)"), + cmds.BoolOption(provideStatWorkersOptionName, "Display worker pool stats (active/available/queued workers)"), + cmds.BoolOption(provideStatOperationsOptionName, "Display operation stats (ongoing/past provides, rates, errors)"), + cmds.BoolOption(provideStatQueuesOptionName, "Display provide and reprovide queue sizes"), + }, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { nd, err := cmdenv.GetNode(env) if err != nil { @@ -119,35 +145,159 @@ This interface is not stable and may change from release to release. return ErrNotOnline } - provideSys, ok := nd.Provider.(provider.System) - if !ok { - return errors.New("stats not available with experimental sweeping provider (Provide.DHT.SweepEnabled=true)") - } + lanStats, _ := req.Options[provideLanOptionName].(bool) - stats, err := provideSys.Stat() - if err != nil { - return err + var sweepingProvider *provider.SweepingProvider + switch prov := nd.Provider.(type) { + case boxoprovider.System: + stats, err := prov.Stat() + if err != nil { + return err + } + _, fullRT := nd.DHTClient.(*fullrt.FullRT) + return res.Emit(provideStats{Legacy: &stats, FullRT: fullRT}) + case *provider.SweepingProvider: + sweepingProvider = prov + case *dual.SweepingProvider: + if lanStats { + sweepingProvider = prov.LAN + } else { + sweepingProvider = prov.WAN + } + case *buffered.SweepingProvider: + switch inner := prov.Provider.(type) { + case *provider.SweepingProvider: + sweepingProvider = inner + case *dual.SweepingProvider: + if lanStats { + sweepingProvider = inner.LAN + } else { + sweepingProvider = inner.WAN + } + default: + } + default: } - _, fullRT := nd.DHTClient.(*fullrt.FullRT) - - if err := res.Emit(provideStats{stats, fullRT}); err != nil { - return err + if sweepingProvider == nil { + return fmt.Errorf("stats not available with current routing system %T", nd.Provider) } - return nil + fmt.Printf("sweepging provider %v\n", sweepingProvider) + s := sweepingProvider.Stats() + fmt.Printf("stats %v\n", s) + return res.Emit(provideStats{Sweep: &s, Legacy: nil, FullRT: false}) }, Encoders: cmds.EncoderMap{ cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, s provideStats) error { wtr := tabwriter.NewWriter(w, 1, 2, 1, ' ', 0) defer wtr.Flush() - fmt.Fprintf(wtr, "TotalReprovides:\t%s\n", humanNumber(s.TotalReprovides)) - fmt.Fprintf(wtr, "AvgReprovideDuration:\t%s\n", humanDuration(s.AvgReprovideDuration)) - fmt.Fprintf(wtr, "LastReprovideDuration:\t%s\n", humanDuration(s.LastReprovideDuration)) - if !s.LastRun.IsZero() { - fmt.Fprintf(wtr, "LastReprovide:\t%s\n", humanTime(s.LastRun)) - if s.fullRT { - fmt.Fprintf(wtr, "NextReprovide:\t%s\n", humanTime(s.LastRun.Add(s.ReprovideInterval))) + if s.Legacy != nil { + fmt.Fprintf(wtr, "TotalReprovides:\t%s\n", humanNumber(s.Legacy.TotalReprovides)) + fmt.Fprintf(wtr, "AvgReprovideDuration:\t%s\n", humanDuration(s.Legacy.AvgReprovideDuration)) + fmt.Fprintf(wtr, "LastReprovideDuration:\t%s\n", humanDuration(s.Legacy.LastReprovideDuration)) + if !s.Legacy.LastRun.IsZero() { + fmt.Fprintf(wtr, "LastReprovide:\t%s\n", humanTime(s.Legacy.LastRun)) + if s.FullRT { + fmt.Fprintf(wtr, "NextReprovide:\t%s\n", humanTime(s.Legacy.LastRun.Add(s.Legacy.ReprovideInterval))) + } + } + return nil + } + if s.Sweep == nil { + return errors.New("no provide stats available") + } + + fmt.Fprintf(wtr, "Provide Sweep Stats:\n\n") + if s.Sweep.Closed { + fmt.Fprintf(wtr, "Status:\tclosed\n") + return nil + } + all, _ := req.Options[provideStatAllOptionName].(bool) + connectivity, _ := req.Options[provideStatConnectivityOptionName].(bool) + queues, _ := req.Options[provideStatQueuesOptionName].(bool) + schedule, _ := req.Options[provideStatScheduleOptionName].(bool) + network, _ := req.Options[provideStatNetworkOptionName].(bool) + timings, _ := req.Options[provideStatTimingsOptionName].(bool) + operations, _ := req.Options[provideStatOperationsOptionName].(bool) + workers, _ := req.Options[provideStatWorkersOptionName].(bool) + + brief := !all && !connectivity && !queues && !schedule && !network && !timings && !operations && !workers + + // Connectivity + if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" { + fmt.Fprintf(wtr, "Connectivity:\t%s, since:\t%s\n", s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) + } + // Queues + if all || queues || brief { + fmt.Fprintf(wtr, "Provide Queue Size:\t%s CIDs, from:\t%s keyspace regions\n", humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) + fmt.Fprintf(wtr, "Reprovide Queue Size:\t%s regions\n", humanNumber(s.Sweep.Queues.PendingRegionReprovides)) + } + // Schedule + if all || schedule || brief { + fmt.Fprintf(wtr, "CIDs scheduled for reprovide:\t%s\n", humanNumber(s.Sweep.Schedule.Keys)) + fmt.Fprintf(wtr, "Regions scheduled for reprovide:\t%s\n", humanNumber(s.Sweep.Schedule.Regions)) + if !brief { + fmt.Fprintf(wtr, "Avg Prefix Length:\t%s\n", humanNumber(s.Sweep.Schedule.AvgPrefixLength)) + fmt.Fprintf(wtr, "Next Reprovide at:\t%s\n", humanTime(s.Sweep.Schedule.NextReprovideAt)) + fmt.Fprintf(wtr, "Next Reprovide Prefix:\t%s\n", key.BitString(s.Sweep.Schedule.NextReprovidePrefix)) + } + } + // Timings + if all || timings { + fmt.Fprintf(wtr, "Uptime:\t%s, Since:\t%s\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) + fmt.Fprintf(wtr, "Current Time Offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) + fmt.Fprintf(wtr, "Cycle Started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) + fmt.Fprintf(wtr, "Reprovides Interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) + } + // Network + if all || network || brief { + fmt.Fprintf(wtr, "Avg Record Holders:\t%.1f\n", s.Sweep.Network.AvgHolders) + if !brief { + fmt.Fprintf(wtr, "Peers Contacted:\t%s\n", humanNumber(s.Sweep.Network.Peers)) + fmt.Fprintf(wtr, "Reachable Peers:\t%s\t(%d%%)\n", humanNumber(s.Sweep.Network.Reachable), 100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers) + fmt.Fprintf(wtr, "Avg Region Size:\t%.f1\n", 0.) // TODO: add region size to stats + fmt.Fprintf(wtr, "Full Keyspace Coverage:\t%t\n", s.Sweep.Network.CompleteKeyspaceCoverage) + fmt.Fprintf(wtr, "Replication Factor:\t%s\n", humanNumber(s.Sweep.Network.ReplicationFactor)) + } + } + // Operations + if all || operations || brief { + fmt.Fprintf(wtr, "Currently Providing:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) + fmt.Fprintf(wtr, "Currently Repoviding:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) + fmt.Fprintf(wtr, "Total Provides:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysProvided)) + if !brief { + fmt.Fprintf(wtr, "Total Records Provided:\t%s\n", humanNumber(s.Sweep.Operations.Past.RecordsProvided)) + fmt.Fprintf(wtr, "Total Provide Errors:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysFailed)) + fmt.Fprintf(wtr, "CIDs Provided per Minute:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysProvidedPerMinute)) + fmt.Fprintf(wtr, "CIDs Reprovided per Minute:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) + fmt.Fprintf(wtr, "Region Reprovide Duration:\t%s\n", humanDuration(s.Sweep.Operations.Past.RegionReprovideDuration)) + fmt.Fprintf(wtr, "Avg CIDs per Reprovide:\t%s\n", humanNumber(s.Sweep.Operations.Past.AvgKeysPerReprovide)) + fmt.Fprintf(wtr, "Regions reprovided last cycle:\t%s\n", humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle)) + } + } + // Workers + displayWorkers := all || workers + if displayWorkers || brief { + availableReservedBurst := max(0, s.Sweep.Workers.DedicatedBurst-s.Sweep.Workers.ActiveBurst) + availableReservedPeriodic := max(0, s.Sweep.Workers.DedicatedPeriodic-s.Sweep.Workers.ActivePeriodic) + availableFreeWorkers := s.Sweep.Workers.Max - max(s.Sweep.Workers.DedicatedBurst, s.Sweep.Workers.ActiveBurst) - max(s.Sweep.Workers.DedicatedPeriodic, s.Sweep.Workers.ActivePeriodic) + availableBurst := availableFreeWorkers + availableReservedBurst + availablePeriodic := availableFreeWorkers + availableReservedPeriodic + if displayWorkers || availableBurst <= 2 || availablePeriodic <= 2 { + // Either we want to display workers information, or we are low on + // available workers and want to warn the user. + fmt.Fprintf(wtr, "Active Workers:\t%s, Max:\t%s\n", humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) + fmt.Fprintf(wtr, "Available Free Worker:\t%s\n", humanNumber(availableFreeWorkers)) + fmt.Fprintf(wtr, "Active Periodic Workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", + humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.DedicatedPeriodic), + humanNumber(availablePeriodic), humanNumber(s.Sweep.Workers.QueuedPeriodic)) + fmt.Fprintf(wtr, "Active Burst Workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", + humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(s.Sweep.Workers.DedicatedBurst), + humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) + } + if displayWorkers { + fmt.Fprintf(wtr, "Max Connections per Worker:\t%s\n", humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) } } return nil diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index e383e1f253c..7c87724fb17 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -115,7 +115,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.35.0 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.14.2 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect @@ -171,7 +171,7 @@ require ( github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/probe-lab/go-libdht v0.2.1 // indirect + github.com/probe-lab/go-libdht v0.3.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 55304498701..004e61e42fc 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -434,8 +434,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.35.0 h1:pWRC4FKR9ptQjA9DuMSrAn2D3vABE8r58iAeoLoK1Ig= -github.com/libp2p/go-libp2p-kad-dht v0.35.0/go.mod h1:s70f017NjhsBx+SVl0/w+x//uyglrFpKLfvuQJj4QAU= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d h1:eVVd7M0AAHfiyEnHht1Tz+aAJQRRLTQ+km67AHeXiXw= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= @@ -632,8 +632,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= -github.com/probe-lab/go-libdht v0.2.1 h1:oBCsKBvS/OVirTO5+BT6/AOocWjdqwpfSfkTfBjUPJE= -github.com/probe-lab/go-libdht v0.2.1/go.mod h1:q+WlGiqs/UIRfdhw9Gmc+fPoAYlOim7VvXTjOI6KJmQ= +github.com/probe-lab/go-libdht v0.3.0 h1:Q3ZXK8wCjZvgeHSTtRrppXobXY/KHPLZJfc+cdTTyqA= +github.com/probe-lab/go-libdht v0.3.0/go.mod h1:hamw22kI6YkPQFGy5P6BrWWDrgE9ety5Si8iWAyuDvc= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= diff --git a/go.mod b/go.mod index 48702ed0f28..afc5b6e0024 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.43.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.35.0 + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.14.2 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 @@ -69,6 +69,7 @@ require ( github.com/multiformats/go-multihash v0.2.3 github.com/opentracing/opentracing-go v1.2.0 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 + github.com/probe-lab/go-libdht v0.3.0 github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d @@ -215,7 +216,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/probe-lab/go-libdht v0.2.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect diff --git a/go.sum b/go.sum index 0cd74a1d1cf..4cf1c0a45f6 100644 --- a/go.sum +++ b/go.sum @@ -518,8 +518,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.35.0 h1:pWRC4FKR9ptQjA9DuMSrAn2D3vABE8r58iAeoLoK1Ig= -github.com/libp2p/go-libp2p-kad-dht v0.35.0/go.mod h1:s70f017NjhsBx+SVl0/w+x//uyglrFpKLfvuQJj4QAU= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d h1:eVVd7M0AAHfiyEnHht1Tz+aAJQRRLTQ+km67AHeXiXw= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= @@ -734,8 +734,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= -github.com/probe-lab/go-libdht v0.2.1 h1:oBCsKBvS/OVirTO5+BT6/AOocWjdqwpfSfkTfBjUPJE= -github.com/probe-lab/go-libdht v0.2.1/go.mod h1:q+WlGiqs/UIRfdhw9Gmc+fPoAYlOim7VvXTjOI6KJmQ= +github.com/probe-lab/go-libdht v0.3.0 h1:Q3ZXK8wCjZvgeHSTtRrppXobXY/KHPLZJfc+cdTTyqA= +github.com/probe-lab/go-libdht v0.3.0/go.mod h1:hamw22kI6YkPQFGy5P6BrWWDrgE9ety5Si8iWAyuDvc= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 268ccae2722..11041d93631 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -184,7 +184,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.43.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.35.0 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 4c3e98cce4d..bcd740f5ce8 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -468,8 +468,8 @@ github.com/libp2p/go-libp2p v0.43.0 h1:b2bg2cRNmY4HpLK8VHYQXLX2d3iND95OjodLFymvq github.com/libp2p/go-libp2p v0.43.0/go.mod h1:IiSqAXDyP2sWH+J2gs43pNmB/y4FOi2XQPbsb+8qvzc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.35.0 h1:pWRC4FKR9ptQjA9DuMSrAn2D3vABE8r58iAeoLoK1Ig= -github.com/libp2p/go-libp2p-kad-dht v0.35.0/go.mod h1:s70f017NjhsBx+SVl0/w+x//uyglrFpKLfvuQJj4QAU= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d h1:eVVd7M0AAHfiyEnHht1Tz+aAJQRRLTQ+km67AHeXiXw= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From d25a502fb16f36ce83bd019cdff518c844ec53cb Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 14:06:45 +0200 Subject: [PATCH 02/26] added N/A --- core/commands/provide.go | 105 +++++++++++++++++-------- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 +- 7 files changed, 81 insertions(+), 42 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 3c240dbc64c..105676aa9b8 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -182,10 +182,8 @@ This interface is not stable and may change from release to release. return fmt.Errorf("stats not available with current routing system %T", nd.Provider) } - fmt.Printf("sweepging provider %v\n", sweepingProvider) s := sweepingProvider.Stats() - fmt.Printf("stats %v\n", s) - return res.Emit(provideStats{Sweep: &s, Legacy: nil, FullRT: false}) + return res.Emit(provideStats{Sweep: &s}) }, Encoders: cmds.EncoderMap{ cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, s provideStats) error { @@ -226,53 +224,66 @@ This interface is not stable and may change from release to release. // Connectivity if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" { - fmt.Fprintf(wtr, "Connectivity:\t%s, since:\t%s\n", s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) + since := s.Sweep.Connectivity.Since + if since.IsZero() { + fmt.Fprintf(wtr, "Connectivity:\t%s\n", s.Sweep.Connectivity.Status) + } else { + fmt.Fprintf(wtr, "Connectivity:\t%s, since:\t%s\n", s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) + } } // Queues if all || queues || brief { - fmt.Fprintf(wtr, "Provide Queue Size:\t%s CIDs, from:\t%s keyspace regions\n", humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) - fmt.Fprintf(wtr, "Reprovide Queue Size:\t%s regions\n", humanNumber(s.Sweep.Queues.PendingRegionReprovides)) + fmt.Fprintf(wtr, "Provide queue size:\t%s CIDs, from:\t%s keyspace regions\n", humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) + fmt.Fprintf(wtr, "Reprovide queue size:\t%s regions\n", humanNumber(s.Sweep.Queues.PendingRegionReprovides)) } // Schedule if all || schedule || brief { fmt.Fprintf(wtr, "CIDs scheduled for reprovide:\t%s\n", humanNumber(s.Sweep.Schedule.Keys)) fmt.Fprintf(wtr, "Regions scheduled for reprovide:\t%s\n", humanNumber(s.Sweep.Schedule.Regions)) if !brief { - fmt.Fprintf(wtr, "Avg Prefix Length:\t%s\n", humanNumber(s.Sweep.Schedule.AvgPrefixLength)) - fmt.Fprintf(wtr, "Next Reprovide at:\t%s\n", humanTime(s.Sweep.Schedule.NextReprovideAt)) - fmt.Fprintf(wtr, "Next Reprovide Prefix:\t%s\n", key.BitString(s.Sweep.Schedule.NextReprovidePrefix)) + fmt.Fprintf(wtr, "Avg prefix length:\t%s\n", humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) + fmt.Fprintf(wtr, "Next reprovide at:\t%s\n", humanTime(s.Sweep.Schedule.NextReprovideAt)) + nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) + if nextPrefix == "" { + nextPrefix = "N/A" + } + fmt.Fprintf(wtr, "Next prefix to be reprovided:\t%s\n", nextPrefix) } } // Timings if all || timings { - fmt.Fprintf(wtr, "Uptime:\t%s, Since:\t%s\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) - fmt.Fprintf(wtr, "Current Time Offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) - fmt.Fprintf(wtr, "Cycle Started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) - fmt.Fprintf(wtr, "Reprovides Interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) + fmt.Fprintf(wtr, "Uptime:\t%s, since:\t%s\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) + fmt.Fprintf(wtr, "Current time offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) + fmt.Fprintf(wtr, "Cycle started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) + fmt.Fprintf(wtr, "Reprovide interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) } // Network if all || network || brief { - fmt.Fprintf(wtr, "Avg Record Holders:\t%.1f\n", s.Sweep.Network.AvgHolders) + fmt.Fprintf(wtr, "Avg record holders:\t%s\n", humanFloatOrNA(s.Sweep.Network.AvgHolders)) if !brief { - fmt.Fprintf(wtr, "Peers Contacted:\t%s\n", humanNumber(s.Sweep.Network.Peers)) - fmt.Fprintf(wtr, "Reachable Peers:\t%s\t(%d%%)\n", humanNumber(s.Sweep.Network.Reachable), 100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers) - fmt.Fprintf(wtr, "Avg Region Size:\t%.f1\n", 0.) // TODO: add region size to stats - fmt.Fprintf(wtr, "Full Keyspace Coverage:\t%t\n", s.Sweep.Network.CompleteKeyspaceCoverage) - fmt.Fprintf(wtr, "Replication Factor:\t%s\n", humanNumber(s.Sweep.Network.ReplicationFactor)) + fmt.Fprintf(wtr, "Peers swept:\t%s\n", humanNumber(s.Sweep.Network.Peers)) + if s.Sweep.Network.Peers > 0 { + fmt.Fprintf(wtr, "Reachable peers:\t%s\t(%s%%)\n", humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers)) + } else { + fmt.Fprintf(wtr, "Reachable peers:\t%s\n", humanNumber(s.Sweep.Network.Reachable)) + } + fmt.Fprintf(wtr, "Avg region size:\t%s\n", humanFloatOrNA(s.Sweep.Network.AvgRegionSize)) + fmt.Fprintf(wtr, "Full keyspace coverage:\t%t\n", s.Sweep.Network.CompleteKeyspaceCoverage) + fmt.Fprintf(wtr, "Replication factor:\t%s\n", humanNumber(s.Sweep.Network.ReplicationFactor)) } } // Operations if all || operations || brief { - fmt.Fprintf(wtr, "Currently Providing:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) - fmt.Fprintf(wtr, "Currently Repoviding:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) - fmt.Fprintf(wtr, "Total Provides:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysProvided)) + fmt.Fprintf(wtr, "Currently providing:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) + fmt.Fprintf(wtr, "Currently repoviding:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) + fmt.Fprintf(wtr, "Total CIDs provided:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysProvided)) if !brief { - fmt.Fprintf(wtr, "Total Records Provided:\t%s\n", humanNumber(s.Sweep.Operations.Past.RecordsProvided)) - fmt.Fprintf(wtr, "Total Provide Errors:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysFailed)) - fmt.Fprintf(wtr, "CIDs Provided per Minute:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysProvidedPerMinute)) - fmt.Fprintf(wtr, "CIDs Reprovided per Minute:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) - fmt.Fprintf(wtr, "Region Reprovide Duration:\t%s\n", humanDuration(s.Sweep.Operations.Past.RegionReprovideDuration)) - fmt.Fprintf(wtr, "Avg CIDs per Reprovide:\t%s\n", humanNumber(s.Sweep.Operations.Past.AvgKeysPerReprovide)) + fmt.Fprintf(wtr, "Total records provided:\t%s\n", humanNumber(s.Sweep.Operations.Past.RecordsProvided)) + fmt.Fprintf(wtr, "Total provide errors:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysFailed)) + fmt.Fprintf(wtr, "CIDs provided per minute:\t%s\n", humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute)) + fmt.Fprintf(wtr, "CIDs reprovided per minute:\t%s\n", humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) + fmt.Fprintf(wtr, "Region reprovide duration:\t%s\n", humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration)) + fmt.Fprintf(wtr, "Avg CIDs per reprovide:\t%s\n", humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide)) fmt.Fprintf(wtr, "Regions reprovided last cycle:\t%s\n", humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle)) } } @@ -287,17 +298,17 @@ This interface is not stable and may change from release to release. if displayWorkers || availableBurst <= 2 || availablePeriodic <= 2 { // Either we want to display workers information, or we are low on // available workers and want to warn the user. - fmt.Fprintf(wtr, "Active Workers:\t%s, Max:\t%s\n", humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) - fmt.Fprintf(wtr, "Available Free Worker:\t%s\n", humanNumber(availableFreeWorkers)) - fmt.Fprintf(wtr, "Active Periodic Workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", + fmt.Fprintf(wtr, "Active workers:\t%s, Max:\t%s\n", humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) + fmt.Fprintf(wtr, "Available free worker:\t%s\n", humanNumber(availableFreeWorkers)) + fmt.Fprintf(wtr, "Active periodic workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(availablePeriodic), humanNumber(s.Sweep.Workers.QueuedPeriodic)) - fmt.Fprintf(wtr, "Active Burst Workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", + fmt.Fprintf(wtr, "Active burst workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(s.Sweep.Workers.DedicatedBurst), humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) } if displayWorkers { - fmt.Fprintf(wtr, "Max Connections per Worker:\t%s\n", humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) + fmt.Fprintf(wtr, "Max connections per worker:\t%s\n", humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) } } return nil @@ -307,10 +318,23 @@ This interface is not stable and may change from release to release. } func humanDuration(val time.Duration) string { + if val > 10*time.Second { + return val.Truncate(100 * time.Millisecond).String() + } return val.Truncate(time.Microsecond).String() } +func humanDurationOrNA(val time.Duration) string { + if val <= 0 { + return "N/A" + } + return humanDuration(val) +} + func humanTime(val time.Time) string { + if val.IsZero() { + return "N/A" + } return val.Format("2006-01-02 15:04:05") } @@ -324,6 +348,21 @@ func humanNumber[T constraints.Float | constraints.Integer](n T) string { return str } +// humanNumberOrNA is like humanNumber but returns "N/A" for non-positive values. +func humanNumberOrNA[T constraints.Float | constraints.Integer](n T) string { + if n <= 0 { + return "N/A" + } + return humanNumber(n) +} + +func humanFloatOrNA(val float64) string { + if val <= 0 { + return "N/A" + } + return fmt.Sprintf("%.1f", val) +} + func humanSI(val float64, decimals int) string { v, unit := humanize.ComputeSI(val) return fmt.Sprintf("%s%s", humanFull(v, decimals), unit) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 7c87724fb17..bc58bdb9537 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -115,7 +115,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d // indirect + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.14.2 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 004e61e42fc..0a150984068 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -434,8 +434,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d h1:eVVd7M0AAHfiyEnHht1Tz+aAJQRRLTQ+km67AHeXiXw= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 h1:7b7vW4qwok2+8ANRAsDFuVboEH3FMBNB4RHKScp5fZ4= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index afc5b6e0024..233a29934a8 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.43.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.14.2 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index 4cf1c0a45f6..547745e3279 100644 --- a/go.sum +++ b/go.sum @@ -518,8 +518,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d h1:eVVd7M0AAHfiyEnHht1Tz+aAJQRRLTQ+km67AHeXiXw= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 h1:7b7vW4qwok2+8ANRAsDFuVboEH3FMBNB4RHKScp5fZ4= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 11041d93631..075ea6f3bcf 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -184,7 +184,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.43.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d // indirect + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index bcd740f5ce8..ab536b7ae7e 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -468,8 +468,8 @@ github.com/libp2p/go-libp2p v0.43.0 h1:b2bg2cRNmY4HpLK8VHYQXLX2d3iND95OjodLFymvq github.com/libp2p/go-libp2p v0.43.0/go.mod h1:IiSqAXDyP2sWH+J2gs43pNmB/y4FOi2XQPbsb+8qvzc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d h1:eVVd7M0AAHfiyEnHht1Tz+aAJQRRLTQ+km67AHeXiXw= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251007114900-cb675c4ec89d/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 h1:7b7vW4qwok2+8ANRAsDFuVboEH3FMBNB4RHKScp5fZ4= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From 8c200f375819f11d967b6193e219c4816943269d Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 14:33:26 +0200 Subject: [PATCH 03/26] format --- core/commands/provide.go | 149 +++++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 39 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 105676aa9b8..9f1538abfde 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -224,67 +224,117 @@ This interface is not stable and may change from release to release. // Connectivity if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" { + if !brief { + fmt.Fprintf(wtr, "Connectivity:\n") + } since := s.Sweep.Connectivity.Since + indent := "" + if !brief { + indent = " " + } if since.IsZero() { - fmt.Fprintf(wtr, "Connectivity:\t%s\n", s.Sweep.Connectivity.Status) + fmt.Fprintf(wtr, "%sStatus:\t%s\n", indent, s.Sweep.Connectivity.Status) } else { - fmt.Fprintf(wtr, "Connectivity:\t%s, since:\t%s\n", s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) + fmt.Fprintf(wtr, "%sStatus:\t%s (since %s)\n", indent, s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) + } + if !brief { + fmt.Fprintf(wtr, "\n") } } // Queues if all || queues || brief { - fmt.Fprintf(wtr, "Provide queue size:\t%s CIDs, from:\t%s keyspace regions\n", humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) - fmt.Fprintf(wtr, "Reprovide queue size:\t%s regions\n", humanNumber(s.Sweep.Queues.PendingRegionReprovides)) + if !brief { + fmt.Fprintf(wtr, "Queues:\n") + } + indent := "" + if !brief { + indent = " " + } + fmt.Fprintf(wtr, "%sProvide queue:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) + fmt.Fprintf(wtr, "%sReprovide queue:\t%s regions\n", indent, humanNumber(s.Sweep.Queues.PendingRegionReprovides)) + if !brief { + fmt.Fprintf(wtr, "\n") + } } // Schedule if all || schedule || brief { - fmt.Fprintf(wtr, "CIDs scheduled for reprovide:\t%s\n", humanNumber(s.Sweep.Schedule.Keys)) - fmt.Fprintf(wtr, "Regions scheduled for reprovide:\t%s\n", humanNumber(s.Sweep.Schedule.Regions)) if !brief { - fmt.Fprintf(wtr, "Avg prefix length:\t%s\n", humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) - fmt.Fprintf(wtr, "Next reprovide at:\t%s\n", humanTime(s.Sweep.Schedule.NextReprovideAt)) + fmt.Fprintf(wtr, "Schedule:\n") + } + indent := "" + if !brief { + indent = " " + } + fmt.Fprintf(wtr, "%sCIDs scheduled:\t%s\n", indent, humanNumber(s.Sweep.Schedule.Keys)) + fmt.Fprintf(wtr, "%sRegions scheduled:\t%s\n", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) + if !brief { + fmt.Fprintf(wtr, "%sAvg prefix length:\t%s\n", indent, humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) + fmt.Fprintf(wtr, "%sNext reprovide at:\t%s\n", indent, humanTime(s.Sweep.Schedule.NextReprovideAt)) nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) if nextPrefix == "" { nextPrefix = "N/A" } - fmt.Fprintf(wtr, "Next prefix to be reprovided:\t%s\n", nextPrefix) + fmt.Fprintf(wtr, "%sNext prefix:\t%s\n", indent, nextPrefix) + } + if !brief { + fmt.Fprintf(wtr, "\n") } } // Timings if all || timings { - fmt.Fprintf(wtr, "Uptime:\t%s, since:\t%s\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) - fmt.Fprintf(wtr, "Current time offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) - fmt.Fprintf(wtr, "Cycle started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) - fmt.Fprintf(wtr, "Reprovide interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) + fmt.Fprintf(wtr, "Timings:\n") + fmt.Fprintf(wtr, " Uptime:\t%s (since %s)\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) + fmt.Fprintf(wtr, " Current time offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) + fmt.Fprintf(wtr, " Cycle started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) + fmt.Fprintf(wtr, " Reprovide interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) + fmt.Fprintf(wtr, "\n") } // Network if all || network || brief { - fmt.Fprintf(wtr, "Avg record holders:\t%s\n", humanFloatOrNA(s.Sweep.Network.AvgHolders)) if !brief { - fmt.Fprintf(wtr, "Peers swept:\t%s\n", humanNumber(s.Sweep.Network.Peers)) + fmt.Fprintf(wtr, "Network:\n") + } + indent := "" + if !brief { + indent = " " + } + fmt.Fprintf(wtr, "%sAvg record holders:\t%s\n", indent, humanFloatOrNA(s.Sweep.Network.AvgHolders)) + if !brief { + fmt.Fprintf(wtr, "%sPeers swept:\t%s\n", indent, humanNumber(s.Sweep.Network.Peers)) if s.Sweep.Network.Peers > 0 { - fmt.Fprintf(wtr, "Reachable peers:\t%s\t(%s%%)\n", humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers)) + fmt.Fprintf(wtr, "%sReachable peers:\t%s (%s%%)\n", indent, humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers)) } else { - fmt.Fprintf(wtr, "Reachable peers:\t%s\n", humanNumber(s.Sweep.Network.Reachable)) + fmt.Fprintf(wtr, "%sReachable peers:\t%s\n", indent, humanNumber(s.Sweep.Network.Reachable)) } - fmt.Fprintf(wtr, "Avg region size:\t%s\n", humanFloatOrNA(s.Sweep.Network.AvgRegionSize)) - fmt.Fprintf(wtr, "Full keyspace coverage:\t%t\n", s.Sweep.Network.CompleteKeyspaceCoverage) - fmt.Fprintf(wtr, "Replication factor:\t%s\n", humanNumber(s.Sweep.Network.ReplicationFactor)) + fmt.Fprintf(wtr, "%sAvg region size:\t%s\n", indent, humanFloatOrNA(s.Sweep.Network.AvgRegionSize)) + fmt.Fprintf(wtr, "%sFull keyspace coverage:\t%t\n", indent, s.Sweep.Network.CompleteKeyspaceCoverage) + fmt.Fprintf(wtr, "%sReplication factor:\t%s\n", indent, humanNumber(s.Sweep.Network.ReplicationFactor)) + fmt.Fprintf(wtr, "\n") } } // Operations if all || operations || brief { - fmt.Fprintf(wtr, "Currently providing:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) - fmt.Fprintf(wtr, "Currently repoviding:\t%s CIDs, In:\t%s Regions\n", humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) - fmt.Fprintf(wtr, "Total CIDs provided:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysProvided)) if !brief { - fmt.Fprintf(wtr, "Total records provided:\t%s\n", humanNumber(s.Sweep.Operations.Past.RecordsProvided)) - fmt.Fprintf(wtr, "Total provide errors:\t%s\n", humanNumber(s.Sweep.Operations.Past.KeysFailed)) - fmt.Fprintf(wtr, "CIDs provided per minute:\t%s\n", humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute)) - fmt.Fprintf(wtr, "CIDs reprovided per minute:\t%s\n", humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) - fmt.Fprintf(wtr, "Region reprovide duration:\t%s\n", humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration)) - fmt.Fprintf(wtr, "Avg CIDs per reprovide:\t%s\n", humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide)) - fmt.Fprintf(wtr, "Regions reprovided last cycle:\t%s\n", humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle)) + fmt.Fprintf(wtr, "Operations:\n") + } + indent := "" + if !brief { + indent = " " + } + // Ongoing operations + fmt.Fprintf(wtr, "%sCurrently providing:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) + fmt.Fprintf(wtr, "%sCurrently reproviding:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) + // Past operations summary + fmt.Fprintf(wtr, "%sTotal CIDs provided:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.KeysProvided)) + if !brief { + fmt.Fprintf(wtr, "%sTotal records provided:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.RecordsProvided)) + fmt.Fprintf(wtr, "%sTotal provide errors:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.KeysFailed)) + fmt.Fprintf(wtr, "%sCIDs provided/min:\t%s\n", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute)) + fmt.Fprintf(wtr, "%sCIDs reprovided/min:\t%s\n", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) + fmt.Fprintf(wtr, "%sRegion reprovide duration:\t%s\n", indent, humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration)) + fmt.Fprintf(wtr, "%sAvg CIDs/reprovide:\t%s\n", indent, humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide)) + fmt.Fprintf(wtr, "%sRegions reprovided (last cycle):\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle)) + fmt.Fprintf(wtr, "\n") } } // Workers @@ -298,17 +348,38 @@ This interface is not stable and may change from release to release. if displayWorkers || availableBurst <= 2 || availablePeriodic <= 2 { // Either we want to display workers information, or we are low on // available workers and want to warn the user. - fmt.Fprintf(wtr, "Active workers:\t%s, Max:\t%s\n", humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) - fmt.Fprintf(wtr, "Available free worker:\t%s\n", humanNumber(availableFreeWorkers)) - fmt.Fprintf(wtr, "Active periodic workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", - humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.DedicatedPeriodic), - humanNumber(availablePeriodic), humanNumber(s.Sweep.Workers.QueuedPeriodic)) - fmt.Fprintf(wtr, "Active burst workers:\t%s, Dedicated:\t%s, Available:\t%s, Queued:\t%s\n", - humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(s.Sweep.Workers.DedicatedBurst), - humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) + if !brief && displayWorkers { + fmt.Fprintf(wtr, "Workers:\n") + } + indent := "" + if !brief && displayWorkers { + indent = " " + } + fmt.Fprintf(wtr, "%sActive workers:\t%s / %s (max)\n", indent, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) + fmt.Fprintf(wtr, "%sFree workers:\t%s\n", indent, humanNumber(availableFreeWorkers)) + if !brief && displayWorkers { + fmt.Fprintf(wtr, "%sWorker stats: %-15s %s\n", indent, "Periodic", "Burst") + fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) + fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) + fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) + fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) + } else { + // Brief mode - show condensed worker info + fmt.Fprintf(wtr, "%sPeriodic:\t%s active, %s available, %s queued\n", indent, + humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(availablePeriodic), humanNumber(s.Sweep.Workers.QueuedPeriodic)) + fmt.Fprintf(wtr, "%sBurst:\t%s active, %s available, %s queued\n", indent, + humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) + } } if displayWorkers { - fmt.Fprintf(wtr, "Max connections per worker:\t%s\n", humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) + indent := "" + if !brief { + indent = " " + } + fmt.Fprintf(wtr, "%sMax connections/worker:\t%s\n", indent, humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) + if !brief { + fmt.Fprintf(wtr, "\n") + } } } return nil From a5924abd1aa690ed6e1b25f4bae4c2aa67f2b46a Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 14:48:20 +0200 Subject: [PATCH 04/26] workers stats alignment --- core/commands/provide.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 9f1538abfde..ba9d4741770 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -358,11 +358,11 @@ This interface is not stable and may change from release to release. fmt.Fprintf(wtr, "%sActive workers:\t%s / %s (max)\n", indent, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) fmt.Fprintf(wtr, "%sFree workers:\t%s\n", indent, humanNumber(availableFreeWorkers)) if !brief && displayWorkers { - fmt.Fprintf(wtr, "%sWorker stats: %-15s %s\n", indent, "Periodic", "Burst") - fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) - fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) - fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) - fmt.Fprintf(wtr, "%s %-11s %-15s %s\n", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) + fmt.Fprintf(wtr, "%sWorker stats: %-11s %s\n", indent, "Periodic", "Burst") + fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) + fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) + fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) + fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) } else { // Brief mode - show condensed worker info fmt.Fprintf(wtr, "%sPeriodic:\t%s active, %s available, %s queued\n", indent, @@ -389,7 +389,7 @@ This interface is not stable and may change from release to release. } func humanDuration(val time.Duration) string { - if val > 10*time.Second { + if val > time.Second { return val.Truncate(100 * time.Millisecond).String() } return val.Truncate(time.Microsecond).String() From fb5c9c70c81160ae16beeccb790974c0fa24a2a0 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 15:51:19 +0200 Subject: [PATCH 05/26] ipfs provide stat --all --compact --- core/commands/provide.go | 137 +++++++++++++++++++++++++++++++++++---- 1 file changed, 123 insertions(+), 14 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index ba9d4741770..5148380399f 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -25,6 +25,7 @@ const ( provideLanOptionName = "lan" provideStatAllOptionName = "all" + provideStatCompactOptionName = "compact" provideStatNetworkOptionName = "network" provideStatConnectivityOptionName = "connectivity" provideStatOperationsOptionName = "operations" @@ -127,6 +128,7 @@ This interface is not stable and may change from release to release. Options: []cmds.Option{ cmds.BoolOption(provideLanOptionName, "Show stats for LAN DHT only (for Sweep+Dual DHT only)"), cmds.BoolOption(provideStatAllOptionName, "a", "Display all provide sweep stats"), + cmds.BoolOption(provideStatCompactOptionName, "Display stats in 2-column layout (requires --all)"), cmds.BoolOption(provideStatConnectivityOptionName, "Display DHT connectivity status"), cmds.BoolOption(provideStatNetworkOptionName, "Display network stats (peers, reachability, region size)"), cmds.BoolOption(provideStatScheduleOptionName, "Display reprovide schedule (CIDs/regions scheduled, next reprovide time)"), @@ -146,6 +148,12 @@ This interface is not stable and may change from release to release. } lanStats, _ := req.Options[provideLanOptionName].(bool) + compact, _ := req.Options[provideStatCompactOptionName].(bool) + all, _ := req.Options[provideStatAllOptionName].(bool) + + if compact && !all { + return fmt.Errorf("--compact flag requires --all flag") + } var sweepingProvider *provider.SweepingProvider switch prov := nd.Provider.(type) { @@ -206,12 +214,12 @@ This interface is not stable and may change from release to release. return errors.New("no provide stats available") } - fmt.Fprintf(wtr, "Provide Sweep Stats:\n\n") if s.Sweep.Closed { - fmt.Fprintf(wtr, "Status:\tclosed\n") + fmt.Fprintf(wtr, "Provider is closed\n") return nil } all, _ := req.Options[provideStatAllOptionName].(bool) + compact, _ := req.Options[provideStatCompactOptionName].(bool) connectivity, _ := req.Options[provideStatConnectivityOptionName].(bool) queues, _ := req.Options[provideStatQueuesOptionName].(bool) schedule, _ := req.Options[provideStatScheduleOptionName].(bool) @@ -222,6 +230,107 @@ This interface is not stable and may change from release to release. brief := !all && !connectivity && !queues && !schedule && !network && !timings && !operations && !workers + // Compact mode: display stats in 2 columns with fixed column 1 width + if all && compact { + const col1Width = 34 // Fixed width for column 1 + var col1, col2 []string + + availableReservedBurst := max(0, s.Sweep.Workers.DedicatedBurst-s.Sweep.Workers.ActiveBurst) + availableReservedPeriodic := max(0, s.Sweep.Workers.DedicatedPeriodic-s.Sweep.Workers.ActivePeriodic) + availableFreeWorkers := s.Sweep.Workers.Max - max(s.Sweep.Workers.DedicatedBurst, s.Sweep.Workers.ActiveBurst) - max(s.Sweep.Workers.DedicatedPeriodic, s.Sweep.Workers.ActivePeriodic) + availableBurst := availableFreeWorkers + availableReservedBurst + availablePeriodic := availableFreeWorkers + availableReservedPeriodic + + // Column 1: Schedule + col1 = append(col1, "Schedule:") + col1 = append(col1, fmt.Sprintf(" CIDs scheduled: %s", humanNumber(s.Sweep.Schedule.Keys))) + col1 = append(col1, fmt.Sprintf(" Regions scheduled: %s", humanNumberOrNA(s.Sweep.Schedule.Regions))) + col1 = append(col1, fmt.Sprintf(" Avg prefix length: %s", humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength))) + col1 = append(col1, fmt.Sprintf(" Next reprovide at: %s", s.Sweep.Schedule.NextReprovideAt.Format("15:04:05"))) + nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) + if nextPrefix == "" { + nextPrefix = "N/A" + } + col1 = append(col1, fmt.Sprintf(" Next prefix: %s", nextPrefix)) + col1 = append(col1, "") + + // Column 1: Network + col1 = append(col1, "Network:") + col1 = append(col1, fmt.Sprintf(" Avg record holders: %s", humanFloatOrNA(s.Sweep.Network.AvgHolders))) + col1 = append(col1, fmt.Sprintf(" Peers swept: %s", humanNumber(s.Sweep.Network.Peers))) + if s.Sweep.Network.Peers > 0 { + col1 = append(col1, fmt.Sprintf(" Reachable peers: %s (%s%%)", humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers))) + } else { + col1 = append(col1, fmt.Sprintf(" Reachable peers: %s", humanNumber(s.Sweep.Network.Reachable))) + } + col1 = append(col1, fmt.Sprintf(" Avg region size: %s", humanFloatOrNA(s.Sweep.Network.AvgRegionSize))) + col1 = append(col1, fmt.Sprintf(" Full keyspace coverage: %t", s.Sweep.Network.CompleteKeyspaceCoverage)) + col1 = append(col1, fmt.Sprintf(" Replication factor: %s", humanNumber(s.Sweep.Network.ReplicationFactor))) + col1 = append(col1, "") + + // Column 1: Workers + col1 = append(col1, "Workers:") + col1 = append(col1, fmt.Sprintf(" Active: %s / %s (max)", humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max))) + col1 = append(col1, fmt.Sprintf(" Free: %s", humanNumber(availableFreeWorkers))) + col1 = append(col1, fmt.Sprintf(" Worker stats: %-9s %s", "Periodic", "Burst")) + col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst))) + col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst))) + col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst))) + col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst))) + col1 = append(col1, fmt.Sprintf(" Max connections/worker: %s", humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker))) + + // Column 2: Connectivity + col2 = append(col2, "Connectivity:") + since := s.Sweep.Connectivity.Since + if since.IsZero() { + col2 = append(col2, fmt.Sprintf(" Status: %s", s.Sweep.Connectivity.Status)) + } else { + col2 = append(col2, fmt.Sprintf(" Status: %s (%s)", s.Sweep.Connectivity.Status, humanTime(since))) + } + col2 = append(col2, "") + + // Column 2: Queues + col2 = append(col2, "Queues:") + col2 = append(col2, fmt.Sprintf(" Provide queue: %s CIDs, %s regions", humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides))) + col2 = append(col2, fmt.Sprintf(" Reprovide queue: %s regions", humanNumber(s.Sweep.Queues.PendingRegionReprovides))) + col2 = append(col2, "") + + // Column 2: Operations + col2 = append(col2, "Operations:") + col2 = append(col2, fmt.Sprintf(" Ongoing provides: %s CIDs, %s regions", humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides))) + col2 = append(col2, fmt.Sprintf(" Ongoing reprovides: %s CIDs, %s regions", humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides))) + col2 = append(col2, fmt.Sprintf(" Total CIDs provided: %s", humanNumber(s.Sweep.Operations.Past.KeysProvided))) + col2 = append(col2, fmt.Sprintf(" Total records provided: %s", humanNumber(s.Sweep.Operations.Past.RecordsProvided))) + col2 = append(col2, fmt.Sprintf(" Total provide errors: %s", humanNumber(s.Sweep.Operations.Past.KeysFailed))) + col2 = append(col2, fmt.Sprintf(" CIDs provided/min: %s", humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute))) + col2 = append(col2, fmt.Sprintf(" CIDs reprovided/min: %s", humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute))) + col2 = append(col2, fmt.Sprintf(" Region reprovide duration: %s", humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration))) + col2 = append(col2, fmt.Sprintf(" Avg CIDs/reprovide: %s", humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide))) + col2 = append(col2, fmt.Sprintf(" Regions reprovided (last cycle): %s", humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle))) + col2 = append(col2, "") + + // Column 2: Timings + col2 = append(col2, "Timings:") + col2 = append(col2, fmt.Sprintf(" Uptime: %s (%s)", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime)))) + col2 = append(col2, fmt.Sprintf(" Current time offset: %s", humanDuration(s.Sweep.Timing.CurrentTimeOffset))) + col2 = append(col2, fmt.Sprintf(" Cycle started: %s", humanTime(s.Sweep.Timing.CycleStart))) + col2 = append(col2, fmt.Sprintf(" Reprovide interval: %s", humanDuration(s.Sweep.Timing.ReprovidesInterval))) + + // Print both columns side by side + maxRows := max(len(col1), len(col2)) + for i := range maxRows { + var left, right string + if i < len(col1) { + left = col1[i] + } + if i < len(col2) { + right = col2[i] + } + fmt.Fprintf(wtr, "%-*s %s\n", col1Width, left, right) + } + return nil + } + // Connectivity if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" { if !brief { @@ -235,7 +344,7 @@ This interface is not stable and may change from release to release. if since.IsZero() { fmt.Fprintf(wtr, "%sStatus:\t%s\n", indent, s.Sweep.Connectivity.Status) } else { - fmt.Fprintf(wtr, "%sStatus:\t%s (since %s)\n", indent, s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) + fmt.Fprintf(wtr, "%sStatus:\t%s (%s)\n", indent, s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) } if !brief { fmt.Fprintf(wtr, "\n") @@ -269,7 +378,7 @@ This interface is not stable and may change from release to release. fmt.Fprintf(wtr, "%sRegions scheduled:\t%s\n", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) if !brief { fmt.Fprintf(wtr, "%sAvg prefix length:\t%s\n", indent, humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) - fmt.Fprintf(wtr, "%sNext reprovide at:\t%s\n", indent, humanTime(s.Sweep.Schedule.NextReprovideAt)) + fmt.Fprintf(wtr, "%sNext reprovide at:\t%s\n", indent, s.Sweep.Schedule.NextReprovideAt.Format("15:04:05")) nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) if nextPrefix == "" { nextPrefix = "N/A" @@ -283,7 +392,7 @@ This interface is not stable and may change from release to release. // Timings if all || timings { fmt.Fprintf(wtr, "Timings:\n") - fmt.Fprintf(wtr, " Uptime:\t%s (since %s)\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) + fmt.Fprintf(wtr, " Uptime:\t%s (%s)\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) fmt.Fprintf(wtr, " Current time offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) fmt.Fprintf(wtr, " Cycle started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) fmt.Fprintf(wtr, " Reprovide interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) @@ -322,8 +431,8 @@ This interface is not stable and may change from release to release. indent = " " } // Ongoing operations - fmt.Fprintf(wtr, "%sCurrently providing:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) - fmt.Fprintf(wtr, "%sCurrently reproviding:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) + fmt.Fprintf(wtr, "%sOngoing provides:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) + fmt.Fprintf(wtr, "%sOngoing reprovides:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) // Past operations summary fmt.Fprintf(wtr, "%sTotal CIDs provided:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.KeysProvided)) if !brief { @@ -355,14 +464,14 @@ This interface is not stable and may change from release to release. if !brief && displayWorkers { indent = " " } - fmt.Fprintf(wtr, "%sActive workers:\t%s / %s (max)\n", indent, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) - fmt.Fprintf(wtr, "%sFree workers:\t%s\n", indent, humanNumber(availableFreeWorkers)) + fmt.Fprintf(wtr, "%sActive:\t%s / %s (max)\n", indent, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) + fmt.Fprintf(wtr, "%sFree:\t%s\n", indent, humanNumber(availableFreeWorkers)) if !brief && displayWorkers { - fmt.Fprintf(wtr, "%sWorker stats: %-11s %s\n", indent, "Periodic", "Burst") - fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) - fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) - fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) - fmt.Fprintf(wtr, "%s %-15s %-11s %s\n", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) + fmt.Fprintf(wtr, "%sWorker stats: %-9s %s\n", indent, "Periodic", "Burst") + fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) + fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) + fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) + fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) } else { // Brief mode - show condensed worker info fmt.Fprintf(wtr, "%sPeriodic:\t%s active, %s available, %s queued\n", indent, From 45aefcab4b17a3349e765a075b36e5192cf4237d Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 17:13:39 +0200 Subject: [PATCH 06/26] consolidating compact stat --- core/commands/provide.go | 315 ++++++++++++++------------------------- 1 file changed, 116 insertions(+), 199 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 5148380399f..17637619942 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "strings" "text/tabwriter" "time" @@ -228,224 +229,127 @@ This interface is not stable and may change from release to release. operations, _ := req.Options[provideStatOperationsOptionName].(bool) workers, _ := req.Options[provideStatWorkersOptionName].(bool) - brief := !all && !connectivity && !queues && !schedule && !network && !timings && !operations && !workers - - // Compact mode: display stats in 2 columns with fixed column 1 width - if all && compact { - const col1Width = 34 // Fixed width for column 1 - var col1, col2 []string - - availableReservedBurst := max(0, s.Sweep.Workers.DedicatedBurst-s.Sweep.Workers.ActiveBurst) - availableReservedPeriodic := max(0, s.Sweep.Workers.DedicatedPeriodic-s.Sweep.Workers.ActivePeriodic) - availableFreeWorkers := s.Sweep.Workers.Max - max(s.Sweep.Workers.DedicatedBurst, s.Sweep.Workers.ActiveBurst) - max(s.Sweep.Workers.DedicatedPeriodic, s.Sweep.Workers.ActivePeriodic) - availableBurst := availableFreeWorkers + availableReservedBurst - availablePeriodic := availableFreeWorkers + availableReservedPeriodic - - // Column 1: Schedule - col1 = append(col1, "Schedule:") - col1 = append(col1, fmt.Sprintf(" CIDs scheduled: %s", humanNumber(s.Sweep.Schedule.Keys))) - col1 = append(col1, fmt.Sprintf(" Regions scheduled: %s", humanNumberOrNA(s.Sweep.Schedule.Regions))) - col1 = append(col1, fmt.Sprintf(" Avg prefix length: %s", humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength))) - col1 = append(col1, fmt.Sprintf(" Next reprovide at: %s", s.Sweep.Schedule.NextReprovideAt.Format("15:04:05"))) - nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) - if nextPrefix == "" { - nextPrefix = "N/A" + flagCount := 0 + for _, b := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} { + if b { + flagCount++ } - col1 = append(col1, fmt.Sprintf(" Next prefix: %s", nextPrefix)) - col1 = append(col1, "") - - // Column 1: Network - col1 = append(col1, "Network:") - col1 = append(col1, fmt.Sprintf(" Avg record holders: %s", humanFloatOrNA(s.Sweep.Network.AvgHolders))) - col1 = append(col1, fmt.Sprintf(" Peers swept: %s", humanNumber(s.Sweep.Network.Peers))) - if s.Sweep.Network.Peers > 0 { - col1 = append(col1, fmt.Sprintf(" Reachable peers: %s (%s%%)", humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers))) - } else { - col1 = append(col1, fmt.Sprintf(" Reachable peers: %s", humanNumber(s.Sweep.Network.Reachable))) + } + brief := flagCount == 0 + showHeadings := flagCount > 1 || all + + compactMode := all && compact + var cols [2][]string + formatLine := func(col int, format string, a ...any) { + if compactMode { + cols[col] = append(cols[col], fmt.Sprintf(format, a...)) + return } - col1 = append(col1, fmt.Sprintf(" Avg region size: %s", humanFloatOrNA(s.Sweep.Network.AvgRegionSize))) - col1 = append(col1, fmt.Sprintf(" Full keyspace coverage: %t", s.Sweep.Network.CompleteKeyspaceCoverage)) - col1 = append(col1, fmt.Sprintf(" Replication factor: %s", humanNumber(s.Sweep.Network.ReplicationFactor))) - col1 = append(col1, "") - - // Column 1: Workers - col1 = append(col1, "Workers:") - col1 = append(col1, fmt.Sprintf(" Active: %s / %s (max)", humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max))) - col1 = append(col1, fmt.Sprintf(" Free: %s", humanNumber(availableFreeWorkers))) - col1 = append(col1, fmt.Sprintf(" Worker stats: %-9s %s", "Periodic", "Burst")) - col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst))) - col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst))) - col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst))) - col1 = append(col1, fmt.Sprintf(" %-12s %-9s %s", "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst))) - col1 = append(col1, fmt.Sprintf(" Max connections/worker: %s", humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker))) - - // Column 2: Connectivity - col2 = append(col2, "Connectivity:") - since := s.Sweep.Connectivity.Since - if since.IsZero() { - col2 = append(col2, fmt.Sprintf(" Status: %s", s.Sweep.Connectivity.Status)) - } else { - col2 = append(col2, fmt.Sprintf(" Status: %s (%s)", s.Sweep.Connectivity.Status, humanTime(since))) + format = strings.Replace(format, ": ", ":\t", 1) + format = strings.Replace(format, ", ", ",\t", 1) + cols[0] = append(cols[0], fmt.Sprintf(format, a...)) + } + addBlankLine := func(col int) { + if !brief { + formatLine(col, "") } - col2 = append(col2, "") - - // Column 2: Queues - col2 = append(col2, "Queues:") - col2 = append(col2, fmt.Sprintf(" Provide queue: %s CIDs, %s regions", humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides))) - col2 = append(col2, fmt.Sprintf(" Reprovide queue: %s regions", humanNumber(s.Sweep.Queues.PendingRegionReprovides))) - col2 = append(col2, "") - - // Column 2: Operations - col2 = append(col2, "Operations:") - col2 = append(col2, fmt.Sprintf(" Ongoing provides: %s CIDs, %s regions", humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides))) - col2 = append(col2, fmt.Sprintf(" Ongoing reprovides: %s CIDs, %s regions", humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides))) - col2 = append(col2, fmt.Sprintf(" Total CIDs provided: %s", humanNumber(s.Sweep.Operations.Past.KeysProvided))) - col2 = append(col2, fmt.Sprintf(" Total records provided: %s", humanNumber(s.Sweep.Operations.Past.RecordsProvided))) - col2 = append(col2, fmt.Sprintf(" Total provide errors: %s", humanNumber(s.Sweep.Operations.Past.KeysFailed))) - col2 = append(col2, fmt.Sprintf(" CIDs provided/min: %s", humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute))) - col2 = append(col2, fmt.Sprintf(" CIDs reprovided/min: %s", humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute))) - col2 = append(col2, fmt.Sprintf(" Region reprovide duration: %s", humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration))) - col2 = append(col2, fmt.Sprintf(" Avg CIDs/reprovide: %s", humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide))) - col2 = append(col2, fmt.Sprintf(" Regions reprovided (last cycle): %s", humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle))) - col2 = append(col2, "") - - // Column 2: Timings - col2 = append(col2, "Timings:") - col2 = append(col2, fmt.Sprintf(" Uptime: %s (%s)", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime)))) - col2 = append(col2, fmt.Sprintf(" Current time offset: %s", humanDuration(s.Sweep.Timing.CurrentTimeOffset))) - col2 = append(col2, fmt.Sprintf(" Cycle started: %s", humanTime(s.Sweep.Timing.CycleStart))) - col2 = append(col2, fmt.Sprintf(" Reprovide interval: %s", humanDuration(s.Sweep.Timing.ReprovidesInterval))) - - // Print both columns side by side - maxRows := max(len(col1), len(col2)) - for i := range maxRows { - var left, right string - if i < len(col1) { - left = col1[i] - } - if i < len(col2) { - right = col2[i] - } - fmt.Fprintf(wtr, "%-*s %s\n", col1Width, left, right) + } + sectionTitle := func(col int, title string) { + if !brief && showHeadings { + formatLine(col, title+":") } - return nil + } + + indent := " " + if brief || !showHeadings { + indent = "" } // Connectivity if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" { - if !brief { - fmt.Fprintf(wtr, "Connectivity:\n") - } + sectionTitle(1, "Connectivity") since := s.Sweep.Connectivity.Since - indent := "" - if !brief { - indent = " " - } if since.IsZero() { - fmt.Fprintf(wtr, "%sStatus:\t%s\n", indent, s.Sweep.Connectivity.Status) + formatLine(1, "%sStatus: %s", indent, s.Sweep.Connectivity.Status) } else { - fmt.Fprintf(wtr, "%sStatus:\t%s (%s)\n", indent, s.Sweep.Connectivity.Status, humanTime(s.Sweep.Connectivity.Since)) - } - if !brief { - fmt.Fprintf(wtr, "\n") + formatLine(1, "%sStatus: %s (%s)", indent, s.Sweep.Connectivity.Status, humanTime(since)) } + addBlankLine(1) } + // Queues if all || queues || brief { - if !brief { - fmt.Fprintf(wtr, "Queues:\n") - } - indent := "" - if !brief { - indent = " " - } - fmt.Fprintf(wtr, "%sProvide queue:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) - fmt.Fprintf(wtr, "%sReprovide queue:\t%s regions\n", indent, humanNumber(s.Sweep.Queues.PendingRegionReprovides)) - if !brief { - fmt.Fprintf(wtr, "\n") - } + sectionTitle(1, "Queues") + formatLine(1, "%sProvide queue: %s CIDs, %s regions", indent, humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides)) + formatLine(1, "%sReprovide queue: %s regions", indent, humanNumber(s.Sweep.Queues.PendingRegionReprovides)) + addBlankLine(1) } + // Schedule if all || schedule || brief { + sectionTitle(0, "Schedule") + formatLine(0, "%sCIDs scheduled: %s", indent, humanNumber(s.Sweep.Schedule.Keys)) + formatLine(0, "%sRegions scheduled: %s", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) if !brief { - fmt.Fprintf(wtr, "Schedule:\n") - } - indent := "" - if !brief { - indent = " " - } - fmt.Fprintf(wtr, "%sCIDs scheduled:\t%s\n", indent, humanNumber(s.Sweep.Schedule.Keys)) - fmt.Fprintf(wtr, "%sRegions scheduled:\t%s\n", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) - if !brief { - fmt.Fprintf(wtr, "%sAvg prefix length:\t%s\n", indent, humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) - fmt.Fprintf(wtr, "%sNext reprovide at:\t%s\n", indent, s.Sweep.Schedule.NextReprovideAt.Format("15:04:05")) + formatLine(0, "%sAvg prefix length: %s", indent, humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) + formatLine(0, "%sNext reprovide at: %s", indent, s.Sweep.Schedule.NextReprovideAt.Format("15:04:05")) nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) if nextPrefix == "" { nextPrefix = "N/A" } - fmt.Fprintf(wtr, "%sNext prefix:\t%s\n", indent, nextPrefix) - } - if !brief { - fmt.Fprintf(wtr, "\n") + formatLine(0, "%sNext prefix: %s", indent, nextPrefix) } + addBlankLine(0) } + // Timings if all || timings { - fmt.Fprintf(wtr, "Timings:\n") - fmt.Fprintf(wtr, " Uptime:\t%s (%s)\n", humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) - fmt.Fprintf(wtr, " Current time offset:\t%s\n", humanDuration(s.Sweep.Timing.CurrentTimeOffset)) - fmt.Fprintf(wtr, " Cycle started:\t%s\n", humanTime(s.Sweep.Timing.CycleStart)) - fmt.Fprintf(wtr, " Reprovide interval:\t%s\n", humanDuration(s.Sweep.Timing.ReprovidesInterval)) - fmt.Fprintf(wtr, "\n") + sectionTitle(1, "Timings") + formatLine(1, "%sUptime: %s (%s)", indent, humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime))) + formatLine(1, "%sCurrent time offset: %s", indent, humanDuration(s.Sweep.Timing.CurrentTimeOffset)) + formatLine(1, "%sCycle started: %s", indent, humanTime(s.Sweep.Timing.CycleStart)) + formatLine(1, "%sReprovide interval: %s", indent, humanDuration(s.Sweep.Timing.ReprovidesInterval)) + addBlankLine(1) } + // Network if all || network || brief { + sectionTitle(0, "Network") + formatLine(0, "%sAvg record holders: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgHolders)) if !brief { - fmt.Fprintf(wtr, "Network:\n") - } - indent := "" - if !brief { - indent = " " - } - fmt.Fprintf(wtr, "%sAvg record holders:\t%s\n", indent, humanFloatOrNA(s.Sweep.Network.AvgHolders)) - if !brief { - fmt.Fprintf(wtr, "%sPeers swept:\t%s\n", indent, humanNumber(s.Sweep.Network.Peers)) + formatLine(0, "%sPeers swept: %s", indent, humanNumber(s.Sweep.Network.Peers)) if s.Sweep.Network.Peers > 0 { - fmt.Fprintf(wtr, "%sReachable peers:\t%s (%s%%)\n", indent, humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers)) + formatLine(0, "%sReachable peers: %s (%s%%)", indent, humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers)) } else { - fmt.Fprintf(wtr, "%sReachable peers:\t%s\n", indent, humanNumber(s.Sweep.Network.Reachable)) + formatLine(0, "%sReachable peers: %s", indent, humanNumber(s.Sweep.Network.Reachable)) } - fmt.Fprintf(wtr, "%sAvg region size:\t%s\n", indent, humanFloatOrNA(s.Sweep.Network.AvgRegionSize)) - fmt.Fprintf(wtr, "%sFull keyspace coverage:\t%t\n", indent, s.Sweep.Network.CompleteKeyspaceCoverage) - fmt.Fprintf(wtr, "%sReplication factor:\t%s\n", indent, humanNumber(s.Sweep.Network.ReplicationFactor)) - fmt.Fprintf(wtr, "\n") + formatLine(0, "%sAvg region size: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgRegionSize)) + formatLine(0, "%sFull keyspace coverage: %t", indent, s.Sweep.Network.CompleteKeyspaceCoverage) + formatLine(0, "%sReplication factor: %s", indent, humanNumber(s.Sweep.Network.ReplicationFactor)) + addBlankLine(0) } } + // Operations if all || operations || brief { - if !brief { - fmt.Fprintf(wtr, "Operations:\n") - } - indent := "" - if !brief { - indent = " " - } + sectionTitle(1, "Operations") // Ongoing operations - fmt.Fprintf(wtr, "%sOngoing provides:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) - fmt.Fprintf(wtr, "%sOngoing reprovides:\t%s CIDs,\t%s regions\n", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) + formatLine(1, "%sOngoing provides: %s CIDs, %s regions", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides)) + formatLine(1, "%sOngoing reprovides: %s CIDs, %s regions", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides)) // Past operations summary - fmt.Fprintf(wtr, "%sTotal CIDs provided:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.KeysProvided)) + formatLine(1, "%sTotal CIDs provided: %s", indent, humanNumber(s.Sweep.Operations.Past.KeysProvided)) if !brief { - fmt.Fprintf(wtr, "%sTotal records provided:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.RecordsProvided)) - fmt.Fprintf(wtr, "%sTotal provide errors:\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.KeysFailed)) - fmt.Fprintf(wtr, "%sCIDs provided/min:\t%s\n", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute)) - fmt.Fprintf(wtr, "%sCIDs reprovided/min:\t%s\n", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) - fmt.Fprintf(wtr, "%sRegion reprovide duration:\t%s\n", indent, humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration)) - fmt.Fprintf(wtr, "%sAvg CIDs/reprovide:\t%s\n", indent, humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide)) - fmt.Fprintf(wtr, "%sRegions reprovided (last cycle):\t%s\n", indent, humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle)) - fmt.Fprintf(wtr, "\n") + formatLine(1, "%sTotal records provided: %s", indent, humanNumber(s.Sweep.Operations.Past.RecordsProvided)) + formatLine(1, "%sTotal provide errors: %s", indent, humanNumber(s.Sweep.Operations.Past.KeysFailed)) + formatLine(1, "%sCIDs provided/min: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute)) + formatLine(1, "%sCIDs reprovided/min: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute)) + formatLine(1, "%sRegion reprovide duration: %s", indent, humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration)) + formatLine(1, "%sAvg CIDs/reprovide: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide)) + formatLine(1, "%sRegions reprovided (last cycle): %s", indent, humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle)) + addBlankLine(1) } } + // Workers displayWorkers := all || workers if displayWorkers || brief { @@ -454,41 +358,54 @@ This interface is not stable and may change from release to release. availableFreeWorkers := s.Sweep.Workers.Max - max(s.Sweep.Workers.DedicatedBurst, s.Sweep.Workers.ActiveBurst) - max(s.Sweep.Workers.DedicatedPeriodic, s.Sweep.Workers.ActivePeriodic) availableBurst := availableFreeWorkers + availableReservedBurst availablePeriodic := availableFreeWorkers + availableReservedPeriodic + if displayWorkers || availableBurst <= 2 || availablePeriodic <= 2 { // Either we want to display workers information, or we are low on // available workers and want to warn the user. - if !brief && displayWorkers { - fmt.Fprintf(wtr, "Workers:\n") - } - indent := "" - if !brief && displayWorkers { - indent = " " + sectionTitle(0, "Workers") + specifyWorkers := " workers" + if compactMode { + specifyWorkers = "" } - fmt.Fprintf(wtr, "%sActive:\t%s / %s (max)\n", indent, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) - fmt.Fprintf(wtr, "%sFree:\t%s\n", indent, humanNumber(availableFreeWorkers)) - if !brief && displayWorkers { - fmt.Fprintf(wtr, "%sWorker stats: %-9s %s\n", indent, "Periodic", "Burst") - fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) - fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) - fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) - fmt.Fprintf(wtr, "%s %-14s %-9s %s\n", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) - } else { + formatLine(0, "%sActive%s: %s / %s (max)", indent, specifyWorkers, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max)) + if brief { // Brief mode - show condensed worker info - fmt.Fprintf(wtr, "%sPeriodic:\t%s active, %s available, %s queued\n", indent, + formatLine(0, "%sPeriodic%s: %s active, %s available, %s queued", indent, specifyWorkers, humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(availablePeriodic), humanNumber(s.Sweep.Workers.QueuedPeriodic)) - fmt.Fprintf(wtr, "%sBurst:\t%s active, %s available, %s queued\n", indent, + formatLine(0, "%sBurst%s: %s active, %s available, %s queued\n", indent, specifyWorkers, humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) + } else { + formatLine(0, "%sFree%s: %s", indent, specifyWorkers, humanNumber(availableFreeWorkers)) + formatLine(0, "%sWorker stats: %-9s %s", indent, "Periodic", "Burst") + formatLine(0, "%s %-14s %-9s %s", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) + formatLine(0, "%s %-14s %-9s %s", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) + formatLine(0, "%s %-14s %-9s %s", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) + formatLine(0, "%s %-14s %-9s %s", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst)) + formatLine(0, "%sMax connections/worker: %s", indent, humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) + addBlankLine(0) } } - if displayWorkers { - indent := "" - if !brief { - indent = " " + } + if compactMode { + col1Width := 34 // Fixed width for column 1 + // Print both columns side by side + maxRows := max(len(cols[0]), len(cols[1])) + for i := range maxRows - 1 { // last line is empty + var left, right string + if i < len(cols[0]) { + left = cols[0][i] } - fmt.Fprintf(wtr, "%sMax connections/worker:\t%s\n", indent, humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker)) - if !brief { - fmt.Fprintf(wtr, "\n") + if i < len(cols[1]) { + right = cols[1][i] } + fmt.Fprintf(wtr, "%-*s %s\n", col1Width, left, right) + } + } else { + if !brief { + cols[0] = cols[0][:len(cols[0])-1] // remove last blank line + } + for _, line := range cols[0] { + fmt.Fprintln(wtr, line) } } return nil From 68eba07bc050af18cbd22e2b95682c0db91194e2 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 8 Oct 2025 21:57:16 +0200 Subject: [PATCH 07/26] update column alignment --- core/commands/provide.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 17637619942..ebd0815e55f 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -240,9 +240,12 @@ This interface is not stable and may change from release to release. compactMode := all && compact var cols [2][]string + col0MaxWidth := 0 formatLine := func(col int, format string, a ...any) { if compactMode { - cols[col] = append(cols[col], fmt.Sprintf(format, a...)) + s := fmt.Sprintf(format, a...) + cols[col] = append(cols[col], s) + col0MaxWidth = max(col0MaxWidth, len(s)) return } format = strings.Replace(format, ": ", ":\t", 1) @@ -387,7 +390,7 @@ This interface is not stable and may change from release to release. } } if compactMode { - col1Width := 34 // Fixed width for column 1 + col0Width := col0MaxWidth + 2 // Print both columns side by side maxRows := max(len(cols[0]), len(cols[1])) for i := range maxRows - 1 { // last line is empty @@ -398,7 +401,7 @@ This interface is not stable and may change from release to release. if i < len(cols[1]) { right = cols[1][i] } - fmt.Fprintf(wtr, "%-*s %s\n", col1Width, left, right) + fmt.Fprintf(wtr, "%-*s %s\n", col0Width, left, right) } } else { if !brief { From 808ac0083d814175c90d99e8ce433ca38cdee528 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 9 Oct 2025 09:35:36 +0200 Subject: [PATCH 08/26] flags combinations errors --- core/commands/provide.go | 47 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index ebd0815e55f..d835060d302 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -149,11 +149,11 @@ This interface is not stable and may change from release to release. } lanStats, _ := req.Options[provideLanOptionName].(bool) - compact, _ := req.Options[provideStatCompactOptionName].(bool) - all, _ := req.Options[provideStatAllOptionName].(bool) - if compact && !all { - return fmt.Errorf("--compact flag requires --all flag") + if lanStats { + if _, ok := nd.Provider.(*dual.SweepingProvider); !ok { + return errors.New("LAN DHT stats only available for Sweep+Dual DHT") + } } var sweepingProvider *provider.SweepingProvider @@ -199,7 +199,27 @@ This interface is not stable and may change from release to release. wtr := tabwriter.NewWriter(w, 1, 2, 1, ' ', 0) defer wtr.Flush() + all, _ := req.Options[provideStatAllOptionName].(bool) + compact, _ := req.Options[provideStatCompactOptionName].(bool) + connectivity, _ := req.Options[provideStatConnectivityOptionName].(bool) + queues, _ := req.Options[provideStatQueuesOptionName].(bool) + schedule, _ := req.Options[provideStatScheduleOptionName].(bool) + network, _ := req.Options[provideStatNetworkOptionName].(bool) + timings, _ := req.Options[provideStatTimingsOptionName].(bool) + operations, _ := req.Options[provideStatOperationsOptionName].(bool) + workers, _ := req.Options[provideStatWorkersOptionName].(bool) + + flagCount := 0 + for _, b := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} { + if b { + flagCount++ + } + } + if s.Legacy != nil { + if flagCount > 0 { + return errors.New("cannot use flags with legacy provide stats") + } fmt.Fprintf(wtr, "TotalReprovides:\t%s\n", humanNumber(s.Legacy.TotalReprovides)) fmt.Fprintf(wtr, "AvgReprovideDuration:\t%s\n", humanDuration(s.Legacy.AvgReprovideDuration)) fmt.Fprintf(wtr, "LastReprovideDuration:\t%s\n", humanDuration(s.Legacy.LastReprovideDuration)) @@ -211,30 +231,21 @@ This interface is not stable and may change from release to release. } return nil } + if s.Sweep == nil { return errors.New("no provide stats available") } + // Sweep provider stats if s.Sweep.Closed { fmt.Fprintf(wtr, "Provider is closed\n") return nil } - all, _ := req.Options[provideStatAllOptionName].(bool) - compact, _ := req.Options[provideStatCompactOptionName].(bool) - connectivity, _ := req.Options[provideStatConnectivityOptionName].(bool) - queues, _ := req.Options[provideStatQueuesOptionName].(bool) - schedule, _ := req.Options[provideStatScheduleOptionName].(bool) - network, _ := req.Options[provideStatNetworkOptionName].(bool) - timings, _ := req.Options[provideStatTimingsOptionName].(bool) - operations, _ := req.Options[provideStatOperationsOptionName].(bool) - workers, _ := req.Options[provideStatWorkersOptionName].(bool) - flagCount := 0 - for _, b := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} { - if b { - flagCount++ - } + if compact && !all { + return errors.New("--compact flag requires --all flag") } + brief := flagCount == 0 showHeadings := flagCount > 1 || all From c9b9ec62d77decdfd3c7a468cc957b23fefb73f7 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 9 Oct 2025 10:07:29 +0200 Subject: [PATCH 09/26] command description --- core/commands/provide.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/commands/provide.go b/core/commands/provide.go index d835060d302..122a54718c2 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -121,6 +121,30 @@ Returns statistics about the content the node is reproviding every Provide.DHT.Interval according to Provide.Strategy: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide +This command displays statistics for the provide system currently in use +(Sweep or Legacy). If using the Legacy provider, basic statistics are shown +and no flags are supported. The following behavior applies to the Sweep +provider only: + +By default, displays a brief summary of key metrics including queue sizes, +scheduled CIDs/regions, average record holders, ongoing/total provides, and +worker status (if low on workers). + +Use --all to display comprehensive statistics organized into sections: +connectivity (DHT status), queues (pending provides/reprovides), schedule +(CIDs/regions to reprovide), timings (uptime, cycle info), network (peers, +reachability, region size), operations (provide rates, errors), and workers +(pool utilization). + +Individual sections can be displayed using their respective flags (e.g., +--network, --operations, --workers). Multiple section flags can be combined. + +The --compact flag provides a 2-column layout suitable for monitoring with +'watch' (requires --all). Example: watch ipfs provide stat --all --compact + +For Dual DHT setups, use --lan to show statistics for the LAN DHT provider +instead of the default WAN DHT provider. + This interface is not stable and may change from release to release. `, From 45fb9b6055f847836b65766222b8198311c1785f Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 9 Oct 2025 10:09:45 +0200 Subject: [PATCH 10/26] change schedule AvgPrefixLen to float --- core/commands/provide.go | 2 +- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 122a54718c2..2f7bfb605e1 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -329,7 +329,7 @@ This interface is not stable and may change from release to release. formatLine(0, "%sCIDs scheduled: %s", indent, humanNumber(s.Sweep.Schedule.Keys)) formatLine(0, "%sRegions scheduled: %s", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) if !brief { - formatLine(0, "%sAvg prefix length: %s", indent, humanNumberOrNA(s.Sweep.Schedule.AvgPrefixLength)) + formatLine(0, "%sAvg prefix length: %s", indent, humanFloatOrNA(s.Sweep.Schedule.AvgPrefixLength)) formatLine(0, "%sNext reprovide at: %s", indent, s.Sweep.Schedule.NextReprovideAt.Format("15:04:05")) nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) if nextPrefix == "" { diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index bc58bdb9537..ca0d3430c39 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -115,7 +115,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.14.2 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 0a150984068..608a8510789 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -434,8 +434,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 h1:7b7vW4qwok2+8ANRAsDFuVboEH3FMBNB4RHKScp5fZ4= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70 h1:INcgbKoHBKND1ynmIhp2Tnq3n2rc+GC5Ll1hZqWswEE= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index 233a29934a8..eb20cf64e1a 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.43.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70 github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.14.2 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index 547745e3279..5b17357da07 100644 --- a/go.sum +++ b/go.sum @@ -518,8 +518,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 h1:7b7vW4qwok2+8ANRAsDFuVboEH3FMBNB4RHKScp5fZ4= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70 h1:INcgbKoHBKND1ynmIhp2Tnq3n2rc+GC5Ll1hZqWswEE= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 075ea6f3bcf..d49ff02a13f 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -184,7 +184,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.43.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index ab536b7ae7e..5999b3dd72d 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -468,8 +468,8 @@ github.com/libp2p/go-libp2p v0.43.0 h1:b2bg2cRNmY4HpLK8VHYQXLX2d3iND95OjodLFymvq github.com/libp2p/go-libp2p v0.43.0/go.mod h1:IiSqAXDyP2sWH+J2gs43pNmB/y4FOi2XQPbsb+8qvzc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863 h1:7b7vW4qwok2+8ANRAsDFuVboEH3FMBNB4RHKScp5fZ4= -github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251008112333-28b59f8b1863/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70 h1:INcgbKoHBKND1ynmIhp2Tnq3n2rc+GC5Ll1hZqWswEE= +github.com/libp2p/go-libp2p-kad-dht v0.35.2-0.20251009080632-2c5b3769ca70/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From 366f444fb4f1c301b08f4df3ac2bc07d2dfc9e8c Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 9 Oct 2025 10:19:06 +0200 Subject: [PATCH 11/26] changelog --- docs/changelogs/v0.39.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/changelogs/v0.39.md b/docs/changelogs/v0.39.md index b35a6ecaea0..072694b122f 100644 --- a/docs/changelogs/v0.39.md +++ b/docs/changelogs/v0.39.md @@ -10,6 +10,8 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. - [Overview](#overview) - [ðŸ”Ķ Highlights](#-highlights) + - [📊 Comprehensive statistics for Sweep provider with `ipfs provide stat`](#-comprehensive-statistics-for-sweep-provider-with-ipfs-provide-stat) + - [ðŸŠĶ Deprecated `go-ipfs` name no longer published](#-deprecated-go-ipfs-name-no-longer-published) - [ðŸ“Ķïļ Important dependency updates](#-important-dependency-updates) - [📝 Changelog](#-changelog) - [ðŸ‘Ļ‍ðŸ‘Đ‍👧‍ðŸ‘Ķ Contributors](#-contributors) @@ -18,13 +20,48 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. ### ðŸ”Ķ Highlights +#### 📊 Comprehensive statistics for Sweep provider with `ipfs provide stat` + +The experimental Sweep provider system ([introduced in +v0.38](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.38.md#-experimental-sweeping-dht-provider)) +now has comprehensive statistics available through `ipfs provide stat`. + +**Default behavior:** Displays a brief summary showing queue sizes, scheduled +CIDs/regions, average record holders, ongoing/total provides, and worker status +when resources are constrained. + +**Detailed statistics with `--all`:** View comprehensive metrics organized into sections: + +- **Connectivity**: DHT connection status +- **Queues**: Pending provide and reprovide operations +- **Schedule**: CIDs/regions scheduled for reprovide +- **Timings**: Uptime, reprovide cycle information +- **Network**: Peer statistics, keyspace region sizes +- **Operations**: Ongoing and past provides, rates, errors +- **Workers**: Worker pool utilization and availability + +**Flexible monitoring:** Individual sections can be displayed using flags like +`--network`, `--operations`, or `--workers`. Multiple flags can be combined for +custom views. The `--compact` flag provides a 2-column layout ideal for +continuous monitoring with `watch ipfs provide stat --all --compact`. + +**Dual DHT support:** For Dual DHT configurations, use `--lan` to view LAN DHT +provider statistics instead of the default WAN DHT stats. + +> [!NOTE] +> These statistics are only available when using the Sweep provider system +> (enabled via +> [`Provide.DHT.SweepEnabled`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtsweepenabled)). +> Legacy provider shows basic statistics without flag support. + #### ðŸŠĶ Deprecated `go-ipfs` name no longer published The `go-ipfs` name was deprecated in 2022 and renamed to `kubo`. Starting with this release, we have stopped publishing Docker images and distribution binaries under the old `go-ipfs` name. Existing users should switch to: + - Docker: `ipfs/kubo` image (instead of `ipfs/go-ipfs`) -- Binaries: download from https://dist.ipfs.tech/kubo/ or https://github.com/ipfs/kubo/releases +- Binaries: download from or For Docker users, the legacy `ipfs/go-ipfs` image name now shows a deprecation notice directing you to `ipfs/kubo`. From 0f9f37c4a9f400caae5380ce8c410f80b9d950f8 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 9 Oct 2025 10:39:22 +0200 Subject: [PATCH 12/26] alignments --- core/commands/provide.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 2f7bfb605e1..a3936f62f4e 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -330,7 +330,11 @@ This interface is not stable and may change from release to release. formatLine(0, "%sRegions scheduled: %s", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) if !brief { formatLine(0, "%sAvg prefix length: %s", indent, humanFloatOrNA(s.Sweep.Schedule.AvgPrefixLength)) - formatLine(0, "%sNext reprovide at: %s", indent, s.Sweep.Schedule.NextReprovideAt.Format("15:04:05")) + nextReprovideAt := s.Sweep.Schedule.NextReprovideAt.Format("15:04:05") + if s.Sweep.Schedule.NextReprovideAt.IsZero() { + nextReprovideAt = "N/A" + } + formatLine(0, "%sNext reprovide at: %s", indent, nextReprovideAt) nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) if nextPrefix == "" { nextPrefix = "N/A" @@ -414,7 +418,7 @@ This interface is not stable and may change from release to release. humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) } else { formatLine(0, "%sFree%s: %s", indent, specifyWorkers, humanNumber(availableFreeWorkers)) - formatLine(0, "%sWorker stats: %-9s %s", indent, "Periodic", "Burst") + formatLine(0, "%sWorker stats:%s %-9s %s", indent, " ", "Periodic", "Burst") formatLine(0, "%s %-14s %-9s %s", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) formatLine(0, "%s %-14s %-9s %s", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) formatLine(0, "%s %-14s %-9s %s", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) From c8ee0d5e23b9f77b2869bdb49504830d35d3f9bc Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 9 Oct 2025 12:53:13 +0200 Subject: [PATCH 13/26] sweep: slow reprovide alerts --- core/node/provider.go | 108 +++++++++++++++++++++++++++++++++++++++ docs/changelogs/v0.39.md | 16 ++++++ 2 files changed, 124 insertions(+) diff --git a/core/node/provider.go b/core/node/provider.go index 2c77e580c8d..18e1cda261c 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -507,10 +507,118 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { }, }) }) + type alertInput struct { + fx.In + Provider DHTProvider + } + reprovideAlert := fx.Invoke(func(lc fx.Lifecycle, in alertInput) { + var ( + cancel context.CancelFunc + done = make(chan struct{}) + ) + + // Select Sweeping Provider to get the stats from. + var prov *dhtprovider.SweepingProvider + switch p := in.Provider.(type) { + case *ddhtprovider.SweepingProvider: + prov = p.WAN + case *dhtprovider.SweepingProvider: + prov = p + case *buffered.SweepingProvider: + switch inner := p.Provider.(type) { + case *ddhtprovider.SweepingProvider: + prov = inner.WAN + case *dhtprovider.SweepingProvider: + prov = inner + } + } + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + if prov == nil { + return nil + } + gcCtx, c := context.WithCancel(context.Background()) + cancel = c + go func() { + defer close(done) + + // Poll stats every 15 minutes + pollInterval := 15 * time.Minute + ticker := time.NewTicker(pollInterval) + var queueSize, prevQueueSize, count int + var queuedWorkers, prevQueuedWorkers bool + for { + select { + case <-gcCtx.Done(): + return + case <-ticker.C: + } + + stats := prov.Stats() + queuedWorkers = stats.Workers.QueuedPeriodic > 0 + queueSize = stats.Queues.PendingRegionReprovides + // If reprovide queue size keeps growing and workers are not + // keeping up, print warning message + if prevQueuedWorkers && queuedWorkers && queueSize > prevQueueSize { + count++ + if count > 2 { + logger.Errorf(` +🔔🔔🔔 Reprovide Operations Too Slow 🔔🔔🔔 + +Your node is falling behind on DHT reprovides, which will affect content availability. + +Keyspace regions enqueued for reprovide: + %s ago:\t%d + Now:\t%d + +All periodic workers are busy! + Active workers:\t%d / %d (max) + Active workers types:\t%d periodic, %d burst + Dedicated workers:\t%d periodic, %d burst + +Solutions (try in order): +1. Increase Provide.DHT.MaxWorkers (current %d) +2. Increase Provide.DHT.DedicatedPeriodicWorkers (current %d) +3. Set Provide.DHT.SweepEnabled=false and Routing.AcceleratedDHTClient=true (last resort, not recommended) + +See how the reprovide queue is processed in real-time with 'watch ipfs provide stat --all --compact' + +See docs: https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtmaxworkers`, + pollInterval.Truncate(time.Minute).String(), prevQueueSize, queueSize, + stats.Workers.Active, stats.Workers.Max, + stats.Workers.ActivePeriodic, stats.Workers.ActiveBurst, + stats.Workers.DedicatedPeriodic, stats.Workers.DedicatedBurst, + stats.Workers.Max, stats.Workers.DedicatedPeriodic) + } + } else if !queuedWorkers { + count = 0 + } + + prevQueueSize, prevQueuedWorkers = queueSize, queuedWorkers + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { + // Cancel the alert loop + if cancel != nil { + cancel() + } + select { + case <-done: + case <-ctx.Done(): + return ctx.Err() + } + return nil + }, + }) + }) return fx.Options( sweepingReprovider, initKeystore, + reprovideAlert, ) } diff --git a/docs/changelogs/v0.39.md b/docs/changelogs/v0.39.md index 072694b122f..4e41ed57ea7 100644 --- a/docs/changelogs/v0.39.md +++ b/docs/changelogs/v0.39.md @@ -11,6 +11,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. - [Overview](#overview) - [ðŸ”Ķ Highlights](#-highlights) - [📊 Comprehensive statistics for Sweep provider with `ipfs provide stat`](#-comprehensive-statistics-for-sweep-provider-with-ipfs-provide-stat) + - [🔔 Sweep provider slow reprovide warnings](#-sweep-provider-slow-reprovide-warnings) - [ðŸŠĶ Deprecated `go-ipfs` name no longer published](#-deprecated-go-ipfs-name-no-longer-published) - [ðŸ“Ķïļ Important dependency updates](#-important-dependency-updates) - [📝 Changelog](#-changelog) @@ -54,6 +55,21 @@ provider statistics instead of the default WAN DHT stats. > [`Provide.DHT.SweepEnabled`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtsweepenabled)). > Legacy provider shows basic statistics without flag support. +#### 🔔 Sweep provider slow reprovide warnings + +Kubo now monitors DHT reprovide operations when `Provide.DHT.SweepEnabled=true` +and alerts you if your node is falling behind on reprovides. + +When the reprovide queue consistently grows and all periodic workers are busy, +a warning displays with: + +- Queue size and worker utilization details +- Recommended solutions: increase `Provide.DHT.MaxWorkers` or `Provide.DHT.DedicatedPeriodicWorkers` +- Command to monitor real-time progress: `watch ipfs provide stat --all --compact` + +The alert polls every 15 minutes and only triggers after sustained growth +across multiple intervals. The legacy provider is unaffected by this change. + #### ðŸŠĶ Deprecated `go-ipfs` name no longer published The `go-ipfs` name was deprecated in 2022 and renamed to `kubo`. Starting with this release, we have stopped publishing Docker images and distribution binaries under the old `go-ipfs` name. From 69007628d71707a870687775db4a8ee7ba5587ca Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 15 Oct 2025 11:20:16 +0200 Subject: [PATCH 14/26] provide stat description draft --- core/commands/provide.go | 2 +- docs/provide-stats.md | 231 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 docs/provide-stats.md diff --git a/core/commands/provide.go b/core/commands/provide.go index a3936f62f4e..588d9c9c1f6 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -360,13 +360,13 @@ This interface is not stable and may change from release to release. formatLine(0, "%sAvg record holders: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgHolders)) if !brief { formatLine(0, "%sPeers swept: %s", indent, humanNumber(s.Sweep.Network.Peers)) + formatLine(0, "%sFull keyspace coverage: %t", indent, s.Sweep.Network.CompleteKeyspaceCoverage) if s.Sweep.Network.Peers > 0 { formatLine(0, "%sReachable peers: %s (%s%%)", indent, humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers)) } else { formatLine(0, "%sReachable peers: %s", indent, humanNumber(s.Sweep.Network.Reachable)) } formatLine(0, "%sAvg region size: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgRegionSize)) - formatLine(0, "%sFull keyspace coverage: %t", indent, s.Sweep.Network.CompleteKeyspaceCoverage) formatLine(0, "%sReplication factor: %s", indent, humanNumber(s.Sweep.Network.ReplicationFactor)) addBlankLine(0) } diff --git a/docs/provide-stats.md b/docs/provide-stats.md new file mode 100644 index 00000000000..6ba2a78d49c --- /dev/null +++ b/docs/provide-stats.md @@ -0,0 +1,231 @@ +# Provide Stats + +The `ipfs provide stat` command gives you statistics about your local provide +system. This file provides a detailed explanation of the metrics reported by +this command. + +## Connectivity + +### Status + +Provides the node's current connectivity status: `online`, `disconnected`, or +`offline`. A node is considered `disconnected`, if it has been recently online +in the last +[`Provide.DHT.OfflineDelay`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtofflinedelay). + +It also contains the timestamp of the last status change. + +## Queues + +### Provide queue + +Display the number of CIDs in the provide queue. In the queue, CIDs are grouped +by keyspace region. Also displays the number of keyspace regions in the queue. + +### Reprovide queue + +Shows the number of regions that are waiting to be reprovided. The regions +sitting in the reprovide queue are late to reprovide, and will be reprovided as +soon as possible. + +A high number of regions in the reprovide queue may indicate a few things: + +1. The node is currently `disconnected` or `offline`, and the reprovides that + should have executed are queued for when the node goes back `online`. +2. The node is currently processing the backlog of late reprovides after a + restart or a period of being `disconnected` or `offline`. +3. The provide system cannot keep up with the rate of late reprovides, if the + queue size keeps inscresing or doesn't decrease over time. This is usually + due to a high number of CIDs to be reprovided and a too low number of + (periodic) workers. Consider increasing the [`Provide.DHT.MaxWorkers`]() and + [`Provide.DHT.DedicatedPeriodicWorkers`](). + +## Schedule + +### CIDs scheduled + +Number of CIDs scheduled to be reprovided. + +### Regions scheduled + +Number of keyspace regions scheduled to be reprovided. Each CID is mapped to a +region, and reprovided as a batch with the other CIDs in the same region. + +### Avg prefix length + +Average prefix length of the scheduled regions. This is an indicator of the +number of DHT servers in the swarm. + +### Next reprovide at + +Timestamp of the next scheduled region reprovide. + +### Next prefix + +Next region prefix to be reprovided. + +## Timings + +### Uptime + +Uptime of the provide system, since Kubo was started. Also includes the +timestamp at which the provide system was started. + +### Current time offset + +Time offset in the current reprovide cycle. This metrics shows the progression +of the provide cycle. + +### Cycle started + +Timestamp of when the current reprovide cycle started. + +### Reprovide interval + +Duration of the reprovide cycle. This is the interval at which all CIDs are +reprovided. + +## Network + +### Avg record holders + +Each CID is sent to multiple DHT servers in the swarm. This metric shows the +average number of DHT servers that have been sent each CID. If this number +matches the [Replication factor](#replication-factor), it means that all CIDs +have been sent to the desired number of DHT servers. + +A lower number indicates that a proportion of the DHT network either isn't +reachable, timed out during the `ADD_PROVIDER` RPC, or doesn't support storing +provider records. + +Note that this metric only displays the number of replicas that were sent +successfully. It is possible that some of the records holders have gone +offline, and the actual number of nodes storing the provider records may be +lower. + +### Peers swept + +Number of DHT servers that were contacted during the last reprovide sweep +cycle. This doesn't include peers that were contacted during the initial +provide of a CID, not during a DHT lookup. It only includes peers that we tried +to send a provider record to. + +If providing CIDs to all keyspace regions (very likely beyond a certain number +of CIDs), this number is expected to grow during the initial reprovide cycle +(up to [`reprovide interval`](#reprovide-interval) after the node started). +After that, the number is expected to stabilize and show the actual size of the +DHT swarm. + +If providing a small number of CIDs, this number will be lower than the network +size, since it only considers the peers to which we sent provider records. + +### Full keyspace coverage + +Boolean value indicating whether the reprovide sweep has covered all the DHT +servers in the swarm or not. It `true` it means that the node has sent provider +records to all DHT servers in the swarm during the last reprovide cycle. + +It means that [`Peers swept`](#peers-swept) is an approximation of the DHT +swarm size over the last [`Reprovide Interval`](#reprovide-interval). + +### Reachable peers + +Number of reachable peers among the [`Peers swept`](#peers-swept). A reachable +peer is a peer that successfully responded to all the `ADD_PROVIDER` RPC that +we sent during the last reprovide cycle. + +Also includes the percentage of reachable peers among the [`Peers swept`](#peers-swept). + +### Avg region size + +Average number of DHT servers in each keyspace region. + +### Replication factor + +Number of DHT servers to which we send a provider record for each CID. + +## Operations + +### Ongoing provides + +Number of CIDs that are currently being provided for the first time. Also shows +the number of keyspace regions to which these CIDs belong. + +Having a higher number of CIDs than number of regions indicates that regions +contain multiple CIDs, which is a sign of efficient batching. + +Each keyspace region corresponds to a [burst worker](). + +### Ongoing reprovides + +Number of CIDs and keyspace regions that are currently being reprovided. + +Each region corresponds to a [periodic worker](). + +### Total CIDs provided + +Number of (non-distinct) CIDs that have been provided since the node started. +Also includes reprovides. + +### Total records provided + +Number of provider records that have successfully been sent to DHT servers +since the node started. Also includes reprovides. + +### Total provide errors + +Number of regions that have failed to be provided since the node started. Also +includes reprovides. Upon failure, the provide system will retry providing the +failed region, unless it is `disconnected` or `offline`, in which case the +retry happens when the node goes back `online`. + +### CIDs provided/min + +Average number of CIDs provided (excluding reprovides) per minute of provide +operation running in the last reprovide cycle. + +### CIDs reprovided/min + +Average number of CIDs reprovided (excluding initial provide) per minute of +reprovide operation running in the last reprovide cycle. + +### Region reprovide duration + +Average duration it took to reprovide all the CIDs in a keyspace region during +the last reprovide cycle. + +### Avg CIDs/reprovide + +Average number of CIDs that were reprovided in each keyspace region during the +last reprovide cycle. + +### Regions reprovided (last cycle) + +Number of keyspace regions that were reprovided during the last reprovide cycle. + +## Workers + +### Active workers + +Number of workers that are currently active, either processing a provide or a +reprovide operation. + +### Free workers + +Number of workers that are currently idle, and not reserved for a specific task +(periodic or burst). + +### Workers stats + +For each kind of worker (periodic and burst), shows the number of active, +dedicated, available and queued workers. The number of available workers is the +sum of idle dedicated workers and [`free workers`](#free-workers). The number +of queued workers can be either `0` or `1` since we don't try to take a new +worker until we successfully can take one. You can look at the [`Provide +queue`](#provide-queue) and [`Reprovide queue`](#reprovide-queue) to see how +many regions are waiting to be processed, which corresponds to queued workers. + +### Max connections/worker + +Maximum number of connections to DHT servers per worker when sending out +proivder records for a region. From ab31b4d2494662849c206f990db5b5e27b9b5c98 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 15 Oct 2025 13:09:49 +0200 Subject: [PATCH 15/26] rephrased provide-stats.md --- docs/provide-stats.md | 187 ++++++++++++++++-------------------------- 1 file changed, 72 insertions(+), 115 deletions(-) diff --git a/docs/provide-stats.md b/docs/provide-stats.md index 6ba2a78d49c..e5101a2ce51 100644 --- a/docs/provide-stats.md +++ b/docs/provide-stats.md @@ -8,224 +8,181 @@ this command. ### Status -Provides the node's current connectivity status: `online`, `disconnected`, or -`offline`. A node is considered `disconnected`, if it has been recently online -in the last -[`Provide.DHT.OfflineDelay`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtofflinedelay). - -It also contains the timestamp of the last status change. +Current connectivity status (`online`, `disconnected`, or `offline`) and when +it last changed (see [provide connectivity +status](./config.md#providedhtofflinedelay)). ## Queues ### Provide queue -Display the number of CIDs in the provide queue. In the queue, CIDs are grouped -by keyspace region. Also displays the number of keyspace regions in the queue. +Number of CIDs waiting for initial provide, and the number of keyspace regions +they're grouped into. ### Reprovide queue -Shows the number of regions that are waiting to be reprovided. The regions -sitting in the reprovide queue are late to reprovide, and will be reprovided as -soon as possible. - -A high number of regions in the reprovide queue may indicate a few things: - -1. The node is currently `disconnected` or `offline`, and the reprovides that - should have executed are queued for when the node goes back `online`. -2. The node is currently processing the backlog of late reprovides after a - restart or a period of being `disconnected` or `offline`. -3. The provide system cannot keep up with the rate of late reprovides, if the - queue size keeps inscresing or doesn't decrease over time. This is usually - due to a high number of CIDs to be reprovided and a too low number of - (periodic) workers. Consider increasing the [`Provide.DHT.MaxWorkers`]() and - [`Provide.DHT.DedicatedPeriodicWorkers`](). +Number of regions with overdue reprovides. These regions missed their scheduled +reprovide time and will be processed as soon as possible. If decreasing, the +node is recovering from downtime. If increasing, either the node is offline or +the provide system needs more workers (see +[`Provide.DHT.MaxWorkers`](./config.md#providedhtmaxworkers) +and +[`Provide.DHT.DedicatedPeriodicWorkers`](./config.md#providedhtdedicatedperiodicworkers)). ## Schedule ### CIDs scheduled -Number of CIDs scheduled to be reprovided. +Total CIDs scheduled for reprovide. ### Regions scheduled -Number of keyspace regions scheduled to be reprovided. Each CID is mapped to a -region, and reprovided as a batch with the other CIDs in the same region. +Number of keyspace regions scheduled for reprovide. Each CID is mapped to a +specific region, and all CIDs within the same region are reprovided together as +a batch for efficient processing. ### Avg prefix length -Average prefix length of the scheduled regions. This is an indicator of the -number of DHT servers in the swarm. +Average length of binary prefixes identifying the scheduled regions. Each +keyspace region is identified by a binary prefix, and this shows the average +prefix length across all regions in the schedule. Longer prefixes indicate more +DHT servers in the swarm. ### Next reprovide at -Timestamp of the next scheduled region reprovide. +When the next region is scheduled to be reprovided. ### Next prefix -Next region prefix to be reprovided. +Keyspace prefix of the next region to be reprovided. ## Timings ### Uptime -Uptime of the provide system, since Kubo was started. Also includes the -timestamp at which the provide system was started. +How long the provide system has been running since Kubo started, along with the +start timestamp. ### Current time offset -Time offset in the current reprovide cycle. This metrics shows the progression -of the provide cycle. +Elapsed time in the current reprovide cycle, showing cycle progress. ### Cycle started -Timestamp of when the current reprovide cycle started. +When the current reprovide cycle began. ### Reprovide interval -Duration of the reprovide cycle. This is the interval at which all CIDs are -reprovided. +How often each CID is reprovided (the complete cycle duration). ## Network ### Avg record holders -Each CID is sent to multiple DHT servers in the swarm. This metric shows the -average number of DHT servers that have been sent each CID. If this number -matches the [Replication factor](#replication-factor), it means that all CIDs -have been sent to the desired number of DHT servers. +Average number of provider records successfully sent for each CID to distinct +DHT servers. In practice, this is often lower than the [replication +factor](#replication-factor) due to unreachable peers or timeouts. Matching the +replication factor would indicate all DHT servers are reachable. -A lower number indicates that a proportion of the DHT network either isn't -reachable, timed out during the `ADD_PROVIDER` RPC, or doesn't support storing -provider records. - -Note that this metric only displays the number of replicas that were sent -successfully. It is possible that some of the records holders have gone -offline, and the actual number of nodes storing the provider records may be -lower. +Note: some holders may have gone offline since receiving the record. ### Peers swept -Number of DHT servers that were contacted during the last reprovide sweep -cycle. This doesn't include peers that were contacted during the initial -provide of a CID, not during a DHT lookup. It only includes peers that we tried -to send a provider record to. - -If providing CIDs to all keyspace regions (very likely beyond a certain number -of CIDs), this number is expected to grow during the initial reprovide cycle -(up to [`reprovide interval`](#reprovide-interval) after the node started). -After that, the number is expected to stabilize and show the actual size of the -DHT swarm. - -If providing a small number of CIDs, this number will be lower than the network -size, since it only considers the peers to which we sent provider records. +Number of DHT servers to which we tried to send provider records in the last +reprovide cycle (sweep). Excludes peers contacted during initial provides or +DHT lookups. ### Full keyspace coverage -Boolean value indicating whether the reprovide sweep has covered all the DHT -servers in the swarm or not. It `true` it means that the node has sent provider -records to all DHT servers in the swarm during the last reprovide cycle. - -It means that [`Peers swept`](#peers-swept) is an approximation of the DHT -swarm size over the last [`Reprovide Interval`](#reprovide-interval). +Whether provider records were sent to all DHT servers in the swarm during the +last reprovide cycle. If true, [peers swept](#peers-swept) approximates the +total DHT swarm size over the last [reprovide interval](#reprovide-interval). ### Reachable peers -Number of reachable peers among the [`Peers swept`](#peers-swept). A reachable -peer is a peer that successfully responded to all the `ADD_PROVIDER` RPC that -we sent during the last reprovide cycle. - -Also includes the percentage of reachable peers among the [`Peers swept`](#peers-swept). +Number and percentage of peers to which we successfully sent all provider +records assigned to them during the last reprovide cycle. ### Avg region size -Average number of DHT servers in each keyspace region. +Average number of DHT servers per keyspace region. ### Replication factor -Number of DHT servers to which we send a provider record for each CID. +Target number of DHT servers to receive each provider record. ## Operations ### Ongoing provides -Number of CIDs that are currently being provided for the first time. Also shows -the number of keyspace regions to which these CIDs belong. - -Having a higher number of CIDs than number of regions indicates that regions -contain multiple CIDs, which is a sign of efficient batching. - -Each keyspace region corresponds to a [burst worker](). +Number of CIDs and regions currently being provided for the first time. More +CIDs than regions indicates efficient batching. Each region provide uses a +[burst +worker](./config.md#providedhtdedicatedburstworkers). ### Ongoing reprovides -Number of CIDs and keyspace regions that are currently being reprovided. - -Each region corresponds to a [periodic worker](). +Number of CIDs and regions currently being reprovided. Each region reprovide +uses a [periodic +worker](./config.md#providedhtdedicatedperiodicworkers). ### Total CIDs provided -Number of (non-distinct) CIDs that have been provided since the node started. -Also includes reprovides. +Total number of provide operations since node startup (includes both provides +and reprovides). ### Total records provided -Number of provider records that have successfully been sent to DHT servers -since the node started. Also includes reprovides. +Total provider records successfully sent to DHT servers since startup (includes +reprovides). ### Total provide errors -Number of regions that have failed to be provided since the node started. Also -includes reprovides. Upon failure, the provide system will retry providing the -failed region, unless it is `disconnected` or `offline`, in which case the -retry happens when the node goes back `online`. +Number of failed region provide/reprovide operations since startup. Failed +regions are automatically retried unless the node is offline. ### CIDs provided/min -Average number of CIDs provided (excluding reprovides) per minute of provide -operation running in the last reprovide cycle. +Average rate of initial provides per minute during the last reprovide cycle +(excludes reprovides). ### CIDs reprovided/min -Average number of CIDs reprovided (excluding initial provide) per minute of -reprovide operation running in the last reprovide cycle. +Average rate of reprovides per minute during the last reprovide cycle (excludes +initial provides). ### Region reprovide duration -Average duration it took to reprovide all the CIDs in a keyspace region during -the last reprovide cycle. +Average time to reprovide all CIDs in a region during the last cycle. ### Avg CIDs/reprovide -Average number of CIDs that were reprovided in each keyspace region during the -last reprovide cycle. +Average number of CIDs per region during the last reprovide cycle. ### Regions reprovided (last cycle) -Number of keyspace regions that were reprovided during the last reprovide cycle. +Number of regions reprovided in the last cycle. ## Workers ### Active workers -Number of workers that are currently active, either processing a provide or a -reprovide operation. +Number of workers currently processing provide or reprovide operations. ### Free workers -Number of workers that are currently idle, and not reserved for a specific task -(periodic or burst). +Number of idle workers not reserved for periodic or burst tasks. ### Workers stats -For each kind of worker (periodic and burst), shows the number of active, -dedicated, available and queued workers. The number of available workers is the -sum of idle dedicated workers and [`free workers`](#free-workers). The number -of queued workers can be either `0` or `1` since we don't try to take a new -worker until we successfully can take one. You can look at the [`Provide -queue`](#provide-queue) and [`Reprovide queue`](#reprovide-queue) to see how -many regions are waiting to be processed, which corresponds to queued workers. +Breakdown of worker status by type (periodic for scheduled reprovides, burst +for initial provides). For each: active (currently processing), dedicated +(reserved for this type), available (idle dedicated + [free +workers](#free-workers)), and queued (0 or 1, since we only acquire when +needed). See [provide queue](#provide-queue) and [reprovide +queue](#reprovide-queue) for regions waiting to be processed. ### Max connections/worker -Maximum number of connections to DHT servers per worker when sending out -proivder records for a region. +Maximum concurrent DHT server connections per worker when sending provider +records for a region. From fd437145f4cd82be3a349fc5bb2af0e45691f589 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 15 Oct 2025 13:13:43 +0200 Subject: [PATCH 16/26] linking provide-stats.md from command description --- core/commands/provide.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 588d9c9c1f6..3c3839ea7ff 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -145,8 +145,10 @@ The --compact flag provides a 2-column layout suitable for monitoring with For Dual DHT setups, use --lan to show statistics for the LAN DHT provider instead of the default WAN DHT provider. -This interface is not stable and may change from release to release. +A detailed description of each metric is available at: +https://github.com/ipfs/kubo/blob/master/docs/provide-stats.md +This interface is not stable and may change from release to release. `, }, Arguments: []cmds.Argument{}, From 763db02c10719dd7794c79c516fb510e7b6692e1 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Wed, 15 Oct 2025 13:42:52 +0200 Subject: [PATCH 17/26] documentation test --- core/commands/provide.go | 2 +- test/cli/provide_stats_test.go | 107 +++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 test/cli/provide_stats_test.go diff --git a/core/commands/provide.go b/core/commands/provide.go index 3c3839ea7ff..ef540c023ca 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -420,7 +420,7 @@ This interface is not stable and may change from release to release. humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst)) } else { formatLine(0, "%sFree%s: %s", indent, specifyWorkers, humanNumber(availableFreeWorkers)) - formatLine(0, "%sWorker stats:%s %-9s %s", indent, " ", "Periodic", "Burst") + formatLine(0, "%sWorkers stats:%s %-9s %s", indent, " ", "Periodic", "Burst") formatLine(0, "%s %-14s %-9s %s", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst)) formatLine(0, "%s %-14s %-9s %s", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst)) formatLine(0, "%s %-14s %-9s %s", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst)) diff --git a/test/cli/provide_stats_test.go b/test/cli/provide_stats_test.go new file mode 100644 index 00000000000..35a9fe8bd8a --- /dev/null +++ b/test/cli/provide_stats_test.go @@ -0,0 +1,107 @@ +package cli + +import ( + "bufio" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ipfs/kubo/test/cli/harness" + "github.com/stretchr/testify/require" +) + +// TestProvideStatAllMetricsDocumented verifies that all metrics output by +// `ipfs provide stat --all` are documented in docs/provide-stats.md. +// +// The test works as follows: +// 1. Starts an IPFS node with Provide.DHT.SweepEnabled=true +// 2. Runs `ipfs provide stat --all` to get all metrics +// 3. Parses the output and extracts all lines with exactly 2 spaces indent +// (these are the actual metric lines) +// 4. Reads docs/provide-stats.md and extracts all ### section headers +// 5. Ensures every metric in the output has a corresponding ### section in the docs +func TestProvideStatAllMetricsDocumented(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + + // Enable sweep provider + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + + node.StartDaemon() + defer node.StopDaemon() + + // Run `ipfs provide stat --all` to get all metrics + res := node.IPFS("provide", "stat", "--all") + require.NoError(t, res.Err) + + // Parse metrics from the command output + // Only consider lines with exactly two spaces of padding (" ") + // These are the actual metric lines as shown in provide.go + outputMetrics := make(map[string]bool) + scanner := bufio.NewScanner(strings.NewReader(res.Stdout.String())) + // Only consider lines that start with exactly two spaces + indent := " " + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, indent) || strings.HasPrefix(line, indent) { + continue + } + + // Remove the indent + line = strings.TrimPrefix(line, indent) + + // Extract metric name - everything before the first ':' + parts := strings.SplitN(line, ":", 2) + if len(parts) >= 1 { + metricName := strings.TrimSpace(parts[0]) + if metricName != "" { + outputMetrics[metricName] = true + } + } + } + require.NoError(t, scanner.Err()) + + // Read docs/provide-stats.md + // Find the repo root by looking for go.mod + repoRoot := ".." + for range 6 { + if _, err := os.Stat(filepath.Join(repoRoot, "go.mod")); err == nil { + break + } + repoRoot = filepath.Join("..", repoRoot) + } + docsPath := filepath.Join(repoRoot, "docs", "provide-stats.md") + docsFile, err := os.Open(docsPath) + require.NoError(t, err, "Failed to open provide-stats.md") + defer docsFile.Close() + + // Parse all ### metric headers from the docs + documentedMetrics := make(map[string]bool) + docsScanner := bufio.NewScanner(docsFile) + for docsScanner.Scan() { + line := docsScanner.Text() + if metricName, found := strings.CutPrefix(line, "### "); found { + metricName = strings.TrimSpace(metricName) + documentedMetrics[metricName] = true + } + } + require.NoError(t, docsScanner.Err()) + + // Check that all output metrics are documented + var undocumentedMetrics []string + for metric := range outputMetrics { + if !documentedMetrics[metric] { + undocumentedMetrics = append(undocumentedMetrics, metric) + } + } + + require.Empty(t, undocumentedMetrics, + "The following metrics from 'ipfs provide stat --all' are not documented in docs/provide-stats.md: %v\n"+ + "All output metrics: %v\n"+ + "Documented metrics: %v", + undocumentedMetrics, outputMetrics, documentedMetrics) +} From d3c8fe58286ab2e3505e32395bd367b2d78d4b44 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 15 Oct 2025 18:13:53 +0200 Subject: [PATCH 18/26] fix: refactor provide stat command type handling - add extractSweepingProvider() helper to reduce nested type switching - extract lowWorkerThreshold constant for worker availability check - fix --lan error handling to work with buffered providers --- core/commands/provide.go | 66 +++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index ef540c023ca..840dee2d1d6 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -34,6 +34,9 @@ const ( provideStatScheduleOptionName = "schedule" provideStatQueuesOptionName = "queues" provideStatWorkersOptionName = "workers" + + // lowWorkerThreshold is the threshold below which worker availability warnings are shown + lowWorkerThreshold = 2 ) var ProvideCmd = &cmds.Command{ @@ -112,6 +115,26 @@ type provideStats struct { FullRT bool // only used for legacy stats } +// extractSweepingProvider extracts a SweepingProvider from the given provider interface. +// It handles unwrapping buffered and dual providers, selecting LAN or WAN as specified. +// Returns nil if the provider is not a sweeping provider type. +func extractSweepingProvider(prov any, useLAN bool) *provider.SweepingProvider { + switch p := prov.(type) { + case *provider.SweepingProvider: + return p + case *dual.SweepingProvider: + if useLAN { + return p.LAN + } + return p.WAN + case *buffered.SweepingProvider: + // Recursively extract from the inner provider + return extractSweepingProvider(p.Provider, useLAN) + default: + return nil + } +} + var provideStatCmd = &cmds.Command{ Status: cmds.Experimental, Helptext: cmds.HelpText{ @@ -130,7 +153,7 @@ By default, displays a brief summary of key metrics including queue sizes, scheduled CIDs/regions, average record holders, ongoing/total provides, and worker status (if low on workers). -Use --all to display comprehensive statistics organized into sections: +Use --all to display detailed statistics organized into sections: connectivity (DHT status), queues (pending provides/reprovides), schedule (CIDs/regions to reprovide), timings (uptime, cycle info), network (peers, reachability, region size), operations (provide rates, errors), and workers @@ -176,44 +199,25 @@ This interface is not stable and may change from release to release. lanStats, _ := req.Options[provideLanOptionName].(bool) - if lanStats { - if _, ok := nd.Provider.(*dual.SweepingProvider); !ok { + // Handle legacy provider + if legacySys, ok := nd.Provider.(boxoprovider.System); ok { + if lanStats { return errors.New("LAN DHT stats only available for Sweep+Dual DHT") } - } - - var sweepingProvider *provider.SweepingProvider - switch prov := nd.Provider.(type) { - case boxoprovider.System: - stats, err := prov.Stat() + stats, err := legacySys.Stat() if err != nil { return err } _, fullRT := nd.DHTClient.(*fullrt.FullRT) return res.Emit(provideStats{Legacy: &stats, FullRT: fullRT}) - case *provider.SweepingProvider: - sweepingProvider = prov - case *dual.SweepingProvider: - if lanStats { - sweepingProvider = prov.LAN - } else { - sweepingProvider = prov.WAN - } - case *buffered.SweepingProvider: - switch inner := prov.Provider.(type) { - case *provider.SweepingProvider: - sweepingProvider = inner - case *dual.SweepingProvider: - if lanStats { - sweepingProvider = inner.LAN - } else { - sweepingProvider = inner.WAN - } - default: - } - default: } + + // Extract sweeping provider (handles buffered and dual unwrapping) + sweepingProvider := extractSweepingProvider(nd.Provider, lanStats) if sweepingProvider == nil { + if lanStats { + return errors.New("LAN DHT stats only available for Sweep+Dual DHT") + } return fmt.Errorf("stats not available with current routing system %T", nd.Provider) } @@ -403,7 +407,7 @@ This interface is not stable and may change from release to release. availableBurst := availableFreeWorkers + availableReservedBurst availablePeriodic := availableFreeWorkers + availableReservedPeriodic - if displayWorkers || availableBurst <= 2 || availablePeriodic <= 2 { + if displayWorkers || availableBurst <= lowWorkerThreshold || availablePeriodic <= lowWorkerThreshold { // Either we want to display workers information, or we are low on // available workers and want to warn the user. sectionTitle(0, "Workers") From 3a2e4e0805ccc3c83ad94f210492636c500d69a9 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 15 Oct 2025 18:21:51 +0200 Subject: [PATCH 19/26] docs: add clarifying comments --- core/commands/provide.go | 7 +++++++ docs/changelogs/v0.39.md | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 840dee2d1d6..bc04c5cf42a 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -282,6 +282,10 @@ This interface is not stable and may change from release to release. compactMode := all && compact var cols [2][]string col0MaxWidth := 0 + // formatLine adds a formatted line to the output. + // In compact mode, the col parameter determines which column (0 or 1) for side-by-side display. + // In normal mode, all output goes to cols[0] regardless of col parameter, allowing + // the same formatLine calls to work for both single-column and two-column layouts. formatLine := func(col int, format string, a ...any) { if compactMode { s := fmt.Sprintf(format, a...) @@ -501,6 +505,9 @@ func humanNumberOrNA[T constraints.Float | constraints.Integer](n T) string { return humanNumber(n) } +// humanFloatOrNA formats a float with 1 decimal place, returning "N/A" for non-positive values. +// This is separate from humanNumberOrNA because it provides simple decimal formatting for +// continuous metrics (averages, rates) rather than SI unit formatting used for discrete counts. func humanFloatOrNA(val float64) string { if val <= 0 { return "N/A" diff --git a/docs/changelogs/v0.39.md b/docs/changelogs/v0.39.md index 072694b122f..ae3a4394528 100644 --- a/docs/changelogs/v0.39.md +++ b/docs/changelogs/v0.39.md @@ -10,7 +10,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. - [Overview](#overview) - [ðŸ”Ķ Highlights](#-highlights) - - [📊 Comprehensive statistics for Sweep provider with `ipfs provide stat`](#-comprehensive-statistics-for-sweep-provider-with-ipfs-provide-stat) + - [📊 Detailed statistics for Sweep provider with `ipfs provide stat`](#-detailed-statistics-for-sweep-provider-with-ipfs-provide-stat) - [ðŸŠĶ Deprecated `go-ipfs` name no longer published](#-deprecated-go-ipfs-name-no-longer-published) - [ðŸ“Ķïļ Important dependency updates](#-important-dependency-updates) - [📝 Changelog](#-changelog) @@ -20,17 +20,17 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. ### ðŸ”Ķ Highlights -#### 📊 Comprehensive statistics for Sweep provider with `ipfs provide stat` +#### 📊 Detailed statistics for Sweep provider with `ipfs provide stat` The experimental Sweep provider system ([introduced in v0.38](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.38.md#-experimental-sweeping-dht-provider)) -now has comprehensive statistics available through `ipfs provide stat`. +now has detailed statistics available through `ipfs provide stat`. **Default behavior:** Displays a brief summary showing queue sizes, scheduled CIDs/regions, average record holders, ongoing/total provides, and worker status when resources are constrained. -**Detailed statistics with `--all`:** View comprehensive metrics organized into sections: +**Detailed statistics with `--all`:** View complete metrics organized into sections: - **Connectivity**: DHT connection status - **Queues**: Pending provide and reprovide operations From 88c0f052093eb68b28bafd9c01106dea90cbc5f8 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 15 Oct 2025 19:20:40 +0200 Subject: [PATCH 20/26] fix(commands): improve provide stat compact mode - prevent panic when both columns are empty - fix column alignment with UTF-8 characters - only track col0MaxWidth for first column (as intended) --- core/commands/provide.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index bc04c5cf42a..0f8c70c8648 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -7,6 +7,7 @@ import ( "strings" "text/tabwriter" "time" + "unicode/utf8" humanize "github.com/dustin/go-humanize" boxoprovider "github.com/ipfs/boxo/provider" @@ -240,8 +241,8 @@ This interface is not stable and may change from release to release. workers, _ := req.Options[provideStatWorkersOptionName].(bool) flagCount := 0 - for _, b := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} { - if b { + for _, enabled := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} { + if enabled { flagCount++ } } @@ -290,7 +291,9 @@ This interface is not stable and may change from release to release. if compactMode { s := fmt.Sprintf(format, a...) cols[col] = append(cols[col], s) - col0MaxWidth = max(col0MaxWidth, len(s)) + if col == 0 { + col0MaxWidth = max(col0MaxWidth, utf8.RuneCountInString(s)) + } return } format = strings.Replace(format, ": ", ":\t", 1) @@ -442,6 +445,9 @@ This interface is not stable and may change from release to release. col0Width := col0MaxWidth + 2 // Print both columns side by side maxRows := max(len(cols[0]), len(cols[1])) + if maxRows == 0 { + return nil + } for i := range maxRows - 1 { // last line is empty var left, right string if i < len(cols[0]) { From ceee3d17ff7053beb746ba451cd3f8a6679b3395 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 16 Oct 2025 00:41:39 +0200 Subject: [PATCH 21/26] test: add tests for ipfs provide stat command - test basic functionality, flags, JSON output - test legacy provider behavior - test integration with content scheduling - test disabled provider configurations - add parseSweepStats helper with t.Helper() --- test/cli/provide_stats_test.go | 417 +++++++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) diff --git a/test/cli/provide_stats_test.go b/test/cli/provide_stats_test.go index 35a9fe8bd8a..bd2d4e740d8 100644 --- a/test/cli/provide_stats_test.go +++ b/test/cli/provide_stats_test.go @@ -2,15 +2,51 @@ package cli import ( "bufio" + "encoding/json" "os" "path/filepath" "strings" "testing" + "time" "github.com/ipfs/kubo/test/cli/harness" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const ( + provideStatEventuallyTimeout = 15 * time.Second + provideStatEventuallyTick = 100 * time.Millisecond +) + +// sweepStats mirrors the subset of JSON fields actually used by tests. +// This type is intentionally independent from upstream types to detect breaking changes. +// Only includes fields that tests actually access to keep it simple and maintainable. +type sweepStats struct { + Sweep struct { + Closed bool `json:"closed"` + Connectivity struct { + Status string `json:"status"` + } `json:"connectivity"` + Queues struct { + PendingKeyProvides int `json:"pending_key_provides"` + } `json:"queues"` + Schedule struct { + Keys int `json:"keys"` + } `json:"schedule"` + } `json:"Sweep"` +} + +// parseSweepStats parses JSON output from ipfs provide stat command. +// Tests will naturally fail if upstream removes/renames fields we depend on. +func parseSweepStats(t *testing.T, jsonOutput string) sweepStats { + t.Helper() + var stats sweepStats + err := json.Unmarshal([]byte(jsonOutput), &stats) + require.NoError(t, err, "failed to parse provide stat JSON output") + return stats +} + // TestProvideStatAllMetricsDocumented verifies that all metrics output by // `ipfs provide stat --all` are documented in docs/provide-stats.md. // @@ -105,3 +141,384 @@ func TestProvideStatAllMetricsDocumented(t *testing.T) { "Documented metrics: %v", undocumentedMetrics, outputMetrics, documentedMetrics) } + +// TestProvideStatBasic tests basic functionality of ipfs provide stat +func TestProvideStatBasic(t *testing.T) { + t.Parallel() + + t.Run("works with Sweep provider and shows brief output", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.IPFS("provide", "stat") + require.NoError(t, res.Err) + assert.Empty(t, res.Stderr.String()) + + output := res.Stdout.String() + // Brief output should contain specific full labels + assert.Contains(t, output, "Provide queue:") + assert.Contains(t, output, "Reprovide queue:") + assert.Contains(t, output, "CIDs scheduled:") + assert.Contains(t, output, "Regions scheduled:") + assert.Contains(t, output, "Avg record holders:") + assert.Contains(t, output, "Ongoing provides:") + assert.Contains(t, output, "Ongoing reprovides:") + assert.Contains(t, output, "Total CIDs provided:") + }) + + t.Run("requires daemon to be online", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + + res := node.RunIPFS("provide", "stat") + assert.Error(t, res.Err) + assert.Contains(t, res.Stderr.String(), "this command must be run in online mode") + }) +} + +// TestProvideStatFlags tests various command flags +func TestProvideStatFlags(t *testing.T) { + t.Parallel() + + t.Run("--all flag shows all sections with headings", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.IPFS("provide", "stat", "--all") + require.NoError(t, res.Err) + + output := res.Stdout.String() + // Should contain section headings with colons + assert.Contains(t, output, "Connectivity:") + assert.Contains(t, output, "Queues:") + assert.Contains(t, output, "Schedule:") + assert.Contains(t, output, "Timings:") + assert.Contains(t, output, "Network:") + assert.Contains(t, output, "Operations:") + assert.Contains(t, output, "Workers:") + + // Should contain detailed metrics not in brief mode + assert.Contains(t, output, "Uptime:") + assert.Contains(t, output, "Cycle started:") + assert.Contains(t, output, "Reprovide interval:") + assert.Contains(t, output, "Peers swept:") + assert.Contains(t, output, "Full keyspace coverage:") + }) + + t.Run("--compact requires --all", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.RunIPFS("provide", "stat", "--compact") + assert.Error(t, res.Err) + assert.Contains(t, res.Stderr.String(), "--compact flag requires --all flag") + }) + + t.Run("--compact with --all shows 2-column layout", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.IPFS("provide", "stat", "--all", "--compact") + require.NoError(t, res.Err) + + output := res.Stdout.String() + lines := strings.Split(strings.TrimSpace(output), "\n") + require.NotEmpty(t, lines) + + // In compact mode, find a line that has both Schedule and Connectivity metrics + // This confirms 2-column layout is working + foundTwoColumns := false + for _, line := range lines { + if strings.Contains(line, "CIDs scheduled:") && strings.Contains(line, "Status:") { + foundTwoColumns = true + break + } + } + assert.True(t, foundTwoColumns, "Should have at least one line with both 'CIDs scheduled:' and 'Status:' confirming 2-column layout") + }) + + t.Run("individual section flags work with full labels", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + testCases := []struct { + flag string + contains []string + }{ + { + flag: "--connectivity", + contains: []string{"Status:"}, + }, + { + flag: "--queues", + contains: []string{"Provide queue:", "Reprovide queue:"}, + }, + { + flag: "--schedule", + contains: []string{"CIDs scheduled:", "Regions scheduled:", "Avg prefix length:", "Next reprovide at:", "Next prefix:"}, + }, + { + flag: "--timings", + contains: []string{"Uptime:", "Current time offset:", "Cycle started:", "Reprovide interval:"}, + }, + { + flag: "--network", + contains: []string{"Avg record holders:", "Peers swept:", "Full keyspace coverage:", "Reachable peers:", "Avg region size:", "Replication factor:"}, + }, + { + flag: "--operations", + contains: []string{"Ongoing provides:", "Ongoing reprovides:", "Total CIDs provided:", "Total records provided:", "Total provide errors:"}, + }, + { + flag: "--workers", + contains: []string{"Active workers:", "Free workers:", "Workers stats:", "Periodic", "Burst"}, + }, + } + + for _, tc := range testCases { + res := node.IPFS("provide", "stat", tc.flag) + require.NoError(t, res.Err, "flag %s should work", tc.flag) + output := res.Stdout.String() + for _, expected := range tc.contains { + assert.Contains(t, output, expected, "flag %s should contain '%s'", tc.flag, expected) + } + } + }) + + t.Run("multiple section flags can be combined", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.IPFS("provide", "stat", "--network", "--operations") + require.NoError(t, res.Err) + + output := res.Stdout.String() + // Should have section headings when multiple flags combined + assert.Contains(t, output, "Network:") + assert.Contains(t, output, "Operations:") + assert.Contains(t, output, "Avg record holders:") + assert.Contains(t, output, "Ongoing provides:") + }) +} + +// TestProvideStatLegacyProvider tests Legacy provider specific behavior +func TestProvideStatLegacyProvider(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", false) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + t.Run("shows legacy stats from old provider system", func(t *testing.T) { + res := node.IPFS("provide", "stat") + require.NoError(t, res.Err) + + // Legacy provider shows stats from the old reprovider system + output := res.Stdout.String() + assert.Contains(t, output, "TotalReprovides:") + assert.Contains(t, output, "AvgReprovideDuration:") + assert.Contains(t, output, "LastReprovideDuration:") + }) + + t.Run("rejects flags with legacy provider", func(t *testing.T) { + flags := []string{"--all", "--connectivity", "--queues", "--network", "--workers"} + for _, flag := range flags { + res := node.RunIPFS("provide", "stat", flag) + assert.Error(t, res.Err, "flag %s should be rejected for legacy provider", flag) + assert.Contains(t, res.Stderr.String(), "cannot use flags with legacy provide stats") + } + }) + + t.Run("rejects --lan flag with legacy provider", func(t *testing.T) { + res := node.RunIPFS("provide", "stat", "--lan") + assert.Error(t, res.Err) + assert.Contains(t, res.Stderr.String(), "LAN DHT stats only available") + }) +} + +// TestProvideStatOutputFormats tests different output formats +func TestProvideStatOutputFormats(t *testing.T) { + t.Parallel() + + t.Run("JSON output with Sweep provider", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.IPFS("provide", "stat", "--enc=json") + require.NoError(t, res.Err) + + // Parse JSON to verify structure + var result struct { + Sweep map[string]interface{} `json:"Sweep"` + Legacy map[string]interface{} `json:"Legacy"` + } + err := json.Unmarshal([]byte(res.Stdout.String()), &result) + require.NoError(t, err, "Output should be valid JSON") + assert.NotNil(t, result.Sweep, "Sweep stats should be present") + assert.Nil(t, result.Legacy, "Legacy stats should not be present") + }) + + t.Run("JSON output with Legacy provider", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", false) + node.SetIPFSConfig("Provide.Enabled", true) + node.StartDaemon() + defer node.StopDaemon() + + res := node.IPFS("provide", "stat", "--enc=json") + require.NoError(t, res.Err) + + // Parse JSON to verify structure + var result struct { + Sweep map[string]interface{} `json:"Sweep"` + Legacy map[string]interface{} `json:"Legacy"` + } + err := json.Unmarshal([]byte(res.Stdout.String()), &result) + require.NoError(t, err, "Output should be valid JSON") + assert.Nil(t, result.Sweep, "Sweep stats should not be present") + assert.NotNil(t, result.Legacy, "Legacy stats should be present") + }) +} + +// TestProvideStatIntegration tests integration with provide operations +func TestProvideStatIntegration(t *testing.T) { + t.Parallel() + + t.Run("stats reflect content being added to schedule", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.SetIPFSConfig("Provide.DHT.Interval", "1h") + node.StartDaemon() + defer node.StopDaemon() + + // Get initial scheduled CID count + res1 := node.IPFS("provide", "stat", "--enc=json") + require.NoError(t, res1.Err) + initialKeys := parseSweepStats(t, res1.Stdout.String()).Sweep.Schedule.Keys + + // Add content - this should increase CIDs scheduled + node.IPFSAddStr("test content for stats") + + // Wait for content to appear in schedule (with timeout) + // The buffered provider may take a moment to schedule items + require.Eventually(t, func() bool { + res := node.IPFS("provide", "stat", "--enc=json") + require.NoError(t, res.Err) + stats := parseSweepStats(t, res.Stdout.String()) + return stats.Sweep.Schedule.Keys > initialKeys + }, provideStatEventuallyTimeout, provideStatEventuallyTick, "Content should appear in schedule after adding") + }) + + t.Run("stats work with all documented strategies", func(t *testing.T) { + t.Parallel() + + // Test all strategies documented in docs/config.md#providestrategy + strategies := []string{"all", "pinned", "roots", "mfs", "pinned+mfs"} + for _, strategy := range strategies { + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.SetIPFSConfig("Provide.Strategy", strategy) + node.StartDaemon() + + res := node.IPFS("provide", "stat") + require.NoError(t, res.Err, "stats should work with strategy %s", strategy) + output := res.Stdout.String() + assert.NotEmpty(t, output) + assert.Contains(t, output, "CIDs scheduled:") + + node.StopDaemon() + } + }) +} + +// TestProvideStatDisabledConfig tests behavior when provide system is disabled +func TestProvideStatDisabledConfig(t *testing.T) { + t.Parallel() + + t.Run("Provide.Enabled=false returns error stats not available", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", false) + node.StartDaemon() + defer node.StopDaemon() + + res := node.RunIPFS("provide", "stat") + assert.Error(t, res.Err) + assert.Contains(t, res.Stderr.String(), "stats not available") + }) + + t.Run("Provide.Enabled=true with Provide.DHT.Interval=0 returns error stats not available", func(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.SetIPFSConfig("Provide.DHT.Interval", "0") + node.StartDaemon() + defer node.StopDaemon() + + res := node.RunIPFS("provide", "stat") + assert.Error(t, res.Err) + assert.Contains(t, res.Stderr.String(), "stats not available") + }) +} From 616a2fc5c274787a556a9dbbf904ba61645e0a78 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 16 Oct 2025 03:14:55 +0200 Subject: [PATCH 22/26] docs: improve provide command help text - update tagline to "Control and monitor content providing" - simplify help descriptions - make error messages more consistent - update tests to match new error messages --- core/commands/provide.go | 111 ++++++++++++++++++++------------- test/cli/provide_stats_test.go | 4 +- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 0f8c70c8648..4c836909465 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -43,15 +43,24 @@ const ( var ProvideCmd = &cmds.Command{ Status: cmds.Experimental, Helptext: cmds.HelpText{ - Tagline: "Control providing operations", + Tagline: "Control and monitor content providing", ShortDescription: ` Control providing operations. -NOTE: This command is experimental and not all provide-related commands have -been migrated to this namespace yet. For example, 'ipfs routing -provide|reprovide' are still under the routing namespace, 'ipfs stats -reprovide' provides statistics. Additionally, 'ipfs bitswap reprovide' and -'ipfs stats provide' are deprecated. +OVERVIEW: + +The provider system advertises content by publishing provider records, +allowing other nodes to discover which peers have specific content. +Content is reprovided periodically (every Provide.DHT.Interval) +according to Provide.Strategy. + +CONFIGURATION: + +Learn more: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide + +SEE ALSO: + +For ad-hoc one-time provide, see 'ipfs routing provide' `, }, @@ -68,10 +77,18 @@ var provideClearCmd = &cmds.Command{ ShortDescription: ` Clear all CIDs pending to be provided for the first time. -Note: Kubo will automatically clear the queue when it detects a change of -Provide.Strategy upon a restart. For more information about provide -strategies, see: -https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy +BEHAVIOR: + +This command removes CIDs from the provide queue that are waiting to be +advertised to the DHT for the first time. It does not affect content that +is already being reprovided on schedule. + +AUTOMATIC CLEARING: + +Kubo will automatically clear the queue when it detects a change of +Provide.Strategy upon a restart. + +Learn: https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy `, }, Options: []cmds.Option{ @@ -139,40 +156,51 @@ func extractSweepingProvider(prov any, useLAN bool) *provider.SweepingProvider { var provideStatCmd = &cmds.Command{ Status: cmds.Experimental, Helptext: cmds.HelpText{ - Tagline: "Returns statistics about the node's provider system.", + Tagline: "Show statistics about the provider system", ShortDescription: ` -Returns statistics about the content the node is reproviding every -Provide.DHT.Interval according to Provide.Strategy: -https://github.com/ipfs/kubo/blob/master/docs/config.md#provide +Returns statistics about the node's provider system. + +OVERVIEW: + +The provide system advertises content to the DHT. Two provider types exist: +- Sweep provider (default): Spreads reproviding load over time by dividing + the keyspace into regions +- Legacy provider: Reprovides all content at once in bursts + +Learn more: +- Config: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide +- Metrics: https://github.com/ipfs/kubo/blob/master/docs/provide-stats.md + +DEFAULT OUTPUT: + +Shows a brief summary including queue sizes, scheduled items, average record +holders, ongoing/total provides, and worker warnings. + +DETAILED OUTPUT: + +Use --all for detailed statistics with these sections: connectivity, queues, +schedule, timings, network, operations, and workers. Individual sections can +be displayed with their flags (e.g., --network, --operations). Multiple flags +can be combined. -This command displays statistics for the provide system currently in use -(Sweep or Legacy). If using the Legacy provider, basic statistics are shown -and no flags are supported. The following behavior applies to the Sweep -provider only: +Use --compact for monitoring-friendly 2-column output (requires --all). -By default, displays a brief summary of key metrics including queue sizes, -scheduled CIDs/regions, average record holders, ongoing/total provides, and -worker status (if low on workers). +EXAMPLES: -Use --all to display detailed statistics organized into sections: -connectivity (DHT status), queues (pending provides/reprovides), schedule -(CIDs/regions to reprovide), timings (uptime, cycle info), network (peers, -reachability, region size), operations (provide rates, errors), and workers -(pool utilization). +Monitor provider statistics in real-time with 2-column layout: -Individual sections can be displayed using their respective flags (e.g., ---network, --operations, --workers). Multiple section flags can be combined. + watch ipfs provide stat --all --compact -The --compact flag provides a 2-column layout suitable for monitoring with -'watch' (requires --all). Example: watch ipfs provide stat --all --compact +Get statistics in JSON format for programmatic processing: -For Dual DHT setups, use --lan to show statistics for the LAN DHT provider -instead of the default WAN DHT provider. + ipfs provide stat --enc=json | jq -A detailed description of each metric is available at: -https://github.com/ipfs/kubo/blob/master/docs/provide-stats.md +NOTES: -This interface is not stable and may change from release to release. +- This interface is experimental and may change between releases +- Legacy provider shows basic stats only (no flags supported) +- "Regions" are keyspace divisions for spreading reprovide work +- For Dual DHT: use --lan for LAN provider stats (default is WAN) `, }, Arguments: []cmds.Argument{}, @@ -203,7 +231,7 @@ This interface is not stable and may change from release to release. // Handle legacy provider if legacySys, ok := nd.Provider.(boxoprovider.System); ok { if lanStats { - return errors.New("LAN DHT stats only available for Sweep+Dual DHT") + return errors.New("LAN stats only available for Sweep provider with Dual DHT") } stats, err := legacySys.Stat() if err != nil { @@ -217,7 +245,7 @@ This interface is not stable and may change from release to release. sweepingProvider := extractSweepingProvider(nd.Provider, lanStats) if sweepingProvider == nil { if lanStats { - return errors.New("LAN DHT stats only available for Sweep+Dual DHT") + return errors.New("LAN stats only available for Sweep provider with Dual DHT") } return fmt.Errorf("stats not available with current routing system %T", nd.Provider) } @@ -274,7 +302,7 @@ This interface is not stable and may change from release to release. } if compact && !all { - return errors.New("--compact flag requires --all flag") + return errors.New("--compact requires --all flag") } brief := flagCount == 0 @@ -283,10 +311,9 @@ This interface is not stable and may change from release to release. compactMode := all && compact var cols [2][]string col0MaxWidth := 0 - // formatLine adds a formatted line to the output. - // In compact mode, the col parameter determines which column (0 or 1) for side-by-side display. - // In normal mode, all output goes to cols[0] regardless of col parameter, allowing - // the same formatLine calls to work for both single-column and two-column layouts. + // formatLine handles both normal and compact output modes: + // - Normal mode: all lines go to cols[0], col parameter is ignored + // - Compact mode: col 0 for left column, col 1 for right column formatLine := func(col int, format string, a ...any) { if compactMode { s := fmt.Sprintf(format, a...) diff --git a/test/cli/provide_stats_test.go b/test/cli/provide_stats_test.go index bd2d4e740d8..a8dfe2d99ab 100644 --- a/test/cli/provide_stats_test.go +++ b/test/cli/provide_stats_test.go @@ -231,7 +231,7 @@ func TestProvideStatFlags(t *testing.T) { res := node.RunIPFS("provide", "stat", "--compact") assert.Error(t, res.Err) - assert.Contains(t, res.Stderr.String(), "--compact flag requires --all flag") + assert.Contains(t, res.Stderr.String(), "--compact requires --all flag") }) t.Run("--compact with --all shows 2-column layout", func(t *testing.T) { @@ -373,7 +373,7 @@ func TestProvideStatLegacyProvider(t *testing.T) { t.Run("rejects --lan flag with legacy provider", func(t *testing.T) { res := node.RunIPFS("provide", "stat", "--lan") assert.Error(t, res.Err) - assert.Contains(t, res.Stderr.String(), "LAN DHT stats only available") + assert.Contains(t, res.Stderr.String(), "LAN stats only available for Sweep provider with Dual DHT") }) } From f2d68bc93d353c3f15b8bfc04af794a50e258a15 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 16 Oct 2025 10:24:30 +0200 Subject: [PATCH 23/26] metrics rename ``` Next reprovide at: Next prefix: ``` updated to: ``` Next region prefix: Next region reprovide: ``` --- core/commands/provide.go | 12 ++++++------ docs/provide-stats.md | 8 ++++---- test/cli/provide_stats_test.go | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index 4c836909465..d2461786dea 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -370,16 +370,16 @@ NOTES: formatLine(0, "%sRegions scheduled: %s", indent, humanNumberOrNA(s.Sweep.Schedule.Regions)) if !brief { formatLine(0, "%sAvg prefix length: %s", indent, humanFloatOrNA(s.Sweep.Schedule.AvgPrefixLength)) - nextReprovideAt := s.Sweep.Schedule.NextReprovideAt.Format("15:04:05") - if s.Sweep.Schedule.NextReprovideAt.IsZero() { - nextReprovideAt = "N/A" - } - formatLine(0, "%sNext reprovide at: %s", indent, nextReprovideAt) nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix) if nextPrefix == "" { nextPrefix = "N/A" } - formatLine(0, "%sNext prefix: %s", indent, nextPrefix) + formatLine(0, "%sNext region prefix: %s", indent, nextPrefix) + nextReprovideAt := s.Sweep.Schedule.NextReprovideAt.Format("15:04:05") + if s.Sweep.Schedule.NextReprovideAt.IsZero() { + nextReprovideAt = "N/A" + } + formatLine(0, "%sNext region reprovide: %s", indent, nextReprovideAt) } addBlankLine(0) } diff --git a/docs/provide-stats.md b/docs/provide-stats.md index e5101a2ce51..30aa3f3c4b5 100644 --- a/docs/provide-stats.md +++ b/docs/provide-stats.md @@ -48,13 +48,13 @@ keyspace region is identified by a binary prefix, and this shows the average prefix length across all regions in the schedule. Longer prefixes indicate more DHT servers in the swarm. -### Next reprovide at +### Next region prefix -When the next region is scheduled to be reprovided. +Keyspace prefix of the next region to be reprovided. -### Next prefix +### Next region reprovide -Keyspace prefix of the next region to be reprovided. +When the next region is scheduled to be reprovided. ## Timings diff --git a/test/cli/provide_stats_test.go b/test/cli/provide_stats_test.go index a8dfe2d99ab..fede31c0fc3 100644 --- a/test/cli/provide_stats_test.go +++ b/test/cli/provide_stats_test.go @@ -287,7 +287,7 @@ func TestProvideStatFlags(t *testing.T) { }, { flag: "--schedule", - contains: []string{"CIDs scheduled:", "Regions scheduled:", "Avg prefix length:", "Next reprovide at:", "Next prefix:"}, + contains: []string{"CIDs scheduled:", "Regions scheduled:", "Avg prefix length:", "Next region prefix:", "Next region reprovide:"}, }, { flag: "--timings", From a1839d5568004ad45a8f7d27011af8f63169b0f5 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 17 Oct 2025 00:02:56 +0200 Subject: [PATCH 24/26] docs: improve Provide system documentation clarity Enhance documentation for the Provide system to better explain how provider records work and the differences between sweep and legacy modes. Changes to docs/config.md: - Provide section: add clear explanation of provider records and their role - Provide.DHT: add provider record lifecycle and two provider systems overview - Provide.DHT.Interval: explain relationship to expiration, contrast sweep vs legacy behavior - Provide.DHT.SweepEnabled: rewrite to explain legacy problem, sweep solution, and efficiency gains - Monitoring section: prioritize command-line tools (ipfs provide stat) before Prometheus Changes to core/commands/provide.go: - ipfs provide stat help: add explanation of provider records, TTL expiration, and how sweep batching works Changes to docs/changelogs/v0.39.md: - Add context about why stats matter for monitoring provider health - Emphasize real-time monitoring workflow with watch command - Explain what users can observe (rates, queues, worker availability) --- core/commands/provide.go | 22 ++++-- docs/changelogs/v0.39.md | 15 ++-- docs/config.md | 143 +++++++++++++++++++++++++++------------ 3 files changed, 129 insertions(+), 51 deletions(-) diff --git a/core/commands/provide.go b/core/commands/provide.go index d2461786dea..1b326efb645 100644 --- a/core/commands/provide.go +++ b/core/commands/provide.go @@ -162,10 +162,24 @@ Returns statistics about the node's provider system. OVERVIEW: -The provide system advertises content to the DHT. Two provider types exist: -- Sweep provider (default): Spreads reproviding load over time by dividing - the keyspace into regions -- Legacy provider: Reprovides all content at once in bursts +The provide system advertises content to the DHT by publishing provider +records that map CIDs to your peer ID. These records expire after a fixed +TTL to account for node churn, so content must be reprovided periodically +to stay discoverable. + +Two provider types exist: + +- Sweep provider: Divides the DHT keyspace into regions and systematically + sweeps through them over the reprovide interval. Batches CIDs allocated + to the same DHT servers, reducing lookups from N (one per CID) to a + small static number based on DHT size (~3k for 10k DHT servers). Spreads + work evenly over time to prevent resource spikes and ensure announcements + happen just before records expire. + +- Legacy provider: Processes each CID individually with separate DHT + lookups. Attempts to reprovide all content as quickly as possible at the + start of each cycle. Works well for small datasets but struggles with + large collections. Learn more: - Config: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide diff --git a/docs/changelogs/v0.39.md b/docs/changelogs/v0.39.md index ae3a4394528..c0a6522b32e 100644 --- a/docs/changelogs/v0.39.md +++ b/docs/changelogs/v0.39.md @@ -26,6 +26,11 @@ The experimental Sweep provider system ([introduced in v0.38](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.38.md#-experimental-sweeping-dht-provider)) now has detailed statistics available through `ipfs provide stat`. +These statistics help you monitor provider health and troubleshoot issues, +especially useful for nodes providing large content collections. You can quickly +identify bottlenecks like queue backlog, worker saturation, or connectivity +problems that might prevent content from being announced to the DHT. + **Default behavior:** Displays a brief summary showing queue sizes, scheduled CIDs/regions, average record holders, ongoing/total provides, and worker status when resources are constrained. @@ -40,10 +45,12 @@ when resources are constrained. - **Operations**: Ongoing and past provides, rates, errors - **Workers**: Worker pool utilization and availability -**Flexible monitoring:** Individual sections can be displayed using flags like -`--network`, `--operations`, or `--workers`. Multiple flags can be combined for -custom views. The `--compact` flag provides a 2-column layout ideal for -continuous monitoring with `watch ipfs provide stat --all --compact`. +**Real-time monitoring:** For continuous monitoring, run +`watch ipfs provide stat --all --compact` to see detailed statistics refreshed +in a 2-column layout. This lets you observe provide rates, queue sizes, and +worker availability in real-time. Individual sections can be displayed using +flags like `--network`, `--operations`, or `--workers`, and multiple flags can +be combined for custom views. **Dual DHT support:** For Dual DHT configurations, use `--lan` to view LAN DHT provider statistics instead of the default WAN DHT stats. diff --git a/docs/config.md b/docs/config.md index 7982cf7f8da..1835ae8b8ea 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1910,10 +1910,17 @@ Type: `duration` ## `Provide` -Configures CID announcements to the routing system, including both immediate -announcements for new content (provide) and periodic re-announcements -(reprovide) on systems that require it, like Amino DHT. While designed to support -multiple routing systems in the future, the current default configuration only supports providing to the Amino DHT. +Configures how your node advertises content to make it discoverable by other +peers. + +**What is providing?** When your node stores content, it publishes provider +records to the routing system announcing "I have this content." These records +map CIDs to your peer ID, enabling content discovery across the network. + +While designed to support multiple routing systems in the future, the current +default configuration only supports [providing to the Amino DHT](#providedht). + + ### `Provide.Enabled` @@ -1964,13 +1971,39 @@ Type: `optionalString` (unset for the default) Configuration for providing data to Amino DHT peers. +**Provider record lifecycle:** On the Amino DHT, provider records expire after +[`amino.DefaultProvideValidity`](https://github.com/libp2p/go-libp2p-kad-dht/blob/v0.34.0/amino/defaults.go#L40-L43) +to account for node churn. Your node must re-announce (reprovide) content +periodically to keep it discoverable. The [`Provide.DHT.Interval`](#providedhtinterval) +setting controls this timing, with the default ensuring records refresh well +before expiration or negative churn effects kick in. + +**Two provider systems:** + +- **Sweep provider**: Divides the DHT keyspace into regions and systematically + sweeps through them over the reprovide interval. This batches CIDs allocated + to the same DHT servers, dramatically reducing the number of DHT lookups and + PUTs needed. Spreads work evenly over time with predictable resource usage. + +- **Legacy provider**: Processes each CID individually with separate DHT + lookups. Works well for small content collections but struggles to complete + reprovide cycles when managing thousands of CIDs. + #### Monitoring Provide Operations -You can monitor the effectiveness of your provide configuration through metrics exposed at the Prometheus endpoint: `{Addresses.API}/debug/metrics/prometheus` (default: `http://127.0.0.1:5001/debug/metrics/prometheus`). +**Quick command-line monitoring:** Use `ipfs provide stat` to view the current +state of the provider system. For real-time monitoring, run +`watch ipfs provide stat --all --compact` to see detailed statistics refreshed +continuously in a 2-column layout. -Different metrics are available depending on whether you use legacy mode (`SweepEnabled=false`) or sweep mode (`SweepEnabled=true`). See [Provide metrics documentation](https://github.com/ipfs/kubo/blob/master/docs/metrics.md#provide) for details. +**Long-term monitoring:** For in-depth or long-term monitoring, metrics are +exposed at the Prometheus endpoint: `{Addresses.API}/debug/metrics/prometheus` +(default: `http://127.0.0.1:5001/debug/metrics/prometheus`). Different metrics +are available depending on whether you use legacy mode (`SweepEnabled=false`) or +sweep mode (`SweepEnabled=true`). See [Provide metrics documentation](https://github.com/ipfs/kubo/blob/master/docs/metrics.md#provide) +for details. -To enable detailed debug logging for both providers, set: +**Debug logging:** For troubleshooting, enable detailed logging by setting: ```sh GOLOG_LOG_LEVEL=error,provider=debug,dht/provider=debug @@ -1982,12 +2015,24 @@ GOLOG_LOG_LEVEL=error,provider=debug,dht/provider=debug #### `Provide.DHT.Interval` Sets how often to re-announce content to the DHT. Provider records on Amino DHT -expire after [`amino.DefaultProvideValidity`](https://github.com/libp2p/go-libp2p-kad-dht/blob/v0.34.0/amino/defaults.go#L40-L43), -also known as Provider Record Expiration Interval. +expire after [`amino.DefaultProvideValidity`](https://github.com/libp2p/go-libp2p-kad-dht/blob/v0.34.0/amino/defaults.go#L40-L43). + +**Why this matters:** The interval must be shorter than the expiration window to +ensure provider records refresh before they expire. The default value is +approximately half of [`amino.DefaultProvideValidity`](https://github.com/libp2p/go-libp2p-kad-dht/blob/v0.34.0/amino/defaults.go#L40-L43), +which accounts for network churn and ensures records stay alive without +overwhelming the network with unnecessary announcements. -An interval of about half the expiration window ensures provider records -are refreshed well before they expire. This keeps your content continuously -discoverable accounting for network churn without overwhelming the network with too frequent announcements. +**With sweep mode enabled ([`Provide.DHT.SweepEnabled`](#providedhtsweepenabled)):** +The system spreads reprovide operations smoothly across this entire interval. +Each keyspace region is reprovided at scheduled times throughout the period, +ensuring announcements happen just before provider records would expire in their +respective DHT regions. + +**With legacy mode:** The system attempts to reprovide all CIDs as quickly as +possible at the start of each interval. If reproviding takes longer than this +interval (common with large datasets), the next cycle is skipped and provider +records may expire. - If unset, it uses the implicit safe default. - If set to the value `"0"` it will disable content reproviding to DHT. @@ -2055,32 +2100,42 @@ Type: `optionalInteger` (non-negative; `0` means unlimited number of workers) #### `Provide.DHT.SweepEnabled` -Whether Provide Sweep is enabled. If not enabled, the legacy -[`boxo/provider`](https://github.com/ipfs/boxo/tree/main/provider) is used for -both provides and reprovides. - -Provide Sweep is a resource efficient technique for advertising content to -the Amino DHT swarm. The Provide Sweep module tracks the keys that should be periodically reprovided in -the `Keystore`. It splits the keys into DHT keyspace regions by proximity (XOR -distance), and schedules when reprovides should happen in order to spread the -reprovide operation over time to avoid a spike in resource utilization. It -basically sweeps the keyspace _from left to right_ over the -[`Provide.DHT.Interval`](#providedhtinterval) time period, and reprovides keys -matching to the visited keyspace region. - -Provide Sweep aims at replacing the inefficient legacy `boxo/provider` -module, and is currently opt-in. You can compare the effectiveness of sweep mode vs legacy mode by monitoring the appropriate metrics (see [Monitoring Provide Operations](#monitoring-provide-operations) above). - -Whenever new keys should be advertised to the Amino DHT, `kubo` calls -`StartProviding()`, triggering an initial `provide` operation for the given -keys. The keys will be added to the `Keystore` tracking which keys should be -reprovided and when they should be reprovided. Calling `StopProviding()` -removes the keys from the `Keystore`. However, it is currently tricky for -`kubo` to detect when a key should stop being advertised. Hence, `kubo` will -periodically refresh the `Keystore` at each [`Provide.DHT.Interval`](#providedhtinterval) -by providing it a channel of all the keys it is expected to contain according -to the [`Provide.Strategy`](#providestrategy). During this operation, -all keys in the `Keystore` are purged, and only the given ones remain scheduled. +Enables the sweep provider for efficient content announcements. When disabled, +the legacy [`boxo/provider`](https://github.com/ipfs/boxo/tree/main/provider) is +used instead. + +**The legacy provider problem:** The legacy system processes CIDs one at a time, +requiring a separate DHT lookup (10-20 seconds each) to find the 20 closest +peers for each CID. This sequential approach only handles about 5,280 CIDs per +reprovide cycle. If your node has more CIDs than can be reprovided within +[`Provide.DHT.Interval`](#providedhtinterval), provider records start expiring +after [`amino.DefaultProvideValidity`](https://github.com/libp2p/go-libp2p-kad-dht/blob/v0.34.0/amino/defaults.go#L40-L43), +making content undiscoverable. + +**How sweep mode works:** The sweep provider divides the DHT keyspace into +regions based on key prefixes. It estimates the Amino DHT size, calculates how +many regions are needed (sized to contain at least 20 peers each), then +schedules region processing evenly across [`Provide.DHT.Interval`](#providedhtinterval). +When processing a region, it discovers the peers in that region once, then sends +all provider records for CIDs allocated to those peers in a batch. This batching +is the key efficiency: instead of N lookups for N CIDs, the number of lookups is +bounded by a small static number based on Amino DHT size (e.g., ~3,000 lookups +when there are ~10,000 DHT servers), regardless of how many CIDs you're +providing. + +**Efficiency gains:** For a node providing 100,000 CIDs, sweep mode reduces +lookups by 97% compared to legacy. The work spreads smoothly over time rather +than completing in bursts, preventing resource spikes and duplicate +announcements. Long-running nodes reprovide systematically just before records +would expire, keeping content continuously discoverable without wasting +bandwidth. + +**Implementation details:** The sweep provider tracks CIDs in a persistent +keystore. New content added via `StartProviding()` enters the provide queue and +gets batched by keyspace region. The keystore is periodically refreshed at each +[`Provide.DHT.Interval`](#providedhtinterval) with CIDs matching +[`Provide.Strategy`](#providestrategy) to ensure only current content remains +scheduled. This handles cases where content is unpinned or removed. > > @@ -2088,13 +2143,15 @@ all keys in the `Keystore` are purged, and only the given ones remain scheduled. > Reprovide Cycle Comparison > > -> The diagram above visualizes the performance patterns: +> The diagram compares performance patterns: > -> - **Legacy mode**: Individual (slow) provides per CID, can struggle with large datasets -> - **Sweep mode**: Even distribution matching the keyspace sweep described with low resource usage -> - **Accelerated DHT**: Hourly traffic spikes with high resource usage +> - **Legacy mode**: Sequential processing, one lookup per CID, struggles with large datasets +> - **Sweep mode**: Smooth distribution over time, batched lookups by keyspace region, predictable resource usage +> - **Accelerated DHT**: Hourly network crawls creating traffic spikes, high resource usage > -> Sweep mode provides similar effectiveness to Accelerated DHT but with steady resource usage - better for machines with limited CPU, memory, or network bandwidth. +> Sweep mode achieves similar effectiveness to the Accelerated DHT client but with steady resource consumption. + +You can compare the effectiveness of sweep mode vs legacy mode by monitoring the appropriate metrics (see [Monitoring Provide Operations](#monitoring-provide-operations) above). > [!NOTE] > This feature is opt-in for now, but will become the default in a future release. From 1edf71535cd05a24b57a782a10f350a49329d38c Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Fri, 17 Oct 2025 14:34:19 +0200 Subject: [PATCH 25/26] use space for warning message indentation --- core/node/provider.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/node/provider.go b/core/node/provider.go index 18e1cda261c..de5a233daa1 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -570,12 +570,12 @@ Your node is falling behind on DHT reprovides, which will affect content availab Keyspace regions enqueued for reprovide: %s ago:\t%d - Now:\t%d + Now:\t%d All periodic workers are busy! - Active workers:\t%d / %d (max) - Active workers types:\t%d periodic, %d burst - Dedicated workers:\t%d periodic, %d burst + Active workers:\t%d / %d (max) + Active workers types:\t%d periodic, %d burst + Dedicated workers:\t%d periodic, %d burst Solutions (try in order): 1. Increase Provide.DHT.MaxWorkers (current %d) From 343360968488ea959ced8ba6abaa87349d5196b3 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 24 Oct 2025 23:26:04 +0200 Subject: [PATCH 26/26] refactor: extract constants - extract reprovideAlertPollInterval and consecutiveAlertsThreshold constants - add extractSweepingProvider helper matching provide.go style - add defer ticker.Stop() for proper cleanup - improve variable grouping and documentation --- core/node/provider.go | 70 +++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/core/node/provider.go b/core/node/provider.go index de5a233daa1..fee95b0ff85 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -39,11 +39,21 @@ import ( // The size of a batch that will be used for calculating average announcement // time per CID, inside of boxo/provider.ThroughputReport // and in 'ipfs stats provide' report. +// Used when Provide.DHT.SweepEnabled=false const sampledBatchSize = 1000 // Datastore key used to store previous reprovide strategy. const reprovideStrategyKey = "/reprovideStrategy" +// Interval between reprovide queue monitoring checks for slow reprovide alerts. +// Used when Provide.DHT.SweepEnabled=true +const reprovideAlertPollInterval = 15 * time.Minute + +// Number of consecutive polling intervals with sustained queue growth before +// triggering a slow reprovide alert (3 intervals = 45 minutes). +// Used when Provide.DHT.SweepEnabled=true +const consecutiveAlertsThreshold = 3 + // DHTProvider is an interface for providing keys to a DHT swarm. It holds a // state of keys to be advertised, and is responsible for periodically // publishing provider records for these keys to the DHT swarm before the @@ -507,32 +517,37 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { }, }) }) + + // extractSweepingProvider extracts a SweepingProvider from the given provider interface. + // It handles unwrapping buffered and dual providers, always selecting WAN for dual DHT. + // Returns nil if the provider is not a sweeping provider type. + var extractSweepingProvider func(prov any) *dhtprovider.SweepingProvider + extractSweepingProvider = func(prov any) *dhtprovider.SweepingProvider { + switch p := prov.(type) { + case *dhtprovider.SweepingProvider: + return p + case *ddhtprovider.SweepingProvider: + return p.WAN + case *buffered.SweepingProvider: + // Recursively extract from the inner provider + return extractSweepingProvider(p.Provider) + default: + return nil + } + } + type alertInput struct { fx.In Provider DHTProvider } reprovideAlert := fx.Invoke(func(lc fx.Lifecycle, in alertInput) { + prov := extractSweepingProvider(in.Provider) + var ( cancel context.CancelFunc done = make(chan struct{}) ) - // Select Sweeping Provider to get the stats from. - var prov *dhtprovider.SweepingProvider - switch p := in.Provider.(type) { - case *ddhtprovider.SweepingProvider: - prov = p.WAN - case *dhtprovider.SweepingProvider: - prov = p - case *buffered.SweepingProvider: - switch inner := p.Provider.(type) { - case *ddhtprovider.SweepingProvider: - prov = inner.WAN - case *dhtprovider.SweepingProvider: - prov = inner - } - } - lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { if prov == nil { @@ -543,11 +558,15 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { go func() { defer close(done) - // Poll stats every 15 minutes - pollInterval := 15 * time.Minute - ticker := time.NewTicker(pollInterval) - var queueSize, prevQueueSize, count int - var queuedWorkers, prevQueuedWorkers bool + ticker := time.NewTicker(reprovideAlertPollInterval) + defer ticker.Stop() + + var ( + queueSize, prevQueueSize int + queuedWorkers, prevQueuedWorkers bool + count int + ) + for { select { case <-gcCtx.Done(): @@ -558,11 +577,12 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { stats := prov.Stats() queuedWorkers = stats.Workers.QueuedPeriodic > 0 queueSize = stats.Queues.PendingRegionReprovides - // If reprovide queue size keeps growing and workers are not - // keeping up, print warning message + + // Alert if reprovide queue keeps growing and all periodic workers are busy. + // Requires consecutiveAlertsThreshold intervals of sustained growth. if prevQueuedWorkers && queuedWorkers && queueSize > prevQueueSize { count++ - if count > 2 { + if count >= consecutiveAlertsThreshold { logger.Errorf(` 🔔🔔🔔 Reprovide Operations Too Slow 🔔🔔🔔 @@ -585,7 +605,7 @@ Solutions (try in order): See how the reprovide queue is processed in real-time with 'watch ipfs provide stat --all --compact' See docs: https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtmaxworkers`, - pollInterval.Truncate(time.Minute).String(), prevQueueSize, queueSize, + reprovideAlertPollInterval.Truncate(time.Minute).String(), prevQueueSize, queueSize, stats.Workers.Active, stats.Workers.Max, stats.Workers.ActivePeriodic, stats.Workers.ActiveBurst, stats.Workers.DedicatedPeriodic, stats.Workers.DedicatedBurst,