Files
UnrealEngine/Engine/Source/Programs/UnrealCloudDDC/Jupiter/Controllers/ReplicationLogController.cs
2025-05-18 13:04:45 +08:00

258 lines
7.8 KiB
C#

// 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> _snapshotSettings;
public ReplicationLogController(IServiceProvider provider, IRequestHelper requestHelper, IReplicationLog replicationLog, IOptionsMonitor<SnapshotSettings> snapshotSettings)
{
_provider = provider;
_requestHelper = requestHelper;
_replicationLog = replicationLog;
_snapshotSettings = snapshotSettings;
}
[HttpGet("snapshots/{ns}")]
[ProducesDefaultResponseType]
[ProducesResponseType(type: typeof(ProblemDetails), 400)]
public async Task<IActionResult> 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<IActionResult> CreateSnapshotAsync(
[Required] NamespaceId ns
)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.WriteTransactionLog });
if (result != null)
{
return result;
}
ReplicationLogSnapshotBuilder builder = ActivatorUtilities.CreateInstance<ReplicationLogSnapshotBuilder>(_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<IActionResult> 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<ReplicationLogEvent> events = _replicationLog.GetAsync(ns, lastBucket, lastEvent);
List<ReplicationLogEvent> 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<IActionResult> 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<BlobReplicationLogEvent> events = _replicationLog.GetBlobEventsAsync(ns, replicationBucket);
List<BlobReplicationLogEvent> 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<SnapshotInfo>();
}
[JsonConstructor]
public ReplicationLogSnapshots(List<SnapshotInfo> snapshots)
{
Snapshots = snapshots;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")]
public List<SnapshotInfo> Snapshots { get; set; }
}
public class ReplicationLogEvents
{
public ReplicationLogEvents()
{
Events = new List<ReplicationLogEvent>();
}
[JsonConstructor]
public ReplicationLogEvents(List<ReplicationLogEvent> events)
{
Events = events;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")]
public List<ReplicationLogEvent> Events { get; set; }
}
public class BlobReplicationLogEvents
{
public BlobReplicationLogEvents()
{
Events = new List<BlobReplicationLogEvent>();
}
[JsonConstructor]
public BlobReplicationLogEvents(List<BlobReplicationLogEvent> events)
{
Events = events;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")]
public List<BlobReplicationLogEvent> Events { get; set; }
}
}