Files
UnrealEngineUWP/Engine/Source/Programs/UnrealCloudDDC/Jupiter/Controllers/ObjectController.cs

298 lines
8.2 KiB
C#
Raw Normal View History

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using EpicGames.AspNet;
using EpicGames.Horde.Storage;
using EpicGames.Serialization;
Refactored blob storage in Horde.Storage to allow for central handling of all blob store requests. This lets us add verification of the blob identifier before submitting content to the blob store. I also added a blob index, a database tracking of which blobs exists, and a optional toggle to use this instead of checking S3 during existence checks (which we think will be faster). Could also support batched requests but that is not yet implemented. Also promoted the current site setting into the common (JupiterSettings) settings, as we now also use this for the blob index, which we intend to use for in line replication of missing blobs in the future. Removed the hirearchal blob stores and moved this into the blob service (the new central place to handle all blob store requests). Change this to process blob stores in parallel which will be a small speedup as we start the s3 requests earlier. Changed GetOldObjects in the IBlobStore to just fetch AllObjects (as we used it for both of those purposes) and now we include the last modified time with the object for the filtering cases. Added IBufferedPayload for reusable control of how we buffer the payloads we need to handle. This means we will now handle large (>2GB) payloads better in more places. Also added a setting to control when we start buffering to disk (defaults to 2GB payloads, e.g. payloads to large to fit into memory). Could be used to reduce the amount of memory required by Horde.Storage at the cost of requiring more temp drive space, and likely some performance as we then end up writing to disk. This buffering is required as we need to hash all objects before we store them, requiring us to have the entire payload before it can be sent anywhere. [CL 18308858 by Joakim Lindqvist in ue5-main branch]
2021-11-29 09:09:10 -05:00
using Jupiter.Common.Implementation;
using Jupiter.Implementation;
using Jupiter.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jupiter.Controllers
{
using BlobNotFoundException = Jupiter.Implementation.BlobNotFoundException;
using IDiagnosticContext = Serilog.IDiagnosticContext;
[ApiController]
[Route("api/v1/objects", Order = 0)]
[Authorize]
[Produces(CustomMediaTypeNames.UnrealCompactBinary, MediaTypeNames.Application.Json)]
public class ObjectController : ControllerBase
{
private readonly IBlobService _storage;
private readonly IDiagnosticContext _diagnosticContext;
private readonly IRequestHelper _requestHelper;
private readonly IReferenceResolver _referenceResolver;
private readonly BufferedPayloadFactory _bufferedPayloadFactory;
private readonly ILogger _logger;
public ObjectController(IBlobService storage, IDiagnosticContext diagnosticContext, IRequestHelper requestHelper, IReferenceResolver referenceResolver, BufferedPayloadFactory bufferedPayloadFactory, ILogger<ObjectController> logger)
{
_storage = storage;
_diagnosticContext = diagnosticContext;
_requestHelper = requestHelper;
_referenceResolver = referenceResolver;
_bufferedPayloadFactory = bufferedPayloadFactory;
_logger = logger;
}
[HttpGet("{ns}/{id}")]
[ProducesDefaultResponseType]
public async Task<IActionResult> GetAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
try
{
BlobContents blobContents = await _storage.GetObjectAsync(ns, id, bucketHint: null);
return File(blobContents.Stream, CustomMediaTypeNames.UnrealCompactBinary);
}
catch (BlobNotFoundException e)
{
return NotFound(new ValidationProblemDetails { Title = $"Object {e.Blob} not found" });
}
}
[HttpHead("{ns}/{id}")]
[ProducesDefaultResponseType]
public async Task<IActionResult> HeadAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
bool exists = await _storage.ExistsAsync(ns, id);
if (!exists)
{
return NotFound(new ValidationProblemDetails { Title = $"Object {id} not found" });
}
return Ok();
}
[HttpPost("{ns}/exists")]
[ProducesDefaultResponseType]
public async Task<IActionResult> ExistsMultipleAsync(
[Required] NamespaceId ns,
[Required][FromQuery] List<BlobId> id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
ConcurrentBag<BlobId> missingBlobs = new ConcurrentBag<BlobId>();
IEnumerable<Task> tasks = id.Select(async blob =>
{
if (!await _storage.ExistsAsync(ns, blob))
{
missingBlobs.Add(blob);
}
});
await Task.WhenAll(tasks);
return Ok(new HeadMultipleResponse { Needs = missingBlobs.ToArray() });
}
[HttpPost("{ns}/exist")]
[ProducesDefaultResponseType]
public async Task<IActionResult> ExistsBodyAsync(
[Required] NamespaceId ns,
[FromBody] BlobId[] bodyIds)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
ConcurrentBag<BlobId> missingBlobs = new ConcurrentBag<BlobId>();
IEnumerable<Task> tasks = bodyIds.Select(async blob =>
{
if (!await _storage.ExistsAsync(ns, blob))
{
missingBlobs.Add(blob);
}
});
await Task.WhenAll(tasks);
return Ok(new HeadMultipleResponse { Needs = missingBlobs.ToArray() });
}
[HttpPut("{ns}/{id}")]
[RequiredContentType(CustomMediaTypeNames.UnrealCompactBinary)]
public async Task<IActionResult> PutAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.WriteObject });
if (result != null)
{
return result;
}
_diagnosticContext.Set("Content-Length", Request.ContentLength ?? -1);
try
{
using IBufferedPayload payload = await _bufferedPayloadFactory.CreateFromRequestAsync(Request, HttpContext.RequestAborted);
BlobId identifier = await _storage.PutObjectAsync(ns, payload, id, bucketHint: null, HttpContext.RequestAborted);
return Ok(new PutBlobResponse(identifier));
}
catch (ClientSendSlowException e)
{
return Problem(e.Message, null, (int)HttpStatusCode.RequestTimeout);
}
}
[HttpGet("{ns}/{id}/references")]
public async Task<IActionResult> ResolveReferencesAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
BlobContents blob;
try
{
blob = await _storage.GetObjectAsync(ns, id, bucketHint: null);
}
catch (BlobNotFoundException e)
{
return NotFound(new ValidationProblemDetails { Title = $"Object {e.Blob} not found" });
}
byte[] blobContents = await blob.Stream.ToByteArrayAsync(HttpContext.RequestAborted);
if (blobContents.Length == 0)
{
_logger.LogWarning("0 byte object found for {Id} {Namespace}", id, ns);
}
CbObject compactBinaryObject;
try
{
compactBinaryObject = new CbObject(blobContents);
}
catch (IndexOutOfRangeException)
{
return Problem(title: $"{id} was not a proper compact binary object.", detail: "Index out of range");
}
try
{
BlobId[] references = await _referenceResolver.GetReferencedBlobsAsync(ns, compactBinaryObject).ToArrayAsync();
return Ok(new ResolvedReferencesResult(references));
}
catch (PartialReferenceResolveException e)
{
return BadRequest(new ValidationProblemDetails { Title = $"Object {id} is missing content ids", Detail = $"Following content ids are invalid: {string.Join(",", e.UnresolvedReferences)}" });
}
catch (ReferenceIsMissingBlobsException e)
{
return BadRequest(new ValidationProblemDetails { Title = $"Object {id} is missing blobs", Detail = $"Following blobs are missing: {string.Join(",", e.MissingBlobs)}" });
}
}
[HttpDelete("{ns}/{id}")]
public async Task<IActionResult> DeleteAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.DeleteObject });
if (result != null)
{
return result;
}
await _storage.DeleteObjectAsync(ns, id, HttpContext.RequestAborted);
return Ok(new DeletedResponse
{
DeletedCount = 1
});
}
[HttpDelete("{ns}")]
public async Task<IActionResult> DeleteNamespaceAsync(
[Required] NamespaceId ns)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.DeleteNamespace });
if (result != null)
{
return result;
}
await _storage.DeleteNamespaceAsync(ns, HttpContext.RequestAborted);
return Ok();
}
}
public class PutBlobResponse
{
public PutBlobResponse()
{
Identifier = null!;
}
public PutBlobResponse(BlobId identifier)
{
Identifier = identifier;
}
[CbField("identifier")]
public BlobId Identifier { get; set; }
}
public class DeletedResponse
{
public int DeletedCount { get; set; }
}
public class ResolvedReferencesResult
{
public ResolvedReferencesResult()
{
References = null!;
}
public ResolvedReferencesResult(BlobId[] references)
{
References = references;
}
[CbField("references")]
public BlobId[] References { get; set; }
}
}