* [PATCH 0/6] File data read/write version 2
@ 2026-05-07 13:21 Valerie Aurora
2026-05-07 13:21 ` [PATCH 1/6] rpdfs: add rpdfs_file_llseek Valerie Aurora
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
This patch set implements file data read/write with most of the
previous code review comments resolved. The main exception is that
->writepages() is not yet implemented.
Valerie Aurora (6):
rpdfs: add rpdfs_file_llseek
rpdfs: add inode change debugging routine
rpdfs: add basic file data initialization
rpdfs: add file data allocation and lookup routines
rpdfs: add rpdfs_write_iter/rpdfs_read_iter
rpdfs: add read_folio, dirty_folio, write_begin, write_end
fs/rpdfs/Makefile | 1 +
fs/rpdfs/data.c | 598 ++++++++++++++++++++++++++++++++++++++++
fs/rpdfs/data.h | 11 +
fs/rpdfs/file.c | 77 ++++++
fs/rpdfs/format-block.h | 32 +--
fs/rpdfs/format-msg.h | 4 +
fs/rpdfs/inode.c | 51 +++-
fs/rpdfs/inode.h | 1 +
8 files changed, 753 insertions(+), 22 deletions(-)
create mode 100644 fs/rpdfs/data.c
create mode 100644 fs/rpdfs/data.h
--
2.49.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 1/6] rpdfs: add rpdfs_file_llseek
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
@ 2026-05-07 13:21 ` Valerie Aurora
2026-05-07 13:21 ` [PATCH 2/6] rpdfs: add inode change debugging routine Valerie Aurora
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
Use generic_file_llseek to implement file seek.
Signed-off-by: Valerie Aurora <val@versity.com>
---
fs/rpdfs/file.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/fs/rpdfs/file.c b/fs/rpdfs/file.c
index 22532d8cdf63..e8918e38b3fe 100644
--- a/fs/rpdfs/file.c
+++ b/fs/rpdfs/file.c
@@ -5,10 +5,28 @@
#include "file.h"
#include "inode.h"
+static loff_t rpdfs_file_llseek(struct file *file, loff_t offset, int whence)
+{
+ struct inode *inode = file->f_inode;
+ struct rpdfs_fs_info *rfi = RPDFS_INODE_FS(inode);
+ struct rpdfs_block_handle *hnd = NULL;
+ int ret;
+
+ ret = rpdfs_inode_acquire(rfi, NULL, inode, &hnd, 0);
+ if (ret < 0)
+ goto out;
+
+ ret = generic_file_llseek(file, offset, whence);
+out:
+ rpdfs_block_release(rfi, &hnd);
+ return ret;
+}
+
const struct inode_operations rpdfs_file_iops = {
.getattr = rpdfs_getattr,
.setattr = rpdfs_setattr,
};
const struct file_operations rpdfs_file_fops = {
+ .llseek = rpdfs_file_llseek,
};
--
2.49.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 2/6] rpdfs: add inode change debugging routine
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
2026-05-07 13:21 ` [PATCH 1/6] rpdfs: add rpdfs_file_llseek Valerie Aurora
@ 2026-05-07 13:21 ` Valerie Aurora
2026-05-07 13:21 ` [PATCH 3/6] rpdfs: add basic file data initialization Valerie Aurora
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
print_inode_change() can be added to copy_vfs_inode_to_rinode() to
log what has changed since the last inode update.
Signed-off-by: Valerie Aurora <val@versity.com>
---
fs/rpdfs/inode.c | 42 +++++++++++++++++++++++++++++++++++++-----
1 file changed, 37 insertions(+), 5 deletions(-)
diff --git a/fs/rpdfs/inode.c b/fs/rpdfs/inode.c
index 428c9081fd75..a2baa862f6a4 100644
--- a/fs/rpdfs/inode.c
+++ b/fs/rpdfs/inode.c
@@ -87,6 +87,43 @@ static __le64 cpu_ts64_to_le64_ns(struct timespec64 ts)
return cpu_to_le64(timespec64_to_ns(&ts));
}
+static u64 ts64_to_ns(struct timespec64 ts)
+{
+ return timespec64_to_ns(&ts);
+}
+
+__always_unused
+static inline void print_diff64(char *name, u64 new, u64 old)
+{
+ if (old != new)
+ rpdfs_prd("%s: %llu -> %llu (%lld)", name, old, new, new - old);
+}
+
+__always_unused
+static void print_inode_change(struct inode *inode, struct rpdfs_inode *rinode)
+{
+ struct rpdfs_inode_info *ri = RPDFS_I(inode);
+
+ rpdfs_prd("copying inode %lu to rinode", inode->i_ino);
+
+ print_diff64("ino", le64_to_cpu(ri->ig.ino), le64_to_cpu(rinode->ig.ino));
+ print_diff64("gen", le64_to_cpu(ri->ig.gen), le64_to_cpu(rinode->ig.gen));
+ print_diff64("i_size", i_size_read(inode), le64_to_cpu(rinode->size));
+ print_diff64("nlink", inode->i_nlink, le32_to_cpu(rinode->nlink));
+ print_diff64("uid", i_uid_read(inode), le32_to_cpu(rinode->uid));
+ print_diff64("gid", i_gid_read(inode), le32_to_cpu(rinode->gid));
+ print_diff64("mode", inode->i_mode, le32_to_cpu(rinode->mode));
+
+ print_diff64("atime", ts64_to_ns(inode_get_atime(inode)), le64_to_cpu(rinode->atime_nsec));
+ print_diff64("mtime", ts64_to_ns(inode_get_mtime(inode)), le64_to_cpu(rinode->mtime_nsec));
+ print_diff64("ctime", ts64_to_ns(inode_get_ctime(inode)), le64_to_cpu(rinode->ctime_nsec));
+ print_diff64("crtime", le64_to_cpu(ri->crtime_nsec), le64_to_cpu(rinode->crtime_nsec));
+
+ print_diff64("dirents", le64_to_cpu(ri->dirents.ref.bnr), le64_to_cpu(rinode->dirents.ref.bnr));
+ print_diff64("xattrs", le64_to_cpu(ri->xattrs.ref.bnr), le64_to_cpu(rinode->xattrs.ref.bnr));
+ print_diff64("xattr_creates", le64_to_cpu(ri->xattr_creates), le64_to_cpu(rinode->xattr_creates));
+}
+
static void copy_vfs_inode_to_rinode(struct rpdfs_inode *rinode, struct inode *inode)
{
struct rpdfs_inode_info *ri = RPDFS_I(inode);
@@ -132,11 +169,6 @@ void rpdfs_inode_init_ops(struct inode *inode)
}
}
-static u64 ts64_to_ns(struct timespec64 ts)
-{
- return timespec64_to_ns(&ts);
-}
-
/*
* Update the vfs inode if the caller's persistent version of the rinode
* is more recent. A lot of tasks can be checking a shared inode to see
--
2.49.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 3/6] rpdfs: add basic file data initialization
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
2026-05-07 13:21 ` [PATCH 1/6] rpdfs: add rpdfs_file_llseek Valerie Aurora
2026-05-07 13:21 ` [PATCH 2/6] rpdfs: add inode change debugging routine Valerie Aurora
@ 2026-05-07 13:21 ` Valerie Aurora
2026-05-07 13:21 ` [PATCH 4/6] rpdfs: add file data allocation and lookup routines Valerie Aurora
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
Initialize the file data root, plus a few simple routines to calculate
mapping block levels. Rename the field in the inode from "data" to
"data_root" to avoid confusion with the "data" member of struct
rpdfs_block_handle.
Signed-off-by: Valerie Aurora <val@versity.com>
---
fs/rpdfs/Makefile | 1 +
fs/rpdfs/data.c | 93 +++++++++++++++++++++++++++++++++++++++++
fs/rpdfs/data.h | 11 +++++
fs/rpdfs/format-block.h | 32 +++++++-------
fs/rpdfs/inode.c | 8 ++++
fs/rpdfs/inode.h | 1 +
6 files changed, 129 insertions(+), 17 deletions(-)
create mode 100644 fs/rpdfs/data.c
create mode 100644 fs/rpdfs/data.h
diff --git a/fs/rpdfs/Makefile b/fs/rpdfs/Makefile
index d995103ea5a8..f8301d86682b 100644
--- a/fs/rpdfs/Makefile
+++ b/fs/rpdfs/Makefile
@@ -12,6 +12,7 @@ rpdfs-y := balloc.o \
block.o \
btree.o \
btree_txn.o \
+ data.o \
dir.o \
file.o \
ht.o \
diff --git a/fs/rpdfs/data.c b/fs/rpdfs/data.c
new file mode 100644
index 000000000000..65b753886a04
--- /dev/null
+++ b/fs/rpdfs/data.c
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#include <linux/fs.h>
+#include <linux/pagemap.h>
+#include <linux/gfp.h>
+#include <linux/writeback.h>
+#include <linux/build_bug.h>
+
+#include "balloc.h"
+#include "inode.h"
+#include "format-block.h"
+#include "pr.h"
+#include "super.h"
+#include "data.h"
+
+/*
+ * File data is stored in simple tree of mapping blocks with data all
+ * at the same level of the tree. The topmost block in the tree and its
+ * level are stored in the inode. The tree is sparse and branches are
+ * grown as necessary to index newly written data blocks. While
+ * manipulating the tree, we use levels to identify the blocks at
+ * various levels of the tree, with the highest levels closer to the
+ * root.
+ *
+ * The data root field in the inode contains both the persistent
+ * reference (block number, etc.) to the root block of the tree, plus
+ * the height of the tree (the level of the block it points to, plus 1).
+ * The root block reference is not part of a mapping block.
+ *
+ * The level of a block is:
+ *
+ * 0 = data block
+ * 1 = references to data blocks
+ * 2 = references to single mapping blocks (pointing to data blocks)
+ * 3 = references to double mapping blocks
+ * 4 = references to triple mapping blocks
+ *
+ * Thus a data root reference with height 1 points to a block of level 0
+ * = a single block of data at logical file offset 0.
+ */
+
+/*
+ * Return the logical block number containing offset within a file.
+ */
+static u64 lblk_from_offset(u64 offset)
+{
+ return offset >> RPDFS_BLOCK_SHIFT;
+}
+
+/*
+ * Calculate the index of the block reference for this logical block
+ * within a mapping block at this level.
+ */
+static u32 calc_ref_ind(u64 lblk, u8 level)
+{
+ u8 ind;
+
+ BUG_ON(level == 0);
+
+ BUILD_BUG_ON_NOT_POWER_OF_2(RPDFS_DATA_REFS_PER_BLK);
+
+ ind = (lblk >> (level - 1) * RPDFS_DATA_REFS_PER_BLK_SHIFT) & RPDFS_DATA_REFS_PER_BLK_MASK;
+
+ rpdfs_prd("lblk %llu level %u ind %u refs_per_blk %llu", lblk, level, ind, RPDFS_DATA_REFS_PER_BLK);
+
+ return ind;
+}
+
+/*
+ * Calculate the height of the tree needed to index the logical block
+ * lblk in this file. This is stored in the inode's data tree root.
+ */
+static u8 height_from_lblk(u64 lblk)
+{
+ u8 height;
+
+ if (lblk == 0)
+ return 1;
+
+ height = ((fls(lblk) - 1)/RPDFS_DATA_REFS_PER_BLK_SHIFT) + 2;
+
+ rpdfs_prd("lblk %llu fls(lblk) - 1 %u refs_per_blk_shift %u + 2 = height %u",
+ lblk, fls(lblk) - 1, RPDFS_DATA_REFS_PER_BLK_SHIFT, height);
+
+ return height;
+}
+
+void rpdfs_data_root_init(struct rpdfs_data_root *data)
+{
+ data->height = 0;
+ data->ref.bnr = 0;
+ data->ref.alloc_counter = 0;
+}
diff --git a/fs/rpdfs/data.h b/fs/rpdfs/data.h
new file mode 100644
index 000000000000..bb6b0c7dea86
--- /dev/null
+++ b/fs/rpdfs/data.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef RPDFS_DATA_H
+#define RPDFS_DATA_H
+
+#include "format-block.h"
+
+void rpdfs_data_root_init(struct rpdfs_data_root *data);
+
+extern const struct address_space_operations rpdfs_aops;
+
+#endif
diff --git a/fs/rpdfs/format-block.h b/fs/rpdfs/format-block.h
index 395e6d2d17e7..c2879e677084 100644
--- a/fs/rpdfs/format-block.h
+++ b/fs/rpdfs/format-block.h
@@ -4,9 +4,10 @@
#include <linux/types.h>
#include <linux/align.h>
+#include <linux/build_bug.h>
#define RPDFS_BLOCK_SHIFT 12
-#define RPDFS_BLOCK_SIZE (1 << RPDFS_BLOCK_SHIFT)
+#define RPDFS_BLOCK_SIZE (1ULL << RPDFS_BLOCK_SHIFT)
#define RPDFS_BLOCK_MASK (RPDFS_BLOCK_SIZE - 1ULL)
struct rpdfs_block_ref {
@@ -113,9 +114,11 @@ struct rpdfs_ino_gen {
};
/*
- * Data blocks are pointed to by a simple tree of indirect blocks rooted
- * in a single field in the inode. The height is one greater than the
- * level of the referenced block. It's 0 for an empty tree.
+ * Data blocks are pointed to by a simple tree of mapping blocks rooted
+ * in a single field in the inode. The height of the tree is the number
+ * of blocks in the mapping chain, including the data block itself. It's
+ * 0 for a file with no data, 1 for a file with 1 data block at logical
+ * block 0, 2 for for a file with 2 data blocks at 0 and 1, etc.
*/
struct rpdfs_data_root {
struct rpdfs_block_ref ref;
@@ -124,21 +127,16 @@ struct rpdfs_data_root {
};
/*
- * Indirect blocks are a simple array of block refs. We rely on the
- * number of references per indirect block being a power of 2, so check
- * that at compile time.
+ * Mapping blocks are a simple array of block refs. Because blocks per
+ * ref is based on the size of a struct, we can't do the smart thing and
+ * define the shift first and then the value, we have to go backwards
+ * and define the shift from the value instead.
*/
-#define RPDFS_DATA_REFS_PER_BLK (RPDFS_BLOCK_SIZE / sizeof(struct rpdfs_block_ref))
-
-/*
- * Because blocks per ref is based on the size of a struct, we can't do
- * the smart thing and define the shift first and then the value, we
- * have to go backwards and define the shift from the value instead.
- */
-
+#define RPDFS_DATA_REFS_PER_BLK (RPDFS_BLOCK_SIZE / sizeof(struct rpdfs_block_ref))
#define RPDFS_DATA_REFS_PER_BLK_SHIFT const_ilog2(RPDFS_DATA_REFS_PER_BLK)
+#define RPDFS_DATA_REFS_PER_BLK_MASK (RPDFS_DATA_REFS_PER_BLK - 1ULL)
-struct rpdfs_indirect_block {
+struct rpdfs_map_block {
struct rpdfs_block_ref refs[RPDFS_DATA_REFS_PER_BLK];
};
@@ -168,7 +166,7 @@ struct rpdfs_inode {
__le64 crtime_nsec;
struct rpdfs_btree_root dirents;
struct rpdfs_btree_root xattrs;
- struct rpdfs_data_root data;
+ struct rpdfs_data_root data_root;
};
#define RPDFS_ROOT_INO 1
diff --git a/fs/rpdfs/inode.c b/fs/rpdfs/inode.c
index a2baa862f6a4..fd0913e0e4b6 100644
--- a/fs/rpdfs/inode.c
+++ b/fs/rpdfs/inode.c
@@ -9,6 +9,7 @@
#include "btree.h"
#include "compare.h"
+#include "data.h"
#include "dir.h"
#include "file.h"
#include "inode.h"
@@ -80,6 +81,8 @@ static void copy_rinode_to_vfs_inode(struct inode *inode, struct rpdfs_inode *ri
ri->xattrs = rinode->xattrs;
ri->xattr_creates = rinode->xattr_creates;
+
+ ri->data_root = rinode->data_root;
}
static __le64 cpu_ts64_to_le64_ns(struct timespec64 ts)
@@ -122,6 +125,8 @@ static void print_inode_change(struct inode *inode, struct rpdfs_inode *rinode)
print_diff64("dirents", le64_to_cpu(ri->dirents.ref.bnr), le64_to_cpu(rinode->dirents.ref.bnr));
print_diff64("xattrs", le64_to_cpu(ri->xattrs.ref.bnr), le64_to_cpu(rinode->xattrs.ref.bnr));
print_diff64("xattr_creates", le64_to_cpu(ri->xattr_creates), le64_to_cpu(rinode->xattr_creates));
+ print_diff64("data_root.height", ri->data_root.height, rinode->data_root.height);
+ print_diff64("data_root.ref.bnr", le64_to_cpu(ri->data_root.ref.bnr), le64_to_cpu(rinode->data_root.ref.bnr));
}
static void copy_vfs_inode_to_rinode(struct rpdfs_inode *rinode, struct inode *inode)
@@ -144,6 +149,8 @@ static void copy_vfs_inode_to_rinode(struct rpdfs_inode *rinode, struct inode *i
rinode->xattrs = ri->xattrs;
rinode->xattr_creates = ri->xattr_creates;
+
+ rinode->data_root = ri->data_root;
}
/*
@@ -336,6 +343,7 @@ struct inode *rpdfs_new_inode(struct super_block *sb, struct rpdfs_ino_gen *ig)
rpdfs_btree_root_init(&ri->dirents);
rpdfs_btree_root_init(&ri->xattrs);
+ rpdfs_data_root_init(&ri->data_root);
ts = inode_set_ctime_current(inode);
inode_set_mtime_to_ts(inode, ts);
diff --git a/fs/rpdfs/inode.h b/fs/rpdfs/inode.h
index 3b0be2d83e61..3892fdcc8b1b 100644
--- a/fs/rpdfs/inode.h
+++ b/fs/rpdfs/inode.h
@@ -25,6 +25,7 @@ struct rpdfs_inode_info {
struct rpdfs_ino_gen ig;
struct rpdfs_btree_root dirents;
struct rpdfs_btree_root xattrs;
+ struct rpdfs_data_root data_root;
struct inode vfs_inode;
};
--
2.49.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 4/6] rpdfs: add file data allocation and lookup routines
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
` (2 preceding siblings ...)
2026-05-07 13:21 ` [PATCH 3/6] rpdfs: add basic file data initialization Valerie Aurora
@ 2026-05-07 13:21 ` Valerie Aurora
2026-05-07 13:21 ` [PATCH 5/6] rpdfs: add rpdfs_write_iter/rpdfs_read_iter Valerie Aurora
2026-05-07 13:21 ` [PATCH 6/6] rpdfs: add read_folio, dirty_folio, write_begin, write_end Valerie Aurora
5 siblings, 0 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
Add routines to lookup and allocate file data and mapping blocks.
Signed-off-by: Valerie Aurora <val@versity.com>
---
fs/rpdfs/data.c | 260 ++++++++++++++++++++++++++++++++++++++++++
fs/rpdfs/format-msg.h | 4 +
2 files changed, 264 insertions(+)
diff --git a/fs/rpdfs/data.c b/fs/rpdfs/data.c
index 65b753886a04..b1ccf77ced7a 100644
--- a/fs/rpdfs/data.c
+++ b/fs/rpdfs/data.c
@@ -91,3 +91,263 @@ void rpdfs_data_root_init(struct rpdfs_data_root *data)
data->ref.bnr = 0;
data->ref.alloc_counter = 0;
}
+
+static int alloc_block_ref(struct rpdfs_fs_info *rfi, struct rpdfs_transaction *txn,
+ u64 lblk, struct rpdfs_block_ref *ref, struct rpdfs_block_handle **hnd)
+{
+ int ret;
+
+ ret = rpdfs_txn_acquire_alloc(rfi, txn, hnd);
+ if (ret < 0)
+ goto out;
+
+ /* XXX let caller write/clear data blocks */
+ memset((*hnd)->data, 0, RPDFS_BLOCK_SIZE);
+
+ ref->bnr = cpu_to_le64((*hnd)->bnr);
+ ref->alloc_counter = cpu_to_le64((*hnd)->alloc_ctr);
+out:
+ rpdfs_prd("ret %d bnr %llu lblk %llu", ret, ref->bnr, lblk);
+ return ret;
+}
+
+/*
+ * The place of a mapping or data block should increase as we descend
+ * towards the data block, and remain constant as the mapping tree grows
+ * in height. A data block has the maximum depth value.
+ */
+static inline void set_data_place(struct rpdfs_block_handle *hnd, u64 ino, u8 level, u64 lblk)
+{
+ rpdfs_block_set_place(hnd, RPDFS_PLACE_DATA, ino, RPDFS_PLACE_DEPTH_MASK - level, lblk);
+}
+
+static int get_or_alloc_block(struct rpdfs_fs_info *rfi, struct rpdfs_transaction *txn,
+ struct inode *inode, struct rpdfs_block_handle *inode_hnd,
+ struct rpdfs_block_handle *parent_hnd, u64 bnr, u64 lblk,
+ u8 level, rbaf_t data_rbaf, struct rpdfs_block_handle **hnd_ret,
+ struct rpdfs_block_ref *refs)
+{
+ rbaf_t rbaf;
+ int write;
+ int ret;
+
+ if (level != 0)
+ rbaf = data_rbaf & RBAF_WRITE ? RBAF_WRITE | RBAF_OVERWRITE : 0;
+
+ write = data_rbaf & RBAF_WRITE ? 1 : 0;
+
+ rpdfs_prd("bnr %llu lblk %llu level %u rbaf %x write %d",
+ bnr, lblk, level, rbaf, write);
+
+ if (bnr == 0 && write) {
+ /* XXX move this out? */
+ if (parent_hnd != inode_hnd) {
+ /* reacquire with write permissions */
+ bnr = parent_hnd->bnr;
+ rpdfs_block_release(rfi, &parent_hnd);
+ ret = rpdfs_block_acquire(rfi, txn, bnr, &parent_hnd, RBAF_WRITE);
+ if (ret < 0)
+ goto out;
+ }
+ ret = alloc_block_ref(rfi, txn, lblk, refs, hnd_ret);
+ if (ret < 0)
+ goto out;
+ set_data_place(*hnd_ret, rpdfs_inode_ino(inode), level, lblk);
+ } else {
+ ret = rpdfs_block_acquire(rfi, txn, bnr, hnd_ret, rbaf);
+ if (ret < 0)
+ goto out;
+ }
+out:
+ return ret;
+}
+
+/*
+ * Grow the height of the existing mapping block tree to that necessary
+ * to index the logical block lblk.
+ */
+static int grow_height(struct rpdfs_fs_info *rfi, struct rpdfs_transaction *txn,
+ struct inode *inode, u64 lblk)
+{
+ struct rpdfs_inode_info *ri = RPDFS_I(inode);
+ struct rpdfs_block_handle *hnd = NULL;
+ struct rpdfs_block_ref ref;
+ struct rpdfs_map_block *iblk;
+ u8 min_height;
+ int ret = 0;
+
+ min_height = height_from_lblk(lblk);
+
+ if ((ri->data_root.height == 0) ||
+ (min_height == 1) ||
+ (min_height <= ri->data_root.height))
+ goto out;
+
+ rpdfs_prd("current height %u goal height %u", ri->data_root.height, min_height);
+
+ /* caller allocates the data block, start at first map block */
+ if (ri->data_root.height == 0)
+ ri->data_root.height++;
+
+ while (ri->data_root.height < min_height) {
+ rpdfs_prd("allocing map block at level %d for lblk %llu", ri->data_root.height, lblk);
+
+ ret = alloc_block_ref(rfi, txn, lblk, &ref, &hnd);
+ if (ret < 0)
+ goto out;
+
+ set_data_place(hnd, rpdfs_inode_ino(inode), ri->data_root.height, lblk);
+ iblk = hnd->data;
+ /* growing height will always index old data to 0 */
+ iblk->refs[0] = ri->data_root.ref;
+ /* insert new map block into inode */
+ ri->data_root.ref = ref;
+ ri->data_root.height++;
+ rpdfs_block_release(rfi, &hnd);
+ }
+out:
+ /* XXX unwind all changes on error */
+ return ret;
+}
+
+/*
+ * Return an array of block references in mapping blocks, beginning with
+ * the one containing the requested offset. It may be the root block
+ * reference in the inode. If it is a write, allocate the mapping blocks
+ * for that file data offset if necessary.
+ *
+ * This function is only called after checking that a read is from a
+ * valid range of the file. If it is a read for an offset with no
+ * mapping block allocated to point to it, return a null refs pointer
+ * and number of references equivalent to a block. If it is a read for
+ * an offset with a mapping block allocated but no data block, return a
+ * valid refs pointer for the range including the unallocated data block.
+ *
+ * The actual data block allocation occurs in the caller. This is so we
+ * don't have to traverse the mapping blocks for every data block
+ * access.
+ */
+static int get_or_alloc_refs(struct rpdfs_fs_info *rfi, struct rpdfs_transaction *txn,
+ struct inode *inode, struct rpdfs_block_handle *inode_hnd,
+ u64 lblk, rbaf_t rbaf, struct rpdfs_block_handle **hnd_ret,
+ struct rpdfs_block_ref **refs_ret, int *nr_ret)
+{
+ struct rpdfs_inode_info *ri = RPDFS_I(inode);
+ struct rpdfs_block_handle *parent_hnd;
+ struct rpdfs_block_handle *blk_hnd = NULL;
+ struct rpdfs_block_ref *refs;
+ struct rpdfs_map_block *iblk;
+ u64 bnr;
+ int write;
+ int nr;
+ u8 level;
+ u8 ind;
+ int ret;
+
+ rpdfs_prd("ino %llu ri->data_root.height %u ri->data_root.ref.bnr %llu lblk %llu rbaf %x",
+ rpdfs_inode_ino(inode), ri->data_root.height, ri->data_root.ref.bnr, lblk, rbaf);
+
+ write = rbaf & RBAF_WRITE ? 1 : 0;
+
+ /* grow the height of existing data, if any */
+ ret = grow_height(rfi, txn, inode, lblk);
+ if (ret < 0)
+ goto out;
+
+ /* start with the root of the mapping tree in the inode */
+ parent_hnd = inode_hnd;
+ refs = &ri->data_root.ref;
+ bnr = le64_to_cpu(ri->data_root.ref.bnr);
+ ind = 0;
+ nr = 1;
+ ret = 0;
+
+ level = ri->data_root.height;
+
+ /* lookup/allocate all map blocks but not the data block itself */
+ while (level-- > 1) {
+ rpdfs_prd("level %d bnr %llu", level, bnr);
+
+ if ((bnr == 0) && !write) {
+ *hnd_ret = NULL;
+ *refs_ret = NULL;
+ *nr_ret = RPDFS_DATA_REFS_PER_BLK;
+ goto out;
+ }
+ ret = get_or_alloc_block(rfi, txn, inode, inode_hnd, parent_hnd, bnr, lblk, level, rbaf, &blk_hnd, refs);
+ if (ret < 0)
+ goto out;
+
+ iblk = blk_hnd->data;
+
+ /* look up next block reference */
+ ind = calc_ref_ind(lblk, level);
+ bnr = le64_to_cpu(iblk->refs[ind].bnr);
+
+ if (parent_hnd != inode_hnd)
+ rpdfs_block_release(rfi, &parent_hnd);
+
+ parent_hnd = blk_hnd;
+ refs = &iblk->refs[ind];
+ nr = RPDFS_DATA_REFS_PER_BLK - ind;
+ blk_hnd = NULL;
+ };
+
+ *hnd_ret = parent_hnd;
+ *refs_ret = refs;
+ *nr_ret = nr;
+out:
+ rpdfs_prd("ri->data_root.height %u bnr %llu ind %d refs %p nr %d",
+ ri->data_root.height, *refs_ret ? (*refs_ret)[0].bnr : 0, ind, refs_ret, *nr_ret);
+ return ret;
+}
+
+static int get_or_alloc_data_block(struct rpdfs_fs_info *rfi, struct rpdfs_transaction *txn,
+ struct inode *inode, struct rpdfs_block_handle *inode_hnd,
+ u64 lblk, rbaf_t data_rbaf, struct rpdfs_block_handle **hnd_ret)
+{
+ struct rpdfs_block_handle *parent_hnd = NULL;
+ struct rpdfs_block_handle *blk_hnd = NULL;
+ struct rpdfs_block_ref *refs = NULL;
+ rbaf_t map_rbaf;
+ int nr;
+ u64 bnr;
+ int ret;
+
+ /*
+ * Set the mode for the map block acquisition. Non-blocking
+ * reads do not need to be non-blocking on map blocks since they
+ * are not in the page cache and cannot have lock inversion
+ * problems. For writes, map blocks will not be completely
+ * overwritten.
+ */
+ if (data_rbaf & RBAF_WRITE)
+ map_rbaf = RBAF_WRITE;
+ else
+ map_rbaf = 0;
+
+ rpdfs_prd("ino %llu lblk %llu data rbaf %x map rbaf %x", rpdfs_inode_ino(inode), lblk, data_rbaf, map_rbaf);
+
+ ret = get_or_alloc_refs(rfi, txn, inode, inode_hnd, lblk, map_rbaf, &parent_hnd, &refs, &nr);
+ if (ret < 0)
+ goto out;
+
+ /* read of range with unallocated map blocks */
+ if (refs == NULL)
+ goto out;
+
+ bnr = le64_to_cpu(refs[0].bnr);
+
+ ret = get_or_alloc_block(rfi, txn, inode, inode_hnd, parent_hnd, bnr, lblk, 0, data_rbaf, &blk_hnd, refs);
+ if (ret < 0)
+ goto out;
+
+ *hnd_ret = blk_hnd;
+out:
+ if (parent_hnd != inode_hnd)
+ rpdfs_block_release(rfi, &parent_hnd);
+
+ rpdfs_prd("ret %d ino %llu lblk %llu bnr %llu *hnd_ret %p",
+ ret, rpdfs_inode_ino(inode), lblk, refs ? le64_to_cpu(refs[0].bnr) : 0, *hnd_ret);
+ return ret;
+}
diff --git a/fs/rpdfs/format-msg.h b/fs/rpdfs/format-msg.h
index 3bee431e96dc..05dee4443a11 100644
--- a/fs/rpdfs/format-msg.h
+++ b/fs/rpdfs/format-msg.h
@@ -88,9 +88,13 @@ struct rpdfs_msg_block_read {
#define RPDFS_PLACE_INO_MASK ((1ULL << RPDFS_PLACE_INO_BITS) - 1)
#define RPDFS_PLACE_TYPE_MASK ((1ULL << RPDFS_PLACE_TYPE_BITS) - 1)
+#define RPDFS_PLACE_DEPTH_MAX RPDFS_PLACE_DEPTH_MASK
+
#define RPDFS_PLACE_INODE 4
#define RPDFS_PLACE_XATTR_BTREE 8
#define RPDFS_PLACE_DIRENT_BTREE 12
+#define RPDFS_PLACE_DATA 16 /* includes mapping blocks */
+
/* free is always last so that it's flushed after other blocks in its txn */
#define RPDFS_PLACE_FREE RPDFS_PLACE_TYPE_MASK
--
2.49.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 5/6] rpdfs: add rpdfs_write_iter/rpdfs_read_iter
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
` (3 preceding siblings ...)
2026-05-07 13:21 ` [PATCH 4/6] rpdfs: add file data allocation and lookup routines Valerie Aurora
@ 2026-05-07 13:21 ` Valerie Aurora
2026-05-07 13:21 ` [PATCH 6/6] rpdfs: add read_folio, dirty_folio, write_begin, write_end Valerie Aurora
5 siblings, 0 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
Implement write_iter/read_iter with generic functions.
Signed-off-by: Valerie Aurora <val@versity.com>
---
fs/rpdfs/file.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/fs/rpdfs/file.c b/fs/rpdfs/file.c
index e8918e38b3fe..060de1070963 100644
--- a/fs/rpdfs/file.c
+++ b/fs/rpdfs/file.c
@@ -1,9 +1,12 @@
/* SPDX-License-Identifier: GPL-2.0 */
+#include <linux/kernel.h>
#include <linux/fs.h>
+#include <linux/slab.h>
#include "file.h"
#include "inode.h"
+#include "pr.h"
static loff_t rpdfs_file_llseek(struct file *file, loff_t offset, int whence)
{
@@ -22,6 +25,60 @@ static loff_t rpdfs_file_llseek(struct file *file, loff_t offset, int whence)
return ret;
}
+static ssize_t rpdfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
+{
+ struct file *file = iocb->ki_filp;
+ struct inode *inode = file->f_mapping->host;
+ struct rpdfs_fs_info *rfi = RPDFS_INODE_FS(inode);
+ struct rpdfs_block_handle *hnd = NULL;
+ struct rpdfs_transaction txn = RPDFS_INIT_TXN;
+ ssize_t ret;
+
+ rpdfs_prd("ino %llu count %lu pos %lu",
+ rpdfs_inode_ino(inode), iov_iter_count(from), (unsigned long) iocb->ki_pos);
+
+ inode_lock(inode);
+
+ ret = rpdfs_inode_acquire(rfi, &txn, inode, &hnd, RBAF_WRITE);
+ if (ret < 0)
+ goto out;
+
+ ret = generic_write_checks(iocb, from);
+ if (ret <= 0)
+ goto out;
+
+ ret = __generic_file_write_iter(iocb, from);
+out:
+ rpdfs_block_release(rfi, &hnd);
+ inode_unlock(inode);
+
+ if (ret > 0)
+ ret = generic_write_sync(iocb, ret);
+
+ return ret;
+}
+
+static ssize_t rpdfs_file_read_iter(struct kiocb *iocb, struct iov_iter *from)
+{
+ struct file *file = iocb->ki_filp;
+ struct inode *inode = file_inode(file);
+ struct rpdfs_fs_info *rfi = RPDFS_INODE_FS(inode);
+ struct rpdfs_block_handle *hnd = NULL;
+ int ret;
+
+ ret = rpdfs_inode_acquire(rfi, NULL, inode, &hnd, 0);
+ if (ret < 0)
+ goto out;
+
+ rpdfs_prd("ino %llu count %lu pos %lu",
+ rpdfs_inode_ino(inode), iov_iter_count(from), (unsigned long) iocb->ki_pos);
+
+ ret = generic_file_read_iter(iocb, from);
+out:
+ rpdfs_block_release(rfi, &hnd);
+ return ret;
+}
+
const struct inode_operations rpdfs_file_iops = {
.getattr = rpdfs_getattr,
.setattr = rpdfs_setattr,
@@ -29,4 +86,6 @@ const struct inode_operations rpdfs_file_iops = {
const struct file_operations rpdfs_file_fops = {
.llseek = rpdfs_file_llseek,
+ .write_iter = rpdfs_file_write_iter,
+ .read_iter = rpdfs_file_read_iter,
};
--
2.49.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 6/6] rpdfs: add read_folio, dirty_folio, write_begin, write_end
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
` (4 preceding siblings ...)
2026-05-07 13:21 ` [PATCH 5/6] rpdfs: add rpdfs_write_iter/rpdfs_read_iter Valerie Aurora
@ 2026-05-07 13:21 ` Valerie Aurora
5 siblings, 0 replies; 7+ messages in thread
From: Valerie Aurora @ 2026-05-07 13:21 UTC (permalink / raw)
To: rpdfs-devel
This commit completes basic file data read/write support, including
avoidance of deadlock in read_folio. It does not include
->writepages() or readahead().
Signed-off-by: Valerie Aurora <val@versity.com>
---
fs/rpdfs/data.c | 245 +++++++++++++++++++++++++++++++++++++++++++++++
fs/rpdfs/inode.c | 1 +
2 files changed, 246 insertions(+)
diff --git a/fs/rpdfs/data.c b/fs/rpdfs/data.c
index b1ccf77ced7a..b3647e1ff42f 100644
--- a/fs/rpdfs/data.c
+++ b/fs/rpdfs/data.c
@@ -351,3 +351,248 @@ static int get_or_alloc_data_block(struct rpdfs_fs_info *rfi, struct rpdfs_trans
ret, rpdfs_inode_ino(inode), lblk, refs ? le64_to_cpu(refs[0].bnr) : 0, *hnd_ret);
return ret;
}
+
+/*
+ * We have to avoid a potential deadlock between the kernel's lock on
+ * each folio and our cache consistency algorithm. The order of
+ * acquisition on a read or write or similar operation is:
+ *
+ * 1. kernel grabs folio lock, calls file system routine
+ * 2. local node attempts to get access to a block
+ *
+ * But invalidation of a page has this order:
+ *
+ * 1. remote node requests exclusive access to a block
+ * 2. local node receives cache invalidate message and tries to get folio lock
+ *
+ * If the local node is trying to get access to a block while another
+ * node tries to get exclusive access, we could end up with:
+ *
+ * 1. local node holds folio lock, can't get access to block
+ * 2. local node attempts to service invalidate request but can't get folio lock
+ *
+ * The solution is to acquire all blocks in non-blocking mode. If that
+ * fails, drop the folio lock and acquire the block in blocking mode,
+ * then release it and return AOP_TRUNCATED_PAGE. This return code means
+ * "the page was truncated away beneath me, please retry." The page
+ * cache will restart the read_folio operation, which will likely
+ * succeed (as long as no other node has requested the block since then).
+ *
+ * Note that readahead() should satisfy most read requests
+ * asynchronously, leaving us to do any remaining synchronous requests
+ * in read_folio().
+ */
+static int rpdfs_read_folio(struct file *file, struct folio *folio)
+{
+ struct inode *inode = folio->mapping->host;
+ struct rpdfs_fs_info *rfi = RPDFS_INODE_FS(inode);
+ loff_t pos = folio_pos(folio);
+ size_t len = folio_size(folio);
+ struct rpdfs_block_handle *inode_hnd = NULL;
+ struct rpdfs_block_handle *blk_hnd = NULL;
+ rbaf_t rbaf = RBAF_NONBLOCK_MODE;
+ u64 lblk;
+ int ret;
+
+ lblk = lblk_from_offset(pos);
+
+ rpdfs_prd("ino %llu lblk %llu pos %lld len %lu",
+ rpdfs_inode_ino(inode), lblk, pos, len);
+
+ /* we turn off atime always, inode will not be written */
+ ret = rpdfs_inode_acquire(rfi, NULL, inode, &inode_hnd, RBAF_NONBLOCK_MODE);
+ if (ret == -EAGAIN) {
+ folio_unlock(folio);
+
+ rpdfs_prd("could not acquire ino %llu non-blocking, ret %d, retrying",
+ rpdfs_inode_ino(inode), ret);
+
+ ret = rpdfs_inode_acquire(rfi, NULL, inode, &inode_hnd, 0);
+ if (ret == 0) {
+ rpdfs_block_release(rfi, &inode_hnd);
+ ret = AOP_TRUNCATED_PAGE;
+ }
+ goto out;
+ }
+ if (ret < 0)
+ goto out_unlock;
+
+ ret = get_or_alloc_data_block(rfi, NULL, inode, inode_hnd, lblk, rbaf, &blk_hnd);
+ if (ret == -EAGAIN) {
+ folio_unlock(folio);
+
+ rpdfs_prd("could not acquire ino %llu lblk %llu non-blocking, ret %d, retrying",
+ rpdfs_inode_ino(inode), lblk, ret);
+
+ rbaf &= ~RBAF_NONBLOCK_MODE;
+ ret = get_or_alloc_data_block(rfi, NULL, inode, inode_hnd, lblk, rbaf, &blk_hnd);
+ if (ret == 0) {
+ rpdfs_block_release(rfi, &blk_hnd);
+ ret = AOP_TRUNCATED_PAGE;
+ }
+ goto out;
+ }
+ if (ret < 0)
+ goto out_unlock;
+
+ /* copy the data into the actual block */
+ if (blk_hnd)
+ memcpy(folio_address(folio), blk_hnd->data, len);
+ else
+ memset(folio_address(folio), 0, len);
+
+ rpdfs_prd("copied %4s len %lu to %4s", (char *) blk_hnd->data, len,
+ (char *) folio_address(folio));
+
+ folio_mark_uptodate(folio);
+out_unlock:
+ folio_unlock(folio);
+out:
+ rpdfs_block_release(rfi, &blk_hnd);
+ rpdfs_block_release(rfi, &inode_hnd);
+
+ rpdfs_prd("ret %d", ret);
+
+ return ret;
+}
+
+static bool rpdfs_dirty_folio(struct address_space *mapping, struct folio *folio)
+{
+ rpdfs_prd("ino %lu index %lu", mapping->host->i_ino, folio->index);
+
+ return filemap_dirty_folio(folio_mapping(folio), folio);
+}
+
+
+/*
+ * Info to be passed from write_begin to write_end to complete the write
+ * within a transaction.
+ */
+struct rpdfs_write_cb {
+ struct rpdfs_block_handle *inode_hnd;
+ struct rpdfs_block_handle *blk_hnd;
+ struct rpdfs_transaction txn;
+};
+
+/*
+ * Do whatever preparation is necessary to allocate space for a
+ * write. In the future it might check quotas, file system error state,
+ * etc.
+ *
+ * Called with the inode block already acquired read/write. Returns a
+ * locked folio on success.
+ */
+static int rpdfs_write_begin(struct file *file, struct address_space *mapping,
+ loff_t pos, unsigned len,
+ struct folio **foliop, void **fsdata)
+{
+ struct inode *inode = mapping->host;
+ struct rpdfs_fs_info *rfi = RPDFS_INODE_FS(inode);
+ struct folio *folio = NULL;
+ struct rpdfs_write_cb *cb;
+ rbaf_t rbaf;
+ u64 lblk;
+ int ret;
+ unsigned offset;
+
+ lblk = lblk_from_offset(pos);
+
+ rpdfs_prd("ino %llu lblk %llu pos %lld len %u", rpdfs_inode_ino(inode), lblk, pos, len);
+
+ /* allocate txn and pass to write_end for updating inode i_size/times */
+ cb = kzalloc(sizeof(struct rpdfs_write_cb), GFP_NOFS);
+ if (!cb) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ folio = __filemap_get_folio(mapping, pos >> PAGE_SHIFT, FGP_WRITEBEGIN,
+ mapping_gfp_mask(mapping));
+ if (IS_ERR(folio)) {
+ ret = PTR_ERR(folio);
+ goto out;
+ }
+
+ offset = offset_in_folio(folio, pos);
+ /* XXX use pos/len/offset to figure out when it is an overwrite */
+ rbaf = RBAF_WRITE;
+
+ ret = get_or_alloc_data_block(rfi, &cb->txn, inode, cb->inode_hnd, lblk, rbaf, &cb->blk_hnd);
+ if (ret < 0)
+ goto out;
+
+ *foliop = folio;
+ *fsdata = cb;
+ ret = len;
+out:
+ if (ret < 0) {
+ if (cb) {
+ rpdfs_txn_finish(rfi, &cb->txn);
+ kfree(cb);
+ }
+ if (!IS_ERR_OR_NULL(folio)) {
+ folio_unlock(folio);
+ folio_put(folio);
+ }
+ *foliop = NULL;
+ }
+ return ret;
+}
+
+static int rpdfs_write_end(struct file *file, struct address_space *mapping,
+ loff_t pos, unsigned len, unsigned copied,
+ struct folio *folio, void *fsdata)
+{
+ struct inode *inode = mapping->host;
+ struct rpdfs_fs_info *rfi = RPDFS_INODE_FS(inode);
+ struct rpdfs_write_cb *cb = fsdata;
+ loff_t old_size = inode->i_size;
+ bool i_size_changed = false;
+ unsigned offset;
+
+ offset = offset_in_folio(folio, pos);
+ /*
+ * Copy the data from the folio into the blcok.
+ *
+ * TODO: replace with writepages.
+ */
+ memcpy(cb->blk_hnd->data + offset, folio_address(folio) + offset, len);
+
+ rpdfs_prd("copied %4s len %d to %4s", (char *) folio_address(folio) + offset, len,
+ (char *) cb->blk_hnd->data + offset);
+
+ if (pos + copied > inode->i_size) {
+ i_size_write(inode, pos + copied);
+ i_size_changed = true;
+ }
+
+ folio_mark_dirty(folio);
+ folio_unlock(folio);
+ folio_put(folio);
+
+ if (old_size < pos)
+ pagecache_isize_extended(inode, old_size, pos);
+
+ /* mark inode dirty outside of folio lock for performance reasons */
+ if (i_size_changed)
+ mark_inode_dirty(inode);
+
+ /* finalize changes to the inode and block */
+ rpdfs_block_release(rfi, &cb->blk_hnd);
+ rpdfs_inode_update(rfi, inode, cb->inode_hnd);
+ rpdfs_block_release(rfi, &cb->inode_hnd);
+
+ rpdfs_txn_finish(rfi, &cb->txn);
+ kfree(cb);
+
+ rpdfs_prd("i_size %lld copied %d", i_size_read(inode), copied);
+
+ return copied;
+}
+
+const struct address_space_operations rpdfs_aops = {
+ .read_folio = rpdfs_read_folio,
+ .dirty_folio = rpdfs_dirty_folio,
+ .write_begin = rpdfs_write_begin,
+ .write_end = rpdfs_write_end,
+};
diff --git a/fs/rpdfs/inode.c b/fs/rpdfs/inode.c
index fd0913e0e4b6..ec7f1e64ba18 100644
--- a/fs/rpdfs/inode.c
+++ b/fs/rpdfs/inode.c
@@ -163,6 +163,7 @@ void rpdfs_inode_init_ops(struct inode *inode)
case S_IFREG:
inode->i_op = &rpdfs_file_iops;
inode->i_fop = &rpdfs_file_fops;
+ inode->i_mapping->a_ops = &rpdfs_aops;
break;
case S_IFDIR:
inode->i_op = &rpdfs_dir_iops;
--
2.49.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-05-07 13:22 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-07 13:21 [PATCH 0/6] File data read/write version 2 Valerie Aurora
2026-05-07 13:21 ` [PATCH 1/6] rpdfs: add rpdfs_file_llseek Valerie Aurora
2026-05-07 13:21 ` [PATCH 2/6] rpdfs: add inode change debugging routine Valerie Aurora
2026-05-07 13:21 ` [PATCH 3/6] rpdfs: add basic file data initialization Valerie Aurora
2026-05-07 13:21 ` [PATCH 4/6] rpdfs: add file data allocation and lookup routines Valerie Aurora
2026-05-07 13:21 ` [PATCH 5/6] rpdfs: add rpdfs_write_iter/rpdfs_read_iter Valerie Aurora
2026-05-07 13:21 ` [PATCH 6/6] rpdfs: add read_folio, dirty_folio, write_begin, write_end Valerie Aurora
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.