// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Storage; using Microsoft.Extensions.Options; namespace Jupiter.Implementation.Blob; public class MemoryBlobIndex : IBlobIndex { private class MemoryBlobInfo { public HashSet Regions { get; init; } = new HashSet(); public NamespaceId Namespace { get; init; } public BlobId BlobIdentifier { get; init; } = null!; public List References { get; init; } = new List(); } private class MemoryBucketInfo { public List<(RefId, BlobId, long)> Entries { get; init; } = new List<(RefId, BlobId, long)>(); public void AddEntry(RefId refId, BlobId blobId, long value) { int index = Entries.FindIndex(tuple => tuple.Item1.Equals(refId) && tuple.Item2.Equals(blobId)); if (index == -1) { // no previous value found, add this entry Entries.Add((refId, blobId, value)); } else { // replace existing value Entries[index] = (refId, blobId, value); } } public void RemoveEntriesForRef(RefId refId) { Entries.RemoveAll(tuple => tuple.Item1.Equals(refId)); } } private readonly ConcurrentDictionary> _index = new(); private readonly ConcurrentDictionary> _bucketIndex = new(); private readonly IOptionsMonitor _jupiterSettings; public MemoryBlobIndex(IOptionsMonitor settings) { _jupiterSettings = settings; } private ConcurrentDictionary GetNamespaceContainer(NamespaceId ns) { return _index.GetOrAdd(ns, id => new ConcurrentDictionary()); } public Task AddBlobToIndexAsync(NamespaceId ns, BlobId id, string? region = null, CancellationToken cancellationToken = default) { region ??= _jupiterSettings.CurrentValue.CurrentSite; ConcurrentDictionary index = GetNamespaceContainer(ns); index[id] = NewBlobInfo(ns, id, region); return Task.CompletedTask; } private Task GetBlobInfo(NamespaceId ns, BlobId id) { ConcurrentDictionary index = GetNamespaceContainer(ns); if (!index.TryGetValue(id, out MemoryBlobInfo? blobInfo)) { return Task.FromResult(null); } return Task.FromResult(blobInfo); } public Task RemoveBlobFromRegionAsync(NamespaceId ns, BlobId id, string? region = null, CancellationToken cancellationToken = default) { region ??= _jupiterSettings.CurrentValue.CurrentSite; ConcurrentDictionary index = GetNamespaceContainer(ns); index.AddOrUpdate(id, _ => { MemoryBlobInfo info = NewBlobInfo(ns, id, region); return info; }, (_, info) => { info.Regions.Remove(region); return info; }); return Task.CompletedTask; } public Task RemoveBlobFromAllRegionsAsync(NamespaceId ns, BlobId id, CancellationToken cancellationToken = default) { ConcurrentDictionary index = GetNamespaceContainer(ns); index.Remove(id, out MemoryBlobInfo? _); return Task.CompletedTask; } public async Task BlobExistsInRegionAsync(NamespaceId ns, BlobId blobIdentifier, string? region = null, CancellationToken cancellationToken = default) { string expectedRegion = region ?? _jupiterSettings.CurrentValue.CurrentSite; MemoryBlobInfo? blobInfo = await GetBlobInfo(ns, blobIdentifier); return blobInfo?.Regions.Contains(expectedRegion) ?? false; } public IAsyncEnumerable<(NamespaceId, BaseBlobReference)> GetAllBlobReferencesAsync(CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public async IAsyncEnumerable GetBlobReferencesAsync(NamespaceId ns, BlobId id, [EnumeratorCancellation] CancellationToken cancellationToken) { MemoryBlobInfo? blobInfo = await GetBlobInfo(ns, id); if (blobInfo != null) { foreach (BaseBlobReference reference in blobInfo.References) { yield return reference; } } } public Task AddRefToBlobsAsync(NamespaceId ns, BucketId bucket, RefId key, BlobId[] blobs, CancellationToken cancellationToken) { foreach (BlobId id in blobs) { ConcurrentDictionary index = GetNamespaceContainer(ns); index.AddOrUpdate(id, _ => { MemoryBlobInfo info = NewBlobInfo(ns, id, _jupiterSettings.CurrentValue.CurrentSite); info.References!.Add(new RefBlobReference(id, bucket, key)); return info; }, (_, info) => { info.References!.Add(new RefBlobReference(id, bucket, key)); return info; }); } return Task.CompletedTask; } public async IAsyncEnumerable<(NamespaceId, BlobId)> GetAllBlobsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { await Task.CompletedTask; foreach (KeyValuePair> pair in _index) { foreach ((BlobId? _, MemoryBlobInfo? blobInfo) in pair.Value) { yield return (blobInfo.Namespace, blobInfo.BlobIdentifier); } } } public Task RemoveReferencesAsync(NamespaceId ns, BlobId id, List? referencesToRemove, CancellationToken cancellationToken) { ConcurrentDictionary index = GetNamespaceContainer(ns); if (index.TryGetValue(id, out MemoryBlobInfo? blobInfo)) { if (referencesToRemove == null) { blobInfo.References.Clear(); } else { foreach (BaseBlobReference r in referencesToRemove) { blobInfo.References.Remove(r); } } } return Task.CompletedTask; } public async Task> GetBlobRegionsAsync(NamespaceId ns, BlobId blob, CancellationToken cancellationToken) { MemoryBlobInfo? blobInfo = await GetBlobInfo(ns, blob); if (blobInfo != null) { return blobInfo.Regions.ToList(); } throw new BlobNotFoundException(ns, blob); } public async Task AddBlobReferencesAsync(NamespaceId ns, BlobId sourceBlob, BlobId targetBlob, CancellationToken cancellationToken) { MemoryBlobInfo? blobInfo = await GetBlobInfo(ns, sourceBlob); if (blobInfo == null) { throw new BlobNotFoundException(ns, sourceBlob); } blobInfo.References.Add(new BlobToBlobReference(sourceBlob, targetBlob)); } public Task AddBlobToBucketListAsync(NamespaceId ns, BucketId bucket, RefId key, BlobId blobId, long blobSize, CancellationToken cancellationToken) { ConcurrentDictionary bucketDict = _bucketIndex.GetOrAdd(ns, id => new ConcurrentDictionary()); MemoryBucketInfo bucketInfo = bucketDict.GetOrAdd(bucket, id => new MemoryBucketInfo()); bucketInfo.AddEntry(key, blobId, blobSize); return Task.CompletedTask; } public Task RemoveBlobFromBucketListAsync(NamespaceId ns, BucketId bucket, RefId key, List blobIds, CancellationToken cancellationToken) { if (_bucketIndex.TryGetValue(ns, out ConcurrentDictionary? bucketDict)) { if (bucketDict.TryGetValue(bucket, out MemoryBucketInfo? bucketInfo)) { bucketInfo.RemoveEntriesForRef(key); } } return Task.CompletedTask; } public Task CalculateBucketStatisticsAsync(NamespaceId ns, BucketId bucket, CancellationToken cancellationToken) { HashSet foundRefs = new HashSet(); int countOfBlobs = 0; long totalBlobSize = 0; long smallestBlobFound = 0; long largestBlobFound = 0; if (_bucketIndex.TryGetValue(ns, out ConcurrentDictionary? bucketDict)) { if (bucketDict.TryGetValue(bucket, out MemoryBucketInfo? bucketInfo)) { foreach ((RefId refId, BlobId? blobId, long size) in bucketInfo.Entries) { foundRefs.Add(refId); countOfBlobs++; totalBlobSize += size; smallestBlobFound = Math.Min(size, smallestBlobFound); largestBlobFound = Math.Max(size, largestBlobFound); } } } return Task.FromResult(new BucketStats() { Namespace = ns, Bucket = bucket, CountOfRefs = foundRefs.Count, CountOfBlobs = countOfBlobs, SmallestBlobFound = smallestBlobFound, LargestBlob = largestBlobFound, TotalSize = totalBlobSize, AvgSize = totalBlobSize / (double)countOfBlobs }); } private static MemoryBlobInfo NewBlobInfo(NamespaceId ns, BlobId blob, string region) { MemoryBlobInfo info = new MemoryBlobInfo { Regions = new HashSet { region }, Namespace = ns, BlobIdentifier = blob, References = new List() }; return info; } }