Skip to content

Commit 6bb29fa

Browse files
committed
Fix concurrent image load errors with CacheBase
1 parent 04c5127 commit 6bb29fa

File tree

1 file changed

+82
-34
lines changed
  • StabilityMatrix.Avalonia/Controls/VendorLabs/Cache

1 file changed

+82
-34
lines changed

StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@
55
// The .NET Foundation licenses this file to you under the MIT license.
66
// See the LICENSE file in the project root for more information.
77

8-
using System;
98
using System.Collections.Concurrent;
10-
using System.Collections.Generic;
9+
using System.ComponentModel;
1110
using System.Diagnostics;
12-
using System.IO;
13-
using System.Linq;
14-
using System.Net.Http;
15-
using System.Threading;
16-
using System.Threading.Tasks;
1711

1812
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
1913

14+
[Localizable(false)]
2015
internal abstract class CacheBase<T>
2116
{
22-
private class ConcurrentRequest
17+
private class ConcurrentRequest(Func<Task<T?>> factory)
2318
{
24-
public Task<T?>? Task { get; set; }
19+
private readonly Lazy<Task<T?>> _factory = new(factory, LazyThreadSafetyMode.ExecutionAndPublication);
20+
21+
public Task<T?> Task => _factory.Value;
2522

2623
public bool EnsureCachedCopy { get; set; }
24+
25+
public bool IsCompletedSuccessfully =>
26+
_factory is { IsValueCreated: true, Value.IsCompletedSuccessfully: true };
2727
}
2828

2929
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1);
@@ -33,8 +33,7 @@ private class ConcurrentRequest
3333
private string? _cacheFolder = null;
3434
protected InMemoryStorage<T>? InMemoryFileStorage = new();
3535

36-
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks =
37-
new ConcurrentDictionary<string, ConcurrentRequest>();
36+
private readonly ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks = new();
3837

3938
private HttpMessageHandler? _httpMessageHandler;
4039
private HttpClient? _httpClient = null;
@@ -346,50 +345,97 @@ private static ulong CreateHash64(string str)
346345
CancellationToken cancellationToken
347346
)
348347
{
349-
var instance = default(T);
350-
351348
var fileName = GetCacheFileName(uri);
352-
_concurrentTasks.TryGetValue(fileName, out var request);
353349

354-
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
355-
if (request != null && request.EnsureCachedCopy && !preCacheOnly)
350+
// Check if already in memory cache
351+
if (InMemoryFileStorage?.MaxItemCount > 0)
356352
{
357-
if (request.Task != null)
358-
await request.Task.ConfigureAwait(false);
359-
request = null;
353+
var msi = InMemoryFileStorage?.GetItem(fileName, CacheDuration);
354+
if (msi != null)
355+
{
356+
return msi.Item;
357+
}
360358
}
361359

362-
if (request == null)
363-
{
364-
request = new ConcurrentRequest()
360+
// Atomically get or add
361+
var request = _concurrentTasks.GetOrAdd(
362+
fileName,
363+
key =>
365364
{
366-
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken),
367-
EnsureCachedCopy = preCacheOnly
368-
};
369-
370-
_concurrentTasks[fileName] = request;
371-
}
365+
return new ConcurrentRequest(
366+
() => GetFromCacheOrDownloadAsync(uri, key, preCacheOnly, cancellationToken)
367+
);
368+
}
369+
);
372370

373371
try
374372
{
375-
if (request.Task != null)
376-
instance = await request.Task.ConfigureAwait(false);
373+
// Wait for the task to complete
374+
var itemTask = request.Task;
375+
var instance = await itemTask.ConfigureAwait(false);
376+
377+
// --- Handle In-Memory Caching ---
378+
// If the current request is not preCacheOnly, and the instance was successfully retrieved,
379+
// ensure it's in the memory cache.
380+
if (!preCacheOnly && instance != null && InMemoryFileStorage is { MaxItemCount: > 0 })
381+
{
382+
var memItem = InMemoryFileStorage.GetItem(fileName, CacheDuration);
383+
if (memItem == null || memItem.Item == null) // Check if not already in memory or expired
384+
{
385+
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
386+
var lastWriteTime = DateTime.Now;
387+
if (folder != null)
388+
{
389+
var baseFile = Path.Combine(folder, fileName);
390+
try
391+
{
392+
if (File.Exists(baseFile)) // Check existence before FileInfo
393+
lastWriteTime = new FileInfo(baseFile).LastWriteTime;
394+
}
395+
catch (IOException ioEx)
396+
{
397+
Debug.WriteLine(
398+
$"CacheBase: Error getting FileInfo for memory cache update on {fileName}: {ioEx.Message}"
399+
);
400+
}
401+
catch (Exception ex)
402+
{
403+
Debug.WriteLine(
404+
$"CacheBase: Error getting FileInfo for memory cache update on {fileName}: {ex.Message}"
405+
);
406+
}
407+
}
408+
409+
var msi = new InMemoryStorageItem<T>(fileName, lastWriteTime, instance);
410+
InMemoryFileStorage.SetItem(msi);
411+
}
412+
}
413+
414+
return instance;
377415
}
378416
catch (Exception ex)
379417
{
380-
Debug.WriteLine($"Image loading failed for (url={uri}, file={fileName}): {ex.Message}");
418+
Debug.WriteLine($"CacheBase: Exception during GetItemAsync for {fileName} (URI: {uri}): {ex}");
419+
420+
// Attempt to remove the entry associated with the failed task.
421+
_concurrentTasks.TryRemove(new KeyValuePair<string, ConcurrentRequest>(fileName, request));
381422

382423
if (throwOnError)
383424
{
384425
throw;
385426
}
427+
return default;
386428
}
387429
finally
388430
{
389-
_concurrentTasks.TryRemove(fileName, out _);
431+
// If the request was created and its underlying task completed successfully, remove the entry.
432+
// Ensure we don't remove entries for tasks still running.
433+
if (request.IsCompletedSuccessfully)
434+
{
435+
// Remove the entry only if it still contains the Lazy instance we worked with.
436+
_concurrentTasks.TryRemove(new KeyValuePair<string, ConcurrentRequest>(fileName, request));
437+
}
390438
}
391-
392-
return instance;
393439
}
394440

395441
private async Task<T?> GetFromCacheOrDownloadAsync(
@@ -519,6 +565,8 @@ CancellationToken cancellationToken
519565
{
520566
var instance = default(T);
521567

568+
Debug.WriteLine($"CacheBase Getting: {uri}");
569+
522570
using var ms = new MemoryStream();
523571
await using (var stream = await HttpClient.GetStreamAsync(uri, cancellationToken))
524572
{

0 commit comments

Comments
 (0)