Repository navigation
fix(core): keep the directory that failed during recursive listing - #8377
aayushbaluni wants to merge 2 commits into
Conversation
|
CI note, so the two red checks don't cost a reviewer time: 365 green, 2 red,
That workflow's own run history has it green through 2026-10-04 (including the
This one predates the PR: the same workflow fails in the same job and the same For completeness, I was initially wrong about how to check this: I compared Happy to rebase once the MoonBit pin is sorted if you'd like a fully green run |
|
Pushed c945a44, adding one test case. No change to the fix. The three cases I opened with all fail a directory while only the root's lister
8 tests pass, Why I went looking: review on an unrelated PR of mine caught a defect that my |
|
The red A 403 fetching the MoonBit toolchain from |
8b27044 to
9fdfac7
Compare
|
Rebased onto current
This is failing repo-wide, not here: the same workflow is red on
Happy to re-run or rebase again if either turns out to be real — but I did not want to leave a red PR without saying which of the two is a known repo-wide break and which is a timing flake. |
`FlatLister::next` and `ServicerFlatLister::next` take the pending directory out of `next_dir` before listing it, and two error paths returned without putting it back. The entry had not been pushed onto `active_lister` yet, so it was simply dropped. That is invisible without a retry, because the error reaches the caller. Under `RetryLayer` it turns into silent data loss: `RetryWrapper::next` hands the same lister back to `backon`, which calls `next()` on it again. The second call finds `next_dir` empty, resumes from the parent lister, and returns `Ok` with that whole directory's entries missing. A recursive list over a tree where one child directory hiccups once comes back short and successful. Restore `next_dir` before returning the error. A retry then re-lists the directory, and a caller that gives up propagates the error instead of under-reporting. Nothing has been yielded from the child lister at either of these points, so listing it again from the start cannot duplicate entries. Both listers carry the same defect and both are fixed. `ServicerFlatLister` is the reachable one, via `SimulateLayer`, and gets the regression test; `FlatLister` has no in-repo caller but is re-exported from `raw::oio` for out-of-tree services. The test drives `ServicerFlatLister` against a mock service whose `list` walks a fixed tree and fails one directory's first `next()` once, with a temporary error, then drains the lister the way `backon` does. Before the fix it returns `Ok` with `a/one.txt` missing after requesting `a/` once; after it, the error surfaces once, `a/` is requested a second time, and both files are returned.
The three cases already here fail a directory while only the root's lister is on the stack. That leaves the more interesting shape untested: when a directory two levels down fails, `/` and `a/` are both open in `active_lister`, and a lost `next_dir` is masked by the parent resuming rather than by the listing ending — it pops back to `/`, moves on to `c/`, and returns `Ok` without `a/b/deep.txt`. Verified the test holds the behaviour rather than assuming it: with the restore in the first-`next()` arm removed it fails on exactly that assertion, reporting the listing short by `deep.txt`. Prompted by review on an unrelated PR of mine, where every mutation was caught and the fix was still wrong because no fixture entered the affected path. A mutation score only covers the paths the fixtures reach.
9fdfac7 to
590a0e8
Compare
|
Rebased onto Both reds on the previous head were outside this change, and I would rather show that than assert it.
The failing cell moved between JDKs while the diff stayed byte-identical. Both failures are the same assertion: The child JVM's stdout came back empty after a 15 s wait, on a test in Happy to re-run or narrow anything if a reviewer wants a different cut of it. |
Which issue does this PR close?
Closes #8376.
Rationale for this change
FlatLister::nextandServicerFlatLister::nexttake the pending directoryout of
next_dirbefore listing it, and two error paths returned withoutputting it back:
At both points the entry has not reached
active_listeryet, so returningdrops it and nothing remembers that the directory still needs listing.
Without a retry this is harmless — the error reaches the caller. Under
RetryLayerit becomes silent data loss.RetryWrapper::next(
core/layers/retry/src/lib.rs) hands the same lister tobackon, whichcalls
next()on it again:The second call finds
next_dirempty and no new lister on the stack, so itresumes from the parent, moves on to the next sibling, and eventually returns
Ok. The temporary error never surfaces and the failed directory's entriesare simply absent. A recursive list over a tree where one child directory
hiccups once comes back short and successful, which is the worst shape for
a caller: there is nothing to retry on and nothing to log.
That matches every row of the table in #8376, including why the non-recursive
case is unaffected (it does not go through the flat lister) and why
/a/isrequested exactly once.
What changes are included in this PR?
Restore
next_dirbefore returning the error, so a retry re-lists thatdirectory and a caller that gives up propagates the error instead of
under-reporting. Neither point has yielded anything from the child lister
yet, so listing it again from the start cannot duplicate entries.
Both listers have the same defect and both are fixed:
ServicerFlatListerincore/core/src/layers/simulate.rsis the reachableone —
SimulateLayerbuilds it for recursive listing — and is what bug: recursive listing silently skips a directory after a temporary error with RetryLayer #8376exercises through WebDAV. It gets the regression tests.
FlatListerincore/core/src/raw/oio/list/flat_list.rshas no in-repocaller, but it is re-exported as
raw::oio::FlatListerfor out-of-treeservices, so the same fix is applied. I did not add a test for it: covering
it means a
Servicemock in that module, and I would rather follow yourpreference than guess — happy to add one, or to drop that half if you would
rather fix it separately.
The
ErrorKind::NotFoundandPermissionDeniedarms are untouched. Thosedeliberately
continue, skipping the directory, and that behaviour isunchanged.
Are there any user-facing changes?
Yes, in that recursive listing stops losing entries. Concretely, a recursive
listthat previously returnedOkwith a directory missing now eitherreturns the complete listing (when the retry succeeds) or returns an error
(when it does not). Code that silently depended on the short result would
now see the error it should always have seen.
No API changes.
Breaking changes
None.
Testing
Three tests in
core/core/src/layers/simulate.rs, driving the realServicerFlatListeragainst a mock service whoselistwalks a fixed treeand can fail one directory once with a temporary error. No network, no
fixture server. The test drains the lister the way
backondoes — on atemporary error, call
next()again on the same lister.The three new cases are the two ways the directory can be lost plus the
no-retry path:
..._retries_a_directory_whose_first_entry_faileda/is requested a second time, both files are returned..._retries_a_directory_whose_list_failedlistitself rather than the firstnext()..._reports_a_directory_that_keeps_failingnext_dircannot mask a real failureBecause "the tests pass" says little on its own, I deleted each restored line
in turn and re-ran, to check the tests actually hold the behaviour:
list(...)error arm..._whose_list_failednext()error arm..._whose_first_entry_failedEach mutation is the original code for that path, so this also confirms both
tests fail against
mainrather than only passing afterwards.One fixture bug of mine is worth mentioning because it shaped a test: my
first attempt built the tree with
""as the root key and saw no entries andno error at all.
oio::Entry::withnormalizes an empty path to"/", solistwas being called with"/". Both retry assertions now print the pathsthat actually reached
list, so the next mismatch of that kind says whathappened instead of
0 != 1.Also run, on
rust-toolchain.toml's stable (1.99.0):cargo clippy -p opendal-core --all-targetswith no warnings, andcargo fmt -p opendal-core --checkclean.AI Usage Statement
claude-opus-5[1m].RetryWrapper::nextto confirm themechanism the issue hypothesised, wrote the fix and the three tests, and ran
the test and mutation runs in a container. The reproduction in bug: recursive listing silently skips a directory after a temporary error with RetryLayer #8376 is the
reporter's, not mine.
Assumptions and unknowns a reviewer should weigh:
confirmed by reading
RetryWrapper::nextand by the mock-level tests, whichreproduce the reported shape —
Okwith entries missing and one request forthe failed directory — but an independent run against the reporter's
harness would be stronger evidence than anything I have.
FlatListeris fixed by inspection and has no test, as described above.been yielded from its lister at either error point. That is true of these
two paths as written; it is the one invariant worth a second pair of eyes.