From cfb925e5ae5da37be7e3356059516b63482c4fa7 Mon Sep 17 00:00:00 2001 From: Antonio SJ Musumeci Date: Mon, 21 Sep 2026 10:01:04 -0500 Subject: [PATCH] docs: clarify branch resolution and mount ordering --- mkdocs/docs/config/branches-mount-timeout.md | 26 +++++++ mkdocs/docs/config/branches.md | 37 ++++++++-- mkdocs/docs/config/rename-exdev.md | 3 +- mkdocs/docs/faq/why_isnt_it_working.md | 2 +- mkdocs/docs/index.md | 2 +- mkdocs/docs/known_issues_bugs.md | 6 +- mkdocs/docs/project_comparisons.md | 4 +- mkdocs/docs/quickstart.md | 76 ++++++++++++++++---- mkdocs/docs/runtime_interface.md | 10 +-- mkdocs/docs/tooling.md | 7 +- 10 files changed, 138 insertions(+), 35 deletions(-) diff --git a/mkdocs/docs/config/branches-mount-timeout.md b/mkdocs/docs/config/branches-mount-timeout.md index c2f35ea8..3f28b5d8 100644 --- a/mkdocs/docs/config/branches-mount-timeout.md +++ b/mkdocs/docs/config/branches-mount-timeout.md @@ -38,6 +38,32 @@ These files and xattrs are used to help `mergerfs` know whether or not a directory happens to be a branch or is the underlying directory mount point. +## Paths which appear after startup + +The [`branches` resolution process](branches.md#resolution) runs before +this option starts waiting. A missing literal path is retained as a +branch, so a configuration such as `/mnt/hdd/disk0/foobar` can work +when `/mnt/hdd/disk0` is mounted asynchronously and `foobar` only +becomes visible afterward, provided the path then meets one of the +"mounted" conditions above. + +For each branch which is not ready, mergerfs first runs `mount` once +with the full branch path as the target and then polls that same path +until the timeout expires. It does not create the path, try to mount +its parent, or rerun `mount`. In the example above, the initial +`mount /mnt/hdd/disk0/foobar` may fail while the path is absent. If +another service mounts `/mnt/hdd/disk0` and +`/mnt/hdd/disk0/foobar` appears and meets one of the "mounted" +conditions before the deadline, mergerfs detects it during polling +and continues. + +Globs are different. If `/mnt/hdd/disk0/*` has no matches during +branch resolution, the literal path `/mnt/hdd/disk0/*` is retained. +The mount wait does not expand it again after `/mnt/hdd/disk0` +becomes available. Ensure the parent filesystem is mounted before +mergerfs, configure a literal branch path, or update `branches` +through the runtime API once the paths exist. + **NOTE:** If on a `systemd` based system and using `fstab` it is a good idea to set the mount option diff --git a/mkdocs/docs/config/branches.md b/mkdocs/docs/config/branches.md index 2f2a5293..f51f1cdf 100644 --- a/mkdocs/docs/config/branches.md +++ b/mkdocs/docs/config/branches.md @@ -14,7 +14,7 @@ Branches currently have two options which can be set. A included in a policy calculation and a individual [minfreespace](#minfreespace) value. The values are set by prepending an `=` at the end of a branch designation and using commas -as delimiters. Example: `/mnt/drive=RW,1234` +as delimiters. Example: `/mnt/hdd/disk0=RW,1234` ### branch mode @@ -34,6 +34,33 @@ Same purpose and syntax as the [global option](minfreespace.md) but specific to the branch. Defaults to the global value. +### resolution + +Each colon-delimited branch designation is resolved independently when +the `branches` option is parsed, both at startup and when it is updated +through the runtime API: + +1. The path portion is expanded as a glob. +2. If the glob matches one or more directories, each match becomes a + branch. +3. If the glob has no matches, mergerfs logs a notice and keeps the + configured path unchanged as a single, literal branch. +4. mergerfs resolves each resulting path with `realpath`. If that + succeeds, the canonical path is stored. If it fails, typically + because the path does not exist yet, the unchanged path is stored. +5. A path which exists but is not a directory is logged and skipped. A + path which does not exist is logged but retained. + +Consequently, a literal branch such as `/mnt/hdd/disk0/foobar` may be +configured before it exists and can become usable after +`/mnt/hdd/disk0` is mounted. Branch resolution is not repeated in the +background, however. If `/mnt/hdd/disk0/*` has no matches when it is +resolved, mergerfs stores the literal path `/mnt/hdd/disk0/*`; +directories which match it later are not discovered automatically. +Update `branches` through the runtime API or restart mergerfs to +resolve the glob again. + + ### globbing To make it easier to include multiple branches mergerfs supports @@ -42,18 +69,18 @@ MUST be escaped when using via the shell else the shell itself will apply the glob itself.** ``` -# mergerfs /mnt/hdd\*:/mnt/ssd /media +# mergerfs /mnt/hdd/\*:/mnt/ssd/\* /media ``` -The above line will use all directories in /mnt prefixed with **hdd** -as well as **ssd**. +The above line will use all directories within `/mnt/hdd` and +`/mnt/ssd`. To have the pool mounted at boot or otherwise accessible from related tools use `/etc/fstab`. ``` # -/mnt/hdd*:/mnt/ssd /media mergerfs minfreespace=16G 0 0 +/mnt/hdd/*:/mnt/ssd/* /media mergerfs minfreespace=16G 0 0 ``` **NOTE:** The globbing is done at mount or when updated using the diff --git a/mkdocs/docs/config/rename-exdev.md b/mkdocs/docs/config/rename-exdev.md index 203e053d..20773d42 100644 --- a/mkdocs/docs/config/rename-exdev.md +++ b/mkdocs/docs/config/rename-exdev.md @@ -2,7 +2,8 @@ If using path preservation and a `rename` fails with `EXDEV`: -1. Move file from `/branch/a/b/c` to `/branch/.mergerfs_rename_exdev/a/b/c`. +1. Move file from `/mnt/hdd/disk0/a/b/c` to + `/mnt/hdd/disk0/.mergerfs_rename_exdev/a/b/c`. 2. symlink the rename's `newpath` to the moved file. The `target` value is determined by the value of `rename-exdev`. diff --git a/mkdocs/docs/faq/why_isnt_it_working.md b/mkdocs/docs/faq/why_isnt_it_working.md index 0bf5f819..bc4f46b8 100644 --- a/mkdocs/docs/faq/why_isnt_it_working.md +++ b/mkdocs/docs/faq/why_isnt_it_working.md @@ -81,7 +81,7 @@ where it was created. $ sudo touch /mnt/mergerfs/new-file $ sudo getfattr -n user.mergerfs.allpaths /mnt/mergerfs/new-file # file: mnt/mergerfs/new-file -user.mergerfs.allpaths="/mnt/hdd/drive1/new-file" +user.mergerfs.allpaths="/mnt/hdd/disk0/new-file" ``` If the location of the file is where it should be according to the diff --git a/mkdocs/docs/index.md b/mkdocs/docs/index.md index 4a293998..ce9558e7 100644 --- a/mkdocs/docs/index.md +++ b/mkdocs/docs/index.md @@ -105,7 +105,7 @@ The way in which mergerfs behaves is controlled by the ``` A + B = C -/disk1 /disk2 /merged +/mnt/hdd/disk0 /mnt/hdd/disk1 /media | | | +-- /dir1 +-- /dir1 +-- /dir1 | | | | | | diff --git a/mkdocs/docs/known_issues_bugs.md b/mkdocs/docs/known_issues_bugs.md index 8817c5e8..58833d4f 100644 --- a/mkdocs/docs/known_issues_bugs.md +++ b/mkdocs/docs/known_issues_bugs.md @@ -92,15 +92,15 @@ If you always want the directory information from the one with the most recent mtime then use the `newest` policy for `getattr`. -### 'mv /mnt/pool/foo /mnt/disk1/foo' removes 'foo' +### 'mv /mnt/pool/foo /mnt/hdd/disk0/foo' removes 'foo' This is not a bug. Run in verbose mode to better understand what's happening: ``` -$ mv -v /mnt/pool/foo /mnt/disk1/foo -copied '/mnt/pool/foo' -> '/mnt/disk1/foo' +$ mv -v /mnt/pool/foo /mnt/hdd/disk0/foo +copied '/mnt/pool/foo' -> '/mnt/hdd/disk0/foo' removed '/mnt/pool/foo' $ ls /mnt/pool/foo ls: cannot access '/mnt/pool/foo': No such file or directory diff --git a/mkdocs/docs/project_comparisons.md b/mkdocs/docs/project_comparisons.md index 8fb2eb01..4e6ef4f5 100644 --- a/mkdocs/docs/project_comparisons.md +++ b/mkdocs/docs/project_comparisons.md @@ -23,9 +23,9 @@ and offers better performance. As of 2020, the author of mhddfs has Below is an example of mhddfs and mergerfs setup to work similarly. ``` -mhddfs -o mlimit=4G,allow_other /mnt/drive1,/mnt/drive2 /mnt/pool +mhddfs -o mlimit=4G,allow_other /mnt/hdd/disk0,/mnt/hdd/disk1 /mnt/pool -mergerfs -o minfreespace=4G,category.create=ff /mnt/drive1:/mnt/drive2 /mnt/pool +mergerfs -o minfreespace=4G,category.create=ff /mnt/hdd/disk0:/mnt/hdd/disk1 /mnt/pool ``` diff --git a/mkdocs/docs/quickstart.md b/mkdocs/docs/quickstart.md index 8db95bc6..374d97c8 100644 --- a/mkdocs/docs/quickstart.md +++ b/mkdocs/docs/quickstart.md @@ -66,24 +66,69 @@ branches](config/branches.md#branch-setup). ``` # Two positional options: `:` colon separated branches and mountpoint -mergerfs -o opt[,opt...] /branch0:/branch1 /mountpoint +mergerfs -o opt[,opt...] /mnt/hdd/disk0:/mnt/hdd/disk1:/mnt/hdd/disk2 /media # or list of positional options where the last is the mountpoint -mergerfs -o opt[,opt...] /branch0 /branch1 /mountpoint +mergerfs -o opt[,opt...] /mnt/hdd/disk0 /mnt/hdd/disk1 /mnt/hdd/disk2 /media # or set mountpoint via -o mountpoint -mergerfs -o mountpoint=/mountpoint /branch0 /branch1 +mergerfs -o mountpoint=/media /mnt/hdd/disk0 /mnt/hdd/disk1 /mnt/hdd/disk2 ``` ``` -mergerfs -o cache.files=off,category.create=pfrd,func.getattr=newest,dropcacheonclose=false /mnt/hdd0:/mnt/hdd1 /media +mergerfs -o cache.files=off,category.create=pfrd,func.getattr=newest,dropcacheonclose=false /mnt/hdd/disk0:/mnt/hdd/disk1:/mnt/hdd/disk2 /media ``` ### /etc/fstab ``` -/mnt/hdd0:/mnt/hdd1 /media mergerfs cache.files=off,category.create=pfrd,func.getattr=newest,dropcacheonclose=false 0 0 +/mnt/hdd/disk0:/mnt/hdd/disk1:/mnt/hdd/disk2 /media mergerfs cache.files=off,category.create=pfrd,func.getattr=newest,dropcacheonclose=false 0 0 ``` +### systemd mount dependencies + +On a `systemd` based system, no custom service or setup script is +needed when the branch filesystems and mergerfs are all configured in +`/etc/fstab`. Add +[`x-systemd.requires-mounts-for=`](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html#x-systemd.wants-mounts-for=) +once for each branch mount target in the mergerfs entry: + +```fstab +LABEL=disk0 /mnt/hdd/disk0 ext4 defaults 0 2 +LABEL=disk1 /mnt/hdd/disk1 ext4 defaults 0 2 +LABEL=disk2 /mnt/hdd/disk2 ext4 defaults 0 2 +/mnt/hdd/disk0:/mnt/hdd/disk1:/mnt/hdd/disk2 /media mergerfs cache.files=off,category.create=pfrd,func.getattr=newest,dropcacheonclose=false,x-systemd.requires-mounts-for=/mnt/hdd/disk0,x-systemd.requires-mounts-for=/mnt/hdd/disk1,x-systemd.requires-mounts-for=/mnt/hdd/disk2 0 0 +``` + +Replace the example labels and filesystem types with those used by the +system. `systemd` creates `Requires=` and `After=` dependencies from +the mergerfs mount to `mnt-hdd-disk0.mount`, +`mnt-hdd-disk1.mount`, and `mnt-hdd-disk2.mount`. It starts those +mounts first and does not start the mergerfs mount if a required mount +fails. This checks the mount units, not merely whether the +`/mnt/hdd/diskN` directories exist. + +After editing `/etc/fstab`, reload the generated units: + +```shell +sudo systemctl daemon-reload +``` + +The dependencies apply the next time `/media` is mounted and on +subsequent boots. + +When using a service such as the +[`mergerfs-media.service`](#systemd-simple) example below instead of an +`/etc/fstab` mergerfs entry, add the equivalent directive to its +`[Unit]` section: + +```systemd +RequiresMountsFor=/mnt/hdd/disk0 /mnt/hdd/disk1 /mnt/hdd/disk2 +``` + +`RequiresMountsFor=` adds both the requirement and ordering +dependencies, so separate `Requires=` and `After=` entries for these +mounts are unnecessary. + ### /etc/fstab w/ config file For more complex setups it can be useful to separate out the config. @@ -114,10 +159,9 @@ resolve the symlinks and use the real path. `ls -lh /etc/mergerfs/branches/media/*` ```text -lrwxrwxrwx 1 root root 21 Aug 4 2023 hdd00 -> /mnt/hdd/hdd00 -lrwxrwxrwx 1 root root 21 Aug 4 2023 hdd01 -> /mnt/hdd/hdd01 -lrwxrwxrwx 1 root root 21 Aug 4 2023 hdd02 -> /mnt/hdd/hdd02 -lrwxrwxrwx 1 root root 21 Aug 4 2023 hdd03 -> /mnt/hdd/hdd03 +lrwxrwxrwx 1 root root 14 Aug 4 2023 disk0 -> /mnt/hdd/disk0 +lrwxrwxrwx 1 root root 14 Aug 4 2023 disk1 -> /mnt/hdd/disk1 +lrwxrwxrwx 1 root root 14 Aug 4 2023 disk2 -> /mnt/hdd/disk2 ``` ### systemd (simple) @@ -127,6 +171,7 @@ lrwxrwxrwx 1 root root 21 Aug 4 2023 hdd03 -> /mnt/hdd/hdd03 ```systemd title="mergerfs-media.service" linenums="1" [Unit] Description=mergerfs /media service +RequiresMountsFor=/mnt/hdd/disk0 /mnt/hdd/disk1 /mnt/hdd/disk2 After=local-fs.target network.target [Service] @@ -138,8 +183,9 @@ ExecStart=/usr/bin/mergerfs \ -o category.create=pfrd \ -o func.getattr=newest \ -o dropcacheonclose=false \ - /mnt/hdd0 \ - /mnt/hdd1 \ + /mnt/hdd/disk0 \ + /mnt/hdd/disk1 \ + /mnt/hdd/disk2 \ /media ExecStop=/usr/bin/umount /media # Or if you need fusermount @@ -193,8 +239,9 @@ WantedBy=default.target ```systemd title="mergerfs-media.service" linenums="1" [Unit] Description=mergerfs /media service +RequiresMountsFor=/mnt/hdd/disk0 /mnt/hdd/disk1 /mnt/hdd/disk2 Requires=setup-for-mergerfs.service -After=local-fs.target network.target prepare-for-mergerfs.service +After=local-fs.target network.target setup-for-mergerfs.service [Service] Type=simple @@ -205,8 +252,9 @@ ExecStart=/usr/bin/mergerfs \ -o category.create=pfrd \ -o func.getattr=newest \ -o dropcacheonclose=false \ - /mnt/hdd0 \ - /mnt/hdd1 \ + /mnt/hdd/disk0 \ + /mnt/hdd/disk1 \ + /mnt/hdd/disk2 \ /media ExecStop=/usr/bin/umount /media # Or if you need fusermount diff --git a/mkdocs/docs/runtime_interface.md b/mkdocs/docs/runtime_interface.md index 8ac61040..3230cbf4 100644 --- a/mkdocs/docs/runtime_interface.md +++ b/mkdocs/docs/runtime_interface.md @@ -110,7 +110,7 @@ situation and continue to be able to access the xattr interface. ``` [trapexit:/mnt/mergerfs] $ getfattr -d .mergerfs -user.mergerfs.branches="/mnt/a=RW:/mnt/b=RW" +user.mergerfs.branches="/mnt/hdd/disk0=RW:/mnt/hdd/disk1=RW" user.mergerfs.minfreespace="4294967295" user.mergerfs.moveonenospc="false" ... @@ -122,9 +122,9 @@ user.mergerfs.category.search="mfs" [trapexit:/mnt/mergerfs] $ getfattr -n user.mergerfs.category.create .mergerfs user.mergerfs.category.search="prfd" -[trapexit:/mnt/mergerfs] $ setfattr -n user.mergerfs.branches -v '+