This points to an abandoned patch and can be removed
LU-19066 ofd: add os_failure_domain to struct obd_statfs Add a new field to lfs df to show the failure domain. Update "lfs df --output=" so that when used to just print a single field we do not pad it to a fixed width with leading spaces. This makes it easier to parse the outputs in tests. Example: lfs df --output=domain Also add a ltq_failure_domain field to lu_tgt_qos and set it when grab the statfs data for an object. We do not yet use this field for anything but will need this information later once we add failure domain awareness to the allocator. Test-Parameters: trivial Signed-off-by: Ronnie Sahlberg <rsahlberg@whamcloud.com> Change-Id: Ic5bcc66b7570ad74886a550b77d91a235e72756d
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-2 retesting | RHEL 10.1 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-dne-zfs-part-5 failed 2× crashed | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: sanityn. %% THIS TEST SESSION CRASHED %% | session |
Going forward, all of these lines should be replaced with a label: Assisted-by: ClaudeCode:MODEL_VERSION [TOOLNAME ...] https://wiki.lustre.org/Commit_Comments#AI/LLM/Tool_Attribution
Why is this for `fortestonly`? Should this be consolidated with another patch, or that annotation be removed?
Probably it was generated initially via AI, and nobody removed that designation as the patch was being updated?
(typo) The third path is `lov_io_lseek_end()`, which is the `.cio_end` entry in `lov_io_ops[CIT_LSEEK]` and runs from `cl_io_end()`, not from unlock. `lov_io_unlock()` is a separate op. Should this read "during sub-IO end"?
[Marc Bot] (style) This attribution line was flagged on patchset 34 and is still unresolved: it should use the `Assisted-by:` label format described at https://wiki.lustre.org/Commit_Comments#AI/LLM/Tool_Attribution instead of the free-form line.
"three independent paths" does not seem to hold for the third one.
The LSEEK sub-lock enqueue happens in cl_lockset_lock(), which cl_io_lock() runs *after* every cio_lock(), so the stripe is already marked by the time lov_io_call(cl_io_start) runs and the sub-IO is skipped there. A sub-IO that never started still has:
sub_io->ci_result = 0 /* lov_io_sub_init() */
sub_io->u.ci_lseek.ls_result = -ENXIO /* inherited from the parent in lov_io_sub_inherit(); ll_lseek() seeds it */
lov_io_lseek_end() already ignores both (`ci_result == 0` is a no-op, `sub_off == -ENXIO` hits the existing continue), so there is nothing for the third hunk to catch.
Also, ci_result propagation happens in .cio_end (lov_io_lseek_end), not during unlock.
(style) This was raised on an earlier patchset and the line is unchanged: tool attribution should use the `Assisted-by:` trailer format documented at https://wiki.lustre.org/Commit_Comments#AI/LLM/Tool_Attribution rather than a free-form sentence.
This guard sits in `lov_io_call()`, which is the shared dispatcher for four different ops:
lov_io_lock() -> lov_io_call(cl_io_lock)
lov_io_start() -> lov_io_call(cl_io_start)
lov_io_iter_fini() -> lov_io_call(lov_io_iter_fini_wrapper)
lov_io_unlock() -> lov_io_call(lov_io_unlock_wrapper)
All four are registered for CIT_LSEEK, so a stripe marked LSS_READ_ERR also skips `cl_io_unlock()` and `cl_io_iter_fini()` on its sub-IO, not just `cl_io_start()`. Its `ci_state` then goes CIS_LOCKED -> CIS_IO_FINISHED (set by `lov_io_end_wrapper()` in `lov_io_lseek_end()`) -> CIS_FINI, never passing through CIS_UNLOCKED/CIS_IT_ENDED.
Nothing leaks today because `osc_io_ops[CIT_LSEEK]` registers only cio_start/cio_end/cio_fini and the LSEEK DLM lock is taken on the top IO by `vvp_io_lseek_lock()`. But the intent is only to skip the data-fetch pass -- would putting the check in `lov_io_start()` (or keying it on `iofunc == cl_io_start`) keep the cleanup passes balanced?
(style) This isn't a bug, but `str` reads like a string; the rest of this file spells it `stripe` (see `lov_io_lseek_end()` a few hundred lines down, which uses `index`/`stripe` for the same two values). Worth matching if the patch is refreshed.
[Marc Bot] (defect) Skipping the stripe treats its extents as holes, but on an EC file that data is still readable via parity reconstruction. If the only data between ls_start and the next healthy-stripe data lives on the degraded stripe, SEEK_DATA returns the later offset, or -ENXIO if none, so sparse-aware tools (cp, tar) silently drop data that read() would return. Also, if every sub-IO in lis_active is skipped (e.g. single-stripe data component), offset stays -ENXIO and SEEK_HOLE fails with -ENXIO even though ls_start < file size, which breaks the SEEK_HOLE contract of a virtual hole at EOF. Would it be safer to treat a degraded stripe's covered range as data instead of skipping it, or to return an error rather than a misleading data map? The commit message claim that the remaining healthy stripes provide valid seek results does not hold in these cases.
(defect) Skipping cl_io_start() for the degraded stripe means its allocation map never contributes to the seek result, and lseek does not reconstruct anything from parity the way CIT_EC_RD does. So the answer is not "the remaining healthy stripes are sufficient" -- it is an answer computed from an incomplete extent map.
Concrete case, 4+2 EC, one data OST deactivated, data written only in the range that maps to that stripe:
lseek(fd, 0, SEEK_DATA)
-> degraded sub skipped, others report -ENXIO
-> offset stays -ENXIO, lseek fails
and with data further out on a healthy stripe it returns that later offset instead. Sparse-aware copies (cp --sparse, tar, rsync) would silently drop the bytes that a plain read() still returns via parity.
SEEK_HOLE has the mirror problem: if every sub-IO covering ls_start is skipped, `offset` stays -ENXIO and ll_lseek() returns -ENXIO for an offset below i_size, which breaks the "there is always a virtual hole at EOF" contract.
Is returning an error preferable to returning a wrong offset here? Alternatively, could the degraded stripe's range be reported as data (conservative) rather than dropped?
+1; error should be preferable compared with a wrong offset
(typo) This comment uses a non-ASCII em dash; the rest of the tree is plain ASCII. Plain "-" or "--" instead.
This hunk looks like it has no effect. Any stripe marked LSS_READ_ERR was already skipped by the new check in lov_io_call(), so its sub-IO never reached cl_io_start(): ci_result is still 0 from lov_io_sub_init(), and ls_result is still the -ENXIO that lov_io_sub_inherit() copied from the parent. The `if (io->ci_result == 0)` assignment and the `sub_off == -ENXIO` continue below both already handle that. Is there a path where a sub-IO is marked LSS_READ_ERR but still ran? If not, dropping this hunk would keep the two skip conditions from having to stay in sync.
LU-12668 lov: handle ESHUTDOWN for LSEEK on EC files When an OST hosting a data stripe of an EC file is deactivated, SEEK_DATA/SEEK_HOLE fails with ESHUTDOWN because the error propagates through the LOV layer during sub-lock enqueue. Fix this by marking the degraded stripe LSS_READ_ERR in lov_lock_enqueue (same mechanism used by CIT_EC_RD for parity recovery), then skipping those stripes in lov_io_call and lov_io_lseek_end. This avoids ESHUTDOWN leaking through three independent paths: sub-lock enqueue, sub-IO function dispatch, and sub-IO ci_result propagation during unlock. Add lov_lsm_has_parity() helper to check if any layout entry has parity, needed because CIT_LSEEK locks the data component while parity lives in a separate entry. This fixes lfs mirror verify failing on EC files when any data OST is deactivated. Generated with Claude Code + Tools Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity-ec fstype=zfs Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Change-Id: I5cce4e0ea51c68b0c6fda1d83b694af19cad57bd
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-ldiskfs-ubuntu retesting | RHEL 8.10 / x86_64, Ubuntu 24.04 / x86_64 | ran 6 tests. 1 tests failed: sanity-lnet. | session |
That seems like a pretty big hammer, basically breaking AIO completely for EC files, rather than just the recovery path? Or is the comment wrong and this is triggered only for EC recovery when `-EIOCBQUEUED` is returned? Would it be better to push **all** EC recovery to a workqueue instead of keeping it directly in the IO path, then it can fire the AIO completion when the reconstruction is complete, and synchronous readers would wait on the completion?
Andreas, yes, correct. This is essentially making AIO synchronous for all reads on EC files because we can't tell at submission time if we need reconstruction. So, this patch is just a stopgap to allow AIO to work for degraded reads, albeit with collateral damage. I'd agree that a workqueue is the better long-term approach, however, I'm not familiar enough with the code path at the minute here to gauge the effort required. So, I'd keep this patch as a short-term fix for AIO for degraded reads, and open a ticket so we address this properly with a workqueue. Do you agree? If yes, I open the ticket
(minor) aio_ec_sync deliberately survives the goto restart at the end of the function, and it has to. cda_no_aio_complete and cda_creator_free stay set on the cl_dio_aio across restarts, so a later pass that ends with anything other than -EIOCBQUEUED (cl_io_rw_init() failing, or cl_io_loop() returning a hard error) still needs both !is_aio || aio_ec_sync tests to be true. If it were cleared at restart, __cl_sync_io_note() would skip the free because creator_free is 1, and the creator branch would skip it too, leaking the cl_dio_aio and its cda_obj reference. This isn't a bug as written, but the coupling between a stack bool and two flags on a heap struct isn't obvious. Could the comment above say the flag is intentionally sticky, so it doesn't get reset in a later cleanup?
on refresh
LU-12669 llite: make AIO reads on EC file synchronous For sync DIO, EC recovery on read failure runs inline in ll_file_io_generic after cl_sync_io_wait_recycle. AIO has the same recovery requirement but cannot run recovery from the sub-DIO completion path -- ll_file_io_generic returns -EIOCBQUEUED to the VFS before the BRW completion fires, so the syscall context recovery needs (the lu_env, the user's iov_iter, the range lock) is gone by the time the read error is known. Rather than building a workqueue-based async recovery path, make AIO reads on EC files behave as sync DIO: after cl_io_loop, if an AIO read on an EC layout (io->ci_cross_ec, set by lov_io_mirror_init during cl_io_rw_init) got -EIOCBQUEUED, set cda_no_aio_complete and cda_creator_free to disarm the async ki_complete path, then drop into the same wait + restart code sync DIO uses. The VFS calls ki_complete itself when we return. The flag flip is race-free because the submission ref on cda_sync is still held -- end_io cannot fire until we drop it in cl_sync_io_wait_recycle. This disables async semantics for every AIO read on an EC file, not only those that need recovery -- we cannot tell at submission time whether reconstruction will be needed, so the sync conversion fires unconditionally on -EIOCBQUEUED for an EC layout. As a side effect, the range_lock taken by ll_file_io_generic is now held until all sub-DIOs have drained, instead of being dropped while they are still in flight. Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity-ec fstype=zfs Assisted-by: Opus:4.8 llm_code_and_review_tools Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Signed-off-by: Marc Vef <mvef@whamcloud.com> Change-Id: I394a3610e33b29ead8f5adb52dfa21db6b721944
| failed enforced test | platform | detail | |
|---|---|---|---|
| custom-1001 crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
| custom-1002 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-subtest-change failed 2× | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-zfs-subtest-change crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
(defect) The body still carries the `--WIP--` marker, and says the tool "can currently only be run by the user, but will be adapted to allow integration to sanity-ec" -- but this revision already adds that integration: sanity-ec test_50/51/52 drive the orchestrator directly. Is this still meant as work in progress, or should the message be rewritten to describe what actually landed? As it stands it reads as a stack of amend notes rather than a description of the change.
(typo) "the user can disabled specific OSTs" -> "can disable".
(defect) Parts of the diff aren't accounted for anywhere in the body, so it's hard to tell what is deliberate: - sanity-ec test_40b/40c/40d, which are plain `fail_loc` degraded-read tests and don't use the orchestrator at all - the `EXCEPT_SLOW="50 51"` gate - the curses TUI in ec_fault_gui.py - the `--matrix`, `--write-verify` and `--layout-audit` modes - the report directory machinery (run.info / summary.txt / per-FAIL `lctl dk` + dmesg capture) - the `--ost-host` ssh routing for multi-node Also "8 OSTs are required to run" no longer matches the code: 52 needs 6, and 40b/40c need 3. Could the body be brought back in line with the diff?
(minor) "Every fault is read back from the owning OSS before the read is issued" holds for --matrix, but not for --soak, which the same message describes a few lines down as injecting the fault mid-I/O. run_soak_loop() starts the read thread first, sleeps a fraction of the baseline, then arms the fault and only afterwards calls _verify_fault_armed(). Worth rewording to "every fault is read back from the owning OSS" without the ordering claim?
BUILD
(defect) This takes a live OST down with `umount -f`, mounts the raw ldiskfs elsewhere, corrupts a block and remounts -- all outside the test framework. Two concerns: The remount is a bare `mount -t lustre $dev $mntpt`, so `$OST_MOUNT_OPTS` / `$MOUNTOPT` from the config are lost. `stop ost$n` / `start ost$n` in test-framework.sh handle those (and `wait_osc_import_state`). Recovery is only a Python `finally`. If the process is SIGKILLed -- an auster timeout, for instance -- the OST stays down and the backend can be left mounted at /tmp/ec_wv_ostbk_N, which breaks every later suite in the run. sanity-ec test_52 registers no `stack_trap` to put the OST back either.
(typo) The bitmask is bits 0-15, not bits 16+. cfs_fail_index() treats any fail_val above 0xffff as bitmask mode and then tests BIT(index) for index < 16, which matches what _apply_fail_loc() builds (0x10000 | bitmask) and what deactivate_ost() logs ("only supports 0-15").
(minor) "At least 8 OSTs configured" is left over from an earlier revision. _soak_random_params(), _wv_geometry() and run_matrix() all scale down to a 2+1 geometry, and the commit message says three OSTs is a real run.
(minor) The MDS is not covered by this. _all_oss_hosts() is built purely from ost.server_host, which comes from --ost-host or the OSC ost_conn_uuid NID, and sanity-ec only passes --ost-host entries. There is no MDS host anywhere in the tool, so on a config where the MDS is a separate node its catastrophe flag is never read. Either drop the MDS from the claim, or add an --mds-host that sanity-ec fills in from facet_host mds1.
(minor) This readback, _apply_fail_loc() and _clear_fault_params() all take run_on_host()'s default timeout of 10 s, while SSH_OPTS alone allows ConnectTimeout=10. On a loaded OSS the ssh gets SIGKILLed at the 10 s mark and this returns "fail_loc readback failed", which the soak and the matrix then report as an EC failure rather than a transport problem. run_on_host()'s own docstring says "server-side callers pass generous timeouts" - these three are the ones that don't. The umount/mount and dmesg paths already pass 30-120 s.
(defect) This poll returns True on the first iteration whether or not the OST came back. `lfs df` prints one line per OST regardless of state: mntdf() synthesises the uuid as "OST%04x" when the statfs failed, and showdf() prints `<uuid>: inactive device` for -ENODATA and `<uuid>: <strerror>` for any other error. Every one of those lines contains "OST", so `grep -c OST` is always OSTCOUNT and the `>= self.ostcount` test is satisfied immediately. That makes the guard in _wv_corrupt_check() dead, and `lfs mirror verify` can run before the just-remounted OST has reconnected. Would checking that each OST line actually reports space (or polling the osc import state, as wait_osc_import_state() does) work better here?
(defect) A degraded read that hangs is scored here as a correct failure. _matrix_degraded_read() gets its verdict from _compute_file_hash(), which returns None both when dd exits with -EIO and when run_cmd() kills it on matrix_read_timeout. Either way read_ok is False, so this counts npass += 1. The comment above _matrix_degraded_read() says a read that hangs instead of returning -EIO "is exactly the regression this mode hunts", but as written the over-tolerance loop cannot fail on it. A handful of hung reads at 120 s each stays well inside the 1800 s budget, so unrun is 0 and the check passes. Could run_cmd()/_compute_file_hash() distinguish a timeout from a non-zero exit, so a timed-out over-tolerance read is a failure rather than the expected outcome?
LU-12668 tests: add EC failure orchestrator Add ec_fault_orchestrator.py, a fault-injection and verification tool for erasure-coded files, plus the sanity-ec tests that drive it. The tool arms OBD_FAIL_OST_BRW_READ_BULK on the OSS owning each target OST, so a read really has to reconstruct from parity rather than being served from cache or a healthy stripe. Every fault is read back from the owning OSS, so a fault that failed to arm is reported instead of passing as a successful reconstruction. Reads are compared against an md5 of the bytes as they were written, captured via tee at write time, rather than against an earlier read of the same file that may already be wrong. Five non-interactive modes, each returning its verdict as an exit code: - --soak: randomized layout, geometry and size per iteration, with a fault injected mid-I/O. Records MB/s and the degraded-vs-baseline slowdown per iteration, and gates on a kernel-health check (the catastrophe flag on every server node it knows of plus the dmesg corruption markers test-framework.sh already curates). - --matrix: on a single-RAID-set file, faults every combination of 1..P stripes and requires reconstruction, then P+1 and requires the read to fail with an error. A P+1 read that hangs instead is scored a failure, not the expected outcome -- that hang is the regression this mode looks for. Bounded by --matrix-budget and --matrix-read-timeout: combinations left unrun when the budget expires are reported as a failure naming the count, never silently dropped. - --cli: one pass over the same machinery -- create the file, take baseline benchmarks, then deactivate each data OST in turn and read it degraded. A hand-driven check that needs no terminal. - --write-verify: the write/resync/verify behaviors -- parity goes stale on write while data stays init, resync restores init with the md5 unchanged, one resync clears a stale plain mirror and stale parity together, writes are refused on a stale data mirror but allowed on stale parity, and 'lfs mirror verify' flags both stale parity and real on-disk corruption of a data or a parity object. The corruption checks take one OST down, corrupt a block on the raw ldiskfs backend and put it back; they self-skip on other backends. - --layout-audit: sweeps EC geometries and checks the OST allocation the kernel produced against a Python port of ec_split_stripes(), including uneven splits. Pure layout, no I/O. A curses TUI (ec_fault_gui.py) drives the same object interactively for manual investigation. Every non-interactive mode writes a report directory (run.info, summary.txt, and 'lctl dk' plus dmesg captured on failure) so a run that crashes the node leaves evidence behind. Server-side actions are routed to the OSS owning each OST through an --ost-host map, so the tool works multi-node; sanity-ec builds that map from facet_host. --mds-host names the MDS nodes, which are never faulted but are still read for the latched catastrophe flag, so an LBUG there during a degraded read is not missed. On a single node the hosts resolve local and no ssh is issued. Tests 76a (soak), 76b (matrix) and 76c (write-verify) wrap the modes. The geometry scales to the active OST count, so all three run on as few as 3 OSTs (2+1); more OSTs widen the parity coverage rather than being required. 76a and 76b need <= 16 OSTs because the fail_val OST mask is a 16-bit field, and are gated behind SLOW since the soak and the matrix are long-running. 76c registers a stack_trap to put an OST back if the orchestrator is killed outright mid-corruption. Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Test-Parameters: testlist=sanity-ec ostcount=8 env=SLOW=yes Test-Parameters: testlist=sanity-ec ostcount=3 Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com> Change-Id: Ic53df83e733c8850a9435b6793e4be431156b24a
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 retesting | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: replay-dual. | session |
| review-dne-zfs-part-2 retesting | RHEL 10.1 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-ldiskfs-ubuntu retesting | RHEL 8.10 / x86_64, Ubuntu 24.04 / x86_64 | ran 6 tests. 1 tests failed: sanity-lnet. | session |
(minor) The ll_readahead_handle_work() piece fixes a pre-existing bug that has nothing to do with EC: ll_ra_count_get() reserves the pages and nothing puts them back when cl_io_rw_init() fails, and ll_ra_count_put() is the only decrement of ra_cur_pages. A Fixes: line would let the maintenance branches pick it up:
Fixes: c2791674260b ("LU-12043 llite: improve single-thread read performance")
(defect) The lov_io_set_range() hunk fixes an LBUG that 84c1a4a074 introduced, and that commit is three patches back in this same unlanded series. That leaves 84c1a4a074, 13af17f231 and b718cadc7b each panicking the client on a two-component EC layout, so the series is not bisectable and those revisions cannot be tested on their own.
Can the clamp be folded into 84c1a4a074 instead? If it has to stay a separate patch, it needs its own tag alongside the existing one:
Fixes: 84c1a4a07423 ("LU-12669 ec: recover data from parity")
Good catch. Definitely best to check for the inactive OSC device early. This would be set some seconds after the OST goes offline, so IO shouldn't wait to detect that every time.
Max, I rebased this on Bobi's latest patch - you were based on an older version
(minor) This label bypasses cl_io_fini(). cl_io_init()'s contract is that the caller calls cl_io_fini() no matter what it returned, and the commit message points out that this exit stops being rare once a dead import can fail an EC read at init. Should the new path run cl_io_fini(env, io) before dropping the reservation?
(defect) On a component with lsme_dstripe_count == 0 this can push eoff well past lio->lis_endpos.
Neither place that sets the cycle end rounds it to a recovery group on that branch: lov_io_set_range() skips its end-rounding block when dstripe_count == 0, and lov_io_ec_rd_iter_init() only rounds when dstripe_count > 1. So lis_endpos is just the request end, while eoff becomes soff + RGs, and RGs there is ss * 4 (or ss * lo_nr).
lov_ec_read_stripe_pages() classifies pages against eoff alone, so the pages in [lis_endpos, soff + RGs) are grabbed and submitted. lov_io_lock() enqueued only [ec_inner.crw_pos, +crw_bytes) for this cycle, and lov_ecio_add_data_sub() skipped those stripes (lov_stripe_intersects() false against the cycle extent), so lov_sub_get() allocates a fresh sub-IO with no DLM lock -- the same "uncovered page!" LBUG in osc_req_attr_set() that the lov_io_set_range() hunk is fixing.
Worked example, layout "-E 128M --ec 4+2 -E 512M -c 4" (EC component followed by a plain one, as in the mirror layouts in this suite), read [0, 130M) with a dead OST:
cycle in the plain component: lis_pos 128M, lis_endpos 130M
dcount 4, ss 1M => RGs 4M, soff 128M
eoff was min(132M, 130M) = 130M, now min(132M, 512M) = 132M
stripes 2 and 3 cover [130M, 132M) -- outside the enqueued lock
Before the change those pages were EC_DPG_ZERO and never touched. Should the clamp stay bounded by lio->lis_endpos on the dcount == 0 branch, where there is no recovery group to complete?
(minor) The sibling switch below clears the retry count before returning:
io->ci_switch_ec_io = 1;
io->ci_need_restart = 1;
io->ci_ndelay_tried = 0;
RETURN(-ENODATA);
This one leaves ci_ndelay_tried alone, and ll_file_io_generic() carries it across the restart (retried = io->ci_ndelay_tried). So if the read had already restarted once before the import went invalid, the CIT_EC_RD pass lands on ndelay_tried: with a non-zero count and can still take the 10 ms schedule_timeout_interruptible() and set ci_tried_all_mirrors -- the backoff the commit message says this path avoids. Should it reset the count too?
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
[AI review - fable] (defect) Including CIT_READ here looks dangerous. After the continue, the top lock is granted without covering this stripe, and nothing in the normal read path checks lso_status (only the EC_RD and LSEEK paths do), so pages on the dead stripe are still generated and submitted, and osc_build_rpc() -> cl_req_attr_set() -> osc_req_attr_set() hits the "uncovered page!" LBUG in osc_object.c when osc_dlmlock_at_pgoff() finds nothing - the very crash the commit message wants to avoid. The lov_io_mirror_init() check does not guard this path when ci_cross_ec is unset (a read of a non-EC component of a file whose layout has parity entries elsewhere - lov_lsm_has_parity() is file-wide, and lov_io_ec_rd_start() explicitly supports dcount == 0 components), or when the import goes inactive after cl_io_init(). Before this change the enqueue failed fast with -ESHUTDOWN and the IO returned an error or restarted into EC_RD instead of crashing. Should the skip be limited to CIT_EC_RD, letting CIT_READ fail the enqueue so the existing restart logic switches to EC_RD? As a side effect the CIT_READ marking also leaves a stale LSS_READ_ERR on the lovsub (the normal read path never resets it), which lov_io_lseek_end() then treats as degraded even after the OST is reactivated.
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
(style) This isn't a bug, but the include groups are inverted here - `<lustre_osc.h>` is a lustre header and belongs before the local `"lov_cl_internal.h"`. The same include added to lov_io.c in this patch is ordered that way.
LU-12668 lov: proactive dead-OST detection for degraded reads
Instead of letting a read proceed to an inactive OST and fail deep in
the BRW/lock path, detect dead imports during mirror selection and
route to the EC recovery path.
- lov_io_mirror_init() rejects a candidate mirror that has a data
stripe on a deactivated or invalid import, so FLR rotation can still
pick an intact mirror. Only when no mirror is intact does it set
ci_switch_ec_io, which also skips the FLR backoff sleep -- we
already know the OSTs are dead. The scan covers the whole mirror
rather than just the stripes the I/O touches: see the comment on
lov_ec_has_inactive_stripe() for why bounding it to the I/O extent
is not safe until the size path handles unreachable stripes.
- lov_ecio_add_data_sub() marks such stripes errored up front, so the
recovery loop reconstructs them instead of building a sub-IO that
cl_io_iter_init() or the lock enqueue would reject.
lov_io_set_range() rounded the CIT_EC_RD range end up to a raid-set
boundary using the geometry of the component holding the read end, but
that rounded value can land past the component. The next component
anchors its raid sets at its own e_start, so the end fell mid-raid-set
there: lov_io_ec_rd_iter_init() cut the cycle short at it while
lov_io_ec_rd_start() still read a whole recovery group, leaving pages
outside the lock lov_io_lock() had enqueued. osc_req_attr_set() then
LBUGged ("uncovered page!") from brw_queue_work and panicked the
client. Clamp the rounded end at the component boundary; the read
cannot need data past it, since the request itself ended inside that
component and each component is erasure coded independently.
Reproduced with a -E 4M -c 3 --ec 2+1, -E -1 -c 5 --ec 4+1 layout.
ll_readahead_handle_work() leaked its ra_cur_pages reservation when
cl_io_rw_init() failed. That exit is rare today but becomes routine
once a dead import can fail an EC read at init, and ll_ra_count_put()
is the counter's only decrement, so readahead would stop for the whole
mount and never recover. Release the reservation from a single exit
label that every path past ll_ra_count_get() reaches, and call
cl_io_fini() on the failed init, which cl_io_init() requires no matter
what it returned.
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Test-Parameters: testlist=sanity-ec ostcount=8
Test-Parameters: testlist=sanity-ec ostcount=8 fstype=zfs
Test-Parameters: testlist=sanity ostcount=8
Fixes: c2791674260b ("LU-12043 llite: improve single-thread read performance")
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com>
Change-Id: I0913e1ee977c9850193c92835edb185b0aedc6d4
| failed enforced test | platform | detail | |
|---|---|---|---|
| custom-1001 crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
| custom-1002 crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
| review-dne-subtest-change failed 30× | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-zfs-subtest-change failed 29× | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
(minor) The range here starts at 49b, but 49a "test concurrent reads during EC recovery" is added by this patch too. Should it read 49a-50b? Every other test added is accounted for by one of the ranges.
(minor) Two hunks are not accounted for by any of the ranges above: the new `[[ "$SLOW" == "no" ]] && EXCEPT_SLOW="74a 75b"` line, and the `head -n1` fix in `enable_ec()` for the multi-mount `get_param` output. The `EXCEPT_SLOW` one also changes an existing test: 74a is not added by this patch and is not named in any of the range lists, so a reader can't tell from the message that it is being moved onto the SLOW list. Worth a sentence for each.
(defect) With 3 of 4 data stripes unreadable and pcount=2, -EIO is the only correct outcome; returning reconstructed-but-wrong bytes is silent corruption. Treating it as an acceptable pass means a real reconstruction bug in this path would go unnoticed.
48a, 48c and 64b in this same patch take the opposite position ("handing back silently wrong data would be a corruption bug, so assert against it"). Should 52b assert rc != 0 instead?
(minor) These eight files (2M..16M) are never removed. The same applies to the extra files in 71b/71c/71d, 73b's .pure, 73c's .zero and the directories in 75e/75f; 73e is the only new test that registers a cleanup. Added up, the new tests write on the order of 1.5G and leave most of it in place for the rest of the run, which on a small test filesystem can push later subtests into ENOSPC. A `stack_trap "rm -f ..."` next to each creation would keep the footprint bounded.
(suggestion) Both of these are parked against the feature ticket itself. 53a in particular is described as intermittently reconstructing wrong data, which is a silent-corruption symptom rather than a test problem, and 41d is an OSC retry loop that never terminates. Would separate LU tickets referenced here keep them from being lost when LU-12668 is closed? The convention elsewhere in this file (12a -> LU-19631, 5b/12b -> LU-20435) is one ticket per known failure.
(minor) A fresh `stack_trap` is pushed on every call, including repeat calls for the same index, so loop-driven tests accumulate identical cleanup entries: 75b registers 50, 74a 20, and 58a/63b/68b one per pair. Each entry re-runs `ec_apply_fault`, which is a `do_nodes` to every OSS plus a `cancel_lru_locks osc`, so teardown does that work dozens of times over. Registering the trap only when the bit was not already set would make it one entry per OST.
(minor) When no stripe list is given this walks every data stripe and returns the first parity-free one, which can be a stripe that holds no data. For a sub-raid-set file that is the vacuous-pass mode this helper is documented as preventing: 42a writes 512K into a 1M-stripe 4+2 layout, so only stripe 0 has data. If stripe 0's OST happens to double as parity, the fault is armed on stripe 1's (empty) object and the checksum comparison succeeds without reconstructing anything. `ec_data_stripe_osts()` handles the analogous case by calling `skip_env`. Would returning non-zero (so `ec_start_read_fault()` skips) be safer than falling through to a later stripe?
(minor) 44c and 44a are the same test - same layout, same `ec_start_all_reads_fail`, same EIO check, same clear-and-reread - differing only in the error strings. 43c is that body minus the reread. Could these collapse into one? While here, 43c's description says "too many OST failures (3+ OSTs)" but `ec_start_all_reads_fail` uses `fail_val=0`, which fails every OST, not three.
(minor) The comment says "an OST that is a safe data OST for all files", but only `$f2` is classified. `$f1` (2+1, 3 objects) and `$f3` (2+2, 4 objects) get whatever the allocator gave them, so on an 8-OST config the victim frequently holds none of their objects and the `$s1`/`$s3` comparisons pass without any recovery running. 71a, 71c, 71d and 75e/75f have the same shape (classify one file, assert on all of them). That is defensible for a batch test, but here the comment claims something stronger than the code does.
(defect) `safe_osts` here still comes from the `ec_classify_osts $tf` above, but the SEEK_DATA/SEEK_HOLE checks below run on `$tfs`, which was created separately and gets its own object placement from the allocator.
So the OST taken out need not hold any of `$tfs`'s stripes, and on a run where it doesn't, the two `lseek_test` assertions execute against a fully healthy file.
73c already documents and avoids exactly this ("safe_osts still describes $tf; $tf2 has its own object placement") by calling `ec_data_stripe_osts $tf2 0` first. Should 75d do the same for `$tfs`?
Related: the comment says the seek "crosses the degraded stripe", but the data lives at 5M, i.e. stripe 1 with `-c 4 -S 1M`, while `safe_osts[0]` is just the lowest-numbered data OST.
(minor) The negative-index handling, and the "Index -1 is the last stripe" note in the header comment, appear to be unreachable: all eleven callers pass 0..4. Worth dropping the branch and the doc line unless a caller is coming.
(style) The suite convention is a `#define` comment naming the fault right above the line that arms it, so a reader does not have to look up the bare hex. `ec_start_all_reads_fail()` and 41d both do this; this call site and the one in `ec_apply_fault()` do not.
#define OBD_FAIL_OST_BRW_READ_BULK 0x20f
ec_ost_fail_loc 0x20f $(( 0x10000 | mask ))
The value itself is right (obd_support.h has 0x20f), it is only the annotation that is missing.
LU-12668 tests: add EC recovery tests Add sanity-ec coverage for erasure-coding recovery. Each test writes an EC file, resyncs parity, fails one or more OSTs, and verifies the client reconstructs the data from parity (CIT_EC_RD) against the pre-failure checksum. Failure is injected with OBD_FAIL_OST_BRW_READ_BULK so the OSC import stays active and only bulk reads fail, which drives genuine parity reconstruction. The fault is set on the OSS nodes, where tgt_brw_read() evaluates it, and osc.*.resend_count is dropped to 1 for the duration so the injected -EIO reaches the LOV layer instead of being absorbed by an OSC resend. Victims are chosen by stripe rather than by OST index. ec_pick_data_ost() walks a file's data stripes in order and takes the first whose OST does not also carry parity: a file smaller than one raid set holds data on stripe 0 alone, so picking the lowest OST index instead would arm the fault on an object the read never reaches and the test would pass without exercising recovery. ec_check_fault_index() skips when a target OST index is >= 16, which cfs_fail_index() cannot express in its 16-bit fail_val bitmask. ec_mirror_victims() fails one parity-free OST in every data mirror, since a file with more than one data copy would otherwise answer the read from an intact mirror rather than reconstructing anything. Reads that check a sub-range cancel their locks first: a range re-read after a whole-file read is otherwise served from the page cache, issues no BRW RPC, and so never reaches the injected fault. Geometry and I/O patterns (40b-44c): - 2+1, 2+2 and 4+1 EC; partial, offset, mmap, direct and async reads; single, maximum and progressive OST failure; graceful failure when too many OSTs are gone Failure placement and layout (45a-48c): - consecutive, non-consecutive, boundary and parity-only OST failures; 64K, 256K and 4M stripe sizes; multiple EC and mixed EC/non-EC PFL components; stale and partially stale parity Concurrency and multi-mount (49a-50b): - concurrent readers over a file with two failed data OSTs; background writes, mirror resync and OST reactivation during recovery; both mounts reading the same EC file Core recovery (51a-53a): - EOF boundary recovery at RAID set / stripe set boundaries; degradation limits and mixed parity+data failure; recovery at non-zero read offsets Layout patterns (55a): - file-size boundaries (1 byte .. multi-stripe) Edge cases (58a-62d): - parity_used combinations and stripe rotation; sparse files with holes; recovery after truncate; sub-stripe files; append writes Multi-target (63a-66d): - OST failure cycling and multi-mount coordination; 3-4 component PFL with per-component EC geometry; multiple EC mirror pairs Write patterns (67a-71d): - writes to healthy stripes during degraded mode and overwrite cycles; varied geometries; O_DIRECT writes; partial and mid-file overwrites; batch recovery of many files Stress and admin (73a-73e, 74a, 75a-75f): - random reads, fallocate, truncate-extend, O_APPEND, and large (128M) file recovery; reads racing with OST deactivation; stat/getattr, stress loops, lfs mirror verify, directory-inherited layout, and stripe rotation Tests 41d and 53a are added but listed in always_except. A degraded mmap read never completes: the OSC alternates between "too many resent retries" and osc_brw_redo_request() forever, so -EIO never reaches the LOV layer and CIT_FAULT never switches to CIT_EC_RD. A recovery read at a non-zero offset intermittently reconstructs wrong data. Both stay off until those are fixed. Test 65c and the sparse half of 75d are skipped on ZFS. Both build a sparse file and resync it, which needs lseek to report the holes so resync knows which stripe sets to skip, and ZFS does not report them reliably for dirty data. Test 12b describes the same problem. Assisted-by: ClaudeCode:opus llm_code_and_review_tools Test-Parameters: trivial testlist=sanity-ec ostcount=8 Test-Parameters: trivial testlist=sanity-ec ostcount=8 fstype=zfs Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Signed-off-by: Max Dilger <mdilger@whamcloud.com> Change-Id: I5a06cd166487e0bff7bfdb6a39414af3f12c4326
(style) The summary reads as past tense; house style is imperative. "update mirror split for EC support" would match the rest of the tree.
(minor) The body calls out `test_44f` and the `test_7b` rewrite by name, but sanity-ec `test_7e`, `test_7f`, `test_7g` and the new `identify_ec_mirrors()` helper (~240 new lines) aren't mentioned anywhere. Worth a sentence so a reader knows the new coverage is intentional and what it exercises.
@mvef@whamcloud.com, @rsahlberg@whamcloud.com, I see that `--force-no-ec` is listed in LU-19548, but I don't recall what the realistic use case is for that option? Leaving an unanchored EC mirror behind on a file doesn't seem very useful, and there doesn't seem to be any way to reconnect it to a new data mirror even if it was created (nor assurance that the OSTs used in the data mirror are suitable for that EC). (Apologies in advance to Max, but ...) I'm wondering if `--force-no-ec` option should be removed, and EC mirrors should always be moved to the victim file or deleted? That would remove a lot of complexity in the code that I doesn't seem to add any real value.
(style) The .TH date is older than the lfs-mirror-delete.1 date set in the same patch, and predates the current content. Worth bumping to the refresh date.
(minor) The victim now gets two mirrors, and lod_declare_layout_merge() rejects any merge source that has more than one:
if (le16_to_cpu(merge_lcm->lcm_mirror_count))
RETURN(-EBUSY);
So `lfs mirror extend -N -f NEW_FILE` (documented in lfs-mirror-extend.1) fails with EBUSY on a file produced this way, unlike every other split victim. Should this paragraph say so, until extend learns to merge a data+parity pair?
(style) function comment should follow the style as `mdd_split_ea`.
(question) For a data+parity victim, if either side was *STALE* on the source, should the new file always set data *in-sync* and parity *STALE*? The victim has no other data mirror, so data has to be the non-stale primary or the layout is unusable (all-stale is -EPERM; in-sync parity as primary is -EUCLEAN). Only when both were already in-sync should both stay in-sync. Can current mask accomplish this or not?
(defect) llapi_mirror_find_stale() skips LCME_FL_PARITY, so a remaining
parity mirror always yields comp_size == 0 and this returns false.
lfs mirror split/delete then skips the last-good-copy resync even when
every remaining data mirror is stale.
Should remaining parity mirrors be ignored here the same way
data_mirror_remains() ignores them? Co-split also takes the paired
parity, so that parity is not a copy that will survive the split.
Example: file has D1 (EC data, in-sync after a write), P1 (parity),
and D2 (data, stale).
lfs mirror split --mirror-id D1
last_non_stale_mirror(D1) sees remaining mirrors {P1, D2}. P1 is
skipped by find_stale, so comp_size == 0 and the function returns
false. Resync is skipped, co-split removes D1+P1, and the source is
left with only stale D2.
(minor) The comment is narrower than the condition. `mflags & MF_DESTROY` with a NULL victim_file is also true for `lfs mirror split -d` (which cannot take -f at all), for `lfs pcc detach` (`MF_DESTROY | MF_FOREIGN`), and for the internal `mirror_split(name, 1, NULL, MF_DESTROY, NULL)` call. Naming only `lfs mirror delete` makes the flag look narrower than it is.
(minor) mirror_split() also backs `lfs mirror delete` (SO_MIRROR_DELETE sets MF_DESTROY) and `lfs pcc detach`, so a user who typed `lfs mirror delete` is told the mirror is being "split". sanity-ec test_7f greps for this text from a `lfs mirror delete` run, so that wording is on a tested path. Also, every other message in this function is prefixed with `progname`; this one isn't, so in a pipeline it isn't obvious which tool emitted it.
Skipping the orphan parity comp leaves `comp_array[i].lrc_synced` at the value `_mirror_find_stale()` set, which is unconditionally `true`:
comp[idx].lrc_synced = true; /* liblustreapi_layout.c */
Back in lfs_mirror_resync_file() every entry with `lrc_synced` is fed to LL_LEASE_RESYNC_DONE, and lod_declare_update_sync_pending() then does `llc_flags &= ~LCME_FL_STALE` for each id.
So `lfs mirror resync` returns success and drops LCME_FL_STALE from a parity component it never recomputed - `lfs getstripe` afterwards shows it as in sync. Would `comp_array[i].lrc_synced = false;` before the `continue` be more honest here?
LU-19548 lfs: updated mirror split for EC support
An EC data mirror and its parity mirror form a pair joined by a
bidirectional link (lcme_mirror_link_id). Splitting one mirror
without the other strands a parity mirror that protects no data,
or leaves a data mirror with no EC protection.
When splitting a data mirror, the MDS now splits (or destroys) its
paired parity mirror in the same atomic operation. The pair is
found via the mirror link id rather than by assuming the data and
parity components are adjacent in the layout, and the link is only
followed while it still describes an intact pair: the linked mirror
must exist, carry LCME_FL_PARITY and link back. A link failing
those tests is stale, and following it would co-split an unrelated
mirror. The link on any mirror left behind is cleared so no
dangling reference remains.
A parity mirror can still end up with no data mirror to pair with,
since a stale link is not followed and is cleared instead. lfs
mirror verify and lfs mirror resync resolve a parity component to
its data component through that link, in
__llapi_layout_find_data_comp_by_parity(), so both used to fail the
whole file with -ENOENT; once anything was written to the file the
orphan was marked stale and could never be resynced again. Skip a
parity component that has no data component to pair with instead.
mirror_split() used its "purge" flag for two things: to mean "the
mirror is deleted by handing fd in as the victim", and as the guard
for the retry that copes with an old MDS rejecting fd == fdv. The
flag started out true for every split, so a split to -f NEW_FILE or
to the default victim file also retried after an -EINVAL or -EBUSY
from the MDS, re-opened with O_CREAT|O_EXCL the victim it had
already created, and reported "create victim file failed: File
exists" in place of the real cause. The same flag guards the close
of the victim descriptor, so those splits leaked one descriptor per
file and splitting a batch of files in a single command ran out of
descriptors partway through. Start "purge" as the condition it
describes.
Such a failure was also invisible to a script, since mirror_split()
returned the leftover 0 of an earlier helper on several error paths:
a split that could not create its victim file, or that named a
mirror, component or pool the file does not have, printed an error
and exited 0. Set an error on those paths. Add sanity-flr test_44f.
A split is refused if it would remove every mirror, and also if it
would leave the file without a data mirror. Parity mirrors alone
cannot serve reads: with no data mirror the layout is no longer
FLR, so lov_io_mirror_init() returns at its !lov_is_flr() check
ahead of the parity checks, the parity mirror is picked for IO, and
reads hand back parity stripes as file data. A lone data+parity
pair therefore cannot be taken apart by splitting the data mirror;
split the parity mirror instead to turn the file back into a plain
data file.
The warning printed when a parity mirror is split names the data
mirror that loses EC protection, and is skipped for a parity mirror
whose link has been cleared, which protects no data to begin with.
Splitting a parity mirror directly no longer requires -d. That
restriction, added by commit dfb93e4978b0 ("LU-19548 lfs: mirror
split -d for parity mirrors"), existed to keep a parity-only
layout off a real file. Refusing to strand the data side now covers
the source file, and a victim file holding only the parity mirror
reports size 0, because lov_attr_get_composite() does not take size
from parity components, so it cannot pass parity off as file data.
A warning is printed instead, since removing the parity eliminates
EC protection of the data mirror. test_7b is updated for the new
behaviour.
A victim that receives a data+parity pair keeps its LCME_FL_STALE
markers, so a parity mirror that was stale at split time no longer
lands in a victim that claims an in-sync pair.
lod_declare_layout_purge() refused a purge buffer holding more than
one mirror. lfs mirror delete of a data mirror now hands it the
data+parity pair, so the check is relaxed from lcm_mirror_count
!= 0 to > 1.
Also fix two endian bugs that predate this work in the rewritten
function, harmless on little-endian but wrong on big-endian:
lcm_flags was converted twice, and the __u16 lcm_entry_count was
written with cpu_to_le32().
collect_mirror_id() fills an array owned by its caller but never
knew how large that array was, and every caller passed 128 entries
while lod.*.mirror_count_max accepts up to LUSTRE_MIRROR_COUNT_MAX
mirrors. Splitting a mirror out of a file with more mirrors than
that wrote past the end of a stack array. Carry the array size in
struct collect_ids_data, check it in the callback, and size the
arrays LUSTRE_MIRROR_COUNT_MAX so the check cannot be reached.
Fixes: 23b2d4781899 ("LU-10420 flr: split a mirror from mirrored file")
Fixes: c6e7c0788d7c ("LU-10258 lfs: lfs mirror copy command")
Fixes: b2d73351e646 ("LU-14521 flr: delete mirror without volatile file")
Fixes: fb790204ce3e ("LU-17908 layout: preserve non-FLR state layout flags")
Test-Parameters: testlist=sanity-ec ostcount=8
Test-Parameters: testlist=sanity-flr
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Assisted-by: ClaudeCode:Opus-4.8 llm_code_and_review_tools
Change-Id: I69ca706127e8e0b0e26c88525ac5509e22fa912a
(suggestion) The body says an interop run skips the new subtests rather than failing them, but nothing here exercises that. Would adding a second line such as `Test-Parameters: testlist=sanity-ec ostcount=8 serverversion=2.17.55` prove the version gate actually skips against an older MDS?
(minor) The extra storage for EC(D+P) is PARITY/DATA of the file size, not (DATA+PARITY)/DATA - (DATA+PARITY)/DATA is the total. The 8+2 example further down gets this right ("1.25x the file size, vs 2x for a plain mirror"), so the two read as contradicting each other. Reword as total storage, or use PARITY/DATA here?
(defect) Should this refuse a data component that is already linked, instead of overwriting its link id?
`data_lcme->lcme_time_and_id` may already carry the mirror id of an existing parity mirror. The old id is dropped here and replaced with the newly merged one, so the earlier parity mirror ends up half-linked from the data side - the exact state the `!link_id` check a few lines up refuses.
The only guard against that today is client-side, in `build_parity_layout_for_mirror()`, and it is not race-free: `llapi_layout_get_by_path()` reads the layout before `mirror_extend_layout()` takes any lease, so two `lfs mirror extend --mirror-id N --ec D+P` runs on different clients both see mirror N unprotected and both merge. The second merge re-stamps the data component; `verify_new_mirror_id()` then reports -EBUSY, but the layout has already been committed. `llapi_layout_comp_add_parity_only()` is also a new public, man-paged entry point, so any application can build the same merge buffer directly.
The result is a file with two parity mirrors on one data mirror, which the commit message says is rejected for now. `lod_verify_striping()` already re-checks the k+m bound with "Cannot trust userspace; enforce here too" - the same reasoning seems to apply to the link.
if (lcme_timestamp_id_unpack(time_and_id))
GOTO(out, rc = -EEXIST);
This needs to be fixed before the EC feature goes into production, but is still under discussion and not the target of this patch.
(minor) `llapi_layout_pool_name_get()` never sets ENODATA - it snprintf()s `comp->llc_pool_name` (empty when there is no pool) and returns 0, failing only on a NULL current component, a NULL `dest`, or a foreign pattern (EINVAL). So the `errno != ENODATA` carve-out never triggers and the comment describes behaviour the API doesn't have. If it stays, the non-ENODATA failure leaves `rc` at -1 until the next assignment overwrites it, which is easy to trip over later.
LU-19548 lfs: mirror extend support for EC
Add three EC modes to 'lfs mirror extend':
--mirror-id N --ec D+P attach a parity mirror to existing data
mirror N (N >= 1; id 0 is reserved for
non-FLR layouts and is rejected)
-N [K] --ec D+P add K new data+parity mirror pairs
(K defaults to 1)
--ec D+P attach a parity mirror to the file's lone
data mirror, for the non-FLR case where the
user just wants EC protection on a plain
file; a file with 2+ mirrors errors out
asking for --mirror-id or -N
As a regular extend never leaves a new mirror stale, the new mirrors
are in sync when the command returns: data mirrors are populated by a
copy before the layout merge, parity mirrors are computed in place
with the machinery 'lfs mirror resync' uses. A partial extend resyncs
the parity mirrors it did attach before reporting the error, and one
that was merged but could not be marked stale is reported with the
command that removes it, since resync finds mirrors by LCME_FL_STALE
and so cannot repair that one.
A parity mirror is attached by merging a layout that holds only the
new parity components, while the data mirror they protect stays where
it is. The new llapi_layout_comp_add_parity_only() builds that layout;
llapi_layout_comp_add_ec() cannot, as it derives the parity extent,
stripe size and pool by searching for the data component in the same
buffer, and pairs the two with a transient link id that is resolved
during creation. Client-side layout sanity likewise had to stop
requiring a data component for every parity component.
Splitting the pair across two files needs two changes in lod.
lod_qos_prep_create() sizes a parity component from the data component
it protects and rejects one that has none; the data component of an
extend is not in the volatile file being created, so the client sends
its stripe count as the parity component's own and lod derives the
raidset count from that. lod_declare_layout_merge() copies the
incoming entries verbatim, so only the parity -> data half of the link
would be stored; it now also stamps the matching data component with
the mirror id assigned to the parity component, without which 'lfs
mirror split' cannot see the pair from the data side and leaves the
parity mirror behind. A link naming a mirror that is not in the
merged layout means the data mirror was split away after the client
read it, and is refused rather than stored as a half-linked pair that
resync would then fail on for the life of the file.
'lfs mirror extend --ec' also refuses a data mirror that is stale or
'nosync'. Parity is computed from the content of the named mirror, and
resyncing that mirror afterwards does not re-stale its parity, so the
parity would silently protect superseded data; a nosync mirror is
skipped by resync, leaving the parity uncomputed.
The rider in lod_qos_prep_create() rejecting an EC data stripe count
of zero is unrelated to extend: lod_verify_striping() bounds only
k + m, so a k == 0 layout reaches ec_split_stripes() and divides by
zero in the kernel.
Attaching a second parity mirror of a different geometry to one data
mirror, which would let a file's EC protection change without
rewriting its data, is not implemented. It is rejected for now, and no
test asserts that rejection.
An MDS that predates this change refuses a parity-only create buffer
outright, so the new sanity-ec subtests are gated on the server
version and an interop run skips them instead of failing.
Components EC cannot be sized for are named rather than left to a bare
errno: a Data-on-MDT component has no OST stripes to spread a raidset
over, and a self-extending component's extent does not stay matched to
its parity component, both as 'lfs setstripe --ec' refuses them. --ec
is refused with -f as the man page says it is, and a second --ec on
one command line no longer reports success after printing a usage
error. A '-N=TOTAL' that finds the file already at TOTAL mirrors adds
none, as it warns, instead of falling through to the auto mode that a
bare --ec selects.
struct collect_ids_data moves ahead of the new EC-extend helpers,
which reuse it and collect_mirror_id() to gather a file's distinct
mirror ids.
Fixes: 8c5f5d3ec9b1 ("LU-12668 lod: bind ec mirror to data mirror during creation")
Test-Parameters: testlist=sanity-ec ostcount=8
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Assisted-by: ClaudeCode:Fable-5 llm_code_and_review_tools
Change-Id: I8d670f0558521063af425e2c75a56b00cdd294d7
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
(minor) "stop handing out EC layouts" doesn't quite match the code. mdt_pattern_types is only read in mdt_connect_internal(); nothing in mdt/ or lod/ consults it when a layout is created, so an MDT with parity removed will still create EC layouts on request. What it stops is clients *using* parity components, and only for clients that mount afterwards. Worth noting too that the negotiated mask does not gate creation on the client either: with lustre.enable_flr_ec=0 (the default) a client can still create an EC file via llite.*.enable_erasure_coding and then get -EIO writing it. Should layout creation be gated on the negotiated mask as well?
This baseline omits LOV_PATTERN_COMPRESS, but lov_pattern_supported() right below still lists `LOV_PATTERN_RAID0 | LOV_PATTERN_COMPRESS` as a pattern this client's IO stack handles.
So for a compressed component lov_lsme_usable() -> lov_pattern_available_mds() computes
pattern_base = 0x801
0x801 & ~LOV_MDS_PATTERN_SUPPORT_217 = 0x800 /* != 0 */
and returns false, on both branches: the compat branch uses this macro, and the negotiated branch uses the MDS mask, which is capped by MDT_PATTERN_TYPES_SUPPORTED = baseline | PARITY. There is no value of mdt.*.pattern_types that can put COMPRESS back (conf-sanity test_163a asserts `+compress` is silently dropped).
The result is that lov_init_composite() leaves every compressed component !lle_valid and skips lco_init(), so lov_io_rw_iter_init() returns -EAGAIN/-EIO for a plain compressed file. This is reachable today: lsme_unpack() accepts COMPRESS components when llite.*.enable_compression is set (lov_ea.c), and lod_generate_lovea() packs them.
Should the mask be derived from what the client actually supports (i.e. include COMPRESS in both the 2.17 baseline and MDT_PATTERN_TYPES_SUPPORTED), rather than a hard-coded RAID0/MDT/OVERSTRIPING set?
(style) F_HOLE and F_RELEASED are layout flags rather than pattern types, and every consumer strips them first (`pattern & ~LOV_PATTERN_F_MASK` in lov_pattern_available_mds(), `& ~(F_RELEASED | F_MASK)` in lov_pattern_supported()). lov_pattern_bit2str() also has no names for bits 30/31, so they never show up in mdt.*.pattern_types or lov.*.mds_pattern_support. They only put two bits nobody reads on the wire in ocd_pattern_support. Could the macro just be RAID0 | MDT | OVERSTRIPING?
(minor) `data` here comes from obd_get_info(KEY_CONN_DATA) on sbi->ll_md_exp, which is the LMV export, and lmv_get_info() forwards that key to `lmv_tgt(lmv, 0)` only. So on DNE the stored mask is whatever MDT index 0 negotiated; mdt.<fs>-MDT0001.pattern_types has no effect on any client, even though the parameter is per-MDT and the man page shows a single-MDT example. Should the client intersect the masks from all MDTs, or should the caveat be documented?
(minor) This version gate was flagged on an earlier patchset ("this version should be updated when the patch is refreshed") and answered "Done", but it is still 2.17.52.224 here and in test_163a/test_163b.
The things these tests check (`pattern_support` in the mdc/osc import, mdt.*.pattern_types) only exist from this patch, so any server in 2.17.53..2.17.57 passes the gate without the feature. The tests do fall through to a skip on the missing parameter, but the gate should be the version this lands in.
LU-12187 lov: MDS layout pattern negotiation
Add 'obd_connect_data::ocd_pattern_support' to allow the client
and MDS to negotiate the file layout patterns that they support.
This prevents clients from trying to use layout types not
recognized by the MDS, and allows the MDS to (potentially)
convert/filter existing file layouts to a format that the
client understands.
This will be used by FLR-EC to negotiate whether the client
and MDS support LOV_PATTERN_PARITY layouts, and others in
the future.
Add LOV_MDS_PATTERN_SUPPORT_217 for compat with pre-2.18
MDS that do not send ocd_pattern_support. Generalize
lov_pattern_available_mds() to check all patterns against
the MDS-negotiated mask. The client sends its supported patterns
to the MDS, which responds with the intersection of both sets.
The client stores the negotiated mask only when the server
grants OBD_CONNECT2_FLR_EC. target_handle_connect() echoes
un-negotiated ocd fields back from the request, so an ungated
store would let the client's own request value defeat the
pre-2.18 fallback.
Gate pattern usability at layout init rather than at parse time:
lov_init_composite() consults lov_lsme_usable() (pattern support
intersected with the MDS-negotiated mask), and leaves unusable
components un-initialized and !lle_valid, the same as components
with an unknown pattern. Parsing itself stays pattern-agnostic
to preserve lsme alloc/free symmetry.
Adds mdt.*.pattern_types which gives an administrator the
ability to enable or disable layout types at runtime, and a
read-only lov.*.mds_pattern_support which exposes the
effective negotiated mask on the client. The mdt_enable_flr_ec
module parameter now only sets the initial pattern_types value
when an MDT starts up, so mdt.*.pattern_types is what changes
the advertised types at runtime. Add man pages for both
parameters and for the renamed lustre.enable_flr_ec parameter.
Rename the 'llite_enable_flr_ec' module parameter to
'enable_flr_ec', so it is accessed as 'lustre.enable_flr_ec'
per LU-14144 convention. Add the 'lustre' module to the libcfs
parameter path list so the client module parameters are reachable
via 'lctl {get,set}_param --module' instead of a hard-coded
/sys/module path.
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Test-Parameters: testlist=sanity-ec
Test-Parameters: testlist=conf-sanity env=ONLY="163 163a 163b"
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Change-Id: Iaef716e28014be5b91dd50b117dd0881f5c37f37
| unique failing test | history |
|---|---|
| sanity2@zfs:test_119l | NEW unique failure for this branch in the last 30 days, and was seen 1 times across 1 other branches 1 reviews |
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 5 tests. 1 tests failed: sanity-sec. | session |
| review-dne-zfs-part-1 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-dne-zfs-part-1 | RHEL 10.1 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-dne-zfs-subtest-change | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-ldiskfs-ubuntu | RHEL 8.10 / x86_64, Ubuntu 24.04 / x86_64 | ran 6 tests. 1 tests failed: sanity-lnet. | session |
(minor) ... so it could also be set and checked via `chattr +t FILE` and `lsattr FILE` commands from e2fsprogs.
The "Changes:" list mentions adding LUSTRE_NOTAIL_FL to LUSTRE_FL_USER_MODIFIABLE, but the same hunk also adds LUSTRE_COMPR_FL to that mask and removes the duplicate LUSTRE_NOATIME_FL entry. Neither is explained. The COMPR change is an independent server-side behavior change - should it be split into its own patch? The lustre/utils/lfs.c hunk (skipping the range validation for nohybrid) isn't described either.
This changes UAPI flag definitions that the MDT interprets, so interop with an older server is worth an explicit test run. Consider adding something like:
Test-Parameters: testlist=sanity serverversion=2.16.0
New wire flag, but wirecheck.c and the two wiretest.c copies don't look updated - the neighbouring LUSTRE_*_FL values have CHECK_VALUE_X entries. Same for LU_LADVISE_NOHYBRID, which needs a CHECK_VALUE next to the other LU_LADVISE_* ones (LU_LADVISE_AHEAD seems to have been missed earlier too).
The compatibility claim points at the wrong side. Older clients aren't the problem; older servers are.
On a pre-patch MDS, LUSTRE_FL_USER_VISIBLE has no NOTAIL bit, so mdt_setattr_unpack() hits
if (rec->sa_attr_flags & ~LUSTRE_FL_USER_VISIBLE)
RETURN(-EOPNOTSUPP);
and lfs ladvise -a nohybrid fails with EOPNOTSUPP. Clearing the flag still "succeeds" as a no-op, so the two directions behave differently. Worth stating the required server version here.
This was asked on patchset 7 and looks unaddressed: adding LUSTRE_COMPR_FL here is unrelated to nohybrid and changes server behavior. mdt_setattr_unpack() masks with LUSTRE_FL_USER_MODIFIABLE, and osd_attr_set() replaces the whole masked set, so this makes the compression flag both settable and clearable on the MDT inode by any client. Is that intended ahead of the compression work landing?
(defect) why is NOATIME being removed?
Is the COMPR flag really user modifiable or just visible? And does it make sense to allow this to be set before CSDC is landed to master?
Should this use NOHYBRID?
Does this also set the flag directly on the inode, or is the inode here the root or parent directory?
parse ll_file_ioctl():error: Function too hairy. Giving up. 4 seconds warn: ll_file_ioctl():Function too hairy. No more merges.
ll_inode2ext_flags() is not a full picture of the file's flags - it rebuilds them from inode->i_flags via ll_inode_to_ext_flags(), which only knows SYNC/NOATIME/APPEND/DIRSYNC/IMMUTABLE/ENCRYPT, plus the PROJINHERIT and (new) NOHYBRID lli_flags bits.
LUSTRE_NODUMP_FL and LUSTRE_NOCOMPR_FL are in LUSTRE_FL_USER_MODIFIABLE but have no i_flags or lli_flags representation, so they come back as 0 here. osd_attr_set() then does a wholesale replace:
ei->i_flags = (ei->i_flags & ~LDISKFS_OSD_USER_MODIFIABLE) |
(attr->la_flags & LDISKFS_OSD_USER_MODIFIABLE);
so those bits get cleared on disk. chattr +d FILE followed by lfs ladvise -a nohybrid FILE should lose the 'd' flag.
The FS_IOC_SETFLAGS path avoids this by calling fileattr_get() first, which fetches body->mbo_flags from the MDT. Should this do the same before OR-ing in LUSTRE_NOHYBRID_FL?
Related: ll_set_project() builds op_attr_flags from ll_xflags_to_ext_flags() and also sets OP_XVALID_FLAGS, so lfs project on a file appears to clear the nohybrid flag for the same reason.
LU-19839 llite: add persistent nohybrid I/O flag Add a persistent flag to prevent hybrid I/O switching for specific files. This uses the NOTAIL flag (0x00008000) which is not used by Lustre/ext4 and unlikely to be used in the future. The flag is set via ladvise interface for discoverability but uses FS_IOC_SETFLAGS internally for implementation. When set, hybrid I/O will not switch the file from buffered to direct I/O regardless of I/O size. Changes: - Define LUSTRE_NOTAIL_FL and LUSTRE_NOHYBRID_FL - Add LUSTRE_NOTAIL_FL to LUSTRE_FL_USER_MODIFIABLE - Implement LU_LADVISE_NOHYBRID using FS_IOC_SETFLAGS - Update ll_update_inode_flags to handle NOTAIL flag - Add test_119l to verify nohybrid flag functionality Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Change-Id: I6a69293801114e2a3015ed87f2258828922ab767
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-5 failed 3× | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: lustre-rsync-test. | session |
(minor) The body only describes `client_limit`. Some of the diff isn't accounted for: the new read-only `client_count` parameter (four man pages plus mdt/ofd sysfs attrs), the per-nodemap `client_limit`/`client_count` files, the `class_exp2tgt()` -> `class_obd2tgt()` split, the `exp` local added to target_handle_disconnect(), and the CERROR reformat in lustre_fill_super(). Could the message name each new parameter, say what the nodemap-level limit is for, and split out the unrelated cleanups? "Server to server connections are unaffected" is also not quite right on the MDT: MDT-MDT connections are exempt from being refused, but they are still part of the count the limit is compared against.
(defect) this doesn't hold on the MDT: MDT-MDT exports are part of `lut_num_clients`, which is what `client_count` prints. On an OST the value is always 0 because nothing increments that counter there. (style) the description paragraph runs on from the `Config` `.TP` entry, so it renders as part of it - it needs a `.PP` to break out of the list.
(defect) the nodemap parameter is read-only, so this `set_param` returns an error; nodemap properties are normally set with `lctl nodemap_modify`. The 644 permissions listed below don't match the RO fops either, and "Present on MDS and OSS nodes" over-promises given that the OST-side limit never triggers.
(style) `is_server_connection()` is a very generic name for a global in a widely included header; something like `target_is_server_connection()` would be less likely to clash. The argument could be `const struct obd_connect_data *`.
(style) plain `int` is the spelling used elsewhere in this struct; `signed int` stands out. The two limits also test differently - the nodemap check uses `!= -1` and the target check uses `>= 0` - so picking one form for both would settle what a value like -5 means.
(defect) `lut_num_clients` includes server-to-server exports, so exempting them here only stops them from being refused - they still consume the limit. lod_lov.c:193 requests `OBD_CONNECT_MULTIMODRPCS` on the MDT-MDT OSP connection and mdt_handler.c:7265 grants it, so tgt_client_new() counts those exports (tgt_lastrcvd.c:1137). On a 4-MDT filesystem each MDT starts at client_count = 3, so `client_limit=N` admits N-3 real clients. This also runs ahead of the `obd_uuid_equals(&cluuid, &target->obd_uuid)` "lctl gets a backstage, all-access pass" bypass, so a self-UUID/administrative connection is refused as well once the target is at its limit. Intended?
(defect) On an OST this counter never moves, so `obdfilter.*.client_limit` cannot work. tgt_client_new() only does `atomic_inc(&tgt->lut_num_clients)` when `tgt_is_multimodrpcs_client()` is true, and ofd_obd.c:131 masks the connect flags with OST_CONNECT_SUPPORTED, which does not include `OBD_CONNECT_MULTIMODRPCS` (only the MDT grants it, mdt_handler.c:7265). So `obdfilter.*.client_count` always reads 0: any positive limit never fires, and `client_limit=0` refuses every client. Should the OST side count exports instead?
(defect) obd_connect() has already added this export to the nodemap by now (mdt_obd_connect() -> nodemap_add_member() -> nm_member_add()), so `count` includes the client currently connecting. With `nm_client_limit = N` the check fires on the Nth client, so only N-1 are admitted. The rejection path also looks incomplete: the export created by obd_connect() holds a last_rcvd slot (tgt_client_new()) and a nodemap membership, and `out:` only does class_export_put() - no obd_disconnect()/class_disconnect(), unlike the -ENODEV path a few lines below. Doesn't each refused client leave a stale export holding a `lut_num_clients` and `nm_client_count` slot until the ping evictor reaps it?
The `BUG: spinlock bad magic` trace posted on patchset 31 points at this call (`nodemap_get_from_exp+0x118 <- target_handle_connect`, thread `ll_mgs_0002` during sanity-sec setup). This call site is byte-identical in patchset 32, so it doesn't look addressed yet. One place to check: mgs_init_export() returns at mgs_handler.c:798, before `spin_lock_init(&exp->exp_target_data.ted_nodemap_lock)`, for an export whose client UUID equals the target UUID - that is the `dont_check_exports` connect, which reaches this check with `export` non-NULL. nodemap_get_from_exp() takes that lock only when nodemap_active is set, which fits a failure that appears in sanity-sec.
(minor) any negative value is accepted, and the two enforcement sites disagree on what one other than -1 means. Rejecting `val < -1` with -EINVAL would keep the documented -1 as the only "unlimited" value.
(typo) is_server_connection() is defined in lustre_net.h, not ldlm_lib.c.
(defect) Both files are `LDEBUGFS_SEQ_FOPS_RO` and there is no other writer - `nm_client_limit` is only ever assigned in nodemap_inherit_properties(), so it stays -1 for the life of the nodemap. Without a `lctl nodemap_modify` property (and the matching IAM/llog persistence the other nodemap properties have), the enforcement block in target_handle_connect() can never run, and `nodemap.client_limit.4` documents a `lctl set_param` that fails.
LU-19054 target: new max client connection limit This introduces the client_limit parameter that allows a maximum number of client connections to be set. client_limit can be set to limit the allowed number of client-server connections. By default set to -1 for unlimited connections. This only affects client to server connections. Server to server connections are unaffected. Signed-off-by: Max Dilger <mdilger@whamcloud.com> Change-Id: Ibef99a9bd1f889abdfeb97942600b66e9a9be123
| failed enforced test | platform | detail | |
|---|---|---|---|
| custom-1001 crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
| custom-1002 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-subtest-change failed 2× | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-zfs-subtest-change crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
(defect) The body still carries the `--WIP--` marker, and says the tool "can currently only be run by the user, but will be adapted to allow integration to sanity-ec" -- but this revision already adds that integration: sanity-ec test_50/51/52 drive the orchestrator directly. Is this still meant as work in progress, or should the message be rewritten to describe what actually landed? As it stands it reads as a stack of amend notes rather than a description of the change.
(typo) "the user can disabled specific OSTs" -> "can disable".
(defect) Parts of the diff aren't accounted for anywhere in the body, so it's hard to tell what is deliberate: - sanity-ec test_40b/40c/40d, which are plain `fail_loc` degraded-read tests and don't use the orchestrator at all - the `EXCEPT_SLOW="50 51"` gate - the curses TUI in ec_fault_gui.py - the `--matrix`, `--write-verify` and `--layout-audit` modes - the report directory machinery (run.info / summary.txt / per-FAIL `lctl dk` + dmesg capture) - the `--ost-host` ssh routing for multi-node Also "8 OSTs are required to run" no longer matches the code: 52 needs 6, and 40b/40c need 3. Could the body be brought back in line with the diff?
(minor) "Every fault is read back from the owning OSS before the read is issued" holds for --matrix, but not for --soak, which the same message describes a few lines down as injecting the fault mid-I/O. run_soak_loop() starts the read thread first, sleeps a fraction of the baseline, then arms the fault and only afterwards calls _verify_fault_armed(). Worth rewording to "every fault is read back from the owning OSS" without the ordering claim?
BUILD
(defect) This takes a live OST down with `umount -f`, mounts the raw ldiskfs elsewhere, corrupts a block and remounts -- all outside the test framework. Two concerns: The remount is a bare `mount -t lustre $dev $mntpt`, so `$OST_MOUNT_OPTS` / `$MOUNTOPT` from the config are lost. `stop ost$n` / `start ost$n` in test-framework.sh handle those (and `wait_osc_import_state`). Recovery is only a Python `finally`. If the process is SIGKILLed -- an auster timeout, for instance -- the OST stays down and the backend can be left mounted at /tmp/ec_wv_ostbk_N, which breaks every later suite in the run. sanity-ec test_52 registers no `stack_trap` to put the OST back either.
(typo) The bitmask is bits 0-15, not bits 16+. cfs_fail_index() treats any fail_val above 0xffff as bitmask mode and then tests BIT(index) for index < 16, which matches what _apply_fail_loc() builds (0x10000 | bitmask) and what deactivate_ost() logs ("only supports 0-15").
(minor) "At least 8 OSTs configured" is left over from an earlier revision. _soak_random_params(), _wv_geometry() and run_matrix() all scale down to a 2+1 geometry, and the commit message says three OSTs is a real run.
(minor) The MDS is not covered by this. _all_oss_hosts() is built purely from ost.server_host, which comes from --ost-host or the OSC ost_conn_uuid NID, and sanity-ec only passes --ost-host entries. There is no MDS host anywhere in the tool, so on a config where the MDS is a separate node its catastrophe flag is never read. Either drop the MDS from the claim, or add an --mds-host that sanity-ec fills in from facet_host mds1.
(minor) This readback, _apply_fail_loc() and _clear_fault_params() all take run_on_host()'s default timeout of 10 s, while SSH_OPTS alone allows ConnectTimeout=10. On a loaded OSS the ssh gets SIGKILLed at the 10 s mark and this returns "fail_loc readback failed", which the soak and the matrix then report as an EC failure rather than a transport problem. run_on_host()'s own docstring says "server-side callers pass generous timeouts" - these three are the ones that don't. The umount/mount and dmesg paths already pass 30-120 s.
(defect) This poll returns True on the first iteration whether or not the OST came back. `lfs df` prints one line per OST regardless of state: mntdf() synthesises the uuid as "OST%04x" when the statfs failed, and showdf() prints `<uuid>: inactive device` for -ENODATA and `<uuid>: <strerror>` for any other error. Every one of those lines contains "OST", so `grep -c OST` is always OSTCOUNT and the `>= self.ostcount` test is satisfied immediately. That makes the guard in _wv_corrupt_check() dead, and `lfs mirror verify` can run before the just-remounted OST has reconnected. Would checking that each OST line actually reports space (or polling the osc import state, as wait_osc_import_state() does) work better here?
(defect) A degraded read that hangs is scored here as a correct failure. _matrix_degraded_read() gets its verdict from _compute_file_hash(), which returns None both when dd exits with -EIO and when run_cmd() kills it on matrix_read_timeout. Either way read_ok is False, so this counts npass += 1. The comment above _matrix_degraded_read() says a read that hangs instead of returning -EIO "is exactly the regression this mode hunts", but as written the over-tolerance loop cannot fail on it. A handful of hung reads at 120 s each stays well inside the 1800 s budget, so unrun is 0 and the check passes. Could run_cmd()/_compute_file_hash() distinguish a timeout from a non-zero exit, so a timed-out over-tolerance read is a failure rather than the expected outcome?
LU-12668 tests: add EC failure orchestrator Add ec_fault_orchestrator.py, a fault-injection and verification tool for erasure-coded files, plus the sanity-ec tests that drive it. The tool arms OBD_FAIL_OST_BRW_READ_BULK on the OSS owning each target OST, so a read really has to reconstruct from parity rather than being served from cache or a healthy stripe. Every fault is read back from the owning OSS, so a fault that failed to arm is reported instead of passing as a successful reconstruction. Reads are compared against an md5 of the bytes as they were written, captured via tee at write time, rather than against an earlier read of the same file that may already be wrong. Five non-interactive modes, each returning its verdict as an exit code: - --soak: randomized layout, geometry and size per iteration, with a fault injected mid-I/O. Records MB/s and the degraded-vs-baseline slowdown per iteration, and gates on a kernel-health check (the catastrophe flag on every server node it knows of plus the dmesg corruption markers test-framework.sh already curates). - --matrix: on a single-RAID-set file, faults every combination of 1..P stripes and requires reconstruction, then P+1 and requires the read to fail with an error. A P+1 read that hangs instead is scored a failure, not the expected outcome -- that hang is the regression this mode looks for. Bounded by --matrix-budget and --matrix-read-timeout: combinations left unrun when the budget expires are reported as a failure naming the count, never silently dropped. - --cli: one pass over the same machinery -- create the file, take baseline benchmarks, then deactivate each data OST in turn and read it degraded. A hand-driven check that needs no terminal. - --write-verify: the write/resync/verify behaviors -- parity goes stale on write while data stays init, resync restores init with the md5 unchanged, one resync clears a stale plain mirror and stale parity together, writes are refused on a stale data mirror but allowed on stale parity, and 'lfs mirror verify' flags both stale parity and real on-disk corruption of a data or a parity object. The corruption checks take one OST down, corrupt a block on the raw ldiskfs backend and put it back; they self-skip on other backends. - --layout-audit: sweeps EC geometries and checks the OST allocation the kernel produced against a Python port of ec_split_stripes(), including uneven splits. Pure layout, no I/O. A curses TUI (ec_fault_gui.py) drives the same object interactively for manual investigation. Every non-interactive mode writes a report directory (run.info, summary.txt, and 'lctl dk' plus dmesg captured on failure) so a run that crashes the node leaves evidence behind. Server-side actions are routed to the OSS owning each OST through an --ost-host map, so the tool works multi-node; sanity-ec builds that map from facet_host. --mds-host names the MDS nodes, which are never faulted but are still read for the latched catastrophe flag, so an LBUG there during a degraded read is not missed. On a single node the hosts resolve local and no ssh is issued. Tests 76a (soak), 76b (matrix) and 76c (write-verify) wrap the modes. The geometry scales to the active OST count, so all three run on as few as 3 OSTs (2+1); more OSTs widen the parity coverage rather than being required. 76a and 76b need <= 16 OSTs because the fail_val OST mask is a 16-bit field, and are gated behind SLOW since the soak and the matrix are long-running. 76c registers a stack_trap to put an OST back if the orchestrator is killed outright mid-corruption. Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Test-Parameters: testlist=sanity-ec ostcount=8 env=SLOW=yes Test-Parameters: testlist=sanity-ec ostcount=3 Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com> Change-Id: Ic53df83e733c8850a9435b6793e4be431156b24a
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
(minor) "stop handing out EC layouts" doesn't quite match the code. mdt_pattern_types is only read in mdt_connect_internal(); nothing in mdt/ or lod/ consults it when a layout is created, so an MDT with parity removed will still create EC layouts on request. What it stops is clients *using* parity components, and only for clients that mount afterwards. Worth noting too that the negotiated mask does not gate creation on the client either: with lustre.enable_flr_ec=0 (the default) a client can still create an EC file via llite.*.enable_erasure_coding and then get -EIO writing it. Should layout creation be gated on the negotiated mask as well?
This baseline omits LOV_PATTERN_COMPRESS, but lov_pattern_supported() right below still lists `LOV_PATTERN_RAID0 | LOV_PATTERN_COMPRESS` as a pattern this client's IO stack handles.
So for a compressed component lov_lsme_usable() -> lov_pattern_available_mds() computes
pattern_base = 0x801
0x801 & ~LOV_MDS_PATTERN_SUPPORT_217 = 0x800 /* != 0 */
and returns false, on both branches: the compat branch uses this macro, and the negotiated branch uses the MDS mask, which is capped by MDT_PATTERN_TYPES_SUPPORTED = baseline | PARITY. There is no value of mdt.*.pattern_types that can put COMPRESS back (conf-sanity test_163a asserts `+compress` is silently dropped).
The result is that lov_init_composite() leaves every compressed component !lle_valid and skips lco_init(), so lov_io_rw_iter_init() returns -EAGAIN/-EIO for a plain compressed file. This is reachable today: lsme_unpack() accepts COMPRESS components when llite.*.enable_compression is set (lov_ea.c), and lod_generate_lovea() packs them.
Should the mask be derived from what the client actually supports (i.e. include COMPRESS in both the 2.17 baseline and MDT_PATTERN_TYPES_SUPPORTED), rather than a hard-coded RAID0/MDT/OVERSTRIPING set?
(style) F_HOLE and F_RELEASED are layout flags rather than pattern types, and every consumer strips them first (`pattern & ~LOV_PATTERN_F_MASK` in lov_pattern_available_mds(), `& ~(F_RELEASED | F_MASK)` in lov_pattern_supported()). lov_pattern_bit2str() also has no names for bits 30/31, so they never show up in mdt.*.pattern_types or lov.*.mds_pattern_support. They only put two bits nobody reads on the wire in ocd_pattern_support. Could the macro just be RAID0 | MDT | OVERSTRIPING?
(minor) `data` here comes from obd_get_info(KEY_CONN_DATA) on sbi->ll_md_exp, which is the LMV export, and lmv_get_info() forwards that key to `lmv_tgt(lmv, 0)` only. So on DNE the stored mask is whatever MDT index 0 negotiated; mdt.<fs>-MDT0001.pattern_types has no effect on any client, even though the parameter is per-MDT and the man page shows a single-MDT example. Should the client intersect the masks from all MDTs, or should the caveat be documented?
(minor) This version gate was flagged on an earlier patchset ("this version should be updated when the patch is refreshed") and answered "Done", but it is still 2.17.52.224 here and in test_163a/test_163b.
The things these tests check (`pattern_support` in the mdc/osc import, mdt.*.pattern_types) only exist from this patch, so any server in 2.17.53..2.17.57 passes the gate without the feature. The tests do fall through to a skip on the missing parameter, but the gate should be the version this lands in.
LU-12187 lov: MDS layout pattern negotiation
Add 'obd_connect_data::ocd_pattern_support' to allow the client
and MDS to negotiate the file layout patterns that they support.
This prevents clients from trying to use layout types not
recognized by the MDS, and allows the MDS to (potentially)
convert/filter existing file layouts to a format that the
client understands.
This will be used by FLR-EC to negotiate whether the client
and MDS support LOV_PATTERN_PARITY layouts, and others in
the future.
Add LOV_MDS_PATTERN_SUPPORT_217 for compat with pre-2.18
MDS that do not send ocd_pattern_support. Generalize
lov_pattern_available_mds() to check all patterns against
the MDS-negotiated mask. The client sends its supported patterns
to the MDS, which responds with the intersection of both sets.
The client stores the negotiated mask only when the server
grants OBD_CONNECT2_FLR_EC. target_handle_connect() echoes
un-negotiated ocd fields back from the request, so an ungated
store would let the client's own request value defeat the
pre-2.18 fallback.
Gate pattern usability at layout init rather than at parse time:
lov_init_composite() consults lov_lsme_usable() (pattern support
intersected with the MDS-negotiated mask), and leaves unusable
components un-initialized and !lle_valid, the same as components
with an unknown pattern. Parsing itself stays pattern-agnostic
to preserve lsme alloc/free symmetry.
Adds mdt.*.pattern_types which gives an administrator the
ability to enable or disable layout types at runtime, and a
read-only lov.*.mds_pattern_support which exposes the
effective negotiated mask on the client. The mdt_enable_flr_ec
module parameter now only sets the initial pattern_types value
when an MDT starts up, so mdt.*.pattern_types is what changes
the advertised types at runtime. Add man pages for both
parameters and for the renamed lustre.enable_flr_ec parameter.
Rename the 'llite_enable_flr_ec' module parameter to
'enable_flr_ec', so it is accessed as 'lustre.enable_flr_ec'
per LU-14144 convention. Add the 'lustre' module to the libcfs
parameter path list so the client module parameters are reachable
via 'lctl {get,set}_param --module' instead of a hard-coded
/sys/module path.
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Test-Parameters: testlist=sanity-ec
Test-Parameters: testlist=conf-sanity env=ONLY="163 163a 163b"
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Change-Id: Iaef716e28014be5b91dd50b117dd0881f5c37f37
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-5 failed 3× | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: lustre-rsync-test. | session |
(minor) The body only describes `client_limit`. Some of the diff isn't accounted for: the new read-only `client_count` parameter (four man pages plus mdt/ofd sysfs attrs), the per-nodemap `client_limit`/`client_count` files, the `class_exp2tgt()` -> `class_obd2tgt()` split, the `exp` local added to target_handle_disconnect(), and the CERROR reformat in lustre_fill_super(). Could the message name each new parameter, say what the nodemap-level limit is for, and split out the unrelated cleanups? "Server to server connections are unaffected" is also not quite right on the MDT: MDT-MDT connections are exempt from being refused, but they are still part of the count the limit is compared against.
(defect) this doesn't hold on the MDT: MDT-MDT exports are part of `lut_num_clients`, which is what `client_count` prints. On an OST the value is always 0 because nothing increments that counter there. (style) the description paragraph runs on from the `Config` `.TP` entry, so it renders as part of it - it needs a `.PP` to break out of the list.
(defect) the nodemap parameter is read-only, so this `set_param` returns an error; nodemap properties are normally set with `lctl nodemap_modify`. The 644 permissions listed below don't match the RO fops either, and "Present on MDS and OSS nodes" over-promises given that the OST-side limit never triggers.
(style) `is_server_connection()` is a very generic name for a global in a widely included header; something like `target_is_server_connection()` would be less likely to clash. The argument could be `const struct obd_connect_data *`.
(style) plain `int` is the spelling used elsewhere in this struct; `signed int` stands out. The two limits also test differently - the nodemap check uses `!= -1` and the target check uses `>= 0` - so picking one form for both would settle what a value like -5 means.
(defect) `lut_num_clients` includes server-to-server exports, so exempting them here only stops them from being refused - they still consume the limit. lod_lov.c:193 requests `OBD_CONNECT_MULTIMODRPCS` on the MDT-MDT OSP connection and mdt_handler.c:7265 grants it, so tgt_client_new() counts those exports (tgt_lastrcvd.c:1137). On a 4-MDT filesystem each MDT starts at client_count = 3, so `client_limit=N` admits N-3 real clients. This also runs ahead of the `obd_uuid_equals(&cluuid, &target->obd_uuid)` "lctl gets a backstage, all-access pass" bypass, so a self-UUID/administrative connection is refused as well once the target is at its limit. Intended?
(defect) On an OST this counter never moves, so `obdfilter.*.client_limit` cannot work. tgt_client_new() only does `atomic_inc(&tgt->lut_num_clients)` when `tgt_is_multimodrpcs_client()` is true, and ofd_obd.c:131 masks the connect flags with OST_CONNECT_SUPPORTED, which does not include `OBD_CONNECT_MULTIMODRPCS` (only the MDT grants it, mdt_handler.c:7265). So `obdfilter.*.client_count` always reads 0: any positive limit never fires, and `client_limit=0` refuses every client. Should the OST side count exports instead?
(defect) obd_connect() has already added this export to the nodemap by now (mdt_obd_connect() -> nodemap_add_member() -> nm_member_add()), so `count` includes the client currently connecting. With `nm_client_limit = N` the check fires on the Nth client, so only N-1 are admitted. The rejection path also looks incomplete: the export created by obd_connect() holds a last_rcvd slot (tgt_client_new()) and a nodemap membership, and `out:` only does class_export_put() - no obd_disconnect()/class_disconnect(), unlike the -ENODEV path a few lines below. Doesn't each refused client leave a stale export holding a `lut_num_clients` and `nm_client_count` slot until the ping evictor reaps it?
The `BUG: spinlock bad magic` trace posted on patchset 31 points at this call (`nodemap_get_from_exp+0x118 <- target_handle_connect`, thread `ll_mgs_0002` during sanity-sec setup). This call site is byte-identical in patchset 32, so it doesn't look addressed yet. One place to check: mgs_init_export() returns at mgs_handler.c:798, before `spin_lock_init(&exp->exp_target_data.ted_nodemap_lock)`, for an export whose client UUID equals the target UUID - that is the `dont_check_exports` connect, which reaches this check with `export` non-NULL. nodemap_get_from_exp() takes that lock only when nodemap_active is set, which fits a failure that appears in sanity-sec.
(minor) any negative value is accepted, and the two enforcement sites disagree on what one other than -1 means. Rejecting `val < -1` with -EINVAL would keep the documented -1 as the only "unlimited" value.
(typo) is_server_connection() is defined in lustre_net.h, not ldlm_lib.c.
(defect) Both files are `LDEBUGFS_SEQ_FOPS_RO` and there is no other writer - `nm_client_limit` is only ever assigned in nodemap_inherit_properties(), so it stays -1 for the life of the nodemap. Without a `lctl nodemap_modify` property (and the matching IAM/llog persistence the other nodemap properties have), the enforcement block in target_handle_connect() can never run, and `nodemap.client_limit.4` documents a `lctl set_param` that fails.
LU-19054 target: new max client connection limit This introduces the client_limit parameter that allows a maximum number of client connections to be set. client_limit can be set to limit the allowed number of client-server connections. By default set to -1 for unlimited connections. This only affects client to server connections. Server to server connections are unaffected. Signed-off-by: Max Dilger <mdilger@whamcloud.com> Change-Id: Ibef99a9bd1f889abdfeb97942600b66e9a9be123
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 retesting | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: replay-dual. | session |
| review-dne-zfs-part-2 retesting | RHEL 10.1 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-ldiskfs-ubuntu retesting | RHEL 8.10 / x86_64, Ubuntu 24.04 / x86_64 | ran 6 tests. 1 tests failed: sanity-lnet. | session |
(minor) The ll_readahead_handle_work() piece fixes a pre-existing bug that has nothing to do with EC: ll_ra_count_get() reserves the pages and nothing puts them back when cl_io_rw_init() fails, and ll_ra_count_put() is the only decrement of ra_cur_pages. A Fixes: line would let the maintenance branches pick it up:
Fixes: c2791674260b ("LU-12043 llite: improve single-thread read performance")
(defect) The lov_io_set_range() hunk fixes an LBUG that 84c1a4a074 introduced, and that commit is three patches back in this same unlanded series. That leaves 84c1a4a074, 13af17f231 and b718cadc7b each panicking the client on a two-component EC layout, so the series is not bisectable and those revisions cannot be tested on their own.
Can the clamp be folded into 84c1a4a074 instead? If it has to stay a separate patch, it needs its own tag alongside the existing one:
Fixes: 84c1a4a07423 ("LU-12669 ec: recover data from parity")
Good catch. Definitely best to check for the inactive OSC device early. This would be set some seconds after the OST goes offline, so IO shouldn't wait to detect that every time.
Max, I rebased this on Bobi's latest patch - you were based on an older version
(minor) This label bypasses cl_io_fini(). cl_io_init()'s contract is that the caller calls cl_io_fini() no matter what it returned, and the commit message points out that this exit stops being rare once a dead import can fail an EC read at init. Should the new path run cl_io_fini(env, io) before dropping the reservation?
(defect) On a component with lsme_dstripe_count == 0 this can push eoff well past lio->lis_endpos.
Neither place that sets the cycle end rounds it to a recovery group on that branch: lov_io_set_range() skips its end-rounding block when dstripe_count == 0, and lov_io_ec_rd_iter_init() only rounds when dstripe_count > 1. So lis_endpos is just the request end, while eoff becomes soff + RGs, and RGs there is ss * 4 (or ss * lo_nr).
lov_ec_read_stripe_pages() classifies pages against eoff alone, so the pages in [lis_endpos, soff + RGs) are grabbed and submitted. lov_io_lock() enqueued only [ec_inner.crw_pos, +crw_bytes) for this cycle, and lov_ecio_add_data_sub() skipped those stripes (lov_stripe_intersects() false against the cycle extent), so lov_sub_get() allocates a fresh sub-IO with no DLM lock -- the same "uncovered page!" LBUG in osc_req_attr_set() that the lov_io_set_range() hunk is fixing.
Worked example, layout "-E 128M --ec 4+2 -E 512M -c 4" (EC component followed by a plain one, as in the mirror layouts in this suite), read [0, 130M) with a dead OST:
cycle in the plain component: lis_pos 128M, lis_endpos 130M
dcount 4, ss 1M => RGs 4M, soff 128M
eoff was min(132M, 130M) = 130M, now min(132M, 512M) = 132M
stripes 2 and 3 cover [130M, 132M) -- outside the enqueued lock
Before the change those pages were EC_DPG_ZERO and never touched. Should the clamp stay bounded by lio->lis_endpos on the dcount == 0 branch, where there is no recovery group to complete?
(minor) The sibling switch below clears the retry count before returning:
io->ci_switch_ec_io = 1;
io->ci_need_restart = 1;
io->ci_ndelay_tried = 0;
RETURN(-ENODATA);
This one leaves ci_ndelay_tried alone, and ll_file_io_generic() carries it across the restart (retried = io->ci_ndelay_tried). So if the read had already restarted once before the import went invalid, the CIT_EC_RD pass lands on ndelay_tried: with a non-zero count and can still take the 10 ms schedule_timeout_interruptible() and set ci_tried_all_mirrors -- the backoff the commit message says this path avoids. Should it reset the count too?
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
[AI review - fable] (defect) Including CIT_READ here looks dangerous. After the continue, the top lock is granted without covering this stripe, and nothing in the normal read path checks lso_status (only the EC_RD and LSEEK paths do), so pages on the dead stripe are still generated and submitted, and osc_build_rpc() -> cl_req_attr_set() -> osc_req_attr_set() hits the "uncovered page!" LBUG in osc_object.c when osc_dlmlock_at_pgoff() finds nothing - the very crash the commit message wants to avoid. The lov_io_mirror_init() check does not guard this path when ci_cross_ec is unset (a read of a non-EC component of a file whose layout has parity entries elsewhere - lov_lsm_has_parity() is file-wide, and lov_io_ec_rd_start() explicitly supports dcount == 0 components), or when the import goes inactive after cl_io_init(). Before this change the enqueue failed fast with -ESHUTDOWN and the IO returned an error or restarted into EC_RD instead of crashing. Should the skip be limited to CIT_EC_RD, letting CIT_READ fail the enqueue so the existing restart logic switches to EC_RD? As a side effect the CIT_READ marking also leaves a stale LSS_READ_ERR on the lovsub (the normal read path never resets it), which lov_io_lseek_end() then treats as degraded even after the OST is reactivated.
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
error: lov_lock_enqueue():'osc' dereferencing possible ERR_PTR()
(style) This isn't a bug, but the include groups are inverted here - `<lustre_osc.h>` is a lustre header and belongs before the local `"lov_cl_internal.h"`. The same include added to lov_io.c in this patch is ordered that way.
LU-12668 lov: proactive dead-OST detection for degraded reads
Instead of letting a read proceed to an inactive OST and fail deep in
the BRW/lock path, detect dead imports during mirror selection and
route to the EC recovery path.
- lov_io_mirror_init() rejects a candidate mirror that has a data
stripe on a deactivated or invalid import, so FLR rotation can still
pick an intact mirror. Only when no mirror is intact does it set
ci_switch_ec_io, which also skips the FLR backoff sleep -- we
already know the OSTs are dead. The scan covers the whole mirror
rather than just the stripes the I/O touches: see the comment on
lov_ec_has_inactive_stripe() for why bounding it to the I/O extent
is not safe until the size path handles unreachable stripes.
- lov_ecio_add_data_sub() marks such stripes errored up front, so the
recovery loop reconstructs them instead of building a sub-IO that
cl_io_iter_init() or the lock enqueue would reject.
lov_io_set_range() rounded the CIT_EC_RD range end up to a raid-set
boundary using the geometry of the component holding the read end, but
that rounded value can land past the component. The next component
anchors its raid sets at its own e_start, so the end fell mid-raid-set
there: lov_io_ec_rd_iter_init() cut the cycle short at it while
lov_io_ec_rd_start() still read a whole recovery group, leaving pages
outside the lock lov_io_lock() had enqueued. osc_req_attr_set() then
LBUGged ("uncovered page!") from brw_queue_work and panicked the
client. Clamp the rounded end at the component boundary; the read
cannot need data past it, since the request itself ended inside that
component and each component is erasure coded independently.
Reproduced with a -E 4M -c 3 --ec 2+1, -E -1 -c 5 --ec 4+1 layout.
ll_readahead_handle_work() leaked its ra_cur_pages reservation when
cl_io_rw_init() failed. That exit is rare today but becomes routine
once a dead import can fail an EC read at init, and ll_ra_count_put()
is the counter's only decrement, so readahead would stop for the whole
mount and never recover. Release the reservation from a single exit
label that every path past ll_ra_count_get() reaches, and call
cl_io_fini() on the failed init, which cl_io_init() requires no matter
what it returned.
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Test-Parameters: testlist=sanity-ec ostcount=8
Test-Parameters: testlist=sanity-ec ostcount=8 fstype=zfs
Test-Parameters: testlist=sanity ostcount=8
Fixes: c2791674260b ("LU-12043 llite: improve single-thread read performance")
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com>
Change-Id: I0913e1ee977c9850193c92835edb185b0aedc6d4
(style) The summary reads as past tense; house style is imperative. "update mirror split for EC support" would match the rest of the tree.
(minor) The body calls out `test_44f` and the `test_7b` rewrite by name, but sanity-ec `test_7e`, `test_7f`, `test_7g` and the new `identify_ec_mirrors()` helper (~240 new lines) aren't mentioned anywhere. Worth a sentence so a reader knows the new coverage is intentional and what it exercises.
@mvef@whamcloud.com, @rsahlberg@whamcloud.com, I see that `--force-no-ec` is listed in LU-19548, but I don't recall what the realistic use case is for that option? Leaving an unanchored EC mirror behind on a file doesn't seem very useful, and there doesn't seem to be any way to reconnect it to a new data mirror even if it was created (nor assurance that the OSTs used in the data mirror are suitable for that EC). (Apologies in advance to Max, but ...) I'm wondering if `--force-no-ec` option should be removed, and EC mirrors should always be moved to the victim file or deleted? That would remove a lot of complexity in the code that I doesn't seem to add any real value.
(style) The .TH date is older than the lfs-mirror-delete.1 date set in the same patch, and predates the current content. Worth bumping to the refresh date.
(minor) The victim now gets two mirrors, and lod_declare_layout_merge() rejects any merge source that has more than one:
if (le16_to_cpu(merge_lcm->lcm_mirror_count))
RETURN(-EBUSY);
So `lfs mirror extend -N -f NEW_FILE` (documented in lfs-mirror-extend.1) fails with EBUSY on a file produced this way, unlike every other split victim. Should this paragraph say so, until extend learns to merge a data+parity pair?
(style) function comment should follow the style as `mdd_split_ea`.
(question) For a data+parity victim, if either side was *STALE* on the source, should the new file always set data *in-sync* and parity *STALE*? The victim has no other data mirror, so data has to be the non-stale primary or the layout is unusable (all-stale is -EPERM; in-sync parity as primary is -EUCLEAN). Only when both were already in-sync should both stay in-sync. Can current mask accomplish this or not?
(defect) llapi_mirror_find_stale() skips LCME_FL_PARITY, so a remaining
parity mirror always yields comp_size == 0 and this returns false.
lfs mirror split/delete then skips the last-good-copy resync even when
every remaining data mirror is stale.
Should remaining parity mirrors be ignored here the same way
data_mirror_remains() ignores them? Co-split also takes the paired
parity, so that parity is not a copy that will survive the split.
Example: file has D1 (EC data, in-sync after a write), P1 (parity),
and D2 (data, stale).
lfs mirror split --mirror-id D1
last_non_stale_mirror(D1) sees remaining mirrors {P1, D2}. P1 is
skipped by find_stale, so comp_size == 0 and the function returns
false. Resync is skipped, co-split removes D1+P1, and the source is
left with only stale D2.
(minor) The comment is narrower than the condition. `mflags & MF_DESTROY` with a NULL victim_file is also true for `lfs mirror split -d` (which cannot take -f at all), for `lfs pcc detach` (`MF_DESTROY | MF_FOREIGN`), and for the internal `mirror_split(name, 1, NULL, MF_DESTROY, NULL)` call. Naming only `lfs mirror delete` makes the flag look narrower than it is.
(minor) mirror_split() also backs `lfs mirror delete` (SO_MIRROR_DELETE sets MF_DESTROY) and `lfs pcc detach`, so a user who typed `lfs mirror delete` is told the mirror is being "split". sanity-ec test_7f greps for this text from a `lfs mirror delete` run, so that wording is on a tested path. Also, every other message in this function is prefixed with `progname`; this one isn't, so in a pipeline it isn't obvious which tool emitted it.
Skipping the orphan parity comp leaves `comp_array[i].lrc_synced` at the value `_mirror_find_stale()` set, which is unconditionally `true`:
comp[idx].lrc_synced = true; /* liblustreapi_layout.c */
Back in lfs_mirror_resync_file() every entry with `lrc_synced` is fed to LL_LEASE_RESYNC_DONE, and lod_declare_update_sync_pending() then does `llc_flags &= ~LCME_FL_STALE` for each id.
So `lfs mirror resync` returns success and drops LCME_FL_STALE from a parity component it never recomputed - `lfs getstripe` afterwards shows it as in sync. Would `comp_array[i].lrc_synced = false;` before the `continue` be more honest here?
LU-19548 lfs: updated mirror split for EC support
An EC data mirror and its parity mirror form a pair joined by a
bidirectional link (lcme_mirror_link_id). Splitting one mirror
without the other strands a parity mirror that protects no data,
or leaves a data mirror with no EC protection.
When splitting a data mirror, the MDS now splits (or destroys) its
paired parity mirror in the same atomic operation. The pair is
found via the mirror link id rather than by assuming the data and
parity components are adjacent in the layout, and the link is only
followed while it still describes an intact pair: the linked mirror
must exist, carry LCME_FL_PARITY and link back. A link failing
those tests is stale, and following it would co-split an unrelated
mirror. The link on any mirror left behind is cleared so no
dangling reference remains.
A parity mirror can still end up with no data mirror to pair with,
since a stale link is not followed and is cleared instead. lfs
mirror verify and lfs mirror resync resolve a parity component to
its data component through that link, in
__llapi_layout_find_data_comp_by_parity(), so both used to fail the
whole file with -ENOENT; once anything was written to the file the
orphan was marked stale and could never be resynced again. Skip a
parity component that has no data component to pair with instead.
mirror_split() used its "purge" flag for two things: to mean "the
mirror is deleted by handing fd in as the victim", and as the guard
for the retry that copes with an old MDS rejecting fd == fdv. The
flag started out true for every split, so a split to -f NEW_FILE or
to the default victim file also retried after an -EINVAL or -EBUSY
from the MDS, re-opened with O_CREAT|O_EXCL the victim it had
already created, and reported "create victim file failed: File
exists" in place of the real cause. The same flag guards the close
of the victim descriptor, so those splits leaked one descriptor per
file and splitting a batch of files in a single command ran out of
descriptors partway through. Start "purge" as the condition it
describes.
Such a failure was also invisible to a script, since mirror_split()
returned the leftover 0 of an earlier helper on several error paths:
a split that could not create its victim file, or that named a
mirror, component or pool the file does not have, printed an error
and exited 0. Set an error on those paths. Add sanity-flr test_44f.
A split is refused if it would remove every mirror, and also if it
would leave the file without a data mirror. Parity mirrors alone
cannot serve reads: with no data mirror the layout is no longer
FLR, so lov_io_mirror_init() returns at its !lov_is_flr() check
ahead of the parity checks, the parity mirror is picked for IO, and
reads hand back parity stripes as file data. A lone data+parity
pair therefore cannot be taken apart by splitting the data mirror;
split the parity mirror instead to turn the file back into a plain
data file.
The warning printed when a parity mirror is split names the data
mirror that loses EC protection, and is skipped for a parity mirror
whose link has been cleared, which protects no data to begin with.
Splitting a parity mirror directly no longer requires -d. That
restriction, added by commit dfb93e4978b0 ("LU-19548 lfs: mirror
split -d for parity mirrors"), existed to keep a parity-only
layout off a real file. Refusing to strand the data side now covers
the source file, and a victim file holding only the parity mirror
reports size 0, because lov_attr_get_composite() does not take size
from parity components, so it cannot pass parity off as file data.
A warning is printed instead, since removing the parity eliminates
EC protection of the data mirror. test_7b is updated for the new
behaviour.
A victim that receives a data+parity pair keeps its LCME_FL_STALE
markers, so a parity mirror that was stale at split time no longer
lands in a victim that claims an in-sync pair.
lod_declare_layout_purge() refused a purge buffer holding more than
one mirror. lfs mirror delete of a data mirror now hands it the
data+parity pair, so the check is relaxed from lcm_mirror_count
!= 0 to > 1.
Also fix two endian bugs that predate this work in the rewritten
function, harmless on little-endian but wrong on big-endian:
lcm_flags was converted twice, and the __u16 lcm_entry_count was
written with cpu_to_le32().
collect_mirror_id() fills an array owned by its caller but never
knew how large that array was, and every caller passed 128 entries
while lod.*.mirror_count_max accepts up to LUSTRE_MIRROR_COUNT_MAX
mirrors. Splitting a mirror out of a file with more mirrors than
that wrote past the end of a stack array. Carry the array size in
struct collect_ids_data, check it in the callback, and size the
arrays LUSTRE_MIRROR_COUNT_MAX so the check cannot be reached.
Fixes: 23b2d4781899 ("LU-10420 flr: split a mirror from mirrored file")
Fixes: c6e7c0788d7c ("LU-10258 lfs: lfs mirror copy command")
Fixes: b2d73351e646 ("LU-14521 flr: delete mirror without volatile file")
Fixes: fb790204ce3e ("LU-17908 layout: preserve non-FLR state layout flags")
Test-Parameters: testlist=sanity-ec ostcount=8
Test-Parameters: testlist=sanity-flr
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Assisted-by: ClaudeCode:Opus-4.8 llm_code_and_review_tools
Change-Id: I69ca706127e8e0b0e26c88525ac5509e22fa912a
(suggestion) The body says an interop run skips the new subtests rather than failing them, but nothing here exercises that. Would adding a second line such as `Test-Parameters: testlist=sanity-ec ostcount=8 serverversion=2.17.55` prove the version gate actually skips against an older MDS?
(minor) The extra storage for EC(D+P) is PARITY/DATA of the file size, not (DATA+PARITY)/DATA - (DATA+PARITY)/DATA is the total. The 8+2 example further down gets this right ("1.25x the file size, vs 2x for a plain mirror"), so the two read as contradicting each other. Reword as total storage, or use PARITY/DATA here?
(defect) Should this refuse a data component that is already linked, instead of overwriting its link id?
`data_lcme->lcme_time_and_id` may already carry the mirror id of an existing parity mirror. The old id is dropped here and replaced with the newly merged one, so the earlier parity mirror ends up half-linked from the data side - the exact state the `!link_id` check a few lines up refuses.
The only guard against that today is client-side, in `build_parity_layout_for_mirror()`, and it is not race-free: `llapi_layout_get_by_path()` reads the layout before `mirror_extend_layout()` takes any lease, so two `lfs mirror extend --mirror-id N --ec D+P` runs on different clients both see mirror N unprotected and both merge. The second merge re-stamps the data component; `verify_new_mirror_id()` then reports -EBUSY, but the layout has already been committed. `llapi_layout_comp_add_parity_only()` is also a new public, man-paged entry point, so any application can build the same merge buffer directly.
The result is a file with two parity mirrors on one data mirror, which the commit message says is rejected for now. `lod_verify_striping()` already re-checks the k+m bound with "Cannot trust userspace; enforce here too" - the same reasoning seems to apply to the link.
if (lcme_timestamp_id_unpack(time_and_id))
GOTO(out, rc = -EEXIST);
This needs to be fixed before the EC feature goes into production, but is still under discussion and not the target of this patch.
(minor) `llapi_layout_pool_name_get()` never sets ENODATA - it snprintf()s `comp->llc_pool_name` (empty when there is no pool) and returns 0, failing only on a NULL current component, a NULL `dest`, or a foreign pattern (EINVAL). So the `errno != ENODATA` carve-out never triggers and the comment describes behaviour the API doesn't have. If it stays, the non-ENODATA failure leaves `rc` at -1 until the next assignment overwrites it, which is easy to trip over later.
LU-19548 lfs: mirror extend support for EC
Add three EC modes to 'lfs mirror extend':
--mirror-id N --ec D+P attach a parity mirror to existing data
mirror N (N >= 1; id 0 is reserved for
non-FLR layouts and is rejected)
-N [K] --ec D+P add K new data+parity mirror pairs
(K defaults to 1)
--ec D+P attach a parity mirror to the file's lone
data mirror, for the non-FLR case where the
user just wants EC protection on a plain
file; a file with 2+ mirrors errors out
asking for --mirror-id or -N
As a regular extend never leaves a new mirror stale, the new mirrors
are in sync when the command returns: data mirrors are populated by a
copy before the layout merge, parity mirrors are computed in place
with the machinery 'lfs mirror resync' uses. A partial extend resyncs
the parity mirrors it did attach before reporting the error, and one
that was merged but could not be marked stale is reported with the
command that removes it, since resync finds mirrors by LCME_FL_STALE
and so cannot repair that one.
A parity mirror is attached by merging a layout that holds only the
new parity components, while the data mirror they protect stays where
it is. The new llapi_layout_comp_add_parity_only() builds that layout;
llapi_layout_comp_add_ec() cannot, as it derives the parity extent,
stripe size and pool by searching for the data component in the same
buffer, and pairs the two with a transient link id that is resolved
during creation. Client-side layout sanity likewise had to stop
requiring a data component for every parity component.
Splitting the pair across two files needs two changes in lod.
lod_qos_prep_create() sizes a parity component from the data component
it protects and rejects one that has none; the data component of an
extend is not in the volatile file being created, so the client sends
its stripe count as the parity component's own and lod derives the
raidset count from that. lod_declare_layout_merge() copies the
incoming entries verbatim, so only the parity -> data half of the link
would be stored; it now also stamps the matching data component with
the mirror id assigned to the parity component, without which 'lfs
mirror split' cannot see the pair from the data side and leaves the
parity mirror behind. A link naming a mirror that is not in the
merged layout means the data mirror was split away after the client
read it, and is refused rather than stored as a half-linked pair that
resync would then fail on for the life of the file.
'lfs mirror extend --ec' also refuses a data mirror that is stale or
'nosync'. Parity is computed from the content of the named mirror, and
resyncing that mirror afterwards does not re-stale its parity, so the
parity would silently protect superseded data; a nosync mirror is
skipped by resync, leaving the parity uncomputed.
The rider in lod_qos_prep_create() rejecting an EC data stripe count
of zero is unrelated to extend: lod_verify_striping() bounds only
k + m, so a k == 0 layout reaches ec_split_stripes() and divides by
zero in the kernel.
Attaching a second parity mirror of a different geometry to one data
mirror, which would let a file's EC protection change without
rewriting its data, is not implemented. It is rejected for now, and no
test asserts that rejection.
An MDS that predates this change refuses a parity-only create buffer
outright, so the new sanity-ec subtests are gated on the server
version and an interop run skips them instead of failing.
Components EC cannot be sized for are named rather than left to a bare
errno: a Data-on-MDT component has no OST stripes to spread a raidset
over, and a self-extending component's extent does not stay matched to
its parity component, both as 'lfs setstripe --ec' refuses them. --ec
is refused with -f as the man page says it is, and a second --ec on
one command line no longer reports success after printing a usage
error. A '-N=TOTAL' that finds the file already at TOTAL mirrors adds
none, as it warns, instead of falling through to the auto mode that a
bare --ec selects.
struct collect_ids_data moves ahead of the new EC-extend helpers,
which reuse it and collect_mirror_id() to gather a file's distinct
mirror ids.
Fixes: 8c5f5d3ec9b1 ("LU-12668 lod: bind ec mirror to data mirror during creation")
Test-Parameters: testlist=sanity-ec ostcount=8
Signed-off-by: Maximilian Dilger <mdilger@whamcloud.com>
Assisted-by: ClaudeCode:Fable-5 llm_code_and_review_tools
Change-Id: I8d670f0558521063af425e2c75a56b00cdd294d7
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-2 retesting | RHEL 10.1 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-dne-zfs-part-5 failed 2× crashed | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: sanityn. %% THIS TEST SESSION CRASHED %% | session |
Going forward, all of these lines should be replaced with a label: Assisted-by: ClaudeCode:MODEL_VERSION [TOOLNAME ...] https://wiki.lustre.org/Commit_Comments#AI/LLM/Tool_Attribution
Why is this for `fortestonly`? Should this be consolidated with another patch, or that annotation be removed?
Probably it was generated initially via AI, and nobody removed that designation as the patch was being updated?
(typo) The third path is `lov_io_lseek_end()`, which is the `.cio_end` entry in `lov_io_ops[CIT_LSEEK]` and runs from `cl_io_end()`, not from unlock. `lov_io_unlock()` is a separate op. Should this read "during sub-IO end"?
[Marc Bot] (style) This attribution line was flagged on patchset 34 and is still unresolved: it should use the `Assisted-by:` label format described at https://wiki.lustre.org/Commit_Comments#AI/LLM/Tool_Attribution instead of the free-form line.
"three independent paths" does not seem to hold for the third one.
The LSEEK sub-lock enqueue happens in cl_lockset_lock(), which cl_io_lock() runs *after* every cio_lock(), so the stripe is already marked by the time lov_io_call(cl_io_start) runs and the sub-IO is skipped there. A sub-IO that never started still has:
sub_io->ci_result = 0 /* lov_io_sub_init() */
sub_io->u.ci_lseek.ls_result = -ENXIO /* inherited from the parent in lov_io_sub_inherit(); ll_lseek() seeds it */
lov_io_lseek_end() already ignores both (`ci_result == 0` is a no-op, `sub_off == -ENXIO` hits the existing continue), so there is nothing for the third hunk to catch.
Also, ci_result propagation happens in .cio_end (lov_io_lseek_end), not during unlock.
(style) This was raised on an earlier patchset and the line is unchanged: tool attribution should use the `Assisted-by:` trailer format documented at https://wiki.lustre.org/Commit_Comments#AI/LLM/Tool_Attribution rather than a free-form sentence.
This guard sits in `lov_io_call()`, which is the shared dispatcher for four different ops:
lov_io_lock() -> lov_io_call(cl_io_lock)
lov_io_start() -> lov_io_call(cl_io_start)
lov_io_iter_fini() -> lov_io_call(lov_io_iter_fini_wrapper)
lov_io_unlock() -> lov_io_call(lov_io_unlock_wrapper)
All four are registered for CIT_LSEEK, so a stripe marked LSS_READ_ERR also skips `cl_io_unlock()` and `cl_io_iter_fini()` on its sub-IO, not just `cl_io_start()`. Its `ci_state` then goes CIS_LOCKED -> CIS_IO_FINISHED (set by `lov_io_end_wrapper()` in `lov_io_lseek_end()`) -> CIS_FINI, never passing through CIS_UNLOCKED/CIS_IT_ENDED.
Nothing leaks today because `osc_io_ops[CIT_LSEEK]` registers only cio_start/cio_end/cio_fini and the LSEEK DLM lock is taken on the top IO by `vvp_io_lseek_lock()`. But the intent is only to skip the data-fetch pass -- would putting the check in `lov_io_start()` (or keying it on `iofunc == cl_io_start`) keep the cleanup passes balanced?
(style) This isn't a bug, but `str` reads like a string; the rest of this file spells it `stripe` (see `lov_io_lseek_end()` a few hundred lines down, which uses `index`/`stripe` for the same two values). Worth matching if the patch is refreshed.
[Marc Bot] (defect) Skipping the stripe treats its extents as holes, but on an EC file that data is still readable via parity reconstruction. If the only data between ls_start and the next healthy-stripe data lives on the degraded stripe, SEEK_DATA returns the later offset, or -ENXIO if none, so sparse-aware tools (cp, tar) silently drop data that read() would return. Also, if every sub-IO in lis_active is skipped (e.g. single-stripe data component), offset stays -ENXIO and SEEK_HOLE fails with -ENXIO even though ls_start < file size, which breaks the SEEK_HOLE contract of a virtual hole at EOF. Would it be safer to treat a degraded stripe's covered range as data instead of skipping it, or to return an error rather than a misleading data map? The commit message claim that the remaining healthy stripes provide valid seek results does not hold in these cases.
(defect) Skipping cl_io_start() for the degraded stripe means its allocation map never contributes to the seek result, and lseek does not reconstruct anything from parity the way CIT_EC_RD does. So the answer is not "the remaining healthy stripes are sufficient" -- it is an answer computed from an incomplete extent map.
Concrete case, 4+2 EC, one data OST deactivated, data written only in the range that maps to that stripe:
lseek(fd, 0, SEEK_DATA)
-> degraded sub skipped, others report -ENXIO
-> offset stays -ENXIO, lseek fails
and with data further out on a healthy stripe it returns that later offset instead. Sparse-aware copies (cp --sparse, tar, rsync) would silently drop the bytes that a plain read() still returns via parity.
SEEK_HOLE has the mirror problem: if every sub-IO covering ls_start is skipped, `offset` stays -ENXIO and ll_lseek() returns -ENXIO for an offset below i_size, which breaks the "there is always a virtual hole at EOF" contract.
Is returning an error preferable to returning a wrong offset here? Alternatively, could the degraded stripe's range be reported as data (conservative) rather than dropped?
+1; error should be preferable compared with a wrong offset
(typo) This comment uses a non-ASCII em dash; the rest of the tree is plain ASCII. Plain "-" or "--" instead.
This hunk looks like it has no effect. Any stripe marked LSS_READ_ERR was already skipped by the new check in lov_io_call(), so its sub-IO never reached cl_io_start(): ci_result is still 0 from lov_io_sub_init(), and ls_result is still the -ENXIO that lov_io_sub_inherit() copied from the parent. The `if (io->ci_result == 0)` assignment and the `sub_off == -ENXIO` continue below both already handle that. Is there a path where a sub-IO is marked LSS_READ_ERR but still ran? If not, dropping this hunk would keep the two skip conditions from having to stay in sync.
LU-12668 lov: handle ESHUTDOWN for LSEEK on EC files When an OST hosting a data stripe of an EC file is deactivated, SEEK_DATA/SEEK_HOLE fails with ESHUTDOWN because the error propagates through the LOV layer during sub-lock enqueue. Fix this by marking the degraded stripe LSS_READ_ERR in lov_lock_enqueue (same mechanism used by CIT_EC_RD for parity recovery), then skipping those stripes in lov_io_call and lov_io_lseek_end. This avoids ESHUTDOWN leaking through three independent paths: sub-lock enqueue, sub-IO function dispatch, and sub-IO ci_result propagation during unlock. Add lov_lsm_has_parity() helper to check if any layout entry has parity, needed because CIT_LSEEK locks the data component while parity lives in a separate entry. This fixes lfs mirror verify failing on EC files when any data OST is deactivated. Generated with Claude Code + Tools Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity-ec fstype=zfs Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Change-Id: I5cce4e0ea51c68b0c6fda1d83b694af19cad57bd
| failed enforced test | platform | detail | |
|---|---|---|---|
| custom-1001 crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
| custom-1002 crashed | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. %% THIS TEST SESSION CRASHED %% | session |
| review-dne-subtest-change failed 30× | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-zfs-subtest-change failed 29× | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
(minor) The range here starts at 49b, but 49a "test concurrent reads during EC recovery" is added by this patch too. Should it read 49a-50b? Every other test added is accounted for by one of the ranges.
(minor) Two hunks are not accounted for by any of the ranges above: the new `[[ "$SLOW" == "no" ]] && EXCEPT_SLOW="74a 75b"` line, and the `head -n1` fix in `enable_ec()` for the multi-mount `get_param` output. The `EXCEPT_SLOW` one also changes an existing test: 74a is not added by this patch and is not named in any of the range lists, so a reader can't tell from the message that it is being moved onto the SLOW list. Worth a sentence for each.
(defect) With 3 of 4 data stripes unreadable and pcount=2, -EIO is the only correct outcome; returning reconstructed-but-wrong bytes is silent corruption. Treating it as an acceptable pass means a real reconstruction bug in this path would go unnoticed.
48a, 48c and 64b in this same patch take the opposite position ("handing back silently wrong data would be a corruption bug, so assert against it"). Should 52b assert rc != 0 instead?
(minor) These eight files (2M..16M) are never removed. The same applies to the extra files in 71b/71c/71d, 73b's .pure, 73c's .zero and the directories in 75e/75f; 73e is the only new test that registers a cleanup. Added up, the new tests write on the order of 1.5G and leave most of it in place for the rest of the run, which on a small test filesystem can push later subtests into ENOSPC. A `stack_trap "rm -f ..."` next to each creation would keep the footprint bounded.
(suggestion) Both of these are parked against the feature ticket itself. 53a in particular is described as intermittently reconstructing wrong data, which is a silent-corruption symptom rather than a test problem, and 41d is an OSC retry loop that never terminates. Would separate LU tickets referenced here keep them from being lost when LU-12668 is closed? The convention elsewhere in this file (12a -> LU-19631, 5b/12b -> LU-20435) is one ticket per known failure.
(minor) A fresh `stack_trap` is pushed on every call, including repeat calls for the same index, so loop-driven tests accumulate identical cleanup entries: 75b registers 50, 74a 20, and 58a/63b/68b one per pair. Each entry re-runs `ec_apply_fault`, which is a `do_nodes` to every OSS plus a `cancel_lru_locks osc`, so teardown does that work dozens of times over. Registering the trap only when the bit was not already set would make it one entry per OST.
(minor) When no stripe list is given this walks every data stripe and returns the first parity-free one, which can be a stripe that holds no data. For a sub-raid-set file that is the vacuous-pass mode this helper is documented as preventing: 42a writes 512K into a 1M-stripe 4+2 layout, so only stripe 0 has data. If stripe 0's OST happens to double as parity, the fault is armed on stripe 1's (empty) object and the checksum comparison succeeds without reconstructing anything. `ec_data_stripe_osts()` handles the analogous case by calling `skip_env`. Would returning non-zero (so `ec_start_read_fault()` skips) be safer than falling through to a later stripe?
(minor) 44c and 44a are the same test - same layout, same `ec_start_all_reads_fail`, same EIO check, same clear-and-reread - differing only in the error strings. 43c is that body minus the reread. Could these collapse into one? While here, 43c's description says "too many OST failures (3+ OSTs)" but `ec_start_all_reads_fail` uses `fail_val=0`, which fails every OST, not three.
(minor) The comment says "an OST that is a safe data OST for all files", but only `$f2` is classified. `$f1` (2+1, 3 objects) and `$f3` (2+2, 4 objects) get whatever the allocator gave them, so on an 8-OST config the victim frequently holds none of their objects and the `$s1`/`$s3` comparisons pass without any recovery running. 71a, 71c, 71d and 75e/75f have the same shape (classify one file, assert on all of them). That is defensible for a batch test, but here the comment claims something stronger than the code does.
(defect) `safe_osts` here still comes from the `ec_classify_osts $tf` above, but the SEEK_DATA/SEEK_HOLE checks below run on `$tfs`, which was created separately and gets its own object placement from the allocator.
So the OST taken out need not hold any of `$tfs`'s stripes, and on a run where it doesn't, the two `lseek_test` assertions execute against a fully healthy file.
73c already documents and avoids exactly this ("safe_osts still describes $tf; $tf2 has its own object placement") by calling `ec_data_stripe_osts $tf2 0` first. Should 75d do the same for `$tfs`?
Related: the comment says the seek "crosses the degraded stripe", but the data lives at 5M, i.e. stripe 1 with `-c 4 -S 1M`, while `safe_osts[0]` is just the lowest-numbered data OST.
(minor) The negative-index handling, and the "Index -1 is the last stripe" note in the header comment, appear to be unreachable: all eleven callers pass 0..4. Worth dropping the branch and the doc line unless a caller is coming.
(style) The suite convention is a `#define` comment naming the fault right above the line that arms it, so a reader does not have to look up the bare hex. `ec_start_all_reads_fail()` and 41d both do this; this call site and the one in `ec_apply_fault()` do not.
#define OBD_FAIL_OST_BRW_READ_BULK 0x20f
ec_ost_fail_loc 0x20f $(( 0x10000 | mask ))
The value itself is right (obd_support.h has 0x20f), it is only the annotation that is missing.
LU-12668 tests: add EC recovery tests Add sanity-ec coverage for erasure-coding recovery. Each test writes an EC file, resyncs parity, fails one or more OSTs, and verifies the client reconstructs the data from parity (CIT_EC_RD) against the pre-failure checksum. Failure is injected with OBD_FAIL_OST_BRW_READ_BULK so the OSC import stays active and only bulk reads fail, which drives genuine parity reconstruction. The fault is set on the OSS nodes, where tgt_brw_read() evaluates it, and osc.*.resend_count is dropped to 1 for the duration so the injected -EIO reaches the LOV layer instead of being absorbed by an OSC resend. Victims are chosen by stripe rather than by OST index. ec_pick_data_ost() walks a file's data stripes in order and takes the first whose OST does not also carry parity: a file smaller than one raid set holds data on stripe 0 alone, so picking the lowest OST index instead would arm the fault on an object the read never reaches and the test would pass without exercising recovery. ec_check_fault_index() skips when a target OST index is >= 16, which cfs_fail_index() cannot express in its 16-bit fail_val bitmask. ec_mirror_victims() fails one parity-free OST in every data mirror, since a file with more than one data copy would otherwise answer the read from an intact mirror rather than reconstructing anything. Reads that check a sub-range cancel their locks first: a range re-read after a whole-file read is otherwise served from the page cache, issues no BRW RPC, and so never reaches the injected fault. Geometry and I/O patterns (40b-44c): - 2+1, 2+2 and 4+1 EC; partial, offset, mmap, direct and async reads; single, maximum and progressive OST failure; graceful failure when too many OSTs are gone Failure placement and layout (45a-48c): - consecutive, non-consecutive, boundary and parity-only OST failures; 64K, 256K and 4M stripe sizes; multiple EC and mixed EC/non-EC PFL components; stale and partially stale parity Concurrency and multi-mount (49a-50b): - concurrent readers over a file with two failed data OSTs; background writes, mirror resync and OST reactivation during recovery; both mounts reading the same EC file Core recovery (51a-53a): - EOF boundary recovery at RAID set / stripe set boundaries; degradation limits and mixed parity+data failure; recovery at non-zero read offsets Layout patterns (55a): - file-size boundaries (1 byte .. multi-stripe) Edge cases (58a-62d): - parity_used combinations and stripe rotation; sparse files with holes; recovery after truncate; sub-stripe files; append writes Multi-target (63a-66d): - OST failure cycling and multi-mount coordination; 3-4 component PFL with per-component EC geometry; multiple EC mirror pairs Write patterns (67a-71d): - writes to healthy stripes during degraded mode and overwrite cycles; varied geometries; O_DIRECT writes; partial and mid-file overwrites; batch recovery of many files Stress and admin (73a-73e, 74a, 75a-75f): - random reads, fallocate, truncate-extend, O_APPEND, and large (128M) file recovery; reads racing with OST deactivation; stat/getattr, stress loops, lfs mirror verify, directory-inherited layout, and stripe rotation Tests 41d and 53a are added but listed in always_except. A degraded mmap read never completes: the OSC alternates between "too many resent retries" and osc_brw_redo_request() forever, so -EIO never reaches the LOV layer and CIT_FAULT never switches to CIT_EC_RD. A recovery read at a non-zero offset intermittently reconstructs wrong data. Both stay off until those are fixed. Test 65c and the sparse half of 75d are skipped on ZFS. Both build a sparse file and resync it, which needs lseek to report the holes so resync knows which stripe sets to skip, and ZFS does not report them reliably for dirty data. Test 12b describes the same problem. Assisted-by: ClaudeCode:opus llm_code_and_review_tools Test-Parameters: trivial testlist=sanity-ec ostcount=8 Test-Parameters: trivial testlist=sanity-ec ostcount=8 fstype=zfs Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Signed-off-by: Max Dilger <mdilger@whamcloud.com> Change-Id: I5a06cd166487e0bff7bfdb6a39414af3f12c4326
| unique failing test | history |
|---|---|
| sanity2@zfs:test_119l | NEW unique failure for this branch in the last 30 days, and was seen 1 times across 1 other branches 1 reviews |
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 5 tests. 1 tests failed: sanity-sec. | session |
| review-dne-zfs-part-1 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-dne-zfs-part-1 | RHEL 10.1 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-dne-zfs-subtest-change | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-ldiskfs-ubuntu | RHEL 8.10 / x86_64, Ubuntu 24.04 / x86_64 | ran 6 tests. 1 tests failed: sanity-lnet. | session |
(minor) ... so it could also be set and checked via `chattr +t FILE` and `lsattr FILE` commands from e2fsprogs.
The "Changes:" list mentions adding LUSTRE_NOTAIL_FL to LUSTRE_FL_USER_MODIFIABLE, but the same hunk also adds LUSTRE_COMPR_FL to that mask and removes the duplicate LUSTRE_NOATIME_FL entry. Neither is explained. The COMPR change is an independent server-side behavior change - should it be split into its own patch? The lustre/utils/lfs.c hunk (skipping the range validation for nohybrid) isn't described either.
This changes UAPI flag definitions that the MDT interprets, so interop with an older server is worth an explicit test run. Consider adding something like:
Test-Parameters: testlist=sanity serverversion=2.16.0
New wire flag, but wirecheck.c and the two wiretest.c copies don't look updated - the neighbouring LUSTRE_*_FL values have CHECK_VALUE_X entries. Same for LU_LADVISE_NOHYBRID, which needs a CHECK_VALUE next to the other LU_LADVISE_* ones (LU_LADVISE_AHEAD seems to have been missed earlier too).
The compatibility claim points at the wrong side. Older clients aren't the problem; older servers are.
On a pre-patch MDS, LUSTRE_FL_USER_VISIBLE has no NOTAIL bit, so mdt_setattr_unpack() hits
if (rec->sa_attr_flags & ~LUSTRE_FL_USER_VISIBLE)
RETURN(-EOPNOTSUPP);
and lfs ladvise -a nohybrid fails with EOPNOTSUPP. Clearing the flag still "succeeds" as a no-op, so the two directions behave differently. Worth stating the required server version here.
This was asked on patchset 7 and looks unaddressed: adding LUSTRE_COMPR_FL here is unrelated to nohybrid and changes server behavior. mdt_setattr_unpack() masks with LUSTRE_FL_USER_MODIFIABLE, and osd_attr_set() replaces the whole masked set, so this makes the compression flag both settable and clearable on the MDT inode by any client. Is that intended ahead of the compression work landing?
(defect) why is NOATIME being removed?
Is the COMPR flag really user modifiable or just visible? And does it make sense to allow this to be set before CSDC is landed to master?
Should this use NOHYBRID?
Does this also set the flag directly on the inode, or is the inode here the root or parent directory?
parse ll_file_ioctl():error: Function too hairy. Giving up. 4 seconds warn: ll_file_ioctl():Function too hairy. No more merges.
ll_inode2ext_flags() is not a full picture of the file's flags - it rebuilds them from inode->i_flags via ll_inode_to_ext_flags(), which only knows SYNC/NOATIME/APPEND/DIRSYNC/IMMUTABLE/ENCRYPT, plus the PROJINHERIT and (new) NOHYBRID lli_flags bits.
LUSTRE_NODUMP_FL and LUSTRE_NOCOMPR_FL are in LUSTRE_FL_USER_MODIFIABLE but have no i_flags or lli_flags representation, so they come back as 0 here. osd_attr_set() then does a wholesale replace:
ei->i_flags = (ei->i_flags & ~LDISKFS_OSD_USER_MODIFIABLE) |
(attr->la_flags & LDISKFS_OSD_USER_MODIFIABLE);
so those bits get cleared on disk. chattr +d FILE followed by lfs ladvise -a nohybrid FILE should lose the 'd' flag.
The FS_IOC_SETFLAGS path avoids this by calling fileattr_get() first, which fetches body->mbo_flags from the MDT. Should this do the same before OR-ing in LUSTRE_NOHYBRID_FL?
Related: ll_set_project() builds op_attr_flags from ll_xflags_to_ext_flags() and also sets OP_XVALID_FLAGS, so lfs project on a file appears to clear the nohybrid flag for the same reason.
LU-19839 llite: add persistent nohybrid I/O flag Add a persistent flag to prevent hybrid I/O switching for specific files. This uses the NOTAIL flag (0x00008000) which is not used by Lustre/ext4 and unlikely to be used in the future. The flag is set via ladvise interface for discoverability but uses FS_IOC_SETFLAGS internally for implementation. When set, hybrid I/O will not switch the file from buffered to direct I/O regardless of I/O size. Changes: - Define LUSTRE_NOTAIL_FL and LUSTRE_NOHYBRID_FL - Add LUSTRE_NOTAIL_FL to LUSTRE_FL_USER_MODIFIABLE - Implement LU_LADVISE_NOHYBRID using FS_IOC_SETFLAGS - Update ll_update_inode_flags to handle NOTAIL flag - Add test_119l to verify nohybrid flag functionality Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Change-Id: I6a69293801114e2a3015ed87f2258828922ab767
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-ldiskfs-ubuntu retesting | RHEL 8.10 / x86_64, Ubuntu 24.04 / x86_64 | ran 6 tests. 1 tests failed: sanity-lnet. | session |
That seems like a pretty big hammer, basically breaking AIO completely for EC files, rather than just the recovery path? Or is the comment wrong and this is triggered only for EC recovery when `-EIOCBQUEUED` is returned? Would it be better to push **all** EC recovery to a workqueue instead of keeping it directly in the IO path, then it can fire the AIO completion when the reconstruction is complete, and synchronous readers would wait on the completion?
Andreas, yes, correct. This is essentially making AIO synchronous for all reads on EC files because we can't tell at submission time if we need reconstruction. So, this patch is just a stopgap to allow AIO to work for degraded reads, albeit with collateral damage. I'd agree that a workqueue is the better long-term approach, however, I'm not familiar enough with the code path at the minute here to gauge the effort required. So, I'd keep this patch as a short-term fix for AIO for degraded reads, and open a ticket so we address this properly with a workqueue. Do you agree? If yes, I open the ticket
(minor) aio_ec_sync deliberately survives the goto restart at the end of the function, and it has to. cda_no_aio_complete and cda_creator_free stay set on the cl_dio_aio across restarts, so a later pass that ends with anything other than -EIOCBQUEUED (cl_io_rw_init() failing, or cl_io_loop() returning a hard error) still needs both !is_aio || aio_ec_sync tests to be true. If it were cleared at restart, __cl_sync_io_note() would skip the free because creator_free is 1, and the creator branch would skip it too, leaking the cl_dio_aio and its cda_obj reference. This isn't a bug as written, but the coupling between a stack bool and two flags on a heap struct isn't obvious. Could the comment above say the flag is intentionally sticky, so it doesn't get reset in a later cleanup?
on refresh
LU-12669 llite: make AIO reads on EC file synchronous For sync DIO, EC recovery on read failure runs inline in ll_file_io_generic after cl_sync_io_wait_recycle. AIO has the same recovery requirement but cannot run recovery from the sub-DIO completion path -- ll_file_io_generic returns -EIOCBQUEUED to the VFS before the BRW completion fires, so the syscall context recovery needs (the lu_env, the user's iov_iter, the range lock) is gone by the time the read error is known. Rather than building a workqueue-based async recovery path, make AIO reads on EC files behave as sync DIO: after cl_io_loop, if an AIO read on an EC layout (io->ci_cross_ec, set by lov_io_mirror_init during cl_io_rw_init) got -EIOCBQUEUED, set cda_no_aio_complete and cda_creator_free to disarm the async ki_complete path, then drop into the same wait + restart code sync DIO uses. The VFS calls ki_complete itself when we return. The flag flip is race-free because the submission ref on cda_sync is still held -- end_io cannot fire until we drop it in cl_sync_io_wait_recycle. This disables async semantics for every AIO read on an EC file, not only those that need recovery -- we cannot tell at submission time whether reconstruction will be needed, so the sync conversion fires unconditionally on -EIOCBQUEUED for an EC layout. As a side effect, the range_lock taken by ll_file_io_generic is now held until all sub-DIOs have drained, instead of being dropped while they are still in flight. Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity-ec fstype=zfs Assisted-by: Opus:4.8 llm_code_and_review_tools Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com> Signed-off-by: Marc Vef <mvef@whamcloud.com> Change-Id: I394a3610e33b29ead8f5adb52dfa21db6b721944
This points to an abandoned patch and can be removed
LU-19066 ofd: add os_failure_domain to struct obd_statfs Add a new field to lfs df to show the failure domain. Update "lfs df --output=" so that when used to just print a single field we do not pad it to a fixed width with leading spaces. This makes it easier to parse the outputs in tests. Example: lfs df --output=domain Also add a ltq_failure_domain field to lu_tgt_qos and set it when grab the statfs data for an object. We do not yet use this field for anything but will need this information later once we add failure domain awareness to the allocator. Test-Parameters: trivial Signed-off-by: Ronnie Sahlberg <rsahlberg@whamcloud.com> Change-Id: Ic5bcc66b7570ad74886a550b77d91a235e72756d
(style) This isn't a bug, but the `.TH` date is still 2026-03-06 while the page is being modified; the convention is to refresh it to the date of the change.
(minor) This now advertises the `%18i` / `%13s` field-width syntax, but the `--printf` directive list further down never mentions that a directive can take a width. 6fe2fcb02c ("LU-16561: find: support width in -printf directive") added the feature without touching this page. Since `--ls` is documented in terms of it, would it make sense to document the width (and the negative width / `0` padding forms) in the `--printf` section here?
(minor) `%8u` and `%8g` pad on the left, so the owner and group end up right-justified, while `ls -l` and `find -ls` left-justify them:
68367 3072 -rw-r--r-- 1 green green 3145728 ...
Would `%-8u %-8g` match the `ls -l` layout the man page describes more closely? Names longer than 8 characters also read better left-aligned. If this changes, the equivalent string in Documentation/man1/lfs-find.1 needs the same update.
LU-15504 utils: fix the 'lfs find -ls' output format
The `lfs find -ls` output was using tabs for field alignment,
but this resulted in misaligned output when fields like the
blocks count or file size were large. Instead, use the field
size option added later in https://review.whamcloud.com/57395
("LU-16561: find: support width in -printf directive") to do
the field alignment.
Update sanity.sh test_56Eab to sort the output files so
'lfs find' and 'find' are comparing the same filenames,
in case they ever change output order (e.g. parallel find).
This will also run additional iterations of this subtest.
Test-Parameters: trivial
Fixes: 1d8164fa16 ("LU-15504 utils: lfs find -ls function")
Signed-off-by: Andreas Dilger <adilger@thelustrecollective.com>
Change-Id: Iba325742923b4b0ffbcb2c06454c51ab82500c1e
A #define only redirects the name at compile time. After this patch liblustreapi.so.1 no longer exports llapi_create_volatile_idx() or llapi_create_volatile_param() (liblustreapi.map exports llapi_*, and the definitions are gone), and -version-info in lustre/utils/Makefile.am stays at 1:0:0, so the soname does not change. Any already-built consumer that links those symbols will fail to load against the new library. This was raised on the first patchset - the suggestion there was thin wrappers so the old names stay as exported functions for compatibility. The #define form does not achieve that. Should the old names be kept as one-line exported wrappers instead?
If this is returning an open file descriptor, IMHO it would be better to have "open" in the name instead of "create". Otherwise, the caller may be calling this to "create" files but leak the returned fd.
Create seems like a better choice because it is clear that create is can only be used once per volatile file. Also creat()returns an writeable file descriptor so it follows that precedent. However, I was just following what we already had so open is fine if you want that.
(style) existing layout "open/create" functions are named llapi_layout_file_{open,create}(), so this doesn't really match the existing "layout" naming convention.
The non-layout functions are named like llapi_file_{open,create}_*(), so this new function name doesn't match that either (it would be llapi_file_open_layout_volatile()).
However, I also see the existing functions named llapi_create_volatile_param() and llapi_create_volatile_idx(), so we can't just get rid of those functions, but it is a bit of a mess.
I think it makes sense to name this function llapi_layout_file_open_volatile(). There could optionally be thin wrappers llapi_create_volatile_layout(), llapi_file_open_volatile_layout() (#define) so that they could be found by the older naming convention but only call the new API. There is no requirement for ABI compatibility for them because they are unused so far.
IMHO it makes sense to also rename the other llapi_create_volatile_*() functions to be more consistent with other functions, like llapi_file_open_volatile_param() and llapi_file_open_volatile_idx(), but add static inline wrappers for compatibility. These need to remain as exported functions from liblustreapi.so for ABI compatibility reasons for at least several years, but could be removed from the man pages to discourage new use.
The llapi_create_volatile_*() callers *could* be transparently remapped at build time to llapi_layout_file_open_volatile() via #define, with a #warn after a few major releases (~= years) that the new function names are available:
#if LUSTRE_VERSION_CODE > OBD_OCD_VERSION(2, 22, 53, 0)
#warn "llapi_create_volatile_idx() deprecated, use llapi_file_open_volatile_idx()"
However, it isn't clear if the complexity is worthwhile to remove a 1-line wrapper function...
I don't think we need the wrappers for the new function - let's reinforce the use of the new naming scheme by only creating the new version for layout.
This passes the layout the test just built itself, so the helper only re-reads values set a few lines earlier - it never looks at the file that llapi_layout_file_open_volatile() created. test1()/test2()/test3() pass a layout obtained from the file (llapi_layout_get_by_path/_by_fd/_by_fid), which is what makes the helper meaningful.
So the test currently only proves the open succeeded; the layout half of the new API is unverified. Since the volatile file is gone after close(), the read-back has to happen while the fd is open:
fd = llapi_layout_file_open_volatile(...);
layout2 = llapi_layout_get_by_fd(fd, 0);
close(fd);
__test1_helper(layout2);
Unrelated nit on the assertion a few lines up: "fd >= 0" has a double space.
All three callers are in this file, so this can be static. As a non-static definition it also lands in liblustreapi.a as a global symbol; the version script keeps it out of the .so, but it can still collide at static-link time. A leading underscore at file scope is reserved by the C standard, and the name carries no llapi/lustre prefix - create_volatile() (static) would fit the surrounding code better.
LU-18842 llapi: create volatile file with a layout Adds a new function to lustreapi to create a volatile file with a layout. This refactors existing volatile functions so more code is shared between volatile file implementations. A new test is added to llapi_layout_test.c Update the llapi_create_volatile.3 man page to describe the new function names and rename it to llapi_layout_file_open_volatile.3 and link to it from the old names. Rename the llapi_create_volatile_idx(), llapi_create_volatile_param(), and llapi_create_volatile() functions llapi_file_open_volatile_idx(), llapi_file_open_volatile_param(), and llapi_file_open_volatile() and all in-tree callers are updated to use the new names. Mark old function prototypes deprecated, but keep their functions in the library so that old applications can still dynamically link to them from the shared library. Signed-off-by: Robert Read <rread@thelustrecollective.com> Signed-off-by: Andreas Dilger <adilger@thelustrecollective.com> Change-Id: I5aed4c9bbd886b40be0e2d6fcbee64996c4bb399
| failed enforced test | platform | detail | |
|---|---|---|---|
| custom-1001 | RHEL 8.10 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| custom-1002 | RHEL 8.10 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-subtest-change failed 2× | RHEL 8.10 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-dne-zfs-subtest-change failed 7× | RHEL 8.10 / x86_64 | ran 3 tests. 1 tests failed: sanity-ec. | session |
| review-ldiskfs-dne-arm | RHEL 8.10 / x86_64, Rocky 9.5 / aarch64 | ran 5 tests. 1 tests failed: sanity. | session |
LU-12668 ec: Add tests for computing the parity coverage When we write or verify the parities we no longer use the whole stripe, instead we compute the coverage of what ranges of parity is important and what can be ignored. This is based on SEEK_DATA/SEEK_HOLE and EOF. Add tests that we compute these ranges correctly. Test-Parameters: trivial Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity-ec fstype=zfs Signed-off-by: Ronnie Sahlberg <rsahlberg@whamcloud.com> Change-Id: Iaee9d0ccb0875f6d515a8adc322fa3cd37bfe0f6
(minor) This corrects a user-visible failure (SEEK_DATA/SEEK_HOLE on a designated parity mirror returning ENXIO), so it would help to carry a Fixes: tag.
e21b93b5f7b4 introduced ci_parity_io/ci_parity_eof and converted vvp_prep_size() and ll_direct_IO_impl() to the parity EOF, but left vvp_io_lseek_start()/vvp_io_lseek_end() clamping on i_size, which is exactly what this patch changes.
Fixes: e21b93b5f7b4 ("LU-19631 llite: fix EOF handling for EC parity mirrors")
(minor) The lseek_test.c hunk also carries two cleanups the message doesn't mention: the open() failure message switching from `error %d`/errno to `strerror(errno)`, and the re-indent of the `return -1;` in the getopt `default:` case. Both are harmless, but a line in the body would keep the diff free of surprises.
(style) This isn't a bug, but the em dash here is a non-ASCII character (U+2014); a plain "-" or a reworded sentence keeps the file 7-bit like the rest of llite.
(suggestion) This glimpses on every designated-mirror SEEK_DATA/SEEK_HOLE, not just parity ones, and "harmless for data mirrors" is doing a lot of work - it is a whole-file `[0, CL_PAGE_EOF]` CEF_GLIMPSE|CEF_MUST enqueue per lseek, which osc_enqueue_base() can only short-circuit if a cached lock already covers the whole object. That lands on a hot loop: llapi_mirror_data_seek() sets the mirror and then does SEEK_DATA + SEEK_HOLE, and it is called per data segment / per raidset from llapi_mirror_resync_many_params(), llapi_ec_resync_or_verify_raidset() and llapi_ec_resync_or_verify_comp(). Resync of a sparse file now pays two extra glimpses per segment against a mirror whose size it never uses. Can this be limited to the case that needs it (parity mirror), or hoisted so a resync pass glimpses once rather than per segment?
(minor) The eof selection keys off `ci_parity_io`, but the glimpse that makes `ci_parity_eof` meaningful is keyed off `fd_designated_mirror > 0` in ll_file_seek(). Those two gates are not the same set. lov_io_mirror_init() also sets `ci_parity_io` on the non-designated mirror-selection path (the `io->ci_parity_io = lov_mirror_entry(obj, index)->lre_parity;` near the end), and the parity skip just above it only covers CIT_READ/CIT_FAULT, not CIT_LSEEK. So a plain SEEK_DATA/SEEK_HOLE that lands on a parity mirror (preferred data mirror not `lre_valid`) takes this branch with a `ci_parity_eof` derived from whatever i_size happened to be at cl_io_init() time, i.e. before the ll_merge_attr() on the line above ever ran. Previously that case used the refreshed i_size. Would gating on `io->ci_designated_mirror` here, or skipping parity mirrors for CIT_LSEEK the same way as CIT_READ, make the two ends agree? The comment above should probably be narrowed too - "after glimpse of data size" only holds for the designated-mirror path.
LU-12668 ec: allow SEEK_DATA/HOLE on parity mirror lseek Parity mirror lseek was rejected at VVP with -ENXIO because vvp_io_lseek_start() and vvp_io_lseek_end() clamped against inode i_size. Parity components do not contribute to cat_size, so i_size only reflects the data mirror and cannot serve as parity EOF. In ll_file_seek(), call ll_glimpse_size() before designated- mirror SEEK_DATA/HOLE so lov_io_mirror_init() can compute ci_parity_eof from a current data size. In vvp_io_lseek_start() and vvp_io_lseek_end(), use ci_parity_eof for parity I/O and i_size for data I/O. Keep start >= eof -> -ENXIO (same as data and iomap) and end result > eof -> -ENXIO so SEEK_HOLE may still return eof for the implicit hole at end of file. Extend lseek_test with -m for designated mirror I/O, and switch sanity-ec 12c-12f from check_parity_read to SEEK_DATA/HOLE checks that verify hole range boundaries instead of sampling 4k at fixed offsets. Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity-ec fstype=zfs Signed-off-by: Keguang Xu <kxu@ddn.com> Change-Id: I26eb4c19d26f01e38ababf303de1df5492056193
If we used https://docs.rs/strum/latest/strum/derive.EnumString.html it would implement FromStr. If you set each variant of the enum to serialize into snake_case it might take care of this for you.
RbacRole is a `bitmask_enum` which is actually a struct, and not a real enum. So strum derives don't apply, unfortunately.
Have you looked at using serde's `serialize_with` and specifying a function? https://serde.rs/field-attrs.html
That would force consumer structs holding an RbacRole to have to use #[serde(with = "…"). The custom trait seems more convenient for callers, though agree it seems like a lot for bitmask field.
Have you checked to see if this can be done with serde's `deserialize_with` and specifying a function? It might be simpler.
Same as above.
Why import everything?
Oops, fixed that. I'll change it to `use super::{RbacRole, pascal_to_snake, sys}`
whats the expected behaviour for all/none roles? I think they will error out here.
Good catch — as written, both would error out. Checked against the kernel: nodemap_rbac_seq_show() prints the literal string "none" when nmf_rbac == NODEMAP_RBAC_NONE (RBAC_ALL is printed as the expanded list of role names), and on input the kernel accepts "all"/"none" tokens via cfs_str2mask(). So "none" in particular must parse, or reading back a locked-down nodemap's rbac value fails. Fixed in the PS5: the deserializer now accepts "all" → RbacRole::all() and "none" → RbacRole::empty(), and the empty set serializes as "none" to match the kernel's output format (an empty string isn't valid input to the kernel parser anyway).
LU-20210 utils: add nodemap RBAC roles to rustreapi Bind the C `enum nodemap_rbac_roles` from `lustre_idl.h` as a rustified bindgen enum in `lustreapi-sys`, exporting it for external tools. Add a new `rustreapi::nodemap` module with an `RbacRole` bitmask type that mirrors the C enum's single-bit flags. The `From<nodemap_rbac_roles>` impl uses an exhaustive `match` over the sys enum so that a new variant added to the C side fails to compile here until a corresponding `RbacRole` variant is added. Variant names are chosen so their snake-case form (`HsmOps` => `hsm_ops`, `IgnoreRootPrjquota` => `ignore_root_prjquota`) matches the kernel's user-facing names. Serde serializes a set of roles as a comma-separated string of those names (e.g. "file_perms,hsm_ops"), matching the wire format used by lctl. Test-Parameters: trivial Signed-off-by: Robert Read <rread@thelustrecollective.com> Change-Id: I1d4f6de19773f5817d0cb81d2a6cbeb1a5f45558
A finding from Fable 5:
I believe this should be:
if let Ok(library) = pkg_config::Config::new().cargo_metadata(false).probe(lib) {
otherwise there may be there may be unintended link-time side effects: probe() defaults to emitting cargo:rustc-link-lib/cargo:rustc-link-search directives from the .pc file's Libs: line, so on machines where lustre.pc/lnet.pc are installed, every crate calling get_include_paths() silently gains link dependencies on both liblustreapi and liblnetconfig (verified: changelog-sys, which intentionally emits no link directives, ends up with -llustreapi -llnetconfig on its final link line). Since this helper is only meant to discover include paths and each -sys crate already declares its own library explicitly, cargo_metadata(false) keeps link behavior explicit and independent of the build machine.
is this needed? this will become out of date quickly, if it's not already by the time this gets landed.
LU-20208 rustreapi: support documentation publishing on docs.rs Enable building and publishing rustreapi documentation without requiring access to Lustre headers in the build environment. This is primarily aimed at supporting automatic documentation generation on docs.rs. The implementation introduces a pre-generated bindings fallback mechanism: - build.rs in -sys crates detects the DOCS_RS environment variable. - If set, they skip bindgen and use pre-generated bindings_docs.rs. - The Lustre version is captured in a snapshot file (lustre_version.txt) embedded in the lu-version crate. - This ensures feature flags (LUSTRE_2_X) stay in sync with the snapshots during isolated documentation builds. Centralized build helpers were added to lu-version to reduce code duplication across the workspace. Shared logic includes include path discovery and documentation build environment detection. A new 'update-doc-snapshots' recipe was added to the justfile to formalize the update process for maintainers. Test-Parameters: trivial Change-Id: I78329ebe911852abdfdd3720e4a419417dae8800 Signed-off-by: Michael MacDonald <mjmac@google.com>