Files
UnrealEngineUWP/Engine/Source/Programs/UnrealCloudDDC/Jupiter/Controllers/ReplicationLogController.cs
joakim lindqvist c0e183edfa Cloud DDC - Added a replication log for blobs similar to what we already had for refs. The goal is to use this for replication of blobs rather then the ref log, as such a new Blob Replicator that consumes this has been added.
This should be make the replication much better at keeping up with the workload as it needs to do much less work to decide on what to replicate, furthermore its organized in such a way that we should be able to cache the replication log reading resulting in less work on the DB. It also resolves some theoritical issues when recompressing blobs should we ever want to start doing that.

[CL 33934744 by joakim lindqvist in ue5-main branch]
2024-05-28 02:33:05 -04: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; }
}
}