Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions config/internal.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package config

const (
// DefaultMFSNoFlushLimit is the default limit for consecutive unflushed MFS operations
DefaultMFSNoFlushLimit = 256
)

type Internal struct {
// All marked as omitempty since we are expecting to make changes to all subcomponents of Internal
Bitswap *InternalBitswap `json:",omitempty"`
UnixFSShardingSizeThreshold *OptionalString `json:",omitempty"` // moved to Import.UnixFSHAMTDirectorySizeThreshold
Libp2pForceReachability *OptionalString `json:",omitempty"`
BackupBootstrapInterval *OptionalDuration `json:",omitempty"`
// MFSAutoflushThreshold controls the number of entries cached in memory
// for each MFS directory before auto-flush is triggered to prevent
// unbounded memory growth when using --flush=false.
// Default: 256 (matches HAMT shard size)
// Set to 0 to disable cache limiting (old behavior, may cause high memory usage)
// MFSNoFlushLimit controls the maximum number of consecutive
// MFS operations allowed with --flush=false before requiring a manual flush.
// This prevents unbounded memory growth and ensures data consistency.
// Set to 0 to disable limiting (old behavior, may cause high memory usage)
// This is an EXPERIMENTAL feature and may change or be removed in future releases.
// See https://github.com/ipfs/kubo/issues/10842
MFSAutoflushThreshold OptionalInteger `json:",omitempty"`
MFSNoFlushLimit *OptionalInteger `json:",omitempty"`
}

type InternalBitswap struct {
Expand Down
73 changes: 65 additions & 8 deletions core/commands/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

humanize "github.com/dustin/go-humanize"
Expand All @@ -35,6 +37,43 @@ import (

var flog = logging.Logger("cmds/files")

// Global counter for unflushed MFS operations
var noFlushOperationCounter atomic.Int64

// Cached limit value (read once on first use)
var (
noFlushLimit int64
noFlushLimitInit sync.Once
)

// updateNoFlushCounter manages the counter for unflushed operations
func updateNoFlushCounter(nd *core.IpfsNode, flush bool) error {
if flush {
// Reset counter when flushing
noFlushOperationCounter.Store(0)
return nil
}

// Cache the limit on first use (config doesn't change at runtime)
noFlushLimitInit.Do(func() {
noFlushLimit = int64(config.DefaultMFSNoFlushLimit)
if cfg, err := nd.Repo.Config(); err == nil && cfg.Internal.MFSNoFlushLimit != nil {
noFlushLimit = cfg.Internal.MFSNoFlushLimit.WithDefault(int64(config.DefaultMFSNoFlushLimit))
}
})

// Check if limit reached
if noFlushLimit > 0 && noFlushOperationCounter.Load() >= noFlushLimit {
return fmt.Errorf("reached limit of %d unflushed MFS operations. "+
"To resolve: 1) run 'ipfs files flush' to persist changes, "+
"2) use --flush=true (default), or "+
"3) increase Internal.MFSNoFlushLimit in config", noFlushLimit)
}

noFlushOperationCounter.Add(1)
return nil
}

// FilesCmd is the 'ipfs files' command
var FilesCmd = &cmds.Command{
Helptext: cmds.HelpText{
Expand Down Expand Up @@ -68,12 +107,11 @@ of consistency guarantees. If the daemon is unexpectedly killed before running
'ipfs files flush' on the files in question, then data may be lost. This also
applies to run 'ipfs repo gc' concurrently with '--flush=false' operations.

When using '--flush=false', directories will automatically flush when the
number of cached entries exceeds the Internal.MFSAutoflushThreshold config.
This prevents unbounded memory growth. We recommend flushing
paths regularly with 'ipfs files flush', specially the folders on which many
write operations are happening, as a way to clear the directory cache, free
memory and speed up read operations.`,
When using '--flush=false', operations are limited to prevent unbounded memory
growth. After reaching Internal.MFSNoFlushLimit operations,
further operations will fail until you run 'ipfs files flush'. We recommend
flushing paths regularly, especially folders with many write operations, to
clear caches, free memory, and maintain good performance.`,
},
Options: []cmds.Option{
cmds.BoolOption(filesFlushOptionName, "f", "Flush target and ancestors after write.").WithDefault(true),
Expand Down Expand Up @@ -516,12 +554,16 @@ being GC'ed.
}
}

flush, _ := req.Options[filesFlushOptionName].(bool)

if err := updateNoFlushCounter(nd, flush); err != nil {
return err
}

err = mfs.PutNode(nd.FilesRoot, dst, node)
if err != nil {
return fmt.Errorf("cp: cannot put node in path %s: %s", dst, err)
}

flush, _ := req.Options[filesFlushOptionName].(bool)
if flush {
if _, err := mfs.FlushPath(req.Context, nd.FilesRoot, dst); err != nil {
return fmt.Errorf("cp: cannot flush the created file %s: %s", dst, err)
Expand Down Expand Up @@ -847,6 +889,10 @@ Example:

flush, _ := req.Options[filesFlushOptionName].(bool)

if err := updateNoFlushCounter(nd, flush); err != nil {
return err
}

src, err := checkPath(req.Arguments[0])
if err != nil {
return err
Expand Down Expand Up @@ -984,6 +1030,10 @@ See '--to-files' in 'ipfs add --help' for more information.
flush, _ := req.Options[filesFlushOptionName].(bool)
rawLeaves, rawLeavesDef := req.Options[filesRawLeavesOptionName].(bool)

if err := updateNoFlushCounter(nd, flush); err != nil {
return err
}

if !rawLeavesDef && cfg.Import.UnixFSRawLeaves != config.Default {
rawLeavesDef = true
rawLeaves = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves)
Expand Down Expand Up @@ -1112,6 +1162,10 @@ Examples:

flush, _ := req.Options[filesFlushOptionName].(bool)

if err := updateNoFlushCounter(n, flush); err != nil {
return err
}

prefix, err := getPrefix(req)
if err != nil {
return err
Expand Down Expand Up @@ -1164,6 +1218,9 @@ are run with the '--flush=false'.
return err
}

// Reset the counter (flush always resets)
noFlushOperationCounter.Store(0)

return cmds.EmitOnce(res, &flushRes{enc.Encode(n.Cid())})
},
Type: flushRes{},
Expand Down
7 changes: 0 additions & 7 deletions core/node/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,6 @@ func Files(strategy string) func(mctx helpers.MetricsCtx, lc fx.Lifecycle, repo
return nil, err
}

// Configure MFS directory cache auto-flush threshold if specified (experimental)
cfg, err := repo.Config()
if err == nil && !cfg.Internal.MFSAutoflushThreshold.IsDefault() {
threshold := int(cfg.Internal.MFSAutoflushThreshold.WithDefault(int64(mfs.DefaultMaxCacheSize)))
root.SetMaxCacheSize(threshold)
}

lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
return root.Close()
Expand Down
4 changes: 2 additions & 2 deletions docs/changelogs/v0.38.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ Identity CIDs use [multihash `0x00`](https://github.com/multiformats/multicodec/

This release resolves several long-standing MFS issues: raw nodes now preserve their codec instead of being forced to dag-pb, append operations on raw nodes work correctly by converting to UnixFS when needed, and identity CIDs properly inherit the full CID prefix from parent directories.

#### MFS directory cache auto-flush
#### MFS operation limit for --flush=false

The new [`Internal.MFSAutoflushThreshold`](https://github.com/ipfs/kubo/blob/master/docs/config.md#internalmfsautoflushthreshold) configuration option prevents unbounded memory growth when using `--flush=false` with `ipfs files` commands by automatically flushing directories when their cache exceeds the configured threshold (default: 256 entries).
The new [`Internal.MFSNoFlushLimit`](https://github.com/ipfs/kubo/blob/master/docs/config.md#internalmfsnoflushlimit) configuration option prevents unbounded memory growth when using `--flush=false` with `ipfs files` commands. After performing the configured number of operations without flushing (default: 256), further operations will fail with a clear error message instructing users to flush manually.

### 📦️ Important dependency updates

Expand Down
27 changes: 12 additions & 15 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -1599,27 +1599,24 @@ Type: `flag`

**MOVED:** see [`Import.UnixFSHAMTDirectorySizeThreshold`](#importunixfshamtdirectorysizethreshold)

### `Internal.MFSAutoflushThreshold`
### `Internal.MFSNoFlushLimit`

Controls the number of entries cached in memory for each MFS directory before
auto-flush is triggered to prevent unbounded memory growth when using `--flush=false`
with `ipfs files` commands.
Controls the maximum number of consecutive MFS operations allowed with `--flush=false`
before requiring a manual flush. This prevents unbounded memory growth and ensures
data consistency when using deferred flushing with `ipfs files` commands.

When a directory's cache reaches this threshold, it will automatically flush to
the blockstore even when `--flush=false` is specified. This prevents excessive
memory usage while still allowing performance benefits of deferred flushing for
smaller operations.
When the limit is reached, further operations will fail with an error message
instructing the user to run `ipfs files flush`, use `--flush=true`, or increase
this limit in the configuration.

**Examples:**
* `256` - Default value. Provides a good balance between performance and memory usage.
* `0` - Disables cache limiting (behavior before Kubo 0.38). May cause high memory
usage with `--flush=false` on large directories.
* `1024` - Higher limit for systems with more available memory that need to perform
many operations before flushing.
**⚠️ WARNING:** Increasing this limit or disabling it (setting to 0) can lead to:
- **Out-of-memory errors (OOM)** - Each unflushed operation consumes memory
- **Data loss** - If the daemon crashes before flushing, all unflushed changes are lost
- **Degraded performance** - Large unflushed caches slow down MFS operations

Default: `256`

Type: `optionalInteger` (0 disables the limit, risky, may lead to errors)
Type: `optionalInteger` (0 disables the limit, strongly discouraged)

**Note:** This is an EXPERIMENTAL feature and may change or be removed in future releases.
See [#10842](https://github.com/ipfs/kubo/issues/10842) for more information.
Expand Down
Loading
Loading