Skip to content

Commit b989312

Browse files
committed
fix: invalidate VFS existence cache when refreshing metadata
1 parent b3a5b00 commit b989312

5 files changed

Lines changed: 49 additions & 2 deletions

File tree

‎Directory.Build.props‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
<RepositoryUrl>https://github.com/managedcode/Storage</RepositoryUrl>
3030
<PackageProjectUrl>https://github.com/managedcode/Storage</PackageProjectUrl>
3131
<Product>Managed Code - Storage</Product>
32-
<Version>10.1.0</Version>
33-
<PackageVersion>10.1.0</PackageVersion>
32+
<Version>10.1.1</Version>
33+
<PackageVersion>10.1.1</PackageVersion>
3434

3535
</PropertyGroup>
3636

‎ManagedCode.Storage.VirtualFileSystem/Implementations/VirtualFile.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default)
9898

9999
if (_vfs.Options.EnableCache)
100100
{
101+
_cache.Remove($"file_exists:{_vfs.ContainerName}:{_path}");
101102
var metadataKey = $"file_metadata:{_vfs.ContainerName}:{_path}";
102103
var entry = new MetadataCacheEntry
103104
{

‎Tests/ManagedCode.Storage.Tests/VirtualFileSystem/VfsImmutableMetadataTests.cs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,49 @@ await storage.RequireObjectStorage().WriteObjectAsync("legacy/content", bytes,
9595
});
9696
}
9797

98+
[Theory]
99+
[InlineData(false)]
100+
[InlineData(true)]
101+
public async Task Refresh_AfterMultipartCommit_ExposesCommittedBytes(bool cache)
102+
{
103+
await using var provider = CreateProvider(cache);
104+
await using var scope = provider.CreateAsyncScope();
105+
var vfs = scope.ServiceProvider.GetRequiredService<IVirtualFileSystem>();
106+
var path = new VfsPath("/learning/commands/output.zip");
107+
var file = await vfs.GetFileAsync(path);
108+
(await file.ExistsAsync()).ShouldBeFalse();
109+
await file.RefreshAsync();
110+
var bytes = Encoding.UTF8.GetBytes("committed archive bytes");
111+
var multipart = vfs.Storage.RequireMultipartStorage();
112+
await multipart.CreatePrivateContainerAsync();
113+
var partId = Convert.ToBase64String(Encoding.UTF8.GetBytes("archive-part-0001"));
114+
using var part = new MemoryStream(bytes);
115+
await multipart.StagePartAsync(path.ToBlobKey(), partId, part);
116+
await multipart.CommitPartsAsync(path.ToBlobKey(), [partId]);
117+
await file.RefreshAsync();
118+
file.Size.ShouldBe(bytes.LongLength);
119+
(await file.ExistsAsync()).ShouldBeTrue();
120+
(await file.ReadAllBytesAsync()).ShouldBe(bytes);
121+
}
122+
123+
[Theory]
124+
[InlineData(false)]
125+
[InlineData(true)]
126+
public async Task Refresh_AfterProviderDeletion_ReportsMissingFile(bool cache)
127+
{
128+
await using var provider = CreateProvider(cache);
129+
await using var scope = provider.CreateAsyncScope();
130+
var vfs = scope.ServiceProvider.GetRequiredService<IVirtualFileSystem>();
131+
var path = new VfsPath("/learning/commands/deleted.zip");
132+
var file = await vfs.GetFileAsync(path);
133+
await file.WriteAllBytesAsync(Encoding.UTF8.GetBytes("previous archive bytes"));
134+
(await file.ExistsAsync()).ShouldBeTrue();
135+
(await vfs.Storage.RequireObjectStorage().DeleteObjectIfExistsAsync(path.ToBlobKey())).ShouldBeTrue();
136+
await file.RefreshAsync();
137+
file.Size.ShouldBe(0);
138+
(await file.ExistsAsync()).ShouldBeFalse();
139+
}
140+
98141
private ServiceProvider CreateProvider(bool cache)
99142
{
100143
var services = new ServiceCollection();

‎docs/Architecture.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ versioned ASCII envelope decoded across all metadata reads. See the
5050
[Azure provider](Features/provider-azure-blob.md#metadata-transport) for its
5151
physical representation, collision handling and metadata-size constraints.
5252
VFS writes persist zero-byte files and truncate existing files on empty overwrite.
53+
`IVirtualFile.RefreshAsync` invalidates cached existence after refreshing provider metadata, so externally committed or deleted objects are visible to subsequent existence checks and reads.
5354
`FileExistsAsync` remains a cached, best-effort convenience query. Consumers that
5455
make conflict, authorization, or recovery decisions use
5556
`IVirtualFileSystem.StorageFileExistsAsync`, which reads the backing provider and

‎docs/Features/virtual-file-system.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ This is *not* a separate cloud provider. It is an abstraction layer that sits ab
1818

1919
### File operations
2020

21+
After a write or deletion through the backing provider, call `IVirtualFile.RefreshAsync` on an existing file handle. It refreshes metadata and invalidates cached existence, including a cached miss before a multipart commit. The next existence check and read observe the committed object.
22+
2123
```mermaid
2224
flowchart TD
2325
A[Caller] --> VFS[IVirtualFileSystem]

0 commit comments

Comments
 (0)