// 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);
}
}
}
}