// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Storage; using EpicGames.Serialization; using Jupiter.Common; using Jupiter.Implementation; using Jupiter.Implementation.TransactionLog; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Jupiter.Controllers { [ApiController] [Route("api/v1/replication-log")] [InternalApiFilter] [Authorize] public class ReplicationLogController : ControllerBase { private readonly IServiceProvider _provider; private readonly IRequestHelper _requestHelper; private readonly IReplicationLog _replicationLog; private readonly IOptionsMonitor _snapshotSettings; public ReplicationLogController(IServiceProvider provider, IRequestHelper requestHelper, IReplicationLog replicationLog, IOptionsMonitor snapshotSettings) { _provider = provider; _requestHelper = requestHelper; _replicationLog = replicationLog; _snapshotSettings = snapshotSettings; } [HttpGet("snapshots/{ns}")] [ProducesDefaultResponseType] [ProducesResponseType(type: typeof(ProblemDetails), 400)] public async Task GetSnapshotsAsync( [Required] NamespaceId ns ) { ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadTransactionLog }); if (result != null) { return result; } return Ok(new ReplicationLogSnapshots(await _replicationLog.GetSnapshotsAsync(ns).ToListAsync())); } [HttpPost("snapshots/{ns}/create")] [ProducesDefaultResponseType] [ProducesResponseType(type: typeof(ProblemDetails), 400)] public async Task CreateSnapshotAsync( [Required] NamespaceId ns ) { ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.WriteTransactionLog }); if (result != null) { return result; } ReplicationLogSnapshotBuilder builder = ActivatorUtilities.CreateInstance(_provider); BlobId snapshotBlob = await builder.BuildSnapshotAsync(ns, _snapshotSettings.CurrentValue.SnapshotStorageNamespace, CancellationToken.None); return Ok(new SnapshotCreatedResponse(snapshotBlob)); } [HttpGet("incremental/{ns}")] [ProducesDefaultResponseType] [ProducesResponseType(type: typeof(ProblemDetails), 400)] public async Task GetIncrementalEventsAsync( [Required] NamespaceId ns, [FromQuery] string? lastBucket, [FromQuery] Guid? lastEvent, [FromQuery] int count = 1000 ) { ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadTransactionLog }); if (result != null) { return result; } if (((lastBucket == null && lastEvent.HasValue) || (lastBucket != null && !lastEvent.HasValue)) && lastBucket != "now") { return BadRequest(new ProblemDetails { Title = $"Both bucket and event has to be specified, or omit both.", }); } try { IAsyncEnumerable events = _replicationLog.GetAsync(ns, lastBucket, lastEvent); List l = await events.Take(count).ToListAsync(); return Ok(new ReplicationLogEvents(l)); } catch (IncrementalLogNotAvailableException) { // failed to resume from the incremental log, check for a snapshot instead SnapshotInfo? snapshot = await _replicationLog.GetLatestSnapshotAsync(ns); if (snapshot != null) { // no log file is available return BadRequest(new ProblemDetails { Title = $"Log file is not available, use snapshot {snapshot.SnapshotBlob} instead", Type = ProblemTypes.UseSnapshot, Extensions = { { "SnapshotId", snapshot.SnapshotBlob }, { "BlobNamespace", snapshot.BlobNamespace }, } }); } // if no snapshot is available we just give up, they can always reset the replication to the default behavior by not sending in lastBucket and lastEvent return BadRequest(new ProblemDetails { Title = $"No snapshot or bucket found for namespace \"{ns}\"", Type = ProblemTypes.NoDataFound, }); } catch (NamespaceNotFoundException) { return NotFound(new ProblemDetails { Title = $"Namespace {ns} was not found", }); } } [HttpGet("blobs/{ns}/{replicationBucket}")] [ProducesDefaultResponseType] [ProducesResponseType(type: typeof(ProblemDetails), 400)] public async Task GetBlobLogAsync( [Required] NamespaceId ns, [Required] string replicationBucket ) { ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadTransactionLog }); if (result != null) { return result; } try { IAsyncEnumerable events = _replicationLog.GetBlobEventsAsync(ns, replicationBucket); List l = await events.ToListAsync(); return Ok(new BlobReplicationLogEvents(l)); } catch (IncrementalLogNotAvailableException) { // failed to resume from the incremental log, check for a snapshot instead SnapshotInfo? snapshot = await _replicationLog.GetLatestSnapshotAsync(ns); if (snapshot != null) { // no log file is available return BadRequest(new ProblemDetails { Title = $"Log file is not available, use snapshot {snapshot.SnapshotBlob} instead", Type = ProblemTypes.UseSnapshot, Extensions = { { "SnapshotId", snapshot.SnapshotBlob }, { "BlobNamespace", snapshot.BlobNamespace }, } }); } // if no snapshot is available we just give up, the replication will be started to a old bucket by the BlobReplicator return BadRequest(new ProblemDetails { Title = $"No snapshot or bucket found for namespace \"{ns}\"", Type = ProblemTypes.NoDataFound, }); } catch (NamespaceNotFoundException) { return NotFound(new ProblemDetails { Title = $"Namespace {ns} was not found", }); } } } public class SnapshotCreatedResponse { public SnapshotCreatedResponse() { SnapshotBlobId = null!; } public SnapshotCreatedResponse(BlobId snapshotBlob) { SnapshotBlobId = snapshotBlob; } [CbField("snapshotBlobId")] public BlobId SnapshotBlobId { get; set; } } public class ReplicationLogSnapshots { public ReplicationLogSnapshots() { Snapshots = new List(); } [JsonConstructor] public ReplicationLogSnapshots(List snapshots) { Snapshots = snapshots; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] public List Snapshots { get; set; } } public class ReplicationLogEvents { public ReplicationLogEvents() { Events = new List(); } [JsonConstructor] public ReplicationLogEvents(List events) { Events = events; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] public List Events { get; set; } } public class BlobReplicationLogEvents { public BlobReplicationLogEvents() { Events = new List(); } [JsonConstructor] public BlobReplicationLogEvents(List events) { Events = events; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] public List Events { get; set; } } }