Files
UnrealEngineUWP/Engine/Source/Developer/Virtualization/Private/VirtualizationFileBackend.cpp
paul chipchase 462290f718 Extending the VA pulling API to allow for batch pulls
#rb Per.Larsson
#jira UE-163093
#rnx
#preflight 63526abcae33b04ec1ee4a65

- The caller can now request more than one payload to be pulled froim the system at a time. In theory this will allow backends to pull data quicker.
-- The file system backend works with the new system but makes no attempt to take advantage as the backend is not intended for serious production use.
-- The DDC backend should be taking full advantage of the new batching API
-- Note that although the source control API does attempt to take advantage of batching, the internal source control API implementation is not doing so. This will be addressed in a future submit.
- This does make the pulling logic a bit more complicated in FVirtualizationManager as we need to deal with some payloads being found in the first backend, some in the second and some in the third etc.
-- To help deal with this a new class FPullRequestCollection has been added which abstracts a lot of complexity away.

[CL 22707955 by paul chipchase in ue5-main branch]
2022-10-21 22:27:40 -04:00

272 lines
8.1 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "VirtualizationFileBackend.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "VirtualizationUtilities.h"
namespace UE::Virtualization
{
FFileSystemBackend::FFileSystemBackend(FStringView ProjectName, FStringView ConfigName, FStringView DebugName)
: IVirtualizationBackend(ConfigName, DebugName, EOperations::Push | EOperations::Pull)
{
}
bool FFileSystemBackend::Initialize(const FString& ConfigEntry)
{
if (!FParse::Value(*ConfigEntry, TEXT("Path="), RootDirectory))
{
UE_LOG(LogVirtualization, Error, TEXT("[%s] 'Path=' not found in the config file"), *GetDebugName());
return false;
}
FPaths::NormalizeDirectoryName(RootDirectory);
if (RootDirectory.IsEmpty())
{
UE_LOG(LogVirtualization, Error, TEXT("[%s] Config file entry 'Path=' was empty"), *GetDebugName());
return false;
}
// TODO: Validate that the given path is usable?
int32 RetryCountIniFile = INDEX_NONE;
if (FParse::Value(*ConfigEntry, TEXT("RetryCount="), RetryCountIniFile))
{
RetryCount = RetryCountIniFile;
}
int32 RetryWaitTimeMSIniFile = INDEX_NONE;
if (FParse::Value(*ConfigEntry, TEXT("RetryWaitTime="), RetryWaitTimeMSIniFile))
{
RetryWaitTimeMS = RetryWaitTimeMSIniFile;
}
// Now log a summary of the backend settings to make issues easier to diagnose
UE_LOG(LogVirtualization, Log, TEXT("[%s] Using path: '%s'"), *GetDebugName(), *RootDirectory);
UE_LOG(LogVirtualization, Log, TEXT("[%s] Will retry failed read attempts %d times with a gap of %dms betwen them"), *GetDebugName(), RetryCount, RetryWaitTimeMS);
return true;
}
IVirtualizationBackend::EConnectionStatus FFileSystemBackend::OnConnect()
{
return IVirtualizationBackend::EConnectionStatus::Connected;
}
bool FFileSystemBackend::PushData(TArrayView<FPushRequest> Requests)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::PushData);
int32 ErrorCount = 0;
for (FPushRequest& Request : Requests)
{
const FIoHash& PayloadId = Request.GetIdentifier();
if (DoesPayloadExist(PayloadId))
{
UE_LOG(LogVirtualization, Verbose, TEXT("[%s] Already has a copy of the payload '%s'."), *GetDebugName(), *LexToString(PayloadId));
Request.SetResult(FPushResult::GetAsAlreadyExists());
continue;
}
// Make sure to log any disk write failures to the user, even if this backend will often be optional as they are
// not expected and could indicate bigger problems.
//
// First we will write out the payload to a temp file, after which we will move it to the correct storage location
// this helps reduce the chance of leaving corrupted data on disk in the case of a power failure etc.
const FString TempFilePath = FPaths::CreateTempFilename(*FPaths::ProjectSavedDir(), TEXT("vapayload"));
TUniquePtr<FArchive> FileAr(IFileManager::Get().CreateFileWriter(*TempFilePath));
if (FileAr == nullptr)
{
TStringBuilder<MAX_SPRINTF> SystemErrorMsg;
Utils::GetFormattedSystemError(SystemErrorMsg);
UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to write payload '%s' to '%s' due to system error: %s"),
*GetDebugName(),
*LexToString(PayloadId),
*TempFilePath,
SystemErrorMsg.ToString());
ErrorCount++;
Request.SetResult(FPushResult::GetAsError());
continue;
}
for (const FSharedBuffer& Buffer : Request.GetPayload().GetCompressed().GetSegments())
{
// Const cast because FArchive requires a non-const pointer!
FileAr->Serialize(const_cast<void*>(Buffer.GetData()), static_cast<int64>(Buffer.GetSize()));
}
if (!FileAr->Close())
{
TStringBuilder<MAX_SPRINTF> SystemErrorMsg;
Utils::GetFormattedSystemError(SystemErrorMsg);
UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to write payload '%s' contents to '%s' due to system error: %s"),
*GetDebugName(),
*LexToString(PayloadId),
*TempFilePath,
SystemErrorMsg.ToString());
IFileManager::Get().Delete(*TempFilePath, true, false, true); // Clean up the temp file if it is still around but do not failure cases to the user
ErrorCount++;
Request.SetResult(FPushResult::GetAsError());
continue;
}
TStringBuilder<512> FilePath;
CreateFilePath(PayloadId, FilePath);
// If the file already exists we don't need to replace it, we will also do our own error logging.
if (IFileManager::Get().Move(FilePath.ToString(), *TempFilePath, /*Replace*/ false, /*EvenIfReadOnly*/ false, /*Attributes*/ false, /*bDoNotRetryOrError*/ true))
{
Request.SetResult(FPushResult::GetAsPushed());
}
else
{
// Store the error message in case we need to display it
TStringBuilder<MAX_SPRINTF> SystemErrorMsg;
Utils::GetFormattedSystemError(SystemErrorMsg);
IFileManager::Get().Delete(*TempFilePath, true, false, true); // Clean up the temp file if it is still around but do not failure cases to the user
// Check if another thread or process was writing out the payload at the same time, if so we
// don't need to give an error message.
if (DoesPayloadExist(PayloadId))
{
UE_LOG(LogVirtualization, Verbose, TEXT("[%s] Already has a copy of the payload '%s'."), *GetDebugName(), *LexToString(PayloadId));
Request.SetResult(FPushResult::GetAsAlreadyExists());
continue;
}
else
{
UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to move payload '%s' to it's final location '%s' due to system error: %s"),
*GetDebugName(),
*LexToString(PayloadId),
*FilePath,
SystemErrorMsg.ToString());
ErrorCount++;
Request.SetResult(FPushResult::GetAsError());
}
}
}
return ErrorCount == 0;
}
bool FFileSystemBackend::PullData(TArrayView<FPullRequest> Requests)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::PullData);
for (FPullRequest& Request : Requests)
{
TStringBuilder<512> FilePath;
CreateFilePath(Request.GetIdentifier(), FilePath);
// TODO: Should we allow the error severity to be configured via ini or just not report this case at all?
if (!IFileManager::Get().FileExists(FilePath.ToString()))
{
UE_LOG(LogVirtualization, Verbose, TEXT("[%s] Does not contain the payload '%s'"),
*GetDebugName(),
*LexToString(Request.GetIdentifier()));
continue;
}
TUniquePtr<FArchive> FileAr = OpenFileForReading(FilePath.ToString());
if (FileAr == nullptr)
{
TStringBuilder<MAX_SPRINTF> SystemErrorMsg;
Utils::GetFormattedSystemError(SystemErrorMsg);
UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to load payload '%s' from file '%s' due to system error: %s"),
*GetDebugName(),
*LexToString(Request.GetIdentifier()),
FilePath.ToString(),
SystemErrorMsg.ToString());
Request.SetError();
continue;
}
Request.SetPayload(FCompressedBuffer::Load(*FileAr));
if (FileAr->IsError())
{
UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to serialize payload '%s' from file '%s'"),
*GetDebugName(),
*LexToString(Request.GetIdentifier()),
FilePath.ToString());
Request.SetError();
continue;
}
}
return true;
}
bool FFileSystemBackend::DoesPayloadExist(const FIoHash& Id)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::DoesPayloadExist);
TStringBuilder<512> FilePath;
CreateFilePath(Id, FilePath);
return IFileManager::Get().FileExists(FilePath.ToString());
}
void FFileSystemBackend::CreateFilePath(const FIoHash& PayloadId, FStringBuilderBase& OutPath)
{
TStringBuilder<52> PayloadPath;
Utils::PayloadIdToPath(PayloadId, PayloadPath);
OutPath << RootDirectory << TEXT("/") << PayloadPath;
}
TUniquePtr<FArchive> FFileSystemBackend::OpenFileForReading(const TCHAR* FilePath)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::OpenFileForReading);
int32 Retries = 0;
while (Retries < RetryCount)
{
TUniquePtr<FArchive> FileAr(IFileManager::Get().CreateFileReader(FilePath));
if (FileAr)
{
return FileAr;
}
else
{
UE_LOG(LogVirtualization, Warning, TEXT("[%s] Failed to open '%s' for reading attempt retrying (%d/%d) in %dms..."), *GetDebugName(), FilePath, Retries, RetryCount, RetryWaitTimeMS);
FPlatformProcess::SleepNoStats(RetryWaitTimeMS * 0.001f);
Retries++;
}
}
return nullptr;
}
UE_REGISTER_VIRTUALIZATION_BACKEND_FACTORY(FFileSystemBackend, FileSystem);
} // namespace UE::Virtualization