dht: redirect SEEK to the migration destination instead of leaking ENXIO - #4772
Draft
ThalesBarretto wants to merge 2 commits into
Draft
dht: redirect SEEK to the migration destination instead of leaking ENXIO#4772ThalesBarretto wants to merge 2 commits into
ThalesBarretto wants to merge 2 commits into
Conversation
During rebalance, a SEEK_DATA/SEEK_HOLE on an fd still open on the source subvolume can return ENXIO to the application even though the file's data is intact on the destination. dht_seek_cbk has no iatt (unlike the data-path callbacks, which detect migration proactively via IS_DHT_MIGRATION_PHASE2), so its only migration signal is the errno. Near the end of dht_migrate_file the source is truncated to 0 before it is unlinked, while its linkto xattr is already set. A SEEK that reaches the 0-byte source returns ENXIO (the normal POSIX "no data at/after offset"), which is neither ENOENT nor ESTALE, so the generic handler in dht_seek_cbk takes goto out and never reaches dht_rebalance_complete_check. fstat() and read() on the same fd redirect correctly in this window, so SEEK is the only FOP that leaks the source's ENXIO. Exempt ENXIO from the generic error exit so the callback falls through to the redirect: dht_rebalance_complete_check reads the source linkto xattr, re-resolves to the destination, and dht_seek2 re-dispatches the SEEK there. For a non-migrating file the check reads ENODATA and unwinds the original ENXIO unchanged, so legitimate end-of-data ENXIO is preserved. Cost: ENXIO is also the normal end-of-data signal for SEEK_DATA/SEEK_HOLE, so this adds one syncop_getxattr(linkto) on the source per terminal ENXIO even for non-migrating files (the same check already done for ENOENT/ESTALE). A cheaper in-memory gate is not available: SEEK has no iatt, and during the in-progress window the inode migration info is not yet set. The cost is accepted because the alternative is a silent wrong result for sparse-file scanners. The explicit ENXIO/EOVERFLOW check that followed the generic handler was dead code (unreachable since it was added, because the generic handler catches those errnos first); remove it. EOVERFLOW is deliberately not exempted: it is a representation error (the seek result does not fit off_t), subvolume-independent, so redirecting it would just return the same error. Fixes: gluster#4771 Signed-off-by: Thales Antunes de Oliveira Barretto <thales.barretto.git@gmail.com>
Holds an fd open across an external remove-brick migration and asserts that a held-fd SEEK_DATA/SEEK_HOLE is redirected to the destination (returns the data offset) instead of leaking the truncated source's spurious ENXIO. fstat() on the same fd is a control: it redirects on both the patched and unpatched build, so it passes either way and keeps the failure isolated to SEEK. RED without the dht_seek_cbk fix (SEEK_DATA/SEEK_HOLE return ENXIO), GREEN with it. Updates: gluster#4771 Signed-off-by: Thales Antunes de Oliveira Barretto <thales.barretto.git@gmail.com>
ThalesBarretto
force-pushed
the
dht-h28-seek-enxio-bypass
branch
from
September 10, 2026 22:43
0b65451 to
d031a29
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes: #4771
Problem
During rebalance, a
SEEK_DATA/SEEK_HOLEon an fd still open on the source subvolumecan return
ENXIOto the application even though the file's data is intact on thedestination.
Every DHT FOP callback redirects an in-flight operation to the migration destination
when it detects the file is migrating. The data-path callbacks receive an
iattanddetect it proactively via
IS_DHT_MIGRATION_PHASE2(stbuf).dht_seek_cbkreceives noiatt, so its only migration signal is the errno — and it only redirects ondht_inode_missing()(ENOENT/ESTALE):Near the end of
dht_migrate_filethe source is truncated to 0(
dht-rebalance.c:2109) before it is unlinked (:2154), while the source'slinktoxattr is already set (
:1224). ASEEK_DATAthat reaches the 0-byte source returnsENXIO(the normal POSIX "no data at/after offset"). Since that is neither ENOENT norESTALE,
dht_seek_cbktakes the genericgoto outand returns it to the client;dht_rebalance_complete_check(the redirect) is never reached.The explicit
ENXIO/EOVERFLOWcheck below the generic handler is dead code — it hasbeen unreachable since it was added alongside the generic handler in the original
SEEK-at-DHT implementation (#3792).
Fix
Exempt
ENXIOfrom the generic handler so the callback falls through to the redirect,and drop the now-redundant dead check:
A window
SEEK_DATAon the source now falls through todht_rebalance_complete_check,which reads the source
linktoxattr, re-resolves to the destination, anddht_seek2re-dispatches the SEEK there. For a non-migrating file the check reads
ENODATA(nolinkto) and unwinds the original errno unchanged, so legitimate end-of-data ENXIO ispreserved.
Only
ENXIOis exempted (the dead check namedEOVERFLOWtoo, but that is notmigration-related).
ENXIOfromSEEK_DATA/SEEK_HOLEmeans "offset ≥ EOF", so it isdata-dependent — the truncated 0-byte source raises it while the full destination does
not, which is exactly why redirecting helps.
EOVERFLOWmeans "the seek result does notfit
off_t" (fs/read_write.cksys_lseek), which is subvolume-independent — thedestination would return the same
EOVERFLOW— so redirecting it is pointless; and with a64-bit
off_tit cannot arise fromSEEK_DATA/SEEK_HOLEat all.Cost / trade-off
ENXIOis also the normal end-of-data signal forSEEK_DATA/SEEK_HOLE, so this adds onesyncop_getxattr(linkto)on the source per terminalENXIO, even for non-migrating files —the same check already done for
ENOENT/ESTALE, but hit far more often (sparse scannersseek to EOF once per file). A cheaper in-memory gate is not available: SEEK has no
iatt,and during the in-progress window the inode's migration info is not yet set, so
dht_inode_ctx_get_mig_infowould not catch it. The cost is accepted because thealternative is a silent wrong result; reviewers who prefer to bound it may want a volume-level
"rebalance active" guard, which is out of scope for this fix.
Impact
Low severity (narrow trigger) but a silent wrong result, not a benign retry. The GlusterFS
SEEK FOP only carries
GF_SEEK_DATA/GF_SEEK_HOLE(gf_seek_what_t), so the exposedworkloads are sparse-extent scanners (
cp --sparse,tar -S,qemu-img), which treatSEEK_DATA=ENXIOas end-of-data and stop — silently producing a short/all-holes copy.On the same fd during the same window
fstat/readreturn correct data (they redirect),so SEEK is the only FOP that leaks the source's ENXIO.
Testing
A regression test is included:
tests/bugs/distribute/seek-during-rebalance.t. It holdsan fd open on a file whose data is on brick 0, migrates it to brick 1 with an external
remove-brick(so the held fd is not re-resolved), and asserts the held-fdSEEK_DATAreturns the data offset (
0) rather than the source's spuriousENXIO.fstat()on thesame fd is a control — it redirects on both builds, keeping the failure isolated to SEEK.
not ok … 'Got "ENXIO" instead of "0"'(SEEK_DATA and SEEK_HOLE),fstatok.0, SEEK_HOLE<size>,fstatok); deterministic acrossrepeated runs.
(The same behavior was also confirmed manually on a 2-node volume, and — to make the
timing deterministic — by freezing the rebalance daemon in the truncate→unlink window
under gdb; the committed
.treproduces it without that, using the fact that the held fdkeeps hitting the emptied source until something re-resolves it.)
Change summary
xlators/cluster/dht/src/dht-inode-read.c—dht_seek_cbk(1 insertion, 4 deletions)tests/bugs/distribute/seek-during-rebalance.{t,py}— regression test (new)