2022-08-10 16:03:37 +00:00
// 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 ;
}
2022-10-27 11:33:48 -04:00
FString EnvOverrideName ;
if ( FParse : : Value ( * ConfigEntry , TEXT ( " EnvPathOverride= " ) , EnvOverrideName ) )
{
FString OverridePath = FPlatformMisc : : GetEnvironmentVariable ( * EnvOverrideName ) ;
if ( ! OverridePath . IsEmpty ( ) )
{
UE_LOG ( LogVirtualization , Log , TEXT ( " [%s] Overriding path with envvar '%s' " ) , * GetDebugName ( ) , * EnvOverrideName ) ;
RootDirectory = OverridePath ;
}
}
2022-08-10 16:03:37 +00:00
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 ;
}
Add a number of ways for the VA backend connections to be changed to lazy initialize on first use.
#rb Devin.Doucette
#jira UE-161599
#rnx
#preflight 6303c8d65a5d4e4624e7bf52
- There are some use cases that require the VA system to be initialized and configured correctly but would prefer that the backend connections only run if absolutely needed (usually when a payload is pulled or pushed for the first time), this change provides four different ways of doing this:
-- Setting [Core.VirtualizationModule]LazyInitConnections=true in the Engine ini file
-- Setting the define 'UE_VIRTUALIZATION_CONNECTION_LAZY_INIT' to 1 in a programs .target.cs
-- Running with the commandline option -VA-LazyInitConnections
-- Setting the cvar 'VA.LazyInitConnections' to 1 (only works if it is set before the VA system is initialized, changing it mid editor via the console does nothing)
--- Note that after the config file, each setting there only opts into lazy initializing the connections, setting the cvar to 0 for example will not prevent the cmdline from opting in etc.
- In the future we will allow the connection code to run async, so the latency can be hidden behind the editor loading, but for the current use case we are taking the minimal approach.
-- This means we only support the backend being in 3 states. No connection has been made yet, the connection is broken and the connection is working.
-- To keep things simple we only record if we have attempted to connect the backends or not. We don't check individual backends nor do we try to reconnect failed ones etc. This is all scheduled for a future work item.
- If the connections are not initialized when the VA system is, we wait until the first time someone calls one of the virtualization methods that will actually use a connection: Push/Pull/Query
-- We try connecting all of the backends at once, even if they won't be used in the call to keep things simple.
- Only the source control backend makes use of the connection system. The horde storage (http) backend could take advantage too, but it is currently unused and most likely going to just be deleted so there seemed little point updating it.
- If we try to run an operation on an unconnected backend we only log to verbose. This is to maintain existing behaviour where a failed backend would not be mounted at all. This logging will likely be revisited in a future work item.
[CL 21511855 by paul chipchase in ue5-main branch]
2022-08-23 13:01:15 -04:00
IVirtualizationBackend : : EConnectionStatus FFileSystemBackend : : OnConnect ( )
{
return IVirtualizationBackend : : EConnectionStatus : : Connected ;
}
2022-09-08 19:35:36 -04:00
bool FFileSystemBackend : : PushData ( TArrayView < FPushRequest > Requests )
2022-08-10 16:03:37 +00:00
{
TRACE_CPUPROFILER_EVENT_SCOPE ( FFileSystemBackend : : PushData ) ;
2022-09-08 19:35:36 -04:00
int32 ErrorCount = 0 ;
for ( FPushRequest & Request : Requests )
2022-08-10 16:03:37 +00:00
{
2022-09-08 19:35:36 -04:00
const FIoHash & PayloadId = Request . GetIdentifier ( ) ;
2022-08-10 16:03:37 +00:00
2022-09-08 19:35:36 -04:00
if ( DoesPayloadExist ( PayloadId ) )
2022-08-10 16:03:37 +00:00
{
2022-09-08 19:35:36 -04:00
UE_LOG ( LogVirtualization , Verbose , TEXT ( " [%s] Already has a copy of the payload '%s'. " ) , * GetDebugName ( ) , * LexToString ( PayloadId ) ) ;
2022-09-30 15:47:04 -04:00
Request . SetResult ( FPushResult : : GetAsAlreadyExists ( ) ) ;
2022-09-08 19:35:36 -04:00
continue ;
2022-08-10 16:03:37 +00:00
}
2022-09-08 19:35:36 -04:00
// 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 )
2022-08-10 16:03:37 +00:00
{
2022-09-08 19:35:36 -04:00
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 + + ;
2022-09-30 15:47:04 -04:00
Request . SetResult ( FPushResult : : GetAsError ( ) ) ;
2022-09-08 19:35:36 -04:00
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 + + ;
2022-09-30 15:47:04 -04:00
Request . SetResult ( FPushResult : : GetAsError ( ) ) ;
2022-09-08 19:35:36 -04:00
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.
2022-09-30 15:47:04 -04:00
if ( IFileManager : : Get ( ) . Move ( FilePath . ToString ( ) , * TempFilePath , /*Replace*/ false , /*EvenIfReadOnly*/ false , /*Attributes*/ false , /*bDoNotRetryOrError*/ true ) )
{
Request . SetResult ( FPushResult : : GetAsPushed ( ) ) ;
}
else
2022-09-08 19:35:36 -04:00
{
// 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 ) ) ;
2022-09-30 15:47:04 -04:00
Request . SetResult ( FPushResult : : GetAsAlreadyExists ( ) ) ;
2022-09-08 19:35:36 -04:00
continue ;
}
else
{
UE_LOG ( LogVirtualization , Error , TEXT ( " [%s] Failed to move payload '%s' to it's final location '%s' due to system error: %s " ) ,
2022-08-10 16:03:37 +00:00
* GetDebugName ( ) ,
2022-09-08 19:35:36 -04:00
* LexToString ( PayloadId ) ,
2022-08-10 16:03:37 +00:00
* FilePath ,
SystemErrorMsg . ToString ( ) ) ;
2022-09-08 19:35:36 -04:00
ErrorCount + + ;
2022-09-30 15:47:04 -04:00
Request . SetResult ( FPushResult : : GetAsError ( ) ) ;
2022-09-08 19:35:36 -04:00
}
2022-08-10 16:03:37 +00:00
}
}
2022-09-08 19:35:36 -04:00
return ErrorCount = = 0 ;
2022-08-10 16:03:37 +00:00
}
2022-10-21 22:27:40 -04:00
bool FFileSystemBackend : : PullData ( TArrayView < FPullRequest > Requests )
2022-08-10 16:03:37 +00:00
{
TRACE_CPUPROFILER_EVENT_SCOPE ( FFileSystemBackend : : PullData ) ;
2022-10-21 22:27:40 -04:00
for ( FPullRequest & Request : Requests )
2022-08-10 16:03:37 +00:00
{
2022-10-21 22:27:40 -04:00
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 ;
}
2022-08-10 16:03:37 +00:00
}
2022-10-21 22:27:40 -04:00
return true ;
2022-08-10 16:03:37 +00:00
}
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