Fix RHEL10 to be semantically identical to Ubuntu HWE 6.17 - #209
Open
hbirth wants to merge 15 commits into
Open
Conversation
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>
hbirth
requested review from
achhenderson,
cding-ddn,
hazhou-ddn and
yongzech
September 5, 2026 09:44
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. |
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.
No description provided.