Skip to content

Commit 0657c97

Browse files
committed
fix: add MFS operation limit for --flush=false
adds a global counter that tracks consecutive MFS operations performed with --flush=false and fails with clear error after limit is reached. this prevents unbounded memory growth while avoiding the data corruption risks of auto-flushing. - adds Internal.MFSNoFlushLimit config - operations fail with actionable error at limit - counter resets on successful flush or any --flush=true operation - operations with --flush=true reset and don't count this commit removes automatic flush from #10971 and instead errors to encourage users of --flush=false to develop a habit of calling 'ipfs files flush' periodically. boxo will no longer auto-flush (ipfs/boxo#1041) to avoid corruption issues, and kubo applies the limit to 'ipfs files' commands instead. closes #10842
1 parent 22f0377 commit 0657c97

File tree

5 files changed

+89
-38
lines changed

5 files changed

+89
-38
lines changed

config/internal.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
package config
22

3+
const (
4+
// DefaultMFSNoFlushLimit is the default limit for consecutive unflushed MFS operations
5+
DefaultMFSNoFlushLimit = 256
6+
)
7+
38
type Internal struct {
49
// All marked as omitempty since we are expecting to make changes to all subcomponents of Internal
510
Bitswap *InternalBitswap `json:",omitempty"`
611
UnixFSShardingSizeThreshold *OptionalString `json:",omitempty"` // moved to Import.UnixFSHAMTDirectorySizeThreshold
712
Libp2pForceReachability *OptionalString `json:",omitempty"`
813
BackupBootstrapInterval *OptionalDuration `json:",omitempty"`
9-
// MFSAutoflushThreshold controls the number of entries cached in memory
10-
// for each MFS directory before auto-flush is triggered to prevent
11-
// unbounded memory growth when using --flush=false.
12-
// Default: 256 (matches HAMT shard size)
13-
// Set to 0 to disable cache limiting (old behavior, may cause high memory usage)
14+
// MFSNoFlushLimit controls the maximum number of consecutive
15+
// MFS operations allowed with --flush=false before requiring a manual flush.
16+
// This prevents unbounded memory growth and ensures data consistency.
17+
// Set to 0 to disable limiting (old behavior, may cause high memory usage)
1418
// This is an EXPERIMENTAL feature and may change or be removed in future releases.
1519
// See https://github.com/ipfs/kubo/issues/10842
16-
MFSAutoflushThreshold OptionalInteger `json:",omitempty"`
20+
MFSNoFlushLimit *OptionalInteger `json:",omitempty"`
1721
}
1822

1923
type InternalBitswap struct {

core/commands/files.go

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"slices"
1212
"strconv"
1313
"strings"
14+
"sync"
15+
"sync/atomic"
1416
"time"
1517

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

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

40+
// Global counter for unflushed MFS operations
41+
var noFlushOperationCounter atomic.Int64
42+
43+
// Cached limit value (read once on first use)
44+
var (
45+
noFlushLimit int64
46+
noFlushLimitInit sync.Once
47+
)
48+
49+
// updateNoFlushCounter manages the counter for unflushed operations
50+
func updateNoFlushCounter(nd *core.IpfsNode, flush bool) error {
51+
if flush {
52+
// Reset counter when flushing
53+
noFlushOperationCounter.Store(0)
54+
return nil
55+
}
56+
57+
// Cache the limit on first use (config doesn't change at runtime)
58+
noFlushLimitInit.Do(func() {
59+
noFlushLimit = int64(config.DefaultMFSNoFlushLimit)
60+
if cfg, err := nd.Repo.Config(); err == nil && cfg.Internal.MFSNoFlushLimit != nil {
61+
noFlushLimit = cfg.Internal.MFSNoFlushLimit.WithDefault(int64(config.DefaultMFSNoFlushLimit))
62+
}
63+
})
64+
65+
// Check if limit reached
66+
if noFlushLimit > 0 && noFlushOperationCounter.Load() >= noFlushLimit {
67+
return fmt.Errorf("reached limit of %d unflushed MFS operations. "+
68+
"To resolve: 1) run 'ipfs files flush' to persist changes, "+
69+
"2) use --flush=true (default), or "+
70+
"3) increase Internal.MFSNoFlushLimit in config", noFlushLimit)
71+
}
72+
73+
noFlushOperationCounter.Add(1)
74+
return nil
75+
}
76+
3877
// FilesCmd is the 'ipfs files' command
3978
var FilesCmd = &cmds.Command{
4079
Helptext: cmds.HelpText{
@@ -68,12 +107,11 @@ of consistency guarantees. If the daemon is unexpectedly killed before running
68107
'ipfs files flush' on the files in question, then data may be lost. This also
69108
applies to run 'ipfs repo gc' concurrently with '--flush=false' operations.
70109
71-
When using '--flush=false', directories will automatically flush when the
72-
number of cached entries exceeds the Internal.MFSAutoflushThreshold config.
73-
This prevents unbounded memory growth. We recommend flushing
74-
paths regularly with 'ipfs files flush', specially the folders on which many
75-
write operations are happening, as a way to clear the directory cache, free
76-
memory and speed up read operations.`,
110+
When using '--flush=false', operations are limited to prevent unbounded memory
111+
growth. After reaching Internal.MFSNoFlushLimit operations,
112+
further operations will fail until you run 'ipfs files flush'. We recommend
113+
flushing paths regularly, especially folders with many write operations, to
114+
clear caches, free memory, and maintain good performance.`,
77115
},
78116
Options: []cmds.Option{
79117
cmds.BoolOption(filesFlushOptionName, "f", "Flush target and ancestors after write.").WithDefault(true),
@@ -516,12 +554,16 @@ being GC'ed.
516554
}
517555
}
518556

557+
flush, _ := req.Options[filesFlushOptionName].(bool)
558+
559+
if err := updateNoFlushCounter(nd, flush); err != nil {
560+
return err
561+
}
562+
519563
err = mfs.PutNode(nd.FilesRoot, dst, node)
520564
if err != nil {
521565
return fmt.Errorf("cp: cannot put node in path %s: %s", dst, err)
522566
}
523-
524-
flush, _ := req.Options[filesFlushOptionName].(bool)
525567
if flush {
526568
if _, err := mfs.FlushPath(req.Context, nd.FilesRoot, dst); err != nil {
527569
return fmt.Errorf("cp: cannot flush the created file %s: %s", dst, err)
@@ -847,6 +889,10 @@ Example:
847889

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

892+
if err := updateNoFlushCounter(nd, flush); err != nil {
893+
return err
894+
}
895+
850896
src, err := checkPath(req.Arguments[0])
851897
if err != nil {
852898
return err
@@ -984,6 +1030,10 @@ See '--to-files' in 'ipfs add --help' for more information.
9841030
flush, _ := req.Options[filesFlushOptionName].(bool)
9851031
rawLeaves, rawLeavesDef := req.Options[filesRawLeavesOptionName].(bool)
9861032

1033+
if err := updateNoFlushCounter(nd, flush); err != nil {
1034+
return err
1035+
}
1036+
9871037
if !rawLeavesDef && cfg.Import.UnixFSRawLeaves != config.Default {
9881038
rawLeavesDef = true
9891039
rawLeaves = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves)
@@ -1112,6 +1162,10 @@ Examples:
11121162

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

1165+
if err := updateNoFlushCounter(n, flush); err != nil {
1166+
return err
1167+
}
1168+
11151169
prefix, err := getPrefix(req)
11161170
if err != nil {
11171171
return err
@@ -1164,6 +1218,9 @@ are run with the '--flush=false'.
11641218
return err
11651219
}
11661220

1221+
// Reset the counter (flush always resets)
1222+
noFlushOperationCounter.Store(0)
1223+
11671224
return cmds.EmitOnce(res, &flushRes{enc.Encode(n.Cid())})
11681225
},
11691226
Type: flushRes{},

core/node/core.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,6 @@ func Files(strategy string) func(mctx helpers.MetricsCtx, lc fx.Lifecycle, repo
246246
return nil, err
247247
}
248248

249-
// Configure MFS directory cache auto-flush threshold if specified (experimental)
250-
cfg, err := repo.Config()
251-
if err == nil && !cfg.Internal.MFSAutoflushThreshold.IsDefault() {
252-
threshold := int(cfg.Internal.MFSAutoflushThreshold.WithDefault(int64(mfs.DefaultMaxCacheSize)))
253-
root.SetMaxCacheSize(threshold)
254-
}
255-
256249
lc.Append(fx.Hook{
257250
OnStop: func(ctx context.Context) error {
258251
return root.Close()

docs/changelogs/v0.38.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ Identity CIDs use [multihash `0x00`](https://github.com/multiformats/multicodec/
106106

107107
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.
108108

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

111-
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).
111+
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.
112112

113113
### 📦️ Important dependency updates
114114

docs/config.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,27 +1599,24 @@ Type: `flag`
15991599

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

1602-
### `Internal.MFSAutoflushThreshold`
1602+
### `Internal.MFSNoFlushLimit`
16031603

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

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

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

16201617
Default: `256`
16211618

1622-
Type: `optionalInteger` (0 disables the limit, risky, may lead to errors)
1619+
Type: `optionalInteger` (0 disables the limit, strongly discouraged)
16231620

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

0 commit comments

Comments
 (0)