Files
UnrealEngine/Engine/Source/Programs/UnrealCloudDDC/Jupiter/Implementation/LastAccess/LastAccessTracker.cs
2025-05-18 13:04:45 +08:00

131 lines
3.6 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Horde.Storage;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Trace;
namespace Jupiter.Implementation
{
public class LastAccessRecord
{
public LastAccessRecord(NamespaceId ns, BucketId bucket, RefId key)
{
Namespace = ns;
Bucket = bucket;
Key = key;
}
public NamespaceId Namespace { get; set; }
public BucketId Bucket { get; set; }
public RefId Key { get; set; }
}
public class LastAccessTrackerReference : LastAccessTracker<LastAccessRecord>
{
public LastAccessTrackerReference(IOptionsMonitor<UnrealCloudDDCSettings> settings, Tracer tracer, ILogger<LastAccessTrackerReference> logger) : base(settings, tracer, logger)
{
}
protected override string BuildCacheKey(LastAccessRecord record)
{
return $"{record.Namespace}.{record.Bucket}.{record.Key}";
}
}
public abstract class LastAccessTracker<T> : ILastAccessTracker<T>, ILastAccessCache<T>
{
private readonly IOptionsMonitor<UnrealCloudDDCSettings> _settings;
private readonly Tracer _tracer;
private readonly ILogger _logger;
private ConcurrentDictionary<string, LastAccessRecord> _cache = new ConcurrentDictionary<string, LastAccessRecord>();
// we will exchange the refs dictionary when fetching the records and use a rw lock to make sure no-one is trying to add things at the same time
private readonly ReaderWriterLock _rwLock = new ReaderWriterLock();
protected LastAccessTracker(IOptionsMonitor<UnrealCloudDDCSettings> settings, Tracer tracer, ILogger logger)
{
_settings = settings;
_tracer = tracer;
_logger = logger;
}
protected abstract string BuildCacheKey(T record);
public Task TrackUsed(T record)
{
if (!_settings.CurrentValue.EnableLastAccessTracking)
{
return Task.CompletedTask;
}
return Task.Run(() =>
{
using TelemetrySpan _ = _tracer.StartActiveSpan("lastAccessTracker.track").SetAttribute("operation.name", "lastAccessTracker.track");
try
{
_rwLock.AcquireReaderLock(-1);
string cacheKey = BuildCacheKey(record);
_logger.LogDebug("Last Access time updated for {RefRecordCacheKey}", cacheKey);
_cache.AddOrUpdate(cacheKey, _ => new LastAccessRecord(record, DateTime.Now),
(_, cacheRecord) =>
{
cacheRecord.LastAccessTime = DateTime.Now;
return cacheRecord;
});
}
finally
{
_rwLock.ReleaseLock();
}
});
}
public async Task<List<(T, DateTime)>> GetLastAccessedRecords()
{
return await Task.Run(() =>
{
try
{
_rwLock.AcquireWriterLock(-1);
_logger.LogDebug("Last Access Records collected");
ConcurrentDictionary<string, LastAccessRecord> localReference = _cache;
_cache = new ConcurrentDictionary<string, LastAccessRecord>();
// ToArray is important here to make sure this is thread safe as just using linq queries on a concurrent dict is not thead safe
// http://blog.i3arnon.com/2018/01/16/concurrent-dictionary-tolist/
return localReference.ToArray().Select(pair => (pair.Value.Record, pair.Value.LastAccessTime))
.ToList();
}
finally
{
_rwLock.ReleaseWriterLock();
}
});
}
private class LastAccessRecord
{
public T Record { get; }
public DateTime LastAccessTime { get; set; }
public LastAccessRecord(T record, in DateTime lastAccessTime)
{
Record = record;
LastAccessTime = lastAccessTime;
}
}
}
}