-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Fix(LocalFileStore): Prevent LocalFileStore corruption from concurrent writes #9381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix(LocalFileStore): Prevent LocalFileStore corruption from concurrent writes #9381
Conversation
|
hntrl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sagarjariwala333! One question about your approach:
| const finalPath = this.getFullPath(key); | ||
| const tempPath = `${finalPath}.${randomUUID()}.tmp`; | ||
|
|
||
| try { | ||
| await fs.writeFile(this.getFullPath(key), content); | ||
| // Write to temporary file first | ||
| await fs.writeFile(tempPath, content); | ||
|
|
||
| // Atomically rename to final destination | ||
| // On most filesystems, rename is atomic - either the old file exists or the new one does | ||
| await fs.rename(tempPath, finalPath); | ||
| } catch (error) { | ||
| // Clean up temporary file if it exists | ||
| try { | ||
| await fs.unlink(tempPath); | ||
| } catch { | ||
| // Ignore cleanup errors - file might not exist | ||
| } | ||
|
|
||
| throw new Error( | ||
| `Error writing file at path: ${this.getFullPath( | ||
| key | ||
| )}.\nError: ${JSON.stringify(error)}` | ||
| `Error writing file at path: ${finalPath}.\nError: ${JSON.stringify( | ||
| error | ||
| )}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of writing a temporary file if the write operations are queued?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reason for writing to a temporary file first
Writing to a temporary file and then renaming it ensures the final file is only replaced once the new data is fully written. If the process crashes mid-write, only the temp file is affected and the original file stays intact.
Why rename is used
The rename() operation is atomic on most filesystems.
This gives us transaction-like semantics:
either the entire write succeeds and the file is replaced atomically,
or nothing changes at all.
There is never a state where the final file is partially written or corrupted.
What if the rename operation fails
While rare, rename() can fail due to permission errors, file locks, disk I/O issues, or filesystem differences.
If rename fails:
the original file stays untouched (so integrity is preserved)
the new data is not applied
we can safely propagate the error, because the operation behaves like a failed transaction
Since the write either fully applies or not at all, the failure mode is predictable and safe.
Summary
Fixes concurrent write corruption in
LocalFileStore.mset()by implementing queue-based write serialization with atomic file operations.Closes #9337
Problem
When multiple concurrent
mset()calls write to the same key, the underlyingfs.writeFile()operations can interleave, corrupting the cache files. This commonly occurs when usingCacheBackedEmbeddingswithLocalFileStorefor persistent embedding caching.Reproduction
Symptoms
{"chunkId":998,"data":"0vCRv..."}rY\"}04Fsfa...Solution
Implemented a queue-based write serialization approach combined with atomic writes:
1. Write Queue (Prevents Race Conditions)
writeQueuesMap to track pending writes per key2. Atomic Writes (Prevents Partial Corruption)
.tmpextension)Changes
Core Implementation
Files Modified:
libs/langchain-classic/src/storage/file_system.tslibs/langchain/src/storage/file_system.tsKey Changes:
writeQueues: Map<string, Promise<void>>propertyqueueWrite()method for serializationsetFileContent()to use atomic write-then-renamemset()to deduplicate and queue writesfromPath()to cleanup orphaned temp filesCode Example
Breaking Changes
None - This is 100% backward compatible.
Verification
Technical Details
Why Queue-Based Approach?
Why Atomic Writes?
fs.rename()is atomic on most filesystemsReal-World Impact
Who This Affects
CacheBackedEmbeddingswithLocalFileStoremset()callsBefore Fix
❌ Concurrent writes → File corruption
❌ JSON parse errors
❌ Cache misses
❌ Unreliable caching
After Fix
✅ Concurrent writes → No corruption
✅ Valid JSON always
✅ Reliable cache hits
✅ Production ready
Checklist
LocalFileStoremsetwrites can corrupt chunked embeddings cache #9337)Related Issues
LocalFileStoremsetwrites can corrupt chunked embeddings cache #9337Credits