Skip to content

Fix RHEL10 to be semantically identical to Ubuntu HWE 6.17 - #209

Open
hbirth wants to merge 15 commits into
DDNStorage:redfs-rhel10_0from
hbirth:redfs-rhel10_0
Open

Fix RHEL10 to be semantically identical to Ubuntu HWE 6.17#209
hbirth wants to merge 15 commits into
DDNStorage:redfs-rhel10_0from
hbirth:redfs-rhel10_0

Conversation

@hbirth

@hbirth hbirth commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

The interval tree recorded a grant as a node, and every path that
touched the page cache had to keep the tree in step with it: a revoked
range was kept so writeback would take the grant again, and each place
that dropped folios then had to free the ranges the drop had emptied.
That is a second description of the page cache, maintained by hand at
every drop site, and wrong wherever a site was missed.

Record the grants in a bitmap instead, a bit per page, in fixed shards
by offset.  Adjacent grants coalesce by setting neighbouring bits, so
nothing is allocated or rearranged to record one, and a revoke clears
bits rather than splitting nodes.  The record then says what the server
has given this client and nothing about the page cache: a revoked grant
is forgotten, not kept, because writeback holds the range again for
every run it sends, and an absent record and a revoked one both make it
ask.

Every fuse_dlm_ranges_dropped() site goes with it, and so does
fuse_dlm_write_grant_exists(): with grants forgotten on revoke there is
no cheap answer to "is anything held for write", and the dirty and
writeback tags already say what the attribute cache needs.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
write_cache_pages() takes the dirty flag off a folio before it calls
->writepage, and fuse_launder_folio() does the same for the submit it
makes itself, so a folio that comes back unsent has thrown its bytes
away unless they are put back.  Every path here already puts them back,
but none of them asks whether there is still a connection to send them
on: after an abort every send fails, and a folio redirtied for a retry
that can no longer happen keeps sync() going forever.

Put the redirty in one helper and make it ask.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_attr_cache_mask() decides whether the server's size wins from an
i_size it reads on entry, and then sleeps in the grant query before
fuse_change_attributes_i() applies the answer under fi->lock.  A GETATTR
that left while i_size still matched the server's is therefore applied
over every extension made since, and i_size drops by exactly the writes
in flight.  truncate_pagecache() then zeroes the tail of the page holding
the new size and drops what is above, which writeback sends as zeros.

Move attr_version where fuse_write_end() commits the extension so those
replies are dropped, the same reason fuse_write_update_attr() moves it,
and count the extension until the folio under it is dirty, for a reply
that leaves after it.  FUSE_I_SIZE_UNSTABLE cannot serve as the count: it
is a single bit and every writer clears it.

[hbi: adapted -- the shared-lock write here commits i_size in
 fuse_write_end() behind the write cursor rather than claiming the whole
 extension up front, so the count is held across that commit instead of
 across the write loop.]

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_writepages_fill() runs with the folio locked and, once
folio_start_writeback() has run for an earlier folio of the same pass,
with those under writeback too.  Asking the server for a grant there is
the ordering Documentation/filesystems/fuse/fuse-AOP_TRUNCATED_PAGE-
reason.txt exists to forbid: no cluster lock may be taken while a page
lock is held.  The read path has AOP_TRUNCATED_PAGE to unlock and retry
with, and ->writepage has nothing of the sort, so the violation sits
where the remedy does not reach.  Until now only a writeback driven by a
revoke handler skipped, and only because that one deadlocks outright.

Skip any folio whose grant has gone: put it back on the dirty list and
remember the range.  fuse_writepages() takes it back once the pass has
let go of every folio, and the pass that follows sends it.  A data
integrity writeback has no later pass, so it goes round again while
anything is left deferred; fsync() and close() would otherwise report
bytes written that are still only in the page cache.

Not from fuse_launder_folio(), which arrives with the folio locked by
folio_unmap_invalidate(), nor from a revoke handler, which would ask for
the range it is revoking.  There the skip simply stands.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_dlm_request_begin() publishes the range asked for, and
fuse_dlm_kill_pending() can only test that one, but the commit records
what the server granted, which may reach FUSE_DLM_MAX_EXTRA_GRANT either
side of it.  A revoke processed while the request was on the wire and
landing in that excess marks nothing: the request does not overlap it, and
the shards hold no bit for it yet because the grant is not recorded.  The
excess is then recorded over the revoked range and no later NOTIFY takes
it back, which is the case the pending list exists to catch.

Publish the widest bounds the commit could record as well, and separate
the two outcomes.  A revoke over the range asked for still kills the
grant.  One over the excess alone leaves the range asked for recorded and
drops the excess, which only costs a re-request.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_writepages_fill() unions every folio it had to skip, and
fuse_writepages() asks for the lot in one grant.  A pass sweeping a large
file skips folios gigabytes apart, so the union grows to the whole sweep
and the request covers a range nothing wanted.

Stop extending at one shard.  The folios left out stay dirty and a later
pass asks for them.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A writer confirms its grant and then copies and dirties, and a revoke
landing in between is answered while those bytes are in no page cache
and on no wire.  The flush the revoke runs cannot find them, so they go
out later under a fresh grant, after the server has handed the lock to
another node.

Add a pin: the range a writer is about to dirty, on a list a revoke
walks before it removes anything.  A revoke publishes the range it takes
away on a second list and waits for the pins overlapping it, and a pin
overlapping a published range is refused.  Refusal and wait test the
same overlap, so the wait converges, and ranges that do not overlap
never meet: a notify leaves the rest of the file writable and a write
outside the notified range does not hold it up.

Both nodes are caller storage, so nothing is allocated to take a pin and
the writeback path can take one with a folio held.

Confirm the grant under the pin, never before, in the three places bytes
become the server's: the cached interior of a chunk, the writethrough
edges around their FUSE_WRITE, and the writeback folio until it is under
writeback.  A grant that has gone is asked for again with the pin
dropped, since that request is answered by the server the revoke came
from.

With the writers fenced, the flush a revoke runs needs a single pass:
nothing can turn up dirty behind it.

[hbi: adapted -- ->write_begin and ->write_end have nowhere to keep a
 pin node that spans the pair, so the cached interior is pinned once per
 chunk rather than once per folio, and fuse_dlm_buffered_write() cuts
 that interior into shard sized chunks so a large write bounds how long
 it holds a revoke up.]

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A folio made uptodate from the server is served to every later reader,
so the grant it was fetched under has to be held from the confirmation
until the bytes are in the page cache.  Without that a revoke sweeping
the range leaves the fill behind it: the folio stays cached, uncovered,
and the server sends no further notify for a lock this client no longer
holds.

Confirm the grant under a pin, as the write path does.  Refused, or not
covered, unlock the folio and back off with AOP_TRUNCATED_PAGE: neither
the wait nor the grant request may be taken with a page lock held.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A readahead reply lands in the page cache from the task that processes
it, so the pin over the folios has to span the request: taken before it
is sent and dropped once the folios are filled and unlocked.  A revoke
of the range waits for that and drops the folios after; one already
draining refuses the pin and the window goes back unfilled.

The node therefore outlives the pinning task, which fuse_dlm_unpin()
cannot express.  Add a span-owned pin, dropped by node and carrying no
owner for the by-owner search to match.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A read grant may not be requested under a page lock, so every buffered
read path asks for one before it enters the page cache: buffered read,
splice read and read fault.  The window covers what readahead may add
beyond the read, bounded by the file.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
->readahead runs with every page of the window locked, so the grant
request it sent went out under those locks and a revoke of the window
had to be given up on.  The same goes for the one-request lookahead
past the window, which runs from inside ->readahead with the window
still in flight.  The read now takes the grant before the page cache is
entered; fill only what it covers.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
POSIX_FADV_WILLNEED and readahead(2) fill through ->readahead, which now
fills only what a grant already covers.  Take the grant for the advised
range first, with no page held.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A streamed write goes to the server out of the caller's pages, so its
bytes are in no page cache and a revoke of the range finds nothing to
flush.  Hold the grant across the FUSE_WRITE, as the writethrough edges
do.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
An O_DIRECT read or write neither fills nor dirties the page cache, so a
grant over its range covers nothing and only conflicts with the rest of
the cluster.  Skip it on both sides.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_readahead() rounds the window mm built up to a whole request and
fuse_readahead_lookahead() puts one more request past that, so the pages
this client fills reach further than the ra_pages the read asked a grant
for.  Now that neither of them requests a grant of its own, those pages
are the ones fuse_send_readpages() declines: the lookahead allocates a
window, hands it straight back unfilled, and the chain it exists to keep
alive dies at the first hop.

Size the grant from the same unit the window is built in, so what
readahead may fill is what the read covers.  A wider read grant conflicts
only with a remote writer; other readers share it.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>

@yongzech yongzech left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hbirth

hbirth commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

@yongzech thanks for taking a look ... I have just found a data corruption problem in 6.12 ... so there will be another update. It looks like we write pages that are not dirtied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants