How Files Are Read and Written Through the Inode-Keyed Cache with Buffered Writes in Cloudflare Computer
The inode-keyed cache stores pending writes in memory keyed by file inode, allowing reads to return uncommitted data from buffer.buf while deferring persistent storage updates until the buffer is released.
In the Cloudflare Computer project, the virtual filesystem (VFS) uses an SQLite-backed storage layer where every file and directory is identified by a unique inode. When applications open files for writing, the system creates an inode-keyed cache entry that stages bytes in memory before flushing to disk. This buffer architecture ensures that reads immediately reflect recent writes without requiring expensive synchronous database operations.
How the Inode-Keyed Cache Structures Buffered Data
The buffer implementation centers on a dirty flag that tracks whether uncommitted data exists for a specific inode. When openWriteBufferSync (or the async openWriteBufferForCreateSync) initializes a write session, it registers a buffer structure containing buffer.buf (the raw bytes), buffer.size (the logical file size), and metadata including the target path (dirtyPath) and permissions mode.
Each buffer is retrieved via getWriteBuffer(db, inode), which checks the cache for existing entries before falling back to the persistent vfs_chunks table. This lookup pattern ensures that operations always access the most recent data—whether committed or pending.
Reading from the Write Buffer
Read operations prioritize the inode-keyed cache over persistent storage when the dirty flag is set. In packages/dofs/src/fs/readFile.ts (lines 74‑76), the readRangeSync function first queries the write buffer for the target inode. If a dirty buffer exists, the read returns data directly from buffer.buf limited to buffer.size, bypassing the SQLite vfs_chunks table entirely.
If no dirty buffer exists for the inode, the read falls back to the standard VFS tables. This dual-path approach guarantees consistency: callers always see the latest writes, including those not yet flushed to the database. The statSync and readdirSync implementations in packages/dofs/src/fs/stat.ts (lines 71‑72) apply the same logic, reporting buffer.size from the dirty buffer rather than the persisted node size stored in vfs_nodes.
The Buffered Write Lifecycle
The complete lifecycle of a buffered write spans five distinct phases, each implemented in packages/dofs/src/fs/writeFile.ts:
-
Opening the Buffer –
openWriteBufferSynccreates a new buffer structure keyed to the file's inode and registers it in the cache. -
Appending Data –
writeRangeSyncwrites incoming bytes intobuffer.bufat the specified offset. The function invokesensureBufferCapacityto expand the underlying array when necessary, then marks the buffer asdirtyand updatesbuffer.sizeto reflect the new logical end-of-file. -
Metadata Reporting – While the buffer remains dirty, calls to
statSyncreturn the buffered size rather than querying thevfs_nodestable, ensuring that file explorers and applications see the correct pending size. -
Pre-Flush Reading – Any
readRangeSynccalls during this window serve data directly frombuffer.buf, providing immediate read-after-write consistency without database round-trips. -
Committing Changes – When the file handle closes or
fsynctriggers,releaseWriteBufferSyncexecutes the materialization sequence (lines 803‑830). This transaction inserts the buffered bytes intovfs_chunks, updates the inode's size invfs_nodes, and clears thedirtyflag, atomically promoting the cached data to persistent storage.
FUSE Integration and Path Semantics
When the VFS is exposed through the FUSE shim in packages/computerd/src/fuse/driver.ts, the driver respects the inode-keyed cache during path-based operations. The getattr implementation (lines 754‑760) explicitly checks for a dirty buffer via the inode-to-buffer mapping, reporting the buffered size to the operating system before the data commits to SQLite.
This integration preserves write semantics across file operations. The driver's rename test in packages/computerd/src/fuse/driver.test.ts (line 339) verifies that buffered bytes move with the file when its path changes, ensuring that hard links share the same staged data. The test suite in packages/dofs/src/fs/writeBuffer.test.ts (lines 71‑83) confirms that multiple paths referencing the same inode access identical buffered content.
Code Example: Buffered Write and Read
import { openDatabase, getWriteBuffer } from '@cloudflare/computer';
// Initialize database connection
const db = await openDatabase();
// Create a buffered write session for a new file
p.openWriteBufferForCreateSync("/example.txt", { mode: 0o644 });
// Write data that exists only in the inode-keyed cache
p.writeRangeSync("/example.txt", Buffer.from("hello world"), 0);
// Read returns data from the dirty buffer, not SQLite
const data = p.readRangeSync("/example.txt", 0, 11);
console.log(new TextDecoder().decode(data)); // → "hello world"
// Commit to vfs_chunks and update vfs_nodes
p.releaseWriteBufferSync("/example.txt");
// Now stat returns the persisted size
console.log(p.statSync("/example.txt").size); // 11
Summary
- The inode-keyed cache maintains an in-memory
buffer.buffor each dirty inode, enabling fast read-after-write performance without SQLite transactions. - Read operations check
getWriteBuffer(db, inode)first; if thedirtyflag is set, data is served directly from the buffer rather thanvfs_chunks. - Write operations stage bytes via
writeRangeSync, which expands capacity viaensureBufferCapacityand updatesbuffer.sizefor accurate stat reporting. - Persistence occurs only during
releaseWriteBufferSync, which atomically writes tovfs_chunks, updatesvfs_nodes, and clears the dirty flag (lines 803‑830 inwriteFile.ts). - The FUSE driver respects buffered writes in
getattr(lines 754‑760) and preserves buffer associations across renames and hard links.
Frequently Asked Questions
What happens when reading a file that has pending buffered writes?
The read operation queries getWriteBuffer(db, inode) to locate any dirty buffer associated with the file's inode. If found, readRangeSync returns bytes directly from buffer.buf up to buffer.size, ensuring the reader sees the most recent uncommitted data. Only when no dirty buffer exists does the system fall back to the persisted vfs_chunks table.
How does the system report file size while writes are buffered?
Calls to statSync and the FUSE driver's getattr check the inode-keyed cache before querying the vfs_nodes table. When a dirty buffer exists, these functions report buffer.size rather than the persisted size, allowing applications to see the logical end-of-file including pending writes.
When are buffered bytes actually written to the database?
Buffered data commits to persistent storage only when releaseWriteBufferSync is invoked—typically when the file descriptor closes or an explicit fsync occurs. This function executes a transaction that inserts chunks into vfs_chunks, updates the inode size in vfs_nodes, and clears the dirty flag, as implemented in packages/dofs/src/fs/writeFile.ts at lines 803‑830.
Do hard links share the same write buffer?
Yes. Because the cache is keyed by inode rather than path, multiple directory entries referencing the same inode share a single buffer structure. The test suite in packages/dofs/src/fs/writeBuffer.test.ts (lines 71‑83) verifies that writes through one hard link are immediately visible when reading through another link to the same inode.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →