config: concurrent updates silently overwrite unrelated settings
What happened:
Two concurrent juicefs config commands can silently overwrite each other's changes, even when they modify different fields and use the same client version. Both commands return success.
A deterministic example is an administrator waiting at the confirmation prompt for --trash-days 0 while another administrator changes the volume capacity. When the first administrator confirms, the capacity change is reverted without a conflict error.
What you expected to happen:
A stale configuration update should either preserve unrelated committed changes or fail with a conflict before writing anything. Two successful updates to different fields should not silently undo one another.
How to reproduce it (as minimally and precisely as possible):
Use the same newly created, empty scratch directory in both terminals and the same JuiceFS binary. No mount is needed.
Initialize an isolated volume:
juicefs format sqlite3://meta.db concurrent-config \ --storage file --bucket ./objects --capacity 1 --trash-days 1In terminal A, run:
juicefs config sqlite3://meta.db --trash-days 0Leave it waiting at
Proceed anyway? [y/N]:without answering yet. It has already read the configuration with a capacity of 1 GiB.In terminal B, run:
juicefs config sqlite3://meta.db --capacity 2 juicefs config sqlite3://meta.dbThe update succeeds and the stored
Capacityis2147483648(2 GiB).Return to terminal A and answer
y. Its update also succeeds. Read the configuration again:juicefs config sqlite3://meta.dbThe stored configuration now contains:
{ "Capacity": 1073741824, "TrashDays": 0 }TrashDayschanged as requested, but terminal B's successful capacity update was lost. Neither command reports a conflict. The reproduction was verified by rereading the raw SQLitejfs_settingrecord as well as checking both commands' exit status.
Anything else we need to know?
The configuration is read into a complete Format snapshot and later written back as a whole. The confirmation prompt makes the race easy to reproduce, but any delay between load and save can allow the same interleaving.
Source references, pinned to the tested commit:
configloads the snapshot, then passes it toInit.Format.updatevalidates some immutable fields but does not detect changes since the caller originally read the configuration.- SQL replaces the whole format value. Redis and KV have the same missing stale-snapshot check in their write paths. SQLite was tested; Redis and KV were checked in source only.
This is an existing configuration concurrency issue, distinct from the excluded-client/unknown-field problem addressed by #7533. The CLI reproduction above succeeds on that PR's head. A separate two-client Meta.Load/Meta.Init reproduction also demonstrated the lost update on main at c9a67b23e8e08ec23ec331aa6f1675e2319e921c, with force=false.
A possible fix is an atomic compare-and-swap against the originally read configuration or a configuration revision, shared by all format writers and all metadata engine families. On conflict, reject the stale write and let the caller reload and retry. A regression test should deterministically interleave two updates to different fields and verify that a successful change cannot be silently reverted.
Environment:
- JuiceFS:
1.5.0-dev+2026-09-14.32af8c126(PR #7533 head32af8c12626bb9474ac915eaac009177a403140e). - OS/architecture: macOS, Darwin arm64.
- Object storage: isolated local file directory.
- Metadata engine: isolated local SQLite database.
- Both processes use the same binary; no mounts, sessions, external services, or user volumes were used.
Source: juicedata/juicefs