Skip to content

Commit b90bbb2

Browse files
committed
Rewritten SilkTouch cache implementation
1 parent 1b20646 commit b90bbb2

16 files changed

+830
-505
lines changed
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
23

34
namespace Silk.NET.SilkTouch.Caching;
45

@@ -9,18 +10,22 @@ namespace Silk.NET.SilkTouch.Caching;
910
public enum CacheFlags
1011
{
1112
/// <summary>
12-
/// If the cache doesn't exist, allow it to be created and lock other callers from accessing the directory until it
13-
/// is committed.
13+
/// No flags set.
1414
/// </summary>
15-
AllowNewLocked = 1,
15+
None = 0,
1616

1717
/// <summary>
18-
/// Same as <see cref="AllowNewLocked"/>, but does not return a directory unless it is brand new.
18+
/// If the cache doesn't exist, allow it to be created.
1919
/// </summary>
20-
RequireNewLocked = 1 | (1 << 1),
20+
AllowNew = 1,
2121

2222
/// <summary>
23-
/// Don't write cache files to disk.
23+
/// Same as <see cref="AllowNew"/> but the new cache will replace the old cache once committed.
2424
/// </summary>
25-
NoHostDirectory = 1 << 2
25+
RequireNew = AllowNew | (1 << 1),
26+
27+
/// <summary>
28+
/// The cache provider needs the cache directory to reside on disk.
29+
/// </summary>
30+
RequireHostDirectory = 1 << 3,
2631
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Silk.NET.SilkTouch.Caching;
5+
6+
internal static class CacheUtils
7+
{
8+
internal static void ThrowAccessException() =>
9+
throw new NotSupportedException(
10+
"The cache directory has not been opened with the correct FileAccess for this operation."
11+
);
12+
13+
internal static void ThrowRequireHostDirectoryNeedsWrite() =>
14+
throw new NotSupportedException(
15+
"CacheFlags.RequireHostDirectory needs FileAccess.Write due to the cache accesses being uncontrolled by "
16+
+ "the cache system."
17+
);
18+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.IO.Compression;
6+
using System.Text;
7+
using Microsoft.Extensions.Logging;
8+
using IOPath = System.IO.Path;
9+
10+
namespace Silk.NET.SilkTouch.Caching;
11+
12+
internal class FileSystemArchiveCacheDirectory(
13+
string cacheKey,
14+
string committedPath,
15+
CacheIntent intent,
16+
CacheFlags flags,
17+
FileAccess access,
18+
FileSystemCacheProvider parent
19+
) : ICacheDirectory
20+
{
21+
private ZipArchive? _committed;
22+
private string? _newPath;
23+
private ZipArchive? _new;
24+
private SemaphoreSlim _sema = new(1, 1);
25+
26+
private async ValueTask<ZipArchive?> GetOrCreateCommittedAsync()
27+
{
28+
if (!File.Exists(committedPath) || (Flags & CacheFlags.RequireNew) == CacheFlags.RequireNew)
29+
{
30+
parent.Logger.LogDebug(
31+
"Cache miss with key \"{0}\"! Expected ZIP archive at {1}",
32+
CacheKey,
33+
committedPath
34+
);
35+
return null;
36+
}
37+
38+
parent.Logger.LogDebug(
39+
"Cache hit with key \"{0}\"! ZIP archive: {1}",
40+
CacheKey,
41+
committedPath
42+
);
43+
return _committed = await ZipArchive.CreateAsync(
44+
File.OpenRead(committedPath),
45+
ZipArchiveMode.Read,
46+
false,
47+
Encoding.UTF8
48+
);
49+
}
50+
51+
private async ValueTask<ZipArchive> GetOrCreateNewAsync()
52+
{
53+
if (_new is not null)
54+
{
55+
return _new;
56+
}
57+
58+
if ((Access & FileAccess.Write) == 0)
59+
{
60+
CacheUtils.ThrowAccessException();
61+
return null!;
62+
}
63+
64+
_newPath = IOPath.GetTempFileName();
65+
parent.Logger.LogDebug(
66+
"Opening cache for write with key \"{0}\"! Temporary ZIP archive path: {1}",
67+
CacheKey,
68+
_newPath
69+
);
70+
return _new = await ZipArchive.CreateAsync(
71+
File.OpenWrite(_newPath),
72+
ZipArchiveMode.Create,
73+
false,
74+
Encoding.UTF8
75+
);
76+
}
77+
78+
public string CacheKey { get; } = cacheKey;
79+
public CacheIntent Intent { get; } = intent;
80+
public CacheFlags Flags { get; } = flags;
81+
public FileAccess Access { get; } = access;
82+
public string? Path => null;
83+
84+
public async IAsyncEnumerable<string> GetCommittedFilesAsync()
85+
{
86+
if ((Access & FileAccess.Read) == 0)
87+
{
88+
CacheUtils.ThrowAccessException();
89+
}
90+
91+
foreach (
92+
var entry in (await GetOrCreateCommittedAsync())?.Entries
93+
?? Enumerable.Empty<ZipArchiveEntry>()
94+
)
95+
{
96+
yield return entry.FullName;
97+
}
98+
}
99+
100+
public async ValueTask<Stream> GetCommittedFileAsync(string filePath)
101+
{
102+
if ((Access & FileAccess.Read) == 0)
103+
{
104+
CacheUtils.ThrowAccessException();
105+
}
106+
107+
parent.Logger.LogTrace("Cache hit (\"{0}\", ZIP): {1}", CacheKey, filePath);
108+
var entry = _committed?.GetEntry(filePath) ?? throw new FileNotFoundException();
109+
return await entry.OpenAsync();
110+
}
111+
112+
public async ValueTask AddFileAsync(string filePath, Stream stream)
113+
{
114+
if ((Access & FileAccess.Write) == 0)
115+
{
116+
CacheUtils.ThrowAccessException();
117+
}
118+
119+
await _sema.WaitAsync();
120+
try
121+
{
122+
parent.Logger.LogTrace("Cache write (\"{0}\", ZIP): {1}", CacheKey, filePath);
123+
await using var dst = await (await GetOrCreateNewAsync())
124+
.CreateEntry(filePath, CompressionLevel.SmallestSize)
125+
.OpenAsync();
126+
await stream.CopyToAsync(dst);
127+
}
128+
finally
129+
{
130+
_sema.Release();
131+
}
132+
}
133+
134+
public async ValueTask CommitAsync()
135+
{
136+
if ((Access & FileAccess.Write) == 0)
137+
{
138+
CacheUtils.ThrowAccessException();
139+
}
140+
141+
await _sema.WaitAsync();
142+
try
143+
{
144+
parent.Logger.LogDebug(
145+
"Cache write with key \"{0}\"! ZIP archive: {1}",
146+
CacheKey,
147+
committedPath
148+
);
149+
var @new = await GetOrCreateNewAsync();
150+
if (_committed is not null)
151+
{
152+
// Copy old entries that haven't been overwritten.
153+
// If the user doesn't want this, they can use RequireNew.
154+
foreach (var entry in _committed.Entries)
155+
{
156+
if (@new.GetEntry(entry.FullName) is null)
157+
{
158+
parent.Logger.LogTrace(
159+
"Cache unchanged (\"{0}\", ZIP): {1}",
160+
CacheKey,
161+
entry.FullName
162+
);
163+
await using var src = await (
164+
_committed?.GetEntry(entry.FullName)
165+
?? throw new InvalidOperationException(
166+
"Failed to open an entry that exists."
167+
)
168+
).OpenAsync();
169+
await using var dst = await @new.CreateEntry(
170+
entry.FullName,
171+
CompressionLevel.SmallestSize
172+
)
173+
.OpenAsync();
174+
await src.CopyToAsync(dst);
175+
}
176+
}
177+
178+
await _committed.DisposeAsync();
179+
_committed = null;
180+
}
181+
182+
await @new.DisposeAsync();
183+
_new = null;
184+
Debug.Assert(_newPath is not null);
185+
File.Move(_newPath, committedPath, true);
186+
}
187+
finally
188+
{
189+
_sema.Release();
190+
}
191+
}
192+
193+
public async ValueTask DisposeAsync()
194+
{
195+
if (_new is not null)
196+
{
197+
parent.Logger.LogWarning(
198+
"Cache update abandoned with key \"{0}\"! ZIP archive: {1}",
199+
CacheKey,
200+
committedPath
201+
);
202+
}
203+
204+
await (_committed?.DisposeAsync() ?? ValueTask.CompletedTask);
205+
await (_new?.DisposeAsync() ?? ValueTask.CompletedTask);
206+
if (_newPath is not null)
207+
{
208+
File.Delete(_newPath);
209+
}
210+
211+
await parent.FreeAsync(CacheKey);
212+
}
213+
}

0 commit comments

Comments
 (0)