kubo/core/commands/provide.go
Guillaume Michel d56fe3a026
Some checks are pending
CodeQL / codeql (push) Waiting to run
Docker Check / lint (push) Waiting to run
Docker Check / build (push) Waiting to run
Gateway Conformance / gateway-conformance (push) Waiting to run
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Waiting to run
Go Build / go-build (push) Waiting to run
Go Check / go-check (push) Waiting to run
Go Lint / go-lint (push) Waiting to run
Go Test / go-test (push) Waiting to run
Interop / interop-prep (push) Waiting to run
Interop / helia-interop (push) Blocked by required conditions
Interop / ipfs-webui (push) Blocked by required conditions
Sharness / sharness-test (push) Waiting to run
Spell Check / spellcheck (push) Waiting to run
feat(cli/rpc/add): fast provide of root CID (#11046)
* feat: fast provide
* Check error from provideRoot
* do not provide if nil router
* fix(commands): prevent panic from typed nil DHTClient interface

Fixes panic when ipfsNode.DHTClient is a non-nil interface containing a
nil pointer value (typed nil). This happened when Routing.Type=delegated
or when using HTTP-only routing without DHT.

The panic occurred because:
- Go interfaces can be non-nil while containing nil pointer values
- Simple `if DHTClient == nil` checks pass, but calling methods panics
- Example: `(*ddht.DHT)(nil)` stored in interface passes nil check

Solution:
- Add HasActiveDHTClient() method to check both interface and concrete value
- Update all 7 call sites to use proper check before DHT operations
- Rename provideRoot → provideCIDSync for clarity
- Add structured logging with "fast-provide" prefix for easier filtering
- Add tests covering nil cases and valid DHT configurations

Fixes: https://github.com/ipfs/kubo/pull/11046#issuecomment-3525313349

* feat(add): split fast-provide into two flags for async/sync control

Renames --fast-provide to --fast-provide-root and adds --fast-provide-wait
to give users control over synchronous vs asynchronous providing behavior.

Changes:
- --fast-provide-root (default: true): enables immediate root CID providing
- --fast-provide-wait (default: false): controls whether to block until complete
- Default behavior: async provide (fast, non-blocking)
- Opt-in: --fast-provide-wait for guaranteed discoverability (slower, blocking)
- Can disable with --fast-provide-root=false to rely on background reproviding

Implementation:
- Async mode: launches goroutine with detached context for fire-and-forget
  - Added 10 second timeout to prevent hanging on network issues
  - Timeout aligns with other kubo operations (ping, DNS resolve, p2p)
  - Sufficient for DHT with sweep provider or accelerated client
- Sync mode: blocks on provideCIDSync until completion (uses req.Context)
- Improved structured logging with "fast-provide-root:" prefix
  - Removed redundant "root CID" from messages (already in prefix)
  - Clear async/sync distinction in log messages
- Added FAST PROVIDE OPTIMIZATION section to ipfs add --help explaining:
  - The problem: background queue takes time, content not immediately discoverable
  - The solution: extra immediate announcement of just the root CID
  - The benefit: peers can find content right away while queue handles rest
  - Usage: async by default, --fast-provide-wait for guaranteed completion

Changelog:
- Added highlight section for fast root CID providing feature
- Updated TOC and overview
- Included usage examples with clear comments explaining each mode
- Emphasized this is extra announcement independent of background queue

The feature works best with sweep provider and accelerated DHT client
where provide operations are significantly faster.

* fix(add): respect Provide config in fast-provide-root

fast-provide-root should honor the same config settings as the regular
provide system:
- skip when Provide.Enabled is false
- skip when Provide.DHT.Interval is 0
- respect Provide.Strategy (all/pinned/roots/mfs/combinations)

This ensures fast-provide only runs when appropriate based on user
configuration and the nature of the content being added (pinned vs
unpinned, added to MFS or not).

* Update core/commands/add.go

---------

Co-authored-by: gammazero <11790789+gammazero@users.noreply.github.com>
Co-authored-by: Marcin Rataj <lidel@lidel.org>
2025-11-14 11:08:29 -08:00

597 lines
21 KiB
Go

package commands
import (
"context"
"errors"
"fmt"
"io"
"strings"
"text/tabwriter"
"time"
"unicode/utf8"
humanize "github.com/dustin/go-humanize"
boxoprovider "github.com/ipfs/boxo/provider"
cid "github.com/ipfs/go-cid"
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"
routing "github.com/libp2p/go-libp2p/core/routing"
"github.com/probe-lab/go-libdht/kad/key"
"golang.org/x/exp/constraints"
)
const (
provideQuietOptionName = "quiet"
provideLanOptionName = "lan"
provideStatAllOptionName = "all"
provideStatCompactOptionName = "compact"
provideStatNetworkOptionName = "network"
provideStatConnectivityOptionName = "connectivity"
provideStatOperationsOptionName = "operations"
provideStatTimingsOptionName = "timings"
provideStatScheduleOptionName = "schedule"
provideStatQueuesOptionName = "queues"
provideStatWorkersOptionName = "workers"
// lowWorkerThreshold is the threshold below which worker availability warnings are shown
lowWorkerThreshold = 2
)
var ProvideCmd = &cmds.Command{
Status: cmds.Experimental,
Helptext: cmds.HelpText{
Tagline: "Control and monitor content providing",
ShortDescription: `
Control providing operations.
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'
`,
},
Subcommands: map[string]*cmds.Command{
"clear": provideClearCmd,
"stat": provideStatCmd,
},
}
var provideClearCmd = &cmds.Command{
Status: cmds.Experimental,
Helptext: cmds.HelpText{
Tagline: "Clear all CIDs from the provide queue.",
ShortDescription: `
Clear all CIDs pending to be provided for the first time.
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{
cmds.BoolOption(provideQuietOptionName, "q", "Do not write output."),
},
Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
n, err := cmdenv.GetNode(env)
if err != nil {
return err
}
quiet, _ := req.Options[provideQuietOptionName].(bool)
if n.Provider == nil {
return nil
}
cleared := n.Provider.Clear()
if quiet {
return nil
}
_ = re.Emit(cleared)
return nil
},
Type: int(0),
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, cleared int) error {
quiet, _ := req.Options[provideQuietOptionName].(bool)
if quiet {
return nil
}
_, err := fmt.Fprintf(w, "removed %d items from provide queue\n", cleared)
return err
}),
},
}
type provideStats struct {
Sweep *stats.Stats
Legacy *boxoprovider.ReproviderStats
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{
Tagline: "Show statistics about the provider system",
ShortDescription: `
Returns statistics about the node's provider system.
OVERVIEW:
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
- 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.
Use --compact for monitoring-friendly 2-column output (requires --all).
EXAMPLES:
Monitor provider statistics in real-time with 2-column layout:
watch ipfs provide stat --all --compact
Get statistics in JSON format for programmatic processing:
ipfs provide stat --enc=json | jq
NOTES:
- 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{},
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)"),
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 {
return err
}
if !nd.IsOnline {
return ErrNotOnline
}
lanStats, _ := req.Options[provideLanOptionName].(bool)
// Handle legacy provider
if legacySys, ok := nd.Provider.(boxoprovider.System); ok {
if lanStats {
return errors.New("LAN stats only available for Sweep provider with Dual DHT")
}
stats, err := legacySys.Stat()
if err != nil {
return err
}
_, fullRT := nd.DHTClient.(*fullrt.FullRT)
return res.Emit(provideStats{Legacy: &stats, FullRT: fullRT})
}
// Extract sweeping provider (handles buffered and dual unwrapping)
sweepingProvider := extractSweepingProvider(nd.Provider, lanStats)
if sweepingProvider == nil {
if lanStats {
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)
}
s := sweepingProvider.Stats()
return res.Emit(provideStats{Sweep: &s})
},
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()
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 _, enabled := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} {
if enabled {
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))
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")
}
// Sweep provider stats
if s.Sweep.Closed {
fmt.Fprintf(wtr, "Provider is closed\n")
return nil
}
if compact && !all {
return errors.New("--compact requires --all flag")
}
brief := flagCount == 0
showHeadings := flagCount > 1 || all
compactMode := all && compact
var cols [2][]string
col0MaxWidth := 0
// 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...)
cols[col] = append(cols[col], s)
if col == 0 {
col0MaxWidth = max(col0MaxWidth, utf8.RuneCountInString(s))
}
return
}
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, "")
}
}
sectionTitle := func(col int, title string) {
if !brief && showHeadings {
//nolint:govet // dynamic format string is intentional
formatLine(col, title+":")
}
}
indent := " "
if brief || !showHeadings {
indent = ""
}
// Connectivity
if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" {
sectionTitle(1, "Connectivity")
since := s.Sweep.Connectivity.Since
if since.IsZero() {
formatLine(1, "%sStatus: %s", indent, s.Sweep.Connectivity.Status)
} else {
formatLine(1, "%sStatus: %s (%s)", indent, s.Sweep.Connectivity.Status, humanTime(since))
}
addBlankLine(1)
}
// Queues
if all || queues || brief {
sectionTitle(1, "Queues")
formatLine(1, "%sProvide queue: %s CIDs, %s regions", indent, humanSI(s.Sweep.Queues.PendingKeyProvides, 1), humanSI(s.Sweep.Queues.PendingRegionProvides, 1))
formatLine(1, "%sReprovide queue: %s regions", indent, humanSI(s.Sweep.Queues.PendingRegionReprovides, 1))
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 {
formatLine(0, "%sAvg prefix length: %s", indent, humanFloatOrNA(s.Sweep.Schedule.AvgPrefixLength))
nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix)
if nextPrefix == "" {
nextPrefix = "N/A"
}
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)
}
// Timings
if all || timings {
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 {
formatLine(0, "%sPeers swept: %s", indent, humanInt(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, humanInt(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers))
} else {
formatLine(0, "%sReachable peers: %s", indent, humanInt(s.Sweep.Network.Reachable))
}
formatLine(0, "%sAvg region size: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgRegionSize))
formatLine(0, "%sReplication factor: %s", indent, humanNumber(s.Sweep.Network.ReplicationFactor))
addBlankLine(0)
}
}
// Operations
if all || operations || brief {
sectionTitle(1, "Operations")
// Ongoing operations
formatLine(1, "%sOngoing provides: %s CIDs, %s regions", indent, humanSI(s.Sweep.Operations.Ongoing.KeyProvides, 1), humanSI(s.Sweep.Operations.Ongoing.RegionProvides, 1))
formatLine(1, "%sOngoing reprovides: %s CIDs, %s regions", indent, humanSI(s.Sweep.Operations.Ongoing.KeyReprovides, 1), humanSI(s.Sweep.Operations.Ongoing.RegionReprovides, 1))
// Past operations summary
formatLine(1, "%sTotal CIDs provided: %s", indent, humanNumber(s.Sweep.Operations.Past.KeysProvided))
if !brief {
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/worker: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute))
formatLine(1, "%sCIDs reprovided/min/worker: %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 {
availableReservedBurst := max(0, s.Sweep.Workers.DedicatedBurst-s.Sweep.Workers.ActiveBurst)
availableReservedPeriodic := max(0, s.Sweep.Workers.DedicatedPeriodic-s.Sweep.Workers.ActivePeriodic)
availableFreeWorkers := max(0, 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 <= 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")
specifyWorkers := " workers"
if compactMode {
specifyWorkers = ""
}
formatLine(0, "%sActive%s: %s / %s (max)", indent, specifyWorkers, humanInt(s.Sweep.Workers.Active), humanInt(s.Sweep.Workers.Max))
if brief {
// Brief mode - show condensed worker info
formatLine(0, "%sPeriodic%s: %s active, %s available, %s queued", indent, specifyWorkers,
humanInt(s.Sweep.Workers.ActivePeriodic), humanInt(availablePeriodic), humanInt(s.Sweep.Workers.QueuedPeriodic))
formatLine(0, "%sBurst%s: %s active, %s available, %s queued\n", indent, specifyWorkers,
humanInt(s.Sweep.Workers.ActiveBurst), humanInt(availableBurst), humanInt(s.Sweep.Workers.QueuedBurst))
} else {
formatLine(0, "%sFree%s: %s", indent, specifyWorkers, humanInt(availableFreeWorkers))
formatLine(0, "%s %-14s %-9s %s", indent, "Workers stats:", "Periodic", "Burst")
formatLine(0, "%s %-14s %-9s %s", indent, "Active:", humanInt(s.Sweep.Workers.ActivePeriodic), humanInt(s.Sweep.Workers.ActiveBurst))
formatLine(0, "%s %-14s %-9s %s", indent, "Dedicated:", humanInt(s.Sweep.Workers.DedicatedPeriodic), humanInt(s.Sweep.Workers.DedicatedBurst))
formatLine(0, "%s %-14s %-9s %s", indent, "Available:", humanInt(availablePeriodic), humanInt(availableBurst))
formatLine(0, "%s %-14s %-9s %s", indent, "Queued:", humanInt(s.Sweep.Workers.QueuedPeriodic), humanInt(s.Sweep.Workers.QueuedBurst))
formatLine(0, "%sMax connections/worker: %s", indent, humanInt(s.Sweep.Workers.MaxProvideConnsPerWorker))
addBlankLine(0)
}
}
}
if compactMode {
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]) {
left = cols[0][i]
}
if i < len(cols[1]) {
right = cols[1][i]
}
fmt.Fprintf(wtr, "%-*s %s\n", col0Width, 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
}),
},
Type: provideStats{},
}
func humanDuration(val time.Duration) string {
if val > 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")
}
func humanNumber[T constraints.Float | constraints.Integer](n T) string {
nf := float64(n)
str := humanSI(nf, 0)
fullStr := humanFull(nf, 0)
if str != fullStr {
return fmt.Sprintf("%s\t(%s)", str, fullStr)
}
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)
}
// 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"
}
return humanFull(val, 1)
}
func humanSI[T constraints.Float | constraints.Integer](val T, decimals int) string {
v, unit := humanize.ComputeSI(float64(val))
return fmt.Sprintf("%s%s", humanFull(v, decimals), unit)
}
func humanInt[T constraints.Integer](val T) string {
return humanFull(float64(val), 0)
}
func humanFull(val float64, decimals int) string {
return humanize.CommafWithDigits(val, decimals)
}
// provideCIDSync performs a synchronous/blocking provide operation to announce
// the given CID to the DHT.
//
// - If the accelerated DHT client is used, a DHT lookup isn't needed, we
// directly allocate provider records to closest peers.
// - If Provide.DHT.SweepEnabled=true or OptimisticProvide=true, we make an
// optimistic provide call.
// - Else we make a standard provide call (much slower).
//
// IMPORTANT: The caller MUST verify DHT availability using HasActiveDHTClient()
// before calling this function. Calling with a nil or invalid router will cause
// a panic - this is the caller's responsibility to prevent.
func provideCIDSync(ctx context.Context, router routing.Routing, c cid.Cid) error {
return router.Provide(ctx, c, true)
}