Files
UnrealEngine/Engine/Source/Developer/DerivedDataCache/Private/S3CacheStore.cpp
2025-05-18 13:04:45 +08:00

1927 lines
58 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DerivedDataCacheStore.h"
#include "DerivedDataLegacyCacheStore.h"
#if WITH_S3_DDC_BACKEND
#if PLATFORM_MICROSOFT
#include "Microsoft/AllowMicrosoftPlatformTypes.h"
#endif
#if PLATFORM_MICROSOFT
#include "Microsoft/HideMicrosoftPlatformTypes.h"
#endif
#include "Algo/Transform.h"
#include "Async/ParallelFor.h"
#include "DerivedDataBackendInterface.h"
#include "DerivedDataCacheRecord.h"
#include "DerivedDataCachePrivate.h"
#include "DerivedDataChunk.h"
#include "DesktopPlatformModule.h"
#include "Dom/JsonObject.h"
#include "HAL/PlatformFile.h"
#include "HAL/PlatformFileManager.h"
#include "HashingArchiveProxy.h"
#include "Memory/SharedBuffer.h"
#include "Misc/Base64.h"
#include "Misc/Compression.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FeedbackContext.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/SecureHash.h"
#include "ProfilingDebugging/CountersTrace.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#include "Serialization/CompactBinary.h"
#include "Serialization/CompactBinaryPackage.h"
#include "Serialization/CompactBinaryValidation.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "curl/curl.h"
#if WITH_SSL
#include "Ssl.h"
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <openssl/ssl.h>
#endif
#define S3DDC_BACKEND_WAIT_INTERVAL 0.01f
#define S3DDC_HTTP_REQUEST_TIMEOUT_SECONDS 30L
#define S3DDC_HTTP_REQUEST_TIMOUT_ENABLED 1
#define S3DDC_REQUEST_POOL_SIZE 16
#define S3DDC_MAX_FAILED_LOGIN_ATTEMPTS 16
#define S3DDC_MAX_ATTEMPTS 4
#define S3DDC_MAX_BUFFER_RESERVE 104857600u
namespace UE::DerivedData
{
TRACE_DECLARE_ATOMIC_INT_COUNTER(S3DDC_Get, TEXT("S3DDC Get"));
TRACE_DECLARE_ATOMIC_INT_COUNTER(S3DDC_GetHit, TEXT("S3DDC Get Hit"));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void BuildPathForCachePackage(const FCacheKey& CacheKey, FStringBuilderBase& Path);
void BuildPathForCacheContent(const FIoHash& RawHash, FStringBuilderBase& Path);
class FStringAnsi
{
public:
FStringAnsi()
{
Inner.Add(0);
}
FStringAnsi(const ANSICHAR* Text)
{
Inner.Append(Text, FCStringAnsi::Strlen(Text) + 1);
}
void Append(ANSICHAR Character)
{
Inner[Inner.Num() - 1] = Character;
Inner.Add(0);
}
void Append(const FStringAnsi& Other)
{
Inner.RemoveAt(Inner.Num() - 1);
Inner.Append(Other.Inner);
}
void Append(const ANSICHAR* Text)
{
Inner.RemoveAt(Inner.Num() - 1);
Inner.Append(Text, FCStringAnsi::Strlen(Text) + 1);
}
void Append(const ANSICHAR* Start, const ANSICHAR* End)
{
Inner.RemoveAt(Inner.Num() - 1);
Inner.Append(Start, UE_PTRDIFF_TO_INT32(End - Start));
Inner.Add(0);
}
static FStringAnsi Printf(const ANSICHAR* Format, ...)
{
ANSICHAR Buffer[1024];
GET_TYPED_VARARGS(ANSICHAR, Buffer, UE_ARRAY_COUNT(Buffer), UE_ARRAY_COUNT(Buffer) - 1, Format, Format);
return Buffer;
}
FString ToWideString() const
{
return ANSI_TO_TCHAR(Inner.GetData());
}
const ANSICHAR* operator*() const
{
return Inner.GetData();
}
int32 Len() const
{
return Inner.Num() - 1;
}
private:
TArray<ANSICHAR> Inner;
};
struct FSHA256
{
uint8 Digest[32];
FStringAnsi ToString() const
{
ANSICHAR Buffer[65];
for (int Idx = 0; Idx < 32; Idx++)
{
FCStringAnsi::Sprintf(Buffer + (Idx * 2), "%02x", Digest[Idx]);
}
return Buffer;
}
};
FSHA256 Sha256(const uint8* Input, size_t InputLen)
{
FSHA256 Output;
SHA256(Input, InputLen, Output.Digest);
return Output;
}
FSHA256 HmacSha256(const uint8* Input, size_t InputLen, const uint8* Key, size_t KeyLen)
{
FSHA256 Output;
unsigned int OutputLen = 0;
HMAC(EVP_sha256(), Key, KeyLen, (const unsigned char*)Input, InputLen, Output.Digest, &OutputLen);
return Output;
}
FSHA256 HmacSha256(const FStringAnsi& Input, const uint8* Key, size_t KeyLen)
{
return HmacSha256((const uint8*)*Input, (size_t)Input.Len(), Key, KeyLen);
}
FSHA256 HmacSha256(const char* Input, const uint8* Key, size_t KeyLen)
{
return HmacSha256((const uint8*)Input, (size_t)FCStringAnsi::Strlen(Input), Key, KeyLen);
}
bool IsSuccessfulHttpResponse(long ResponseCode)
{
return (ResponseCode >= 200 && ResponseCode <= 299);
}
struct IRequestCallback
{
virtual ~IRequestCallback() = default;
virtual bool Update(int NumBytes, int TotalBytes) = 0;
};
/**
* Backend for a read-only AWS S3 based caching service.
**/
class FS3CacheStore final : public ILegacyCacheStore
{
public:
/**
* Creates the cache store, checks health status and attempts to acquire an access token.
*
* @param InRootManifestPath Local path to the JSON manifest in the workspace containing a list of files to download
* @param InBaseUrl Base URL for the bucket, with trailing slash (eg. https://foo.s3.us-east-1.amazonaws.com/)
* @param InRegion Name of the AWS region (eg. us-east-1)
* @param InCanaryObjectKey Key for a canary object used to test whether this backend is usable
* @param InCachePath Path to cache the DDC files
*/
FS3CacheStore(const TCHAR* InName, const TCHAR* InRootManifestPath, const TCHAR* InBaseUrl, const TCHAR* InRegion, const TCHAR* InCanaryObjectKey, const TCHAR* InCachePath, ICacheStoreOwner& Owner);
~FS3CacheStore() final;
inline const FString& GetName() const { return BaseUrl; }
/**
* Checks if cache store is usable (reachable and accessible).
* @return true if usable
*/
inline bool IsUsable() const { return bEnabled; }
// ICacheStore
void Put(
TConstArrayView<FCachePutRequest> Requests,
IRequestOwner& Owner,
FOnCachePutComplete&& OnComplete) final;
void Get(
TConstArrayView<FCacheGetRequest> Requests,
IRequestOwner& Owner,
FOnCacheGetComplete&& OnComplete) final;
void PutValue(
TConstArrayView<FCachePutValueRequest> Requests,
IRequestOwner& Owner,
FOnCachePutValueComplete&& OnComplete) final;
void GetValue(
TConstArrayView<FCacheGetValueRequest> Requests,
IRequestOwner& Owner,
FOnCacheGetValueComplete&& OnComplete) final;
void GetChunks(
TConstArrayView<FCacheGetChunkRequest> Requests,
IRequestOwner& Owner,
FOnCacheGetChunkComplete&& OnComplete) final;
// ILegacyCacheStore
void LegacyStats(FDerivedDataCacheStatsNode& OutNode) final;
bool LegacyDebugOptions(FBackendDebugOptions& Options) final;
private:
struct FBundle;
struct FBundleEntry;
struct FBundleDownload;
struct FRootManifest;
class FHttpRequest;
class FRequestPool;
FString RootManifestPath;
FString BaseUrl;
FString Region;
FString CanaryObjectKey;
FString CacheDir;
ICacheStoreOwner& StoreOwner;
ICacheStoreStats* StoreStats = nullptr;
TArray<FBundle> Bundles;
TUniquePtr<FRequestPool> RequestPool;
bool bEnabled;
[[nodiscard]] FOptionalCacheRecord GetCacheRecordOnly(
FStringView Name,
const FCacheKey& Key,
const FCacheRecordPolicy& Policy,
FRequestStats& Stats);
[[nodiscard]] FOptionalCacheRecord GetCacheRecord(
FStringView Name,
const FCacheKey& Key,
const FCacheRecordPolicy& Policy,
EStatus& OutStatus,
FRequestStats& Stats);
[[nodiscard]] bool GetCacheValueOnly(FStringView Name, const FCacheKey& Key, ECachePolicy Policy, FValue& OutValue, FRequestStats& Stats);
[[nodiscard]] bool GetCacheValue(FStringView Name, const FCacheKey& Key, ECachePolicy Policy, FValue& OutValue, FRequestStats& Stats);
[[nodiscard]] bool GetCacheContentExists(const FCacheKey& Key, const FIoHash& RawHash, FRequestStats& Stats) const;
[[nodiscard]] bool GetCacheContent(
FStringView Name,
const FCacheKey& Key,
const FValueId& Id,
const FValue& Value,
ECachePolicy Policy,
FValue& OutValue,
FRequestStats& Stats) const;
void GetCacheContent(
FStringView Name,
const FCacheKey& Key,
const FValueId& Id,
const FValue& Value,
ECachePolicy Policy,
FCompressedBufferReader& Reader,
TUniquePtr<FArchive>& OutArchive,
FRequestStats& Stats) const;
void BuildCachePackagePath(const FCacheKey& CacheKey, FStringBuilderBase& Path) const;
void BuildCacheContentPath(const FIoHash& RawHash, FStringBuilderBase& Path) const;
[[nodiscard]] bool LoadFileWithHash(FStringBuilderBase& Path, FStringView DebugName, FRequestStats& Stats, TFunctionRef<void(FArchive&)> ReadFunction) const;
[[nodiscard]] bool LoadFile(FStringBuilderBase& Path, FStringView DebugName, FRequestStats& Stats, TFunctionRef<void(FArchive&)> ReadFunction) const;
[[nodiscard]] TUniquePtr<FArchive> OpenFile(FStringBuilderBase& Path, FStringView DebugName, FRequestStats& Stats) const;
[[nodiscard]] bool FileExists(FStringBuilderBase& Path, FRequestStats& Stats) const;
bool DownloadManifest(const FRootManifest& RootManifest, FFeedbackContext* Context);
void RemoveUnusedBundles();
void ReadBundle(FBundle& Bundle);
FBackendDebugOptions DebugOptions;
};
/**
* Minimal HTTP request type wrapping CURL without the need for managers. This request
* is written to allow reuse of request objects, in order to allow connections to be reused.
*
* CURL has a global library initialization (curl_global_init). We rely on this happening in
* the Online/HTTP library which is a dependency on this module.
*/
class FS3CacheStore::FHttpRequest
{
public:
FHttpRequest(const ANSICHAR* InRegion, const ANSICHAR* InAccessKey, const ANSICHAR* InSecretKey)
: Region(InRegion)
, AccessKey(InAccessKey)
, SecretKey(InSecretKey)
{
Curl = curl_easy_init();
}
~FHttpRequest()
{
curl_easy_cleanup(Curl);
}
/**
* Performs the request, blocking until finished.
* @param Url HTTP URL to fetch
* @param Callback Object used to convey state to/from the operation
* @param Buffer Optional buffer to directly receive the result of the request.
* If unset the response body will be stored in the request.
*/
long PerformBlocking(const ANSICHAR* Url, IRequestCallback* Callback, TArray<uint8>& OutResponseBody, FOutputDevice* Log)
{
TRACE_CPUPROFILER_EVENT_SCOPE(S3DDC_CurlPerform);
// Find the host from the URL
const ANSICHAR* ProtocolEnd = FCStringAnsi::Strchr(Url, ':');
check(ProtocolEnd != nullptr && *(ProtocolEnd + 1) == '/' && *(ProtocolEnd + 2) == '/');
const ANSICHAR* UrlHost = ProtocolEnd + 3;
const ANSICHAR* UrlHostEnd = FCStringAnsi::Strchr(UrlHost, '/');
check(UrlHostEnd != nullptr);
FStringAnsi Host;
Host.Append(UrlHost, UrlHostEnd);
// Get the header strings
FDateTime Timestamp = FDateTime::UtcNow();// FDateTime(2015, 9, 15, 12, 45, 0);
FStringAnsi TimeString = FStringAnsi::Printf("%04d%02d%02dT%02d%02d%02dZ", Timestamp.GetYear(), Timestamp.GetMonth(), Timestamp.GetDay(), Timestamp.GetHour(), Timestamp.GetMinute(), Timestamp.GetSecond());
// Payload string
FStringAnsi EmptyPayloadSha256 = Sha256(nullptr, 0).ToString();
// Create the headers
curl_slist* CurlHeaders = nullptr;
CurlHeaders = curl_slist_append(CurlHeaders, *FStringAnsi::Printf("Host: %s", *Host));
CurlHeaders = curl_slist_append(CurlHeaders, *FStringAnsi::Printf("x-amz-content-sha256: %s", *EmptyPayloadSha256));
CurlHeaders = curl_slist_append(CurlHeaders, *FStringAnsi::Printf("x-amz-date: %s", *TimeString));
CurlHeaders = curl_slist_append(CurlHeaders, *GetAuthorizationHeader("GET", UrlHostEnd, "", CurlHeaders, *TimeString, *EmptyPayloadSha256));
// Create the callback data
FStringAnsi Domain;
Domain.Append(Url, UrlHostEnd);
FCallbackData CallbackData(Domain, OutResponseBody);
// Setup the request
curl_easy_reset(Curl);
curl_easy_setopt(Curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(Curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(Curl, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(Curl, CURLOPT_URL, Url);
curl_easy_setopt(Curl, CURLOPT_ACCEPT_ENCODING, "gzip");
#if S3DDC_HTTP_REQUEST_TIMOUT_ENABLED
curl_easy_setopt(Curl, CURLOPT_CONNECTTIMEOUT, S3DDC_HTTP_REQUEST_TIMEOUT_SECONDS);
#endif
// Headers
curl_easy_setopt(Curl, CURLOPT_HTTPHEADER, CurlHeaders);
curl_easy_setopt(Curl, CURLOPT_HEADERDATA, CurlHeaders);
// Progress
curl_easy_setopt(Curl, CURLOPT_NOPROGRESS, 0);
curl_easy_setopt(Curl, CURLOPT_XFERINFODATA, Callback);
curl_easy_setopt(Curl, CURLOPT_XFERINFOFUNCTION, &FHttpRequest::StaticStatusFn);
// Response
curl_easy_setopt(Curl, CURLOPT_HEADERDATA, &CallbackData);
curl_easy_setopt(Curl, CURLOPT_HEADERFUNCTION, &FHttpRequest::StaticWriteHeaderFn);
curl_easy_setopt(Curl, CURLOPT_WRITEDATA, &CallbackData);
curl_easy_setopt(Curl, CURLOPT_WRITEFUNCTION, StaticWriteBodyFn);
// SSL options
curl_easy_setopt(Curl, CURLOPT_USE_SSL, CURLUSESSL_ALL);
curl_easy_setopt(Curl, CURLOPT_SSL_VERIFYPEER, 1);
curl_easy_setopt(Curl, CURLOPT_SSL_VERIFYHOST, 1);
curl_easy_setopt(Curl, CURLOPT_SSLCERTTYPE, "PEM");
// SSL certification verification
curl_easy_setopt(Curl, CURLOPT_CAINFO, nullptr);
curl_easy_setopt(Curl, CURLOPT_SSL_CTX_FUNCTION, *sslctx_function);
curl_easy_setopt(Curl, CURLOPT_SSL_CTX_DATA, &CallbackData);
// Send the request
CURLcode CurlResult = curl_easy_perform(Curl);
// Free the headers object
curl_slist_free_all(CurlHeaders);
curl_easy_setopt(Curl, CURLOPT_HEADERDATA, nullptr);
// Get the response code
long ResponseCode = 0;
if (CurlResult == CURLE_OK)
{
CurlResult = curl_easy_getinfo(Curl, CURLINFO_RESPONSE_CODE, &ResponseCode);
}
if (CurlResult != CURLE_OK)
{
if (CurlResult != CURLE_ABORTED_BY_CALLBACK)
{
Log->Logf(ELogVerbosity::Error, TEXT("Error while connecting to %s: %d (%s)"), ANSI_TO_TCHAR(Url), CurlResult, ANSI_TO_TCHAR(curl_easy_strerror(CurlResult)));
}
return 500;
}
// Print any diagnostic output
if (!(ResponseCode >= 200 && ResponseCode <= 299))
{
if (FAnsiStringView(Url).StartsWith("file://"))
{
ResponseCode = 200;
}
else
{
Log->Logf(ELogVerbosity::Error, TEXT("Download failed for %s (response %d):\n%s\n%s"), ANSI_TO_TCHAR(Url), ResponseCode, *CallbackData.ResponseHeader.ToWideString(), ANSI_TO_TCHAR((const ANSICHAR*)OutResponseBody.GetData()));
}
}
return ResponseCode;
}
private:
struct FCallbackData
{
const FStringAnsi& Domain;
FStringAnsi ResponseHeader;
TArray<uint8>& ResponseBody;
FCallbackData(const FStringAnsi& InDomain, TArray<uint8>& InResponseBody)
: Domain(InDomain)
, ResponseBody(InResponseBody)
{
}
};
CURL* Curl;
FStringAnsi Region;
FStringAnsi AccessKey;
FStringAnsi SecretKey;
FStringAnsi GetAuthorizationHeader(const ANSICHAR* Verb, const ANSICHAR* RelativeUrl, const ANSICHAR* QueryString, const curl_slist* Headers, const ANSICHAR* Timestamp, const ANSICHAR* Digest)
{
// Create the canonical list of headers
FStringAnsi CanonicalHeaders;
for (const curl_slist* Header = Headers; Header != nullptr; Header = Header->next)
{
const ANSICHAR* Colon = FCStringAnsi::Strchr(Header->data, ':');
if (Colon != nullptr)
{
for (const ANSICHAR* Char = Header->data; Char != Colon; Char++)
{
CanonicalHeaders.Append(FCharAnsi::ToLower(*Char));
}
CanonicalHeaders.Append(':');
const ANSICHAR* Value = Colon + 1;
while (*Value == ' ')
{
Value++;
}
for (; *Value != 0; Value++)
{
CanonicalHeaders.Append(*Value);
}
CanonicalHeaders.Append('\n');
}
}
// Create the list of signed headers
FStringAnsi SignedHeaders;
for (const curl_slist* Header = Headers; Header != nullptr; Header = Header->next)
{
const ANSICHAR* Colon = FCStringAnsi::Strchr(Header->data, ':');
if (Colon != nullptr)
{
if (SignedHeaders.Len() > 0)
{
SignedHeaders.Append(';');
}
for (const ANSICHAR* Char = Header->data; Char != Colon; Char++)
{
SignedHeaders.Append(FCharAnsi::ToLower(*Char));
}
}
}
// Build the canonical request string
FStringAnsi CanonicalRequest;
CanonicalRequest.Append(Verb);
CanonicalRequest.Append('\n');
CanonicalRequest.Append(RelativeUrl);
CanonicalRequest.Append('\n');
CanonicalRequest.Append(QueryString);
CanonicalRequest.Append('\n');
CanonicalRequest.Append(CanonicalHeaders);
CanonicalRequest.Append('\n');
CanonicalRequest.Append(SignedHeaders);
CanonicalRequest.Append('\n');
CanonicalRequest.Append(Digest);
// Get the date
FStringAnsi DateString;
for (int32 Idx = 0; Timestamp[Idx] != 0 && Timestamp[Idx] != 'T'; Idx++)
{
DateString.Append(Timestamp[Idx]);
}
// Generate the signature key
FStringAnsi Key = FStringAnsi::Printf("AWS4%s", *SecretKey);
FSHA256 DateHash = HmacSha256(DateString, (const uint8*)*Key, Key.Len());
FSHA256 RegionHash = HmacSha256(Region, DateHash.Digest, sizeof(DateHash.Digest));
FSHA256 ServiceHash = HmacSha256("s3", RegionHash.Digest, sizeof(RegionHash.Digest));
FSHA256 SigningKeyHash = HmacSha256("aws4_request", ServiceHash.Digest, sizeof(ServiceHash.Digest));
// Calculate the signature
FStringAnsi DateRequest = FStringAnsi::Printf("%s/%s/s3/aws4_request", *DateString, *Region);
FStringAnsi CanonicalRequestSha256 = Sha256((const uint8*)*CanonicalRequest, CanonicalRequest.Len()).ToString();
FStringAnsi StringToSign = FStringAnsi::Printf("AWS4-HMAC-SHA256\n%s\n%s\n%s", Timestamp, *DateRequest, *CanonicalRequestSha256);
FStringAnsi Signature = HmacSha256(*StringToSign, SigningKeyHash.Digest, sizeof(SigningKeyHash.Digest)).ToString();
// Format the final header
return FStringAnsi::Printf("Authorization: AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", *AccessKey, *DateRequest, *SignedHeaders, *Signature);
}
/**
* Returns the response buffer as a string. Note that is the request is performed
* with an external buffer as target buffer this string will be empty.
*/
static FString GetResponseAsString(const TArray<uint8>& Buffer)
{
FUTF8ToTCHAR TCHARData(reinterpret_cast<const ANSICHAR*>(Buffer.GetData()), Buffer.Num());
return FString::ConstructFromPtrSize(TCHARData.Get(), TCHARData.Length());
}
static int StaticStatusFn(void* Ptr, curl_off_t TotalDownloadSize, curl_off_t CurrentDownloadSize, curl_off_t TotalUploadSize, curl_off_t CurrentUploadSize)
{
IRequestCallback* Callback = (IRequestCallback*)Ptr;
if (Callback != nullptr)
{
return Callback->Update(IntCastChecked<int>(CurrentDownloadSize), IntCastChecked<int>(TotalDownloadSize)) ? 0 : 1;
}
return 0;
}
static size_t StaticWriteHeaderFn(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* UserData)
{
const size_t WriteSize = SizeInBlocks * BlockSizeInBytes;
if (WriteSize > 0)
{
FCallbackData* CallbackData = static_cast<FCallbackData*>(UserData);
CallbackData->ResponseHeader.Append((const ANSICHAR*)Ptr, (const ANSICHAR*)Ptr + WriteSize);
return WriteSize;
}
return 0;
}
static size_t StaticWriteBodyFn(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* UserData)
{
const size_t WriteSize = SizeInBlocks * BlockSizeInBytes;
if (WriteSize > 0)
{
FCallbackData* CallbackData = static_cast<FCallbackData*>(UserData);
// If this is the first part of the body being received, try to reserve
// memory if content length is defined in the header.
if (CallbackData->ResponseBody.Num() == 0)
{
static const ANSICHAR Prefix[] = "Content-Length: ";
static const size_t PrefixLen = UE_ARRAY_COUNT(Prefix) - 1;
for(const ANSICHAR* Header = *CallbackData->ResponseHeader;;Header++)
{
// Check this header
if (FCStringAnsi::Strnicmp(Header, Prefix, PrefixLen) == 0)
{
size_t ContentLength = (size_t)atol(Header + PrefixLen);
if (ContentLength > 0u && ContentLength < S3DDC_MAX_BUFFER_RESERVE)
{
CallbackData->ResponseBody.Reserve(ContentLength);
}
break;
}
// Move to the next string
Header = FCStringAnsi::Strchr(Header, '\n');
if (Header == nullptr)
{
break;
}
}
}
// Write to the target buffer
CallbackData->ResponseBody.Append((const uint8*)Ptr, WriteSize);
return WriteSize;
}
return 0;
}
static int SslCertVerify(int PreverifyOk, X509_STORE_CTX* Context)
{
if (PreverifyOk == 1)
{
SSL* Handle = static_cast<SSL*>(X509_STORE_CTX_get_ex_data(Context, SSL_get_ex_data_X509_STORE_CTX_idx()));
check(Handle);
SSL_CTX* SslContext = SSL_get_SSL_CTX(Handle);
check(SslContext);
FCallbackData* CallbackData = reinterpret_cast<FCallbackData*>(SSL_CTX_get_app_data(SslContext));
check(CallbackData);
if (!FSslModule::Get().GetCertificateManager().VerifySslCertificates(Context, *CallbackData->Domain))
{
PreverifyOk = 0;
}
}
return PreverifyOk;
}
static CURLcode sslctx_function(CURL* curl, void* sslctx, void* parm)
{
SSL_CTX* Context = static_cast<SSL_CTX*>(sslctx);
const ISslCertificateManager& CertificateManager = FSslModule::Get().GetCertificateManager();
CertificateManager.AddCertificatesToSslContext(Context);
SSL_CTX_set_verify(Context, SSL_CTX_get_verify_mode(Context), SslCertVerify);
SSL_CTX_set_app_data(Context, parm);
/* all set to go */
return CURLE_OK;
}
};
//----------------------------------------------------------------------------------------------------------
// FS3CacheStore::FRequestPool
//----------------------------------------------------------------------------------------------------------
class FS3CacheStore::FRequestPool
{
public:
FRequestPool(const ANSICHAR* Region, const ANSICHAR* AccessKey, const ANSICHAR* SecretKey)
{
Pool.SetNum(S3DDC_REQUEST_POOL_SIZE);
for (uint8 i = 0; i < Pool.Num(); ++i)
{
Pool[i].Usage = 0u;
Pool[i].Request = new FHttpRequest(Region, AccessKey, SecretKey);
}
}
~FRequestPool()
{
for (uint8 i = 0; i < Pool.Num(); ++i)
{
// No requests should be in use by now.
check(Pool[i].Usage.Load(EMemoryOrder::Relaxed) == 0u);
delete Pool[i].Request;
}
}
long Download(const TCHAR* Url, IRequestCallback* Callback, TArray<uint8>& OutData, FOutputDevice* Log)
{
FHttpRequest* Request = WaitForFreeRequest();
long ResponseCode = Request->PerformBlocking(TCHAR_TO_ANSI(Url), Callback, OutData, Log);
ReleaseRequestToPool(Request);
return ResponseCode;
}
private:
struct FEntry
{
TAtomic<uint8> Usage;
FHttpRequest* Request;
};
TArray<FEntry> Pool;
FHttpRequest* WaitForFreeRequest()
{
TRACE_CPUPROFILER_EVENT_SCOPE(S3DDC_WaitForConnPool);
while (true)
{
for (uint8 i = 0; i < Pool.Num(); ++i)
{
if (!Pool[i].Usage.Load(EMemoryOrder::Relaxed))
{
uint8 Expected = 0u;
if (Pool[i].Usage.CompareExchange(Expected, 1u))
{
return Pool[i].Request;
}
}
}
FPlatformProcess::Sleep(S3DDC_BACKEND_WAIT_INTERVAL);
}
}
void ReleaseRequestToPool(FHttpRequest* Request)
{
for (uint8 i = 0; i < Pool.Num(); ++i)
{
if (Pool[i].Request == Request)
{
uint8 Expected = 1u;
Pool[i].Usage.CompareExchange(Expected, 0u);
return;
}
}
check(false);
}
};
//----------------------------------------------------------------------------------------------------------
// FS3CacheStore::FBundleEntry
//----------------------------------------------------------------------------------------------------------
struct FS3CacheStore::FBundleEntry
{
int64 Offset;
int32 Length;
};
//----------------------------------------------------------------------------------------------------------
// FS3CacheStore::FBundle
//----------------------------------------------------------------------------------------------------------
struct FS3CacheStore::FBundle
{
FString Name;
FString ObjectKey;
FString LocalFile;
int32 CompressedLength;
int32 UncompressedLength;
TMap<FSHAHash, FBundleEntry> Entries;
};
//----------------------------------------------------------------------------------------------------------
// FS3CacheStore::FBundleDownloadInfo
//----------------------------------------------------------------------------------------------------------
struct FS3CacheStore::FBundleDownload final : IRequestCallback
{
FCriticalSection& CriticalSection;
FBundle& Bundle;
FString BundleUrl;
FRequestPool& RequestPool;
FFeedbackContext* Context;
FGraphEventRef Event;
int DownloadedBytes;
FBundleDownload(FCriticalSection& InCriticalSection, FBundle& InBundle, FString InBundleUrl, FRequestPool& InRequestPool, FFeedbackContext* InContext)
: CriticalSection(InCriticalSection)
, Bundle(InBundle)
, BundleUrl(InBundleUrl)
, RequestPool(InRequestPool)
, Context(InContext)
, DownloadedBytes(0)
{
}
void Execute()
{
if (Context->ReceivedUserCancel())
{
return;
}
Context->Logf(TEXT("Downloading %s (%d bytes)"), *BundleUrl, Bundle.CompressedLength);
TArray<uint8> CompressedData;
CompressedData.Reserve(Bundle.CompressedLength);
long ResponseCode = RequestPool.Download(*BundleUrl, this, CompressedData, Context);
if(!IsSuccessfulHttpResponse(ResponseCode))
{
if (!Context->ReceivedUserCancel())
{
Context->Logf(ELogVerbosity::Warning, TEXT("Unable to download bundle %s (%d)"), *BundleUrl, ResponseCode);
}
return;
}
Context->Logf(TEXT("Decompressing %s (%d bytes)"), *BundleUrl, Bundle.UncompressedLength);
TArray<uint8> UncompressedData;
UncompressedData.SetNum(Bundle.UncompressedLength);
if (!FCompression::UncompressMemory(NAME_Gzip, UncompressedData.GetData(), Bundle.UncompressedLength, CompressedData.GetData(), CompressedData.Num()))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Unable to decompress bundle %s"), *BundleUrl);
return;
}
FString TempFile = Bundle.LocalFile + TEXT(".incoming");
if (!FFileHelper::SaveArrayToFile(UncompressedData, *TempFile))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Unable to save bundle to %s"), *TempFile);
return;
}
IFileManager::Get().Move(*Bundle.LocalFile, *TempFile);
Context->Logf(TEXT("Finished downloading %s to %s"), *BundleUrl, *Bundle.LocalFile);
}
bool Update(int NumBytes, int MaxBytes) final
{
FScopeLock Lock(&CriticalSection);
DownloadedBytes = NumBytes;
return !Context->ReceivedUserCancel();
}
};
//----------------------------------------------------------------------------------------------------------
// FS3CacheStore::FRootManifest
//----------------------------------------------------------------------------------------------------------
struct FS3CacheStore::FRootManifest
{
FString AccessKey;
FString SecretKey;
TArray<FString> Keys;
bool Load(const FString& InRootManifestPath)
{
// Read the root manifest from disk
FString RootManifestText;
if (!FFileHelper::LoadFileToString(RootManifestText, *InRootManifestPath))
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to read manifest from %s"), *InRootManifestPath);
return false;
}
// Deserialize a JSON object from the string
TSharedPtr<FJsonObject> RootManifestObject;
if (!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(RootManifestText), RootManifestObject) || !RootManifestObject.IsValid())
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to parse manifest from %s"), *InRootManifestPath);
return false;
}
// Read the access and secret keys
if (!RootManifestObject->TryGetStringField(TEXT("AccessKey"), AccessKey))
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Root manifest %s does not specify AccessKey"), *InRootManifestPath);
return false;
}
if (!RootManifestObject->TryGetStringField(TEXT("SecretKey"), SecretKey))
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Root manifest %s does not specify SecretKey"), *InRootManifestPath);
return false;
}
// Parse out the list of manifests
const TArray<TSharedPtr<FJsonValue>>* RootEntriesArray;
if (!RootManifestObject->TryGetArrayField(TEXT("Entries"), RootEntriesArray))
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Root manifest from %s is missing entries array"), *InRootManifestPath);
return false;
}
for (const TSharedPtr<FJsonValue>& Value : *RootEntriesArray)
{
const TSharedPtr<FJsonObject>& LastRootManifestEntry = (*RootEntriesArray)[RootEntriesArray->Num() - 1]->AsObject();
Keys.Add(LastRootManifestEntry->GetStringField(TEXT("Key")));
}
return true;
}
};
//----------------------------------------------------------------------------------------------------------
// FS3CacheStore
//----------------------------------------------------------------------------------------------------------
FS3CacheStore::FS3CacheStore(const TCHAR* InName, const TCHAR* InRootManifestPath, const TCHAR* InBaseUrl, const TCHAR* InRegion, const TCHAR* InCanaryObjectKey, const TCHAR* InCachePath, ICacheStoreOwner& InOwner)
: RootManifestPath(InRootManifestPath)
, BaseUrl(InBaseUrl)
, Region(InRegion)
, CanaryObjectKey(InCanaryObjectKey)
, CacheDir(InCachePath)
, StoreOwner(InOwner)
, bEnabled(false)
{
FRootManifest RootManifest;
if (RootManifest.Load(InRootManifestPath))
{
RequestPool.Reset(new FRequestPool(TCHAR_TO_ANSI(InRegion), TCHAR_TO_ANSI(*RootManifest.AccessKey), TCHAR_TO_ANSI(*RootManifest.SecretKey)));
FString LocalManifestPath;
if (RootManifest.Keys.Last().StartsWith(TEXT("file://")))
{
LocalManifestPath = FPaths::GetPath(RootManifest.Keys.Last());
}
// Test whether we can reach the canary URL
bool bCanaryValid = true;
if (LocalManifestPath.IsEmpty())
{
if (GIsBuildMachine)
{
UE_LOG(LogDerivedDataCache, Log, TEXT("S3DerivedDataBackend: Disabling on build machine"));
bCanaryValid = false;
}
else if (CanaryObjectKey.Len() > 0)
{
TArray<uint8> Data;
FStringOutputDevice DummyOutputDevice;
if (!IsSuccessfulHttpResponse(RequestPool->Download(*(BaseUrl / CanaryObjectKey), nullptr, Data, &DummyOutputDevice)))
{
UE_LOG(LogDerivedDataCache, Log, TEXT("S3DerivedDataBackend: Unable to download canary file. Disabling."));
bCanaryValid = false;
}
}
}
// Allow the user to override it from the editor
bool bSetting;
if (GConfig->GetBool(TEXT("/Script/UnrealEd.EditorSettings"), TEXT("bEnableS3DDC"), bSetting, GEditorSettingsIni) && !bSetting)
{
UE_LOG(LogDerivedDataCache, Log, TEXT("S3DerivedDataBackend: Disabling due to config setting"));
bCanaryValid = false;
}
// Try to read the bundles
if (bCanaryValid)
{
UE_LOG(LogDerivedDataCache, Log, TEXT("Using %s S3 backend at %s"), *Region, *BaseUrl);
FFeedbackContext* Context = FDesktopPlatformModule::Get()->GetNativeFeedbackContext();
Context->BeginSlowTask(NSLOCTEXT("S3DerivedDataBackend", "DownloadingDDCBundles", "Downloading DDC bundles..."), true, true);
if (DownloadManifest(RootManifest, Context))
{
// Get the path for each bundle that needs downloading
for (FBundle& Bundle : Bundles)
{
Bundle.LocalFile = CacheDir / Bundle.Name;
}
// Remove any bundles that are no longer required
RemoveUnusedBundles();
// Create a critical section used for updating download state
FCriticalSection CriticalSection;
// Create all the download tasks
TArray<TSharedPtr<FBundleDownload>> Downloads;
for (FBundle& Bundle : Bundles)
{
if (!FPaths::FileExists(Bundle.LocalFile))
{
FString BundleUrl = LocalManifestPath.IsEmpty() ? BaseUrl + Bundle.ObjectKey : LocalManifestPath / Bundle.Name + TEXT(".gz");
TSharedPtr<FBundleDownload> Download(new FBundleDownload(CriticalSection, Bundle, BundleUrl, *RequestPool.Get(), Context));
Download->Event = FFunctionGraphTask::CreateAndDispatchWhenReady([Download]() { Download->Execute(); }, TStatId());
Downloads.Add(MoveTemp(Download));
}
}
// Loop until the downloads have all finished
for (bool bComplete = false; !bComplete; )
{
FPlatformProcess::Sleep(0.1f);
int64 NumBytes = 0;
int64 MaxBytes = 0;
bComplete = true;
CriticalSection.Lock();
for (TSharedPtr<FBundleDownload>& Download : Downloads)
{
NumBytes += Download->DownloadedBytes;
MaxBytes += Download->Bundle.CompressedLength;
bComplete &= Download->Event->IsComplete();
}
CriticalSection.Unlock();
int NumMB = (int)((NumBytes + (1024 * 1024) - 1) / (1024 * 1024));
int MaxMB = (int)((MaxBytes + (1024 * 1024) - 1) / (1024 * 1024));
if (MaxBytes > 0)
{
FText StatusText = FText::Format(NSLOCTEXT("S3DerivedDataBackend", "DownloadingDDCBundlesPct", "Downloading DDC bundles... ({0}Mb/{1}Mb)"), NumMB, MaxMB);
Context->StatusUpdate((int)((NumBytes * 1000) / MaxBytes), 1000, StatusText);
}
}
// Mount all the bundles
ParallelFor(Bundles.Num(), [this](int32 Index) { ReadBundle(Bundles[Index]); });
bEnabled = true;
constexpr ECacheStoreFlags Flags = ECacheStoreFlags::Local | ECacheStoreFlags::Query | ECacheStoreFlags::StopStore;
StoreOwner.Add(this, Flags);
StoreStats = StoreOwner.CreateStats(this, Flags, TEXTVIEW("S3"), InName, BaseUrl);
}
Context->EndSlowTask();
}
}
}
FS3CacheStore::~FS3CacheStore()
{
if (StoreStats)
{
StoreOwner.DestroyStats(StoreStats);
}
}
void FS3CacheStore::LegacyStats(FDerivedDataCacheStatsNode& OutNode)
{
checkNoEntry();
}
bool FS3CacheStore::DownloadManifest(const FRootManifest& RootManifest, FFeedbackContext* Context)
{
// Read the root manifest from disk
if (RootManifest.Keys.Num() == 0)
{
Context->Logf(ELogVerbosity::Warning, TEXT("Root manifest has empty entries array"));
return false;
}
// Get the object key for the last entry
FString BundleManifestKey = RootManifest.Keys.Last();
// Download the bundle manifest
FString BundleManifestUrl = BundleManifestKey.StartsWith(TEXT("file://")) ? BundleManifestKey : BaseUrl + BundleManifestKey;
TArray<uint8> BundleManifestData;
long ResponseCode = RequestPool->Download(*BundleManifestUrl, nullptr, BundleManifestData, Context);
if (!IsSuccessfulHttpResponse(ResponseCode))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Unable to download bundle manifest from %s (%d)"), *BundleManifestKey, (int)ResponseCode);
return false;
}
// Convert it to text
BundleManifestData.Add(0);
FString BundleManifestText = ANSI_TO_TCHAR((const ANSICHAR*)BundleManifestData.GetData());
// Deserialize a JSON object from the string
TSharedPtr<FJsonObject> BundleManifestObject;
if (!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(BundleManifestText), BundleManifestObject) || !BundleManifestObject.IsValid())
{
Context->Logf(ELogVerbosity::Warning, TEXT("Unable to parse manifest from %s"), *BundleManifestKey);
return false;
}
// Parse out the list of bundles
const TArray<TSharedPtr<FJsonValue>>* BundlesArray;
if (!BundleManifestObject->TryGetArrayField(TEXT("Entries"), BundlesArray))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Manifest from %s is missing bundles array"), *BundleManifestKey);
return false;
}
// Parse out each bundle
for (const TSharedPtr<FJsonValue>& BundleValue : *BundlesArray)
{
const FJsonObject& BundleObject = *BundleValue->AsObject();
FBundle Bundle;
if (!BundleObject.TryGetStringField(TEXT("Name"), Bundle.Name))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Manifest from %s is missing a bundle name"), *BundleManifestKey);
return false;
}
if (!BundleObject.TryGetStringField(TEXT("ObjectKey"), Bundle.ObjectKey))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Manifest from %s is missing an bundle object key"), *BundleManifestKey);
return false;
}
if (!BundleObject.TryGetNumberField(TEXT("CompressedLength"), Bundle.CompressedLength))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Manifest from %s is missing the compressed length"), *BundleManifestKey);
return false;
}
if (!BundleObject.TryGetNumberField(TEXT("UncompressedLength"), Bundle.UncompressedLength))
{
Context->Logf(ELogVerbosity::Warning, TEXT("Manifest from %s is missing the uncompressed length"), *BundleManifestKey);
return false;
}
Bundles.Add(MoveTemp(Bundle));
}
return true;
}
void FS3CacheStore::RemoveUnusedBundles()
{
IFileManager& FileManager = IFileManager::Get();
// Find all the files we want to keep
TSet<FString> KeepFiles;
for (const FBundle& Bundle : Bundles)
{
KeepFiles.Add(Bundle.Name);
}
// Find all the files on disk
TArray<FString> Files;
FileManager.FindFiles(Files, *CacheDir);
// Remove anything left over
for (const FString& File : Files)
{
if (!KeepFiles.Contains(File))
{
FileManager.Delete(*(CacheDir / File));
}
}
}
void FS3CacheStore::ReadBundle(FBundle& Bundle)
{
IFileManager& FileManager = IFileManager::Get();
// Open the file for reading. If this fails, assume it's because the download was aborted.
TUniquePtr<FArchive> Reader(FileManager.CreateFileReader(*Bundle.LocalFile));
if (!Reader.IsValid() || Reader->IsError())
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to open bundle %s for reading. Ignoring."), *Bundle.LocalFile);
return;
}
struct FFileHeader
{
uint32 Signature;
int32 NumRecords;
};
FFileHeader Header;
Reader->Serialize(&Header, sizeof(Header));
const uint32 BundleSignature = (uint32)'D' | ((uint32)'D' << 8) | ((uint32)'B' << 16);
const uint32 BundleSignatureV1 = BundleSignature | (1U << 24);
if (Header.Signature != BundleSignatureV1)
{
UE_LOG(LogDerivedDataCache, Warning, TEXT("Unable to read bundle with signature %08x"), Header.Signature);
return;
}
struct FFileRecord
{
FSHAHash Hash;
uint32 Length;
};
TArray<FFileRecord> Records;
Records.SetNum(Header.NumRecords);
Reader->Serialize(Records.GetData(), Header.NumRecords * sizeof(FFileRecord));
Bundle.Entries.Reserve(Records.Num());
int64 Offset = Reader->Tell();
for (const FFileRecord& Record : Records)
{
FBundleEntry& Entry = Bundle.Entries.Add(Record.Hash);
Entry.Offset = Offset;
Entry.Length = Record.Length;
Offset += Record.Length;
check(Offset <= Bundle.UncompressedLength);
}
}
bool FS3CacheStore::LegacyDebugOptions(FBackendDebugOptions& InOptions)
{
DebugOptions = InOptions;
return true;
}
FOptionalCacheRecord FS3CacheStore::GetCacheRecordOnly(
const FStringView Name,
const FCacheKey& Key,
const FCacheRecordPolicy& Policy,
FRequestStats& Stats)
{
if (!IsUsable())
{
UE_LOG(LogDerivedDataCache, VeryVerbose,
TEXT("%s: Skipped get of %s from '%.*s' because this cache store is not available"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return FOptionalCacheRecord();
}
// Skip the request if querying the cache is disabled.
if (!EnumHasAnyFlags(Policy.GetRecordPolicy(), ECachePolicy::QueryLocal))
{
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Skipped get of %s from '%.*s' due to cache policy"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return FOptionalCacheRecord();
}
if (DebugOptions.ShouldSimulateGetMiss(Key))
{
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Simulated miss for get of %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return FOptionalCacheRecord();
}
TStringBuilder<256> Path;
BuildCachePackagePath(Key, Path);
FOptionalCacheRecord Record;
{
FCbPackage Package;
if (!LoadFileWithHash(Path, Name, Stats, [&Package](FArchive& Ar) { Package.TryLoad(Ar); }))
{
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache miss with missing package for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return Record;
}
if (ValidateCompactBinary(Package, ECbValidateMode::Default | ECbValidateMode::Package) != ECbValidateError::None)
{
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with invalid package for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return Record;
}
Record = FCacheRecord::Load(Package);
if (Record.IsNull())
{
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with record load failure for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return Record;
}
}
return Record;
}
FOptionalCacheRecord FS3CacheStore::GetCacheRecord(
const FStringView Name,
const FCacheKey& Key,
const FCacheRecordPolicy& Policy,
EStatus& OutStatus,
FRequestStats& Stats)
{
FOptionalCacheRecord Record = GetCacheRecordOnly(Name, Key, Policy, Stats);
if (Record.IsNull())
{
OutStatus = EStatus::Error;
return Record;
}
OutStatus = EStatus::Ok;
FCacheRecordBuilder RecordBuilder(Key);
const ECachePolicy RecordPolicy = Policy.GetRecordPolicy();
if (!EnumHasAnyFlags(RecordPolicy, ECachePolicy::SkipMeta))
{
RecordBuilder.SetMeta(FCbObject(Record.Get().GetMeta()));
}
for (const FValueWithId& Value : Record.Get().GetValues())
{
const FValueId& Id = Value.GetId();
const ECachePolicy ValuePolicy = Policy.GetValuePolicy(Id);
FValue Content;
if (GetCacheContent(Name, Key, Id, Value, ValuePolicy, Content, Stats))
{
RecordBuilder.AddValue(Id, MoveTemp(Content));
}
else if (EnumHasAnyFlags(RecordPolicy, ECachePolicy::PartialRecord))
{
OutStatus = EStatus::Error;
RecordBuilder.AddValue(Value);
}
else
{
OutStatus = EStatus::Error;
return FOptionalCacheRecord();
}
}
return RecordBuilder.Build();
}
bool FS3CacheStore::GetCacheValueOnly(
const FStringView Name,
const FCacheKey& Key,
const ECachePolicy Policy,
FValue& OutValue,
FRequestStats& Stats)
{
if (!IsUsable())
{
UE_LOG(LogDerivedDataCache, VeryVerbose,
TEXT("%s: Skipped get of %s from '%.*s' because this cache store is not available"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
// Skip the request if querying the cache is disabled.
if (!EnumHasAnyFlags(Policy, ECachePolicy::QueryLocal))
{
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Skipped get of %s from '%.*s' due to cache policy"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
if (DebugOptions.ShouldSimulateGetMiss(Key))
{
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Simulated miss for get of %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
TStringBuilder<256> Path;
BuildCachePackagePath(Key, Path);
FCbPackage Package;
if (!LoadFileWithHash(Path, Name, Stats, [&Package](FArchive& Ar) { Package.TryLoad(Ar); }))
{
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache miss with missing package for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
if (ValidateCompactBinary(Package, ECbValidateMode::Default | ECbValidateMode::Package) != ECbValidateError::None)
{
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with invalid package for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
const FCbObjectView Object = Package.GetObject();
const FIoHash RawHash = Object["RawHash"].AsHash();
const uint64 RawSize = Object["RawSize"].AsUInt64(MAX_uint64);
if (RawHash.IsZero() || RawSize == MAX_uint64)
{
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with invalid value for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
if (const FCbAttachment* const Attachment = Package.FindAttachment(RawHash))
{
const FCompressedBuffer& Data = Attachment->AsCompressedBinary();
if (Data.GetRawHash() != RawHash || Data.GetRawSize() != RawSize)
{
UE_LOG(LogDerivedDataCache, Display,
TEXT("%s: Cache miss with invalid value attachment for %s from '%.*s'"),
*GetName(), *WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
OutValue = FValue(Data);
}
else
{
OutValue = FValue(RawHash, RawSize);
}
return true;
}
bool FS3CacheStore::GetCacheValue(
const FStringView Name,
const FCacheKey& Key,
const ECachePolicy Policy,
FValue& OutValue,
FRequestStats& Stats)
{
return GetCacheValueOnly(Name, Key, Policy, OutValue, Stats) && GetCacheContent(Name, Key, {}, OutValue, Policy, OutValue, Stats);
}
bool FS3CacheStore::GetCacheContentExists(const FCacheKey& Key, const FIoHash& RawHash, FRequestStats& Stats) const
{
TStringBuilder<256> Path;
BuildCacheContentPath(RawHash, Path);
return FileExists(Path, Stats);
}
bool FS3CacheStore::GetCacheContent(
const FStringView Name,
const FCacheKey& Key,
const FValueId& Id,
const FValue& Value,
const ECachePolicy Policy,
FValue& OutValue,
FRequestStats& Stats) const
{
if (!EnumHasAnyFlags(Policy, ECachePolicy::Query))
{
OutValue = Value.RemoveData();
return true;
}
if (Value.HasData())
{
OutValue = EnumHasAnyFlags(Policy, ECachePolicy::SkipData) ? Value.RemoveData() : Value;
return true;
}
const FIoHash& RawHash = Value.GetRawHash();
TStringBuilder<256> Path;
BuildCacheContentPath(RawHash, Path);
if (EnumHasAnyFlags(Policy, ECachePolicy::SkipData))
{
if (FileExists(Path, Stats))
{
OutValue = Value;
return true;
}
}
else
{
FCompressedBuffer CompressedBuffer;
if (LoadFileWithHash(Path, Name, Stats, [&CompressedBuffer](FArchive& Ar) { CompressedBuffer = FCompressedBuffer::Load(Ar); }))
{
if (CompressedBuffer.GetRawHash() == RawHash)
{
OutValue = FValue(MoveTemp(CompressedBuffer));
return true;
}
UE_LOG(LogDerivedDataCache, Display,
TEXT("%s: Cache miss with corrupted value %s with hash %s for %s from '%.*s'"),
*GetName(), *WriteToString<16>(Id), *WriteToString<48>(RawHash),
*WriteToString<96>(Key), Name.Len(), Name.GetData());
return false;
}
}
UE_LOG(LogDerivedDataCache, Verbose,
TEXT("%s: Cache miss with missing value %s with hash %s for %s from '%.*s'"),
*GetName(), *WriteToString<16>(Id), *WriteToString<48>(RawHash), *WriteToString<96>(Key),
Name.Len(), Name.GetData());
return false;
}
void FS3CacheStore::GetCacheContent(
const FStringView Name,
const FCacheKey& Key,
const FValueId& Id,
const FValue& Value,
const ECachePolicy Policy,
FCompressedBufferReader& Reader,
TUniquePtr<FArchive>& OutArchive,
FRequestStats& Stats) const
{
class FStatsArchive final : TUniquePtr<FArchive>, public FArchiveProxy
{
public:
FStatsArchive(FArchive& InArchive, FRequestStats& InStats)
: TUniquePtr<FArchive>(&InArchive)
, FArchiveProxy(InArchive)
, Stats(InStats)
{
}
void Serialize(void* V, int64 Length) final
{
Stats.PhysicalReadSize += uint64(Length);
FArchiveProxy::Serialize(V, Length);
}
private:
FRequestStats& Stats;
};
if (!EnumHasAnyFlags(Policy, ECachePolicy::Query))
{
return;
}
if (Value.HasData())
{
if (!EnumHasAnyFlags(Policy, ECachePolicy::SkipData))
{
Reader.SetSource(Value.GetData());
}
OutArchive.Reset();
return;
}
const FIoHash& RawHash = Value.GetRawHash();
TStringBuilder<256> Path;
BuildCacheContentPath(RawHash, Path);
if (EnumHasAllFlags(Policy, ECachePolicy::SkipData))
{
if (FileExists(Path, Stats))
{
return;
}
}
else
{
OutArchive = OpenFile(Path, Name, Stats);
if (OutArchive)
{
OutArchive.Reset(new FStatsArchive(*OutArchive.Release(), Stats));
UE_LOG(LogDerivedDataCache, VeryVerbose,
TEXT("%s: Opened %s from '%.*s'"),
*GetName(), *Path, Name.Len(), Name.GetData());
Reader.SetSource(*OutArchive);
if (Reader.GetRawHash() == RawHash)
{
return;
}
UE_LOG(LogDerivedDataCache, Display,
TEXT("%s: Cache miss with corrupted value %s with hash %s for %s from '%.*s'"),
*GetName(), *WriteToString<16>(Id), *WriteToString<48>(RawHash),
*WriteToString<96>(Key), Name.Len(), Name.GetData());
Reader.ResetSource();
OutArchive.Reset();
return;
}
}
UE_LOG(LogDerivedDataCache, Verbose,
TEXT("%s: Cache miss with missing value %s with hash %s for %s from '%.*s'"),
*GetName(), *WriteToString<16>(Id), *WriteToString<48>(RawHash), *WriteToString<96>(Key),
Name.Len(), Name.GetData());
}
void FS3CacheStore::BuildCachePackagePath(const FCacheKey& CacheKey, FStringBuilderBase& Path) const
{
BuildPathForCachePackage(CacheKey, Path);
}
void FS3CacheStore::BuildCacheContentPath(const FIoHash& RawHash, FStringBuilderBase& Path) const
{
BuildPathForCacheContent(RawHash, Path);
}
bool FS3CacheStore::LoadFileWithHash(
FStringBuilderBase& Path,
const FStringView DebugName,
FRequestStats& Stats,
const TFunctionRef<void (FArchive& Ar)> ReadFunction) const
{
return LoadFile(Path, DebugName, Stats, [this, &Path, &DebugName, &ReadFunction](FArchive& Ar)
{
THashingArchiveProxy<FBlake3> HashAr(Ar);
ReadFunction(HashAr);
const FBlake3Hash Hash = HashAr.GetHash();
FBlake3Hash SavedHash;
Ar << SavedHash;
if (Hash != SavedHash && !Ar.IsError())
{
Ar.SetError();
UE_LOG(LogDerivedDataCache, Display,
TEXT("%s: File %s from '%.*s' is corrupted and has hash %s when %s is expected."),
*GetName(), *Path, DebugName.Len(), DebugName.GetData(),
*WriteToString<80>(Hash), *WriteToString<80>(SavedHash));
}
});
}
bool FS3CacheStore::LoadFile(
FStringBuilderBase& Path,
const FStringView DebugName,
FRequestStats& Stats,
const TFunctionRef<void (FArchive& Ar)> ReadFunction) const
{
check(IsUsable());
const double StartTime = FPlatformTime::Seconds();
int64 ReadSize = 0;
bool bError = false;
if (TUniquePtr<FArchive> Ar = OpenFile(Path, DebugName, Stats))
{
int64 ReadStart = Ar->Tell();
ReadFunction(*Ar);
ReadSize = Ar->Tell() - ReadStart;
bError = !Ar->Close();
if (bError)
{
UE_LOG(LogDerivedDataCache, Display,
TEXT("%s: Failed to load file %s from '%.*s'."),
*GetName(), *Path, DebugName.Len(), DebugName.GetData());
}
}
const double ReadDuration = FPlatformTime::Seconds() - StartTime;
const double ReadSpeed = ReadDuration > 0.001 ? ((double)ReadSize / ReadDuration) / (1024.0 * 1024.0) : 0.0;
UE_LOG(LogDerivedDataCache, VeryVerbose,
TEXT("%s: Loaded %s from '%.*s' (%" INT64_FMT " bytes, %.02f secs, %.2f MiB/s)"),
*GetName(), *Path, DebugName.Len(), DebugName.GetData(), ReadSize, ReadDuration, ReadSpeed);
if (ReadSize > 0)
{
Stats.PhysicalReadSize += uint64(ReadSize);
}
return !bError && ReadSize > 0;
}
TUniquePtr<FArchive> FS3CacheStore::OpenFile(FStringBuilderBase& Path, const FStringView DebugName, FRequestStats& Stats) const
{
const FMonotonicTimePoint StartTime = FMonotonicTimePoint::Now();
ON_SCOPE_EXIT { Stats.AddLatency(FMonotonicTimePoint::Now() - StartTime); };
FSHAHash Hash;
TAnsiStringBuilder<256> AnsiString;
Algo::Transform(Path, AnsiString, FChar::ToUpper);
FSHA1::HashBuffer(AnsiString.GetData(), AnsiString.Len(), Hash.Hash);
for (const FBundle& Bundle : Bundles)
{
const FBundleEntry* Entry = Bundle.Entries.Find(Hash);
if (Entry != nullptr)
{
TUniquePtr<FArchive> Reader(IFileManager::Get().CreateFileReader(*Bundle.LocalFile));
if (Reader.IsValid() && !Reader->IsError())
{
Reader->Seek(Entry->Offset);
return Reader;
}
}
}
return nullptr;
}
bool FS3CacheStore::FileExists(FStringBuilderBase& Path, FRequestStats& Stats) const
{
const FMonotonicTimePoint StartTime = FMonotonicTimePoint::Now();
ON_SCOPE_EXIT { Stats.AddLatency(FMonotonicTimePoint::Now() - StartTime); };
FSHAHash Hash;
TAnsiStringBuilder<256> AnsiString;
Algo::Transform(Path, AnsiString, FChar::ToUpper);
FSHA1::HashBuffer(AnsiString.GetData(), AnsiString.Len(), Hash.Hash);
for (const FBundle& Bundle : Bundles)
{
const FBundleEntry* Entry = Bundle.Entries.Find(Hash);
if (Entry != nullptr)
{
return IFileManager::Get().FileExists(*Bundle.LocalFile);
}
}
return false;
}
void FS3CacheStore::Put(
const TConstArrayView<FCachePutRequest> Requests,
IRequestOwner& Owner,
FOnCachePutComplete&& OnComplete)
{
CompleteWithStatus(Requests, OnComplete, EStatus::Error);
}
void FS3CacheStore::Get(
const TConstArrayView<FCacheGetRequest> Requests,
IRequestOwner& Owner,
FOnCacheGetComplete&& OnComplete)
{
for (const FCacheGetRequest& Request : Requests)
{
EStatus Status = EStatus::Error;
FOptionalCacheRecord Record;
FRequestStats RequestStats;
RequestStats.Name = Request.Name;
RequestStats.Bucket = Request.Key.Bucket;
RequestStats.Type = ERequestType::Record;
RequestStats.Op = ERequestOp::Get;
{
TRACE_CPUPROFILER_EVENT_SCOPE(S3DDC_Get);
TRACE_COUNTER_INCREMENT(S3DDC_Get);
FRequestTimer RequestTimer(RequestStats);
if ((Record = GetCacheRecord(Request.Name, Request.Key, Request.Policy, Status, RequestStats)))
{
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache hit for %s from '%s'"),
*GetName(), *WriteToString<96>(Request.Key), *Request.Name);
TRACE_COUNTER_INCREMENT(S3DDC_GetHit);
}
else
{
Record = FCacheRecordBuilder(Request.Key).Build();
}
}
RequestStats.AddLogicalRead(Record.Get());
RequestStats.Status = Status;
StoreStats->AddRequest(RequestStats);
OnComplete({Request.Name, MoveTemp(Record).Get(), Request.UserData, Status});
}
}
void FS3CacheStore::PutValue(
const TConstArrayView<FCachePutValueRequest> Requests,
IRequestOwner& Owner,
FOnCachePutValueComplete&& OnComplete)
{
CompleteWithStatus(Requests, OnComplete, EStatus::Error);
}
void FS3CacheStore::GetValue(
const TConstArrayView<FCacheGetValueRequest> Requests,
IRequestOwner& Owner,
FOnCacheGetValueComplete&& OnComplete)
{
for (const FCacheGetValueRequest& Request : Requests)
{
bool bOk;
FValue Value;
FRequestStats RequestStats;
RequestStats.Name = Request.Name;
RequestStats.Bucket = Request.Key.Bucket;
RequestStats.Type = ERequestType::Value;
RequestStats.Op = ERequestOp::Get;
{
TRACE_CPUPROFILER_EVENT_SCOPE(S3DDC_GetValue);
TRACE_COUNTER_INCREMENT(S3DDC_Get);
FRequestTimer RequestTimer(RequestStats);
bOk = GetCacheValue(Request.Name, Request.Key, Request.Policy, Value, RequestStats);
if (bOk)
{
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache hit for %s from '%s'"),
*GetName(), *WriteToString<96>(Request.Key), *Request.Name);
TRACE_COUNTER_INCREMENT(S3DDC_GetHit);
}
}
RequestStats.AddLogicalRead(Value);
RequestStats.Status = bOk ? EStatus::Ok : EStatus::Error;
StoreStats->AddRequest(RequestStats);
OnComplete({Request.Name, Request.Key, Value, Request.UserData, bOk ? EStatus::Ok : EStatus::Error});
}
}
void FS3CacheStore::GetChunks(
const TConstArrayView<FCacheGetChunkRequest> Requests,
IRequestOwner& Owner,
FOnCacheGetChunkComplete&& OnComplete)
{
TArray<FCacheGetChunkRequest, TInlineAllocator<16>> SortedRequests(Requests);
SortedRequests.StableSort(TChunkLess());
bool bHasValue = false;
FValue Value;
FValueId ValueId;
FCacheKey ValueKey;
TUniquePtr<FArchive> ValueAr;
FCompressedBufferReader ValueReader;
FOptionalCacheRecord Record;
for (const FCacheGetChunkRequest& Request : SortedRequests)
{
EStatus Status = EStatus::Error;
FSharedBuffer Buffer;
uint64 RawSize = 0;
FRequestStats RequestStats;
RequestStats.Name = Request.Name;
RequestStats.Bucket = Request.Key.Bucket;
RequestStats.Type = Request.Id.IsNull() ? ERequestType::Value : ERequestType::Record;
RequestStats.Op = ERequestOp::GetChunk;
{
TRACE_CPUPROFILER_EVENT_SCOPE(S3DDC_GetChunks);
TRACE_COUNTER_INCREMENT(S3DDC_Get);
const bool bExistsOnly = EnumHasAnyFlags(Request.Policy, ECachePolicy::SkipData);
FRequestTimer RequestTimer(RequestStats);
if (!(bHasValue && ValueKey == Request.Key && ValueId == Request.Id) || ValueReader.HasSource() < !bExistsOnly)
{
ValueReader.ResetSource();
ValueAr.Reset();
ValueKey = {};
ValueId.Reset();
Value.Reset();
bHasValue = false;
if (Request.Id.IsValid())
{
if (!(Record && Record.Get().GetKey() == Request.Key))
{
FCacheRecordPolicyBuilder PolicyBuilder(ECachePolicy::None);
PolicyBuilder.AddValuePolicy(Request.Id, Request.Policy);
Record.Reset();
Record = GetCacheRecordOnly(Request.Name, Request.Key, PolicyBuilder.Build(), RequestStats);
}
if (Record)
{
if (const FValueWithId& ValueWithId = Record.Get().GetValue(Request.Id))
{
bHasValue = true;
Value = ValueWithId;
ValueId = Request.Id;
ValueKey = Request.Key;
GetCacheContent(Request.Name, Request.Key, ValueId, Value, Request.Policy, ValueReader, ValueAr, RequestStats);
}
}
}
else
{
ValueKey = Request.Key;
bHasValue = GetCacheValueOnly(Request.Name, Request.Key, Request.Policy, Value, RequestStats);
if (bHasValue)
{
GetCacheContent(Request.Name, Request.Key, Request.Id, Value, Request.Policy, ValueReader, ValueAr, RequestStats);
}
}
}
if (bHasValue)
{
const uint64 RawOffset = FMath::Min(Value.GetRawSize(), Request.RawOffset);
RawSize = FMath::Min(Value.GetRawSize() - RawOffset, Request.RawSize);
TRACE_COUNTER_INCREMENT(S3DDC_GetHit);
if (!bExistsOnly)
{
Buffer = ValueReader.Decompress(RawOffset, RawSize);
RequestStats.LogicalReadSize += Buffer.GetSize();
}
Status = bExistsOnly || Buffer.GetSize() == RawSize ? EStatus::Ok : EStatus::Error;
}
}
RequestStats.Status = Status;
StoreStats->AddRequest(RequestStats);
UE_CLOG(Status == EStatus::Ok, LogDerivedDataCache, Verbose, TEXT("%s: Cache hit for %s from '%s'"),
*GetName(), *WriteToString<96>(Request.Key, '/', Request.Id), *Request.Name);
OnComplete({Request.Name, Request.Key, Request.Id, Request.RawOffset,
RawSize, Value.GetRawHash(), MoveTemp(Buffer), Request.UserData, Status});
}
}
} // UE::DerivedData
#endif // WITH_S3_DDC_BACKEND
namespace UE::DerivedData
{
ILegacyCacheStore* CreateS3CacheStore(const TCHAR* Name, const TCHAR* Config, ICacheStoreOwner& Owner)
{
#if WITH_S3_DDC_BACKEND
FString ManifestPath;
if (!FParse::Value(Config, TEXT("Manifest="), ManifestPath))
{
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Missing required parameter 'Manifest'"), Name);
return nullptr;
}
FString BaseUrl;
if (!FParse::Value(Config, TEXT("BaseUrl="), BaseUrl))
{
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Missing required parameter 'BaseUrl'"), Name);
return nullptr;
}
FString Region;
if (!FParse::Value(Config, TEXT("Region="), Region))
{
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Missing required parameter 'Region'"), Name);
return nullptr;
}
FString CanaryObjectKey;
FParse::Value(Config, TEXT("Canary="), CanaryObjectKey);
FString CachePath = FPaths::ProjectSavedDir() / TEXT("S3DDC");
FString Key;
if (FParse::Value(Config, TEXT("EnvPathOverride="), Key))
{
if (FString Value = FPlatformMisc::GetEnvironmentVariable(*Key); !Value.IsEmpty())
{
CachePath = MoveTemp(Value);
UE_LOG(LogDerivedDataCache, Log, TEXT("%s: Found environment variable %s=%s"), Name, *Key, *CachePath);
}
if (FString Value; FPlatformMisc::GetStoredValue(TEXT("Epic Games"), TEXT("GlobalDataCachePath"), *Key, Value) && !Value.IsEmpty())
{
CachePath = MoveTemp(Value);
UE_LOG(LogDerivedDataCache, Log, TEXT("%s: Found registry key GlobalDataCachePath %s=%s"), Name, *Key, *CachePath);
}
}
if (CachePath == TEXTVIEW("None"))
{
UE_LOG(LogDerivedDataCache, Log, TEXT("%s: Disabled because the path is configured to 'None'"), Name);
return nullptr;
}
TUniquePtr<FS3CacheStore> Store(new FS3CacheStore(Name, *ManifestPath, *BaseUrl, *Region, *CanaryObjectKey, *CachePath, Owner));
if (!Store->IsUsable())
{
return nullptr;
}
return Store.Release();
#else
UE_LOG(LogDerivedDataCache, Log, TEXT("S3 backend is not supported on the current platform."));
return nullptr;
#endif
}
} // UE::DerivedData