// Copyright Epic Games, Inc. All Rights Reserved. using Horde.Build.Server; using Microsoft.Extensions.Caching.Memory; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; using System; using System.Linq; using System.Text.Json; using System.Threading.Tasks; namespace Horde.Build.Configuration { /// /// Collection of config documents /// public class ConfigCollection { class ConfigDoc { [BsonId] public string Id { get; set; } = String.Empty; [BsonElement("dat")] public byte[] Data { get; set; } = Array.Empty(); } private readonly IMongoCollection _configs; private readonly IMemoryCache _memoryCache; /// /// Constructor /// public ConfigCollection(MongoService mongoService, IMemoryCache memoryCache) { _configs = mongoService.GetCollection("Configs"); _memoryCache = memoryCache; } static string GetDocumentCacheKey(string revision) => $"config:{revision}"; static string GetTypedCacheKey(string revision) => $"config-typed:{nameof(T)}:{revision}"; /// /// Adds a config document with a particular revision /// /// /// /// Identifier for the config data public async Task AddConfigDataAsync(string revision, ReadOnlyMemory data) { ConfigDoc config = new ConfigDoc(); config.Id = revision; config.Data = data.ToArray(); await _configs.ReplaceOneAsync(x => x.Id == revision, config, new ReplaceOptions { IsUpsert = true }); AddConfigDataToCache(revision, config.Data); } /// /// Gets raw config data with the given revision number /// /// The revision number /// Raw config data with the given id public async ValueTask> GetConfigDataAsync(string revision) { string cacheKey = GetDocumentCacheKey(revision); if (!_memoryCache.TryGetValue(cacheKey, out ReadOnlyMemory data)) { ConfigDoc config = await _configs.Find(x => x.Id == revision).FirstAsync(); using (ICacheEntry entry = _memoryCache.CreateEntry(GetDocumentCacheKey(config.Id))) { entry.SetValue(config); entry.SetSize(config.Data.Length); } data = config.Data; } return data; } /// /// Adds config data for a given revision to the store, and returns the value of it parsed as a JSON object /// /// /// /// /// public async Task AddConfigAsync(string revision, T config) { JsonSerializerOptions options = new JsonSerializerOptions(); Startup.ConfigureJsonSerializer(options); byte[] data = JsonSerializer.SerializeToUtf8Bytes(config, options); await AddConfigDataAsync(revision, data); } /// /// Gets typed config data with a particular id /// /// Type of data to return /// The unique revision string /// Parsed config data for the given revision number public async ValueTask GetConfigAsync(string revision) { string typedCacheKey = GetTypedCacheKey(revision); if (!_memoryCache.TryGetValue(typedCacheKey, out T config)) { ReadOnlyMemory data = await GetConfigDataAsync(revision); JsonSerializerOptions options = new JsonSerializerOptions(); Startup.ConfigureJsonSerializer(options); config = JsonSerializer.Deserialize(data.Span, options)!; using (ICacheEntry entry = _memoryCache.CreateEntry(typedCacheKey)) { entry.SetValue(config); entry.SetSize(data.Length); } } return config; } void AddConfigDataToCache(string revision, ReadOnlyMemory data) { using (ICacheEntry entry = _memoryCache.CreateEntry(GetDocumentCacheKey(revision))) { entry.SetValue(data); entry.SetSize(data.Length); } } } }