From 17c0972801eb2f338d5d91c1b1250f74bd58b7b8 Mon Sep 17 00:00:00 2001 From: Sven Oehme Date: Fri, 16 Oct 2026 06:29:06 +0000 Subject: [PATCH] fuse: add tracepoints for i_size changes, writeback ranges and READ replies Under DLM with the writeback cache, i_size is only this node's view of the file, and whether writeback treats bytes as past EOF depends on what i_size was at that exact moment. When RED-51747 lost another node's bytes to zeroes, the local i_size at writeback had to be reconstructed from the order of DLM grants, because nothing records it. Add three tracepoints: fuse_isize_change every i_size change and why: the cached-write extension claim, its retraction, or a size taken from an attribute reply fuse_writeback_range each writeback chunk with pos, len, bytes sent, iomap's end_pos and the current i_size fuse_readfolio_reply each fuse_do_readfolio() READ with the length asked, the bytes returned and i_size With these, the RED-51747 race shows up directly as a writeback chunk whose end_pos is below i_size while its whole block is sent, preceded by a READ reply that returned bytes past the local EOF. Signed-off-by: Sven Oehme Co-Authored-By: Claude Opus 5.5 --- fs/fuse/file.c | 11 ++++- fs/fuse/fuse_i.h | 6 +++ fs/fuse/fuse_trace.h | 108 +++++++++++++++++++++++++++++++++++++++++++ fs/fuse/inode.c | 7 ++- 4 files changed, 130 insertions(+), 2 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index a9294abe7feb4c..f8f47b448f66e8 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -8,6 +8,7 @@ #include "fuse_i.h" #include "fuse_dlm_cache.h" +#include "fuse_trace.h" #include #include @@ -1069,6 +1070,7 @@ static int fuse_do_readfolio(struct file *file, struct folio *folio, fuse_read_args_fill(&ia, file, pos, desc.length, FUSE_READ); res = fuse_simple_request(fm, &ia.ap.args); + trace_fuse_readfolio_reply(inode, pos, desc.length, res); if (res < 0) { /* * Please refer to Documentation/filesystems/fuse/fuse-AOP_TRUNCATED_PAGE-reason.txt @@ -2529,6 +2531,8 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) orig_size = i_size_read(inode); if (end > orig_size) { i_size_write(inode, end); + trace_fuse_isize_change(inode, orig_size, end, + FUSE_ISIZE_WRITE_CLAIM); extended = true; } spin_unlock(&fi->lock); @@ -2553,8 +2557,11 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) if (reached < end) { spin_lock(&fi->lock); - if (i_size_read(inode) == end) + if (i_size_read(inode) == end) { i_size_write(inode, reached); + trace_fuse_isize_change(inode, end, reached, + FUSE_ISIZE_WRITE_RETRACT); + } spin_unlock(&fi->lock); } } @@ -3320,6 +3327,8 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, if (pos + len > end_pos) send_len = end_pos - pos; + trace_fuse_writeback_range(inode, pos, len, end_pos, send_len); + if (!data->ff) { data->ff = fuse_write_file_get(fi); if (!data->ff) diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 89b75b3c2eb2ba..72ecb2bfa5e4ab 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -1230,6 +1230,12 @@ static inline struct fuse_mount *get_fuse_mount(struct inode *inode) return get_fuse_mount_super(inode->i_sb); } +enum fuse_isize_reason { + FUSE_ISIZE_WRITE_CLAIM, + FUSE_ISIZE_WRITE_RETRACT, + FUSE_ISIZE_ATTR, +}; + static inline struct fuse_conn *get_fuse_conn(struct inode *inode) { return get_fuse_mount_super(inode->i_sb)->fc; diff --git a/fs/fuse/fuse_trace.h b/fs/fuse/fuse_trace.h index e81c93b9614627..5d5045354fec37 100644 --- a/fs/fuse/fuse_trace.h +++ b/fs/fuse/fuse_trace.h @@ -150,6 +150,114 @@ TRACE_EVENT(fuse_request_end, __entry->unique, __entry->len, __entry->error) ); +#define FUSE_ISIZE_REASONS \ + EM( FUSE_ISIZE_WRITE_CLAIM, "write_claim") \ + EM( FUSE_ISIZE_WRITE_RETRACT, "write_retract")\ + EMe(FUSE_ISIZE_ATTR, "attr") + +#undef EM +#undef EMe +#define EM(a, b) TRACE_DEFINE_ENUM(a); +#define EMe(a, b) TRACE_DEFINE_ENUM(a); +FUSE_ISIZE_REASONS +#undef EM +#undef EMe +#define EM(a, b) { a, b }, +#define EMe(a, b) { a, b } + +/* + * Under DLM i_size is this node's view only; recording every change with its + * reason is what lets a lost update be tied to the local EOF at writeback. + */ +TRACE_EVENT(fuse_isize_change, + TP_PROTO(struct inode *inode, loff_t old_size, loff_t new_size, + int reason), + + TP_ARGS(inode, old_size, new_size, reason), + + TP_STRUCT__entry( + __field(dev_t, connection) + __field(uint64_t, nodeid) + __field(loff_t, old_size) + __field(loff_t, new_size) + __field(int, reason) + ), + + TP_fast_assign( + __entry->connection = get_fuse_conn(inode)->dev; + __entry->nodeid = get_fuse_inode(inode)->nodeid; + __entry->old_size = old_size; + __entry->new_size = new_size; + __entry->reason = reason; + ), + + TP_printk("connection %u nodeid %llu i_size %lld -> %lld (%s)", + __entry->connection, __entry->nodeid, __entry->old_size, + __entry->new_size, + __print_symbolic(__entry->reason, FUSE_ISIZE_REASONS)) +); + +TRACE_EVENT(fuse_writeback_range, + TP_PROTO(struct inode *inode, u64 pos, unsigned int len, + u64 end_pos, unsigned int send_len), + + TP_ARGS(inode, pos, len, end_pos, send_len), + + TP_STRUCT__entry( + __field(dev_t, connection) + __field(uint64_t, nodeid) + __field(uint64_t, pos) + __field(uint32_t, len) + __field(uint32_t, send_len) + __field(uint64_t, end_pos) + __field(loff_t, i_size) + ), + + TP_fast_assign( + __entry->connection = get_fuse_conn(inode)->dev; + __entry->nodeid = get_fuse_inode(inode)->nodeid; + __entry->pos = pos; + __entry->len = len; + __entry->send_len = send_len; + __entry->end_pos = end_pos; + __entry->i_size = i_size_read(inode); + ), + + TP_printk("connection %u nodeid %llu pos %llu len %u send %u end_pos %llu i_size %lld", + __entry->connection, __entry->nodeid, __entry->pos, + __entry->len, __entry->send_len, __entry->end_pos, + __entry->i_size) +); + +TRACE_EVENT(fuse_readfolio_reply, + TP_PROTO(struct inode *inode, loff_t pos, size_t len, + ssize_t res), + + TP_ARGS(inode, pos, len, res), + + TP_STRUCT__entry( + __field(dev_t, connection) + __field(uint64_t, nodeid) + __field(loff_t, pos) + __field(size_t, len) + __field(ssize_t, res) + __field(loff_t, i_size) + ), + + TP_fast_assign( + __entry->connection = get_fuse_conn(inode)->dev; + __entry->nodeid = get_fuse_inode(inode)->nodeid; + __entry->pos = pos; + __entry->len = len; + __entry->res = res; + __entry->i_size = i_size_read(inode); + ), + + TP_printk("connection %u nodeid %llu pos %lld len %zu res %zd i_size %lld", + __entry->connection, __entry->nodeid, __entry->pos, + __entry->len, __entry->res, __entry->i_size) +); + #endif /* _TRACE_FUSE_H */ #undef TRACE_INCLUDE_PATH diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 8fa91064999194..2d01d2bf3c549d 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -10,6 +10,7 @@ #include "fuse_dlm_cache.h" #include "fuse_dev_i.h" #include "dev_uring_i.h" +#include "fuse_trace.h" #include #include @@ -660,8 +661,12 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr * attr->size coming from server can be stale. We cannot trust it. * Only update i_size if SIZE was actually returned by the server. */ - if (have_size && !(cache_mask & STATX_SIZE)) + if (have_size && !(cache_mask & STATX_SIZE)) { i_size_write(inode, attr->size); + if (oldsize != attr->size) + trace_fuse_isize_change(inode, oldsize, attr->size, + FUSE_ISIZE_ATTR); + } spin_unlock(&fi->lock); /*