// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Horde; using HordeServer.Server; using MongoDB.Bson.Serialization.Attributes; namespace HordeServer.Utilities { /// /// Identifier for a pool /// /// Id to construct from [StringIdConverter(typeof(SingletonIdConverter))] public record struct SingletonId(StringId Id) { /// /// Constructor /// public SingletonId(string id) : this(new StringId(id)) { } /// public override string ToString() => Id.ToString(); } /// /// Converter to and from instances. /// class SingletonIdConverter : StringIdConverter { /// public override SingletonId FromStringId(StringId id) => new SingletonId(id); /// public override StringId ToStringId(SingletonId value) => value.Id; } /// /// Base class for singleton documents /// public abstract class SingletonBase { /// /// Unique id for this singleton /// [BsonId] public SingletonId Id { get; set; } /// /// The revision index of this document /// public int Revision { get; set; } /// /// Callback to allow the singleton to fix up itself after being read /// public virtual void PostLoad() { } } /// /// Attribute specifying the unique id for a singleton document /// [AttributeUsage(AttributeTargets.Class)] public sealed class SingletonDocumentAttribute : Attribute { /// /// Unique id for the singleton document /// public string Id { get; } /// /// ObjectId for the legacy document /// public string? LegacyId { get; } /// /// Constructor /// /// Unique id for the singleton document. Should be a valid StringId. /// Legacy identifier for the document public SingletonDocumentAttribute(string id, string? legacyId = null) { Id = id; LegacyId = legacyId; } } /// /// Interface for the getting and setting the singleton /// /// Type of document public interface ISingletonDocument { /// /// Gets the current document /// /// Cancellation token for the operation /// The current document Task GetAsync(CancellationToken cancellationToken); /// /// Attempts to update the document /// /// New state of the document /// Cancellation token for the operation /// True if the document was updated, false otherwise Task TryUpdateAsync(T value, CancellationToken cancellationToken); } /// /// Concrete implementation of /// /// The document type public class SingletonDocument : ISingletonDocument where T : SingletonBase, new() { /// /// The database service instance /// readonly IMongoService _mongoService; /// /// Constructor /// /// The database service instance public SingletonDocument(IMongoService mongoService) { _mongoService = mongoService; } /// public async Task GetAsync(CancellationToken cancellationToken) { return await _mongoService.GetSingletonAsync(cancellationToken); } /// public Task TryUpdateAsync(T value, CancellationToken cancellationToken) { return _mongoService.TryUpdateSingletonAsync(value, cancellationToken); } } /// /// Extension methods for singletons /// public static class SingletonDocumentExtensions { /// /// Update a singleton /// /// /// /// /// Cancellation token for the operation /// public static async Task UpdateAsync(this ISingletonDocument singleton, Action updateAction, CancellationToken cancellationToken) where T : SingletonBase, new() { for (; ; ) { T value = await singleton.GetAsync(cancellationToken); updateAction(value); if (await singleton.TryUpdateAsync(value, cancellationToken)) { return value; } } } } }