diff --git a/src/Build5Nines.SharpVector/VectorStore/BasicDiskVectorStore.cs b/src/Build5Nines.SharpVector/VectorStore/BasicDiskVectorStore.cs index 137d725..5cabe01 100644 --- a/src/Build5Nines.SharpVector/VectorStore/BasicDiskVectorStore.cs +++ b/src/Build5Nines.SharpVector/VectorStore/BasicDiskVectorStore.cs @@ -191,33 +191,40 @@ private void RecoverFromWalOrIndex() // Replay WAL to recover any operations after the last checkpoint if (!File.Exists(_walPath)) return; - using var fs = new FileStream(_walPath, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs); - while (fs.Position < fs.Length) + + // Scope the read handles so the file is closed before we overwrite it + // below; `using var` would otherwise hold the handle until the end of + // the method and File.WriteAllBytes would race against it on Windows. + using (var fs = new FileStream(_walPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var br = new BinaryReader(fs)) { - bool isDelete = br.ReadBoolean(); - var idJson = br.ReadString(); - var id = JsonSerializer.Deserialize(idJson)!; - if (isDelete) - { - _index.TryRemove(id, out _); - _cache.TryRemove(id, out _); - } - else + while (fs.Position < fs.Length) { - var itemJson = br.ReadString(); - var item = JsonSerializer.Deserialize>(itemJson)!; - - // Append item to items file to bring storage up-to-date - using var ofs = new FileStream(_itemsPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - ofs.Seek(0, SeekOrigin.End); - var offset = ofs.Position; - WriteItem(ofs, item); - ofs.Flush(true); - _index[id] = offset; - _cache[id] = item; + bool isDelete = br.ReadBoolean(); + var idJson = br.ReadString(); + var id = JsonSerializer.Deserialize(idJson)!; + if (isDelete) + { + _index.TryRemove(id, out _); + _cache.TryRemove(id, out _); + } + else + { + var itemJson = br.ReadString(); + var item = JsonSerializer.Deserialize>(itemJson)!; + + // Append item to items file to bring storage up-to-date + using var ofs = new FileStream(_itemsPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + ofs.Seek(0, SeekOrigin.End); + var offset = ofs.Position; + WriteItem(ofs, item); + ofs.Flush(true); + _index[id] = offset; + _cache[id] = item; + } } } + // After successful replay, truncate WAL (commit) File.WriteAllBytes(_walPath, Array.Empty()); PersistIndex(); diff --git a/src/Build5Nines.SharpVector/Vocabulary/BasicDiskVocabularyStore.cs b/src/Build5Nines.SharpVector/Vocabulary/BasicDiskVocabularyStore.cs index 2b1c6cd..3552c18 100644 --- a/src/Build5Nines.SharpVector/Vocabulary/BasicDiskVocabularyStore.cs +++ b/src/Build5Nines.SharpVector/Vocabulary/BasicDiskVocabularyStore.cs @@ -118,23 +118,30 @@ private void RecoverFromWalOrIndex() { LoadIfExists(); if (!File.Exists(_walPath)) return; - using var fs = new FileStream(_walPath, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs); - while (fs.Position < fs.Length) + + // Scope the read handles so the file is closed before we overwrite it + // below; `using var` would otherwise hold the handle until the end of + // the method and File.WriteAllBytes would race against it on Windows. + using (var fs = new FileStream(_walPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var br = new BinaryReader(fs)) { - int count = br.ReadInt32(); - for (int i = 0; i < count; i++) + while (fs.Position < fs.Length) { - var tokenJson = br.ReadString(); - var token = JsonSerializer.Deserialize(tokenJson)!; - if (!_vocab.ContainsKey(token)) + int count = br.ReadInt32(); + for (int i = 0; i < count; i++) { - var idx = _vocab.Count; - _vocab[token] = idx; - _cache[token] = idx; + var tokenJson = br.ReadString(); + var token = JsonSerializer.Deserialize(tokenJson)!; + if (!_vocab.ContainsKey(token)) + { + var idx = _vocab.Count; + _vocab[token] = idx; + _cache[token] = idx; + } } } } + File.WriteAllBytes(_walPath, Array.Empty()); Persist(); }