// Copyright Epic Games, Inc. All Rights Reserved. #include "StorageServerConnection.h" #include "IO/IoChunkId.h" #include "IO/IoDispatcher.h" #include "IPAddress.h" #include "Misc/App.h" #include "Misc/ScopeLock.h" #include "Misc/OutputDeviceRedirector.h" #include "Misc/StringBuilder.h" #include "Serialization/MemoryReader.h" #include "Serialization/CompactBinary.h" #include "Serialization/CompactBinarySerialization.h" #include "Serialization/CompactBinaryWriter.h" #include "Memory/MemoryView.h" #include "Memory/SharedBuffer.h" #include "ProfilingDebugging/CountersTrace.h" #include "SocketSubsystem.h" #include "Serialization/PackageStore.h" #include "BuiltInHttpClient/BuiltInHttpClient.h" #include "BuiltInHttpClient/BuiltInHttpClientFSocket.h" #include "BuiltInHttpClient/BuiltInHttpClientPlatformSocket.h" #include "HAL/PlatformMath.h" #include "HAL/RunnableThread.h" #include "HAL/Event.h" #include "Cache/CacheJournalSimple.h" #include "Cache/CacheJournalSectioned.h" #include "Cache/CacheStorageBlocking.h" #include "Cache/CacheStorageMmap.h" #include "Cache/CacheStrategyLinear.h" #if !UE_BUILD_SHIPPING #ifndef STORAGE_SERVER_PLATFORM_CACHE_SIZE_KB #define STORAGE_SERVER_PLATFORM_CACHE_SIZE_KB 4 * 1024 * 1024 // 4 GB by default #endif #ifndef STORAGE_SERVER_PLATFORM_CACHE_ABANDON_SIZE_KB #define STORAGE_SERVER_PLATFORM_CACHE_ABANDON_SIZE_KB 512 * 1024 // 512 MB by default #endif DEFINE_LOG_CATEGORY(LogStorageServerConnection); TRACE_DECLARE_INT_COUNTER(ZenHttpClientSerializedBytes, TEXT("ZenClient/SerializedBytes (compressed)")); TRACE_DECLARE_INT_COUNTER(ZenHttpClientThroughputBytes, TEXT("ZenClient/ThroughputBytes (decompressed)")); TRACE_DECLARE_FLOAT_COUNTER (ZenCacheRatio, TEXT("ZenClient/Cache/HitRatio")); TRACE_DECLARE_INT_COUNTER (ZenCacheHit, TEXT("ZenClient/Cache/Hit")); TRACE_DECLARE_INT_COUNTER (ZenCacheMiss, TEXT("ZenClient/Cache/Miss")); TRACE_DECLARE_MEMORY_COUNTER(ZenCacheHitBytes, TEXT("ZenClient/Cache/HitBytes")); TRACE_DECLARE_MEMORY_COUNTER(ZenCacheMissBytes, TEXT("ZenClient/Cache/MissBytes")); static void SetZenCacheRatio() { const int64 HitCount = TRACE_COUNTER_GET(ZenCacheHit); const int64 MissCount = TRACE_COUNTER_GET(ZenCacheMiss); const float Ratio = (HitCount + MissCount > 0) ? ((float)HitCount / (HitCount + MissCount)) : 0.0f; TRACE_COUNTER_SET(ZenCacheRatio, Ratio * 100.0f); } static void ZenCacheHit(const uint64 Bytes) { TRACE_COUNTER_ADD(ZenCacheHit, 1); TRACE_COUNTER_ADD(ZenCacheHitBytes, Bytes); SetZenCacheRatio(); } static void ZenCacheMiss(const uint64 Bytes) { TRACE_COUNTER_ADD(ZenCacheMiss, 1); TRACE_COUNTER_ADD(ZenCacheMissBytes, Bytes); SetZenCacheRatio(); } static FCbObject ProcessResponse(FIoBuffer IoBuffer, EStorageServerContentType ContentType) { FCbObject ResponseObj; if (ContentType == EStorageServerContentType::CbObject) { FMemoryReaderView Reader(IoBuffer.GetView()); ResponseObj = LoadCompactBinary(Reader).AsObject(); } else if (ContentType == EStorageServerContentType::CompressedBinary) { FCompressedBuffer Compressed = FCompressedBuffer::FromCompressed(FSharedBuffer::MakeView(IoBuffer.GetData(), IoBuffer.GetSize())); FIoBuffer Decompressed(Compressed.GetRawSize()); if (FCompressedBufferReader(Compressed).TryDecompressTo(Decompressed.GetMutableView(), 0)) { FBufferReader DecompressedAr(Decompressed.GetData(), Decompressed.GetSize(), false); ResponseObj = LoadCompactBinary(DecompressedAr).AsObject(); } } return ResponseObj; } bool FStorageServerConnection::Initialize(TArrayView HostAddresses, const int32 Port, const FAnsiStringView& InBaseURI) { BaseURI = InBaseURI; TArray SortedHostAddresses = SortHostAddressesByLocalSubnet(HostAddresses, Port); if (BaseURI.StartsWith("/ws/")) { bIsUsingZenWorkspace = true; } for (const FString& HostAddress : SortedHostAddresses) { HttpClient = CreateHttpClient(HostAddress, Port); CurrentHostAddr = HostAddress; if (HandshakeRequest()) { UE_LOG(LogStorageServerConnection, Display, TEXT("Zen store connection established to %s:%i."), *CurrentHostAddr, Port); SetupCacheStrategy(); return true; } } HttpClient.Reset(); return false; } TIoStatusOr FStorageServerConnection::GetWorkspaces() { IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync(BaseURI, EStorageServerContentType::CbObject); TIoStatusOr RequestResult = ResultTuple.Get<0>(); if (RequestResult.IsOk()) { FMemoryReaderView Reader(RequestResult.ValueOrDie().GetView()); FCbObject ResponseObj = LoadCompactBinary(Reader).AsObject(); Workspaces Result; FCbArrayView WorkspacesArray = ResponseObj["workspaces"].AsArrayView(); for (FCbFieldView WorkspaceField : WorkspacesArray) { FCbObjectView WorkspaceObject = WorkspaceField.AsObjectView(); Workspaces::Workspace Workspace = { .Id = *WriteToString<64>(WorkspaceObject["id"].AsObjectId()), .Root = FString(WorkspaceObject["root_path"].AsString()), .AllowShareCreationFromHttp = WorkspaceObject["allow_share_creation_from_http"].AsBool() }; if (!Workspace.Id.IsEmpty()) { FCbArrayView SharesArray = WorkspaceObject["shares"].AsArrayView(); for (FCbFieldView ShareField : SharesArray) { FCbObjectView ShareObject = ShareField.AsObjectView(); Workspaces::Share Share = { .Id = *WriteToString<64>(ShareObject["id"].AsObjectId()), .Path = FString(ShareObject["share_path"].AsString()), .Alias = FString(ShareObject["alias"].AsString()) }; if (!Share.Id.IsEmpty()) { Workspace.Shares.Add(Share); } } Result.Workspaces.Add(Workspace); } } return Result; } return RequestResult.Status(); } static void PercentEncodeString(FAnsiStringBuilderBase& EncodedBuilder, const FString& String) { for (FString::ElementType C : String) { switch (C) { case '!': case '#': case '$': case '&': case '\'': case '(': case ')': case '*': case '+': case ',': case '/': case ':': case ';': case '=': case '?': case '@': case '[': case ']': EncodedBuilder.Append("%02X", C); break; default: EncodedBuilder.Appendf("%c", C); } } } TIoStatusOr FStorageServerConnection::CreateShare(const FString& WorkspaceId, const FString& SharePath, const FString& Alias) { TAnsiStringBuilder<256> ResourceBuilder; ResourceBuilder.Append(BaseURI); ResourceBuilder.Append("/"); ResourceBuilder.Append(WorkspaceId); ResourceBuilder.Append("/"); ResourceBuilder.Append("000000000000000000000000"); ResourceBuilder.Append("?share_path="); PercentEncodeString(ResourceBuilder, SharePath); if (!Alias.IsEmpty()) { ResourceBuilder.Append("&alias="); PercentEncodeString(ResourceBuilder, Alias); } IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync(*ResourceBuilder, EStorageServerContentType::Unknown, "PUT"); TIoStatusOr Result = ResultTuple.Get<0>(); if (Result.IsOk()) { FMemoryView TextView(Result.ValueOrDie().GetView()); return FString::ConstructFromPtrSize(reinterpret_cast(TextView.GetData()), TextView.GetSize()); } return Result.Status(); } void FStorageServerConnection::PackageStoreRequest(TFunctionRef Callback) { TAnsiStringBuilder<256> ResourceBuilder; ResourceBuilder.Append(BaseURI).Append("/entries?fieldfilter=packagestoreentry"); IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync(*ResourceBuilder, EStorageServerContentType::CompressedBinary); TIoStatusOr Result = ResultTuple.Get<0>(); if (Result.IsOk()) { EStorageServerContentType ContentType = ResultTuple.Get<1>(); FCbObject ResponseObj = ProcessResponse(Result.ValueOrDie(), ContentType); { TRACE_CPUPROFILER_EVENT_SCOPE(StorageServerPackageStoreRequestParseEntries); for (FCbField& OplogEntry : ResponseObj["entries"].AsArray()) { FCbObject OplogObj = OplogEntry.AsObject(); FPackageStoreEntryResource Entry = FPackageStoreEntryResource::FromCbObject(OplogObj["packagestoreentry"].AsObject()); Callback(MoveTemp(Entry)); } } } else { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Failed to read oplog from storage server. '%s'"), *Result.Status().ToString()); } } void FStorageServerConnection::FileManifestRequest(TFunctionRef Callback) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::FileManifestRequest); TAnsiStringBuilder<256> ResourceBuilder; ResourceBuilder.Append(BaseURI).Append("/files?refresh=true&fieldnames=id,clientpath,rawsize"); IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync(*ResourceBuilder, EStorageServerContentType::CompressedBinary); TIoStatusOr Result = ResultTuple.Get<0>(); if (Result.IsOk()) { EStorageServerContentType ContentType = ResultTuple.Get<1>(); FCbObject ResponseObj = ProcessResponse(Result.ValueOrDie(), ContentType); for (FCbField& FileArrayEntry : ResponseObj["files"].AsArray()) { FCbObject Entry = FileArrayEntry.AsObject(); FCbObjectId Id = Entry["id"].AsObjectId(); int64 ResponseRawSize = Entry["rawsize"].AsInt64(-1); TStringBuilder<128> WidePath; WidePath.Append(FUTF8ToTCHAR(Entry["clientpath"].AsString())); FIoChunkId ChunkId; ChunkId.Set(Id.GetView()); Callback(ChunkId, WidePath, ResponseRawSize); } } else { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Failed to read file manifest from storage server. '%s'"), *Result.Status().ToString()); } } void FStorageServerConnection::ChunkInfosRequest(TFunctionRef Callback) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ChunkInfosRequest); TAnsiStringBuilder<256> ResourceBuilder; ResourceBuilder.Append(BaseURI).Append("/chunkinfos?fieldnames=id,rawhash,rawsize"); IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync(*ResourceBuilder, EStorageServerContentType::CbObject); TIoStatusOr Result = ResultTuple.Get<0>(); if (Result.IsOk()) { FMemoryReaderView Reader(Result.ValueOrDie().GetView()); FCbObject ResponseObj = LoadCompactBinary(Reader).AsObject(); for (FCbField& FileArrayEntry : ResponseObj["chunkinfos"].AsArray()) { FCbObject Entry = FileArrayEntry.AsObject(); FCbObjectId Id = Entry["id"].AsObjectId(); FIoHash RawHash = Entry["rawhash"].AsHash(); int64 ResponseRawSize = Entry["rawsize"].AsInt64(-1); FIoChunkId ChunkId; ChunkId.Set(Id.GetView()); Callback(ChunkId, RawHash, ResponseRawSize); } } else { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Failed to read file manifest from storage server. '%s'"), *Result.Status().ToString()); } } int64 FStorageServerConnection::ChunkSizeRequest(const FIoChunkId& ChunkId) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ChunkSizeRequest); bool bCacheAvailable = FinalizeSetupCacheStrategy(); int64 ChunkSize = 0; if (bCacheAvailable && CacheStrategy.IsValid() && CacheStrategy->TryGetChunkSize(ChunkId, ChunkSize)) { return ChunkSize; } TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ChunkSizeRequest::Http); TAnsiStringBuilder<256> ResourceBuilder; ResourceBuilder.Append(BaseURI); ResourceBuilder << "/" << ChunkId << "/info"; const double StartTime = FPlatformTime::Seconds(); IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync(*ResourceBuilder, EStorageServerContentType::CbObject); TIoStatusOr Result = ResultTuple.Get<0>(); if (Result.IsOk()) { const double Duration = FPlatformTime::Seconds() - StartTime; AddTimingInstance(Duration, Result.ValueOrDie().GetSize()); FMemoryReaderView Reader(Result.ValueOrDie().GetView()); FCbObject ResponseObj = LoadCompactBinary(Reader).AsObject(); ChunkSize = ResponseObj["size"].AsInt64(0); if (CacheStrategy.IsValid()) { CacheStrategy->CacheChunkSize(ChunkId, ChunkSize); } return ChunkSize; } else if (Result.Status().GetErrorCode() != EIoErrorCode::NotFound) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Failed to get chunk size from storage server. '%s'"), *Result.Status().ToString()); } return -1; } TIoStatusOr FStorageServerConnection::ReadChunkRequest( const FIoChunkId& ChunkId, const uint64 Offset, const uint64 Size, const TOptional OptDestination, const bool bHardwareTargetBuffer ) { // TODO move caching functionality to ReadChunkBatchRequest and remove ReadChunkRequest. TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkRequest); bool bCacheAvailable = FinalizeSetupCacheStrategy(); const double StartTime = FPlatformTime::Seconds(); IStorageServerHttpClient::FResult ResultTuple; uint64 ResultModTag = 0; bool bWasCached = false; FIoBuffer CacheChunkBuffer; EStorageServerContentType CacheContentType; // TODO is there a way to pass destination directly? if (bCacheAvailable && CacheStrategy.IsValid() && CacheStrategy->ReadChunk(ChunkId, Offset, Size, TOptional(), CacheChunkBuffer, CacheContentType)) { bWasCached = true; ZenCacheHit(CacheChunkBuffer.GetSize()); ResultTuple = IStorageServerHttpClient::FResult(CacheChunkBuffer, CacheContentType); } else { #if HAS_STORAGE_SERVER_RPC_GETCHUNKS_API if (bIsUsingZenWorkspace) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkRequest::Http); TAnsiStringBuilder<256> ResourceBuilder; BuildReadChunkRequestUrl(ResourceBuilder, ChunkId, Offset, Size); ResultTuple = HttpClient->RequestSync(*ResourceBuilder); ResultModTag = 0; // This endpoint doesn't support ModTag. } else { TArray ChunkBatchRequests; ChunkBatchRequests.Add(FChunkBatchRequestEntry::DataRequest(ChunkId, Offset, Size)); FIoStatus ResultStatus = ReadChunkBatchRequest(ChunkBatchRequests, [&](FIoChunkId Id, EStorageServerContentType MimeType, FIoBuffer Data, const TOptional& ModTag) { ensure(ChunkId == Id); Data.MakeOwned(); ResultTuple = IStorageServerHttpClient::FResult(Data, MimeType); ResultModTag = ModTag.GetValue(); // ModTag must be present for cache invalidation to work }); if (!ResultStatus.IsOk()) { ResultTuple = IStorageServerHttpClient::FResult(ResultStatus, EStorageServerContentType::Unknown); } } #else TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkRequest::Http); TAnsiStringBuilder<256> ResourceBuilder; BuildReadChunkRequestUrl(ResourceBuilder, ChunkId, Offset, Size); ResultTuple = HttpClient->RequestSync(*ResourceBuilder); ResultModTag = 0; // This endpoint doesn't support ModTag. #endif } TIoStatusOr ResultBuffer = ReadChunkRequestProcessHttpResult(ResultTuple, Offset, Size, OptDestination, bHardwareTargetBuffer); if (ResultBuffer.IsOk()) { if (!bWasCached && CacheStrategy.IsValid()) { ZenCacheMiss(ResultTuple.Key.ValueOrDie().GetSize()); CacheStrategy->CacheChunk(ChunkId, Offset, Size, ResultTuple.Key.ValueOrDie(), ResultTuple.Value, ResultModTag); } const double Duration = FPlatformTime::Seconds() - StartTime; AddTimingInstance(Duration, ResultBuffer.ValueOrDie().GetSize()); } return ResultBuffer; } struct FOptionalCbField { static TOptional AsHash(FCbFieldView FieldView) { return FieldView.IsHash() ? FieldView.AsHash() : TOptional(); } static TOptional AsUInt64(FCbFieldView FieldView) { return FieldView.IsInteger() ? FieldView.AsUInt64() : TOptional(); } }; #if HAS_STORAGE_SERVER_RPC_GETCHUNKS_API FIoStatus FStorageServerConnection::ReadChunkBatchRequest(const TArray& Chunks, TFunctionRef& ModTag)> OnResponse, bool bSkipData) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkBatchRequest); if (Chunks.Num() == 0) { return FIoStatus::Ok; } else if (Chunks.Num() > 1) { // TODO Implement multiple requests to same chunkid in same batch. TSet RequestedChunkIds; for (const FChunkBatchRequestEntry& Chunk: Chunks) { if (RequestedChunkIds.Contains(Chunk.ChunkId)) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Multiple requests to the same chunkid are not supported (%s is already present)"), *LexToString(Chunk.ChunkId)); return FIoStatus::Unknown; } RequestedChunkIds.Add(Chunk.ChunkId); } } const double StartTime = FPlatformTime::Seconds(); FCbWriter Writer; Writer.BeginObject(); Writer << "method" << "getchunks"; Writer.BeginObject("Request"); if (bSkipData) { Writer << "SkipData" << bSkipData; } Writer.BeginArray("Chunks"); for (const FChunkBatchRequestEntry& Chunk : Chunks) { Writer.BeginObject(); if (!bSkipData) { Writer << "Offset" << Chunk.Offset; Writer << "Size" << Chunk.Size; } Writer << "Oid" << Chunk.ChunkId; if (Chunk.ModTag.IsSet()) { Writer << "ModTag" << *Chunk.ModTag; } Writer.EndObject(); } Writer.EndArray(); Writer.EndObject(); Writer.EndObject(); TAnsiStringBuilder<256> Uri; Uri.Append(BaseURI).Append("/rpc"); FIoBuffer PostPayload(Writer.GetSaveSize()); Writer.Save(PostPayload.GetMutableView()); IStorageServerHttpClient::FResult HttpResult = HttpClient->RequestSync( *Uri, EStorageServerContentType::CompressedBinary, "POST", PostPayload, EStorageServerContentType::CbObject ); TIoStatusOr Result = HttpResult.Get<0>(); if (!Result.IsOk()) { return Result.Status(); } const double Duration = FPlatformTime::Seconds() - StartTime; AddTimingInstance(Duration, Result.ValueOrDie().GetSize()); // TODO Replace parsing code with FCbPackage. struct ResponseHeader { uint32 Magic; uint32 AttachmentCount; uint32 Reserved[2]; } Header; FMemoryReaderView View(Result.ValueOrDie().GetView()); View.Serialize(&Header, sizeof(Header)); struct AttachmentInfo { uint64 PayloadSize; uint32 Flags; FIoHash Hash; enum { IsCompressed = (1u << 0), IsObject = (1u << 1), }; }; if (Header.Magic != 0xaa77aacc) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Read incorrect header magic from server response - expected %x, got %x"), 0xaa77aacc, Header.Magic); return FIoStatus::Unknown; } TArray Info; Info.AddUninitialized(Header.AttachmentCount + 1); // One extra for the CbPackage root object. TMap AttachmentHashToIndex; for (int32 i = 0; i < Info.Num(); ++i) { View.Serialize(&Info[i], sizeof(Info[i])); AttachmentHashToIndex.Add(Info[i].Hash, i); } FCbObjectView Root(Result.ValueOrDie().GetData() + View.Tell()); TArray InfoOffsets; InfoOffsets.AddUninitialized(Header.AttachmentCount + 1); for (int32 i = 0; i < Info.Num(); ++i) { InfoOffsets[i] = (i == 0) ? View.Tell() : InfoOffsets[i - 1] + Info[i - 1].PayloadSize; } FCbArrayView ResponseChunks = Root["Chunks"].AsArrayView(); uint64 ResponseCount = 0; for (FCbFieldView Field : ResponseChunks) { const FCbObjectView Chunk = Field.AsObjectView(); const FCbObjectId Id = Chunk["Id"].AsObjectId(); const FIoHash RawHash = Chunk["RawHash"].AsHash(); const FIoHash FragmentHash = Chunk["FragmentHash"].AsHash(); const FIoHash Hash = Chunk["Hash"].AsHash(); const TOptional ModTag = FOptionalCbField::AsUInt64(Chunk["ModTag"]); FIoChunkId ChunkId; ChunkId.Set(Id.GetView()); if (bSkipData) { OnResponse(ChunkId, EStorageServerContentType::Unknown, FIoBuffer(), ModTag); } else { if (!ensureMsgf(RawHash != FIoHash() || FragmentHash != FIoHash() || Hash != FIoHash(), TEXT("Failed to find hash in chunk info returned from server"))) { continue; } const FIoHash& AttachmentHash = (Hash != FIoHash() ? Hash : (RawHash != FIoHash() ? RawHash : FragmentHash)); int32* InfoIndex = AttachmentHashToIndex.Find(AttachmentHash); if (!ensureMsgf(InfoIndex, TEXT("Failed to find hash in attachments returned from server"))) { continue; } const AttachmentInfo& Attachment = Info[*InfoIndex]; ensureAlways(Attachment.Hash == AttachmentHash); EStorageServerContentType MimeType = EStorageServerContentType::CompressedBinary; if (Attachment.Flags & AttachmentInfo::IsCompressed) { MimeType = EStorageServerContentType::CompressedBinary; } else if (Attachment.Flags & AttachmentInfo::IsObject) { MimeType = EStorageServerContentType::CbObject; } else { MimeType = EStorageServerContentType::Binary; } FIoBuffer Data(FIoBuffer::Wrap, Result.ValueOrDie().GetData() + InfoOffsets[*InfoIndex], Attachment.PayloadSize); OnResponse(ChunkId, MimeType, Data, ModTag); } ResponseCount++; } return ResponseCount ? FIoStatus::Ok : EIoErrorCode::NotFound; } #endif void FStorageServerConnection::ReadChunkRequestAsync( const FIoChunkId& ChunkId, const uint64 Offset, const uint64 Size, const TOptional OptDestination, const bool bHardwareTargetBuffer, TFunctionRef Data)> OnResponse ) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkRequestAsync); const double StartTime = FPlatformTime::Seconds(); TAnsiStringBuilder<256> ResourceBuilder; BuildReadChunkRequestUrl(ResourceBuilder, ChunkId, Offset, Size); // TODO use CacheStrategy HttpClient->RequestAsync([this, Offset, Size, OptDestination, bHardwareTargetBuffer, OnResponse, StartTime](IStorageServerHttpClient::FResult HttpResultTuple) { TIoStatusOr ResultBuffer = ReadChunkRequestProcessHttpResult(HttpResultTuple, Offset, Size, OptDestination, bHardwareTargetBuffer); if (ResultBuffer.IsOk()) { const double Duration = FPlatformTime::Seconds() - StartTime; AddTimingInstance(Duration, ResultBuffer.ValueOrDie().GetSize()); } OnResponse(ResultBuffer); }, *ResourceBuilder); } void FStorageServerConnection::GetAndResetStats(IStorageServerPlatformFile::FConnectionStats& OutStats) { OutStats.AccumulatedBytes = AccumulatedBytes.exchange(0, std::memory_order_relaxed); OutStats.RequestCount = RequestCount.exchange(0, std::memory_order_relaxed); OutStats.MinRequestThroughput = MinRequestThroughput.exchange(DBL_MAX, std::memory_order_relaxed); OutStats.MaxRequestThroughput = MaxRequestThroughput.exchange(-DBL_MAX, std::memory_order_relaxed); } TArray FStorageServerConnection::SortHostAddressesByLocalSubnet(TArrayView HostAddresses, const int32 Port) { bool bAllArePlatformSocketAddresses = true; for (const FString& HostAddress : HostAddresses) { if (!IsPlatformSocketAddress(HostAddress)) { bAllArePlatformSocketAddresses = false; break; } } // return array without sorting if it's 0 or 1 addresses or all of them are platform sockets if (HostAddresses.Num() <= 1 || bAllArePlatformSocketAddresses) { return TArray(HostAddresses); } TArray Result; ISocketSubsystem& SocketSubsystem = *ISocketSubsystem::Get(); // Sorting logic in order: // - special platform socket address, see PlatformSocketAddress // - on desktop, if it's an IPV6 address loopback (ends with ":1") // - on desktop, if it's and IPV4 address loopback (starts with "127.0.0") // - host IPV4 subnet matches the client subnet (xxx.xxx.xxx) // - remaining addresses bool bCanBindAll = false; bool bAppendPort = false; TSharedPtr localAddr = SocketSubsystem.GetLocalHostAddr(*GLog, bCanBindAll); FString localAddrStringSubnet = localAddr->ToString(bAppendPort); int32 localLastDotPos = INDEX_NONE; if (localAddrStringSubnet.FindLastChar(TEXT('.'), localLastDotPos)) { localAddrStringSubnet = localAddrStringSubnet.LeftChop(localAddrStringSubnet.Len() - localLastDotPos); } TArray PlatformSocketAddresses; TArray IPV6Loopback; TArray IPV4Loopback; TArray RegularAddresses; TArray HostnameAddresses; for (const FString& HostAddress : HostAddresses) { if (IsPlatformSocketAddress(HostAddress)) { PlatformSocketAddresses.Push(HostAddress); continue; } if (IsHostnameAddress(HostAddress)) { HostnameAddresses.Push(HostAddress); continue; } TSharedPtr Addr = StringToInternetAddr(HostAddress, Port); if (!Addr) { continue; } FString tempAddrStringSubnet = Addr->ToString(bAppendPort); #if PLATFORM_DESKTOP || PLATFORM_ANDROID if (Addr->GetProtocolType() == FNetworkProtocolTypes::IPv6) { if (tempAddrStringSubnet.EndsWith(":1")) { IPV6Loopback.Push(HostAddress); continue; } } else { if (tempAddrStringSubnet.StartsWith("127.0.0.")) { IPV4Loopback.Push(HostAddress); continue; } } #elif PLATFORM_IOS if (Addr->GetProtocolType() == FNetworkProtocolTypes::IPv4) { // iOS and Mac have an APIPA address for the ethernet-over-usb connection // between the devices. If we have an address that matches that pattern // supplied by the Mac, we should prefer to use it above other reachable // addresses. Treating it as IPV4 loopback accomplishes that. if (tempAddrStringSubnet.StartsWith("169.254.")) { IPV4Loopback.Push(HostAddress); continue; } } #endif int32 LastDotPos = INDEX_NONE; if (tempAddrStringSubnet.FindLastChar(TEXT('.'), LastDotPos)) { tempAddrStringSubnet = tempAddrStringSubnet.LeftChop(tempAddrStringSubnet.Len() - LastDotPos); } if (localAddrStringSubnet.Equals(tempAddrStringSubnet)) { RegularAddresses.Insert(HostAddress, 0); } else { RegularAddresses.Push(HostAddress); } } Result.Append(PlatformSocketAddresses); Result.Append(IPV6Loopback); Result.Append(IPV4Loopback); Result.Append(RegularAddresses); Result.Append(HostnameAddresses); return Result; } bool FStorageServerConnection::IsPlatformSocketAddress(const FString Address) { return Address.StartsWith(TEXT("platform://")); } bool FStorageServerConnection::IsHostnameAddress(const FString Address) { return Address.StartsWith(TEXT("hostname://")); } TUniquePtr FStorageServerConnection::CreateHttpClient(const FString Address, const int32 Port) { TSharedPtr Addr = StringToInternetAddr(Address, Port); // Use Address as Hostname if we can't resolve FInternetAddr FString HostName = Addr.IsValid() ? Addr->ToString(false) : Address; UE_LOG(LogStorageServerConnection, Display, TEXT("Creating zen store connection to %s:%i (\"%s\")."), *Address, Port, *HostName); TUniquePtr SocketPool; if (IsPlatformSocketAddress(Address)) { SocketPool = MakeUnique(Address); } else { SocketPool = MakeUnique(Addr, *ISocketSubsystem::Get()); } return MakeUnique(MoveTemp(SocketPool), HostName); } TSharedPtr FStorageServerConnection::StringToInternetAddr(const FString HostAddr, const int32 Port) { TSharedPtr Result = TSharedPtr(); ISocketSubsystem& SocketSubsystem = *ISocketSubsystem::Get(); if (IsPlatformSocketAddress(HostAddr)) { return Result; } // Numeric IPV6 addresses can be enclosed in brackets, and must have the brackets stripped before calling GetAddressFromString FString ModifiedHostAddr; const FString* EffectiveHostAddr = &HostAddr; bool bIsHostname = false; if (IsHostnameAddress(HostAddr)) { ModifiedHostAddr = FStringView(HostAddr).RightChop(11); // cut off "hostname://" EffectiveHostAddr = &ModifiedHostAddr; bIsHostname = true; } else if (!HostAddr.IsEmpty() && HostAddr[0] == TEXT('[') && HostAddr[HostAddr.Len() - 1] == TEXT(']')) { #if PLATFORM_HAS_BSD_SOCKETS && !PLATFORM_HAS_BSD_IPV6_SOCKETS // If the platform doesn't have IPV6 BSD Sockets, then handle an attempt at conversion of loopback addresses, and skip and warn about other addresses if (HostAddr == TEXT("[::1]")) { // Substitute IPV4 loopback for IPV6 loopback ModifiedHostAddr = TEXT("127.0.0.1"); } else { UE_LOG(LogStorageServerConnection, Warning, TEXT("Ignoring storage server host IPV6 address on platform that doesn't support IPV6: %s"), *HostAddr); return TSharedPtr(); } #else ModifiedHostAddr = FStringView(HostAddr).Mid(1, HostAddr.Len() - 2); #endif EffectiveHostAddr = &ModifiedHostAddr; } if (!bIsHostname) { Result = SocketSubsystem.GetAddressFromString(*EffectiveHostAddr); } if (!Result.IsValid() || !Result->IsValid()) { FAddressInfoResult GAIRequest = SocketSubsystem.GetAddressInfo(**EffectiveHostAddr, nullptr, EAddressInfoFlags::Default, NAME_None); if (GAIRequest.ReturnCode == SE_NO_ERROR && GAIRequest.Results.Num() > 0) { Result = GAIRequest.Results[0].Address; } } if (Result.IsValid() && Result->IsValid()) { Result->SetPort(Port); } return Result; } bool FStorageServerConnection::HandshakeRequest() { // Handshakes are done with a limited connection timeout so that we can find out if the destination is unreachable in a timely manner. const float ConnectionTimeoutSeconds = 5.0f; IStorageServerHttpClient::FResult ResultTuple = HttpClient->RequestSync( BaseURI, EStorageServerContentType::Unknown, "GET", TOptional(), EStorageServerContentType::Unknown, TOptional(), ConnectionTimeoutSeconds, false ); TIoStatusOr Result = ResultTuple.Get<0>(); if (Result.IsOk()) { FMemoryReaderView Reader(Result.ValueOrDie().GetView()); FCbObject ResponseObj = LoadCompactBinary(Reader).AsObject(); // we currently don't have any concept of protocol versioning, if // we succeed in communicating with the endpoint we're good since // any breaking API change would need to be done in a backward // compatible manner return true; } return false; } void FStorageServerConnection::GetDefaultCacheConfiguration(FCacheConfiguration& OutConfiguration) { const TCHAR* CmdLine = FCommandLine::Get(); const bool bPlatformSupportsCaching = PLATFORM_SUPPORTS_STORAGE_SERVER_CACHE; const bool bPlatformEnablesCachingByDefault = PLATFORM_ENABLES_STORAGE_SERVER_CACHE_BY_DEFAULT; OutConfiguration.bEnable = bPlatformSupportsCaching && bPlatformEnablesCachingByDefault; // always check it first to ensure we disable cache if cmd arg is provided if ((FCString::Strstr(CmdLine, TEXT("-ZenDisableCache")) != nullptr) || UE::IsUsingZenPakFileStreaming()) { OutConfiguration.bEnable = false; } else if (bPlatformSupportsCaching && (FCString::Strstr(CmdLine, TEXT("-ZenEnableCache")) != nullptr)) { OutConfiguration.bEnable = true; } if (!OutConfiguration.bEnable) { return; } OutConfiguration.bForceInvalidate = FCString::Strstr(CmdLine, TEXT("-ZenInvalidateCache")) != nullptr; if (!FParse::Value(CmdLine, TEXT("-ZenCacheSizeKB="), OutConfiguration.CacheSizeKB)) { OutConfiguration.CacheSizeKB = STORAGE_SERVER_PLATFORM_CACHE_SIZE_KB; } if (!FParse::Value(CmdLine, TEXT("-ZenCacheAbandonSizeKB="), OutConfiguration.AbandonSizeKB)) { OutConfiguration.AbandonSizeKB = STORAGE_SERVER_PLATFORM_CACHE_ABANDON_SIZE_KB; } OutConfiguration.FlushInterval = 10.0f; OutConfiguration.FlushEveryNEntries = 0; // use sectioned journal by default OutConfiguration.bUseSectionedJournal = FCString::Strstr(CmdLine, TEXT("-ZenUseSimpleJournal")) == nullptr; // use mmapp'ed storage if available and not explicitely disabled OutConfiguration.bUseMemoryMappedStorage = FPlatformProperties::SupportsMemoryMappedFiles() && (FCString::Strstr(CmdLine, TEXT("-ZenNoMmappedStorage")) == nullptr); } #if !PLATFORM_HAS_CUSTOM_STORAGE_SERVER_CACHE_STRATEGY void FStorageServerConnection::SetupCacheStrategy() { FCacheConfiguration Configuration = {}; GetDefaultCacheConfiguration(Configuration); if (!Configuration.bEnable) { CacheStrategy.Reset(); return; } FString DevStoragePath; #if defined(STORAGE_SERVER_PLATFORM_CACHE_ROOT_DIR) DevStoragePath = STORAGE_SERVER_PLATFORM_CACHE_ROOT_DIR; #elif PLATFORM_ANDROID DevStoragePath = FPlatformMisc::GamePersistentDownloadDir(); #elif PLATFORM_IOS DevStoragePath = FPlatformMisc::GetDiscardableCacheDir(); #endif const uint64 Size = ((uint64)Configuration.CacheSizeKB) * 1024; const float FlushInterval = Configuration.FlushInterval; const uint64 FlushEveryNEntries = Configuration.FlushEveryNEntries; const uint64 AbandonSize = ((uint64)Configuration.AbandonSizeKB) * 1024; const bool bShouldInvalidate = Configuration.bForceInvalidate; TUniquePtr Journal; if (Configuration.bUseSectionedJournal) { Journal = MakeUnique(*(DevStoragePath / TEXT("ZenCacheJournalSectioned.db"))); } else { Journal = MakeUnique(*(DevStoragePath / TEXT("ZenCache.db")), FlushEveryNEntries); } TUniquePtr Storage; if (Configuration.bUseMemoryMappedStorage) { Storage = MakeUnique(*(DevStoragePath / TEXT("ZenCache.storage")), Size); } else { Storage = MakeUnique(*(DevStoragePath / TEXT("ZenCache.storage")), Size); } CacheStrategy = MakeUnique(MoveTemp(Journal), MoveTemp(Storage), AbandonSize, FlushInterval, bShouldInvalidate); AsyncQueryLatestServerChunkInfo = MakeShared(*this); } #endif // !PLATFORM_HAS_CUSTOM_STORAGE_SERVER_CACHE_STRATEGY bool FStorageServerConnection::FinalizeSetupCacheStrategy() { if (!CacheStrategy.IsValid()) { return false; } // we can read from the cache if the asyncronous initialization has completed if (!AsyncQueryLatestServerChunkInfo.IsValid()) { return true; } // don't try to read from the cache until we've got the latest chunk hashes from the server // this will cause files to be read from the server instead of the cache but all early files are // fairly small and it is the only way to guarantee they are up to date if (!AsyncQueryLatestServerChunkInfo->IsFinished()) { return false; } // the server chunk info will have been updated by the async init thread. // if there is a platform-specific implementation of SetupCacheStrategy that defers cache creation, the expectation is that it will be handled there AsyncQueryLatestServerChunkInfo.Reset(); return true; } FStorageServerConnection::FAsyncQueryLatestServerChunkInfo::FAsyncQueryLatestServerChunkInfo(FStorageServerConnection& InOwner) : Owner(InOwner) , IsCompleted(FPlatformProcess::GetSynchEventFromPool(true)) { if (FRunnableThread::Create( this, TEXT("StorageServerCacheEntriesInit"), 0, EThreadPriority::TPri_Normal) == nullptr) { IsCompleted->Trigger(); } } FStorageServerConnection::FAsyncQueryLatestServerChunkInfo::~FAsyncQueryLatestServerChunkInfo() { IsCompleted->Wait(); FPlatformProcess::ReturnSynchEventToPool(IsCompleted); } bool FStorageServerConnection::FAsyncQueryLatestServerChunkInfo::IsFinished() const { return IsCompleted->Wait(0); } void FStorageServerConnection::FAsyncQueryLatestServerChunkInfo::Wait() { IsCompleted->Wait(); } uint32 FStorageServerConnection::FAsyncQueryLatestServerChunkInfo::Run() { TRACE_CPUPROFILER_EVENT_SCOPE(StorageServerRefreshCacheEntries); #if HAS_STORAGE_SERVER_RPC_GETCHUNKS_API TArray ChunkBatchRequests; Owner.CacheStrategy->IterateChunkIds([&](const FIoChunkId& ChunkId, const StorageServer::FCacheChunkInfo& ChunkInfo) { // Force a default ModTag value so we invalidate the chunk if we don't have modtag for some reason const uint64 ModTag = ChunkInfo.ModTag.Get(0); ChunkBatchRequests.Emplace(FChunkBatchRequestEntry::VerifyModTagRequest(ChunkId, ModTag)); }); TSet ValidChunkIds; ValidChunkIds.Reserve(ChunkBatchRequests.Num()); Owner.ReadChunkBatchRequest(ChunkBatchRequests, [&](FIoChunkId Id, EStorageServerContentType MimeType, FIoBuffer Data, const TOptional& ModTag) { if (ModTag.IsSet()) { // Chunks rpc endpoint indicate invalid chunks by presence of ModTag in result. TRACE_CPUPROFILER_EVENT_SCOPE(StorageServerRefreshCacheEntries::Invalidate); Owner.CacheStrategy->Invalidate(Id); } else { ValidChunkIds.Add(Id); } }, true); for (const FChunkBatchRequestEntry& Request: ChunkBatchRequests) { if (!ValidChunkIds.Contains(Request.ChunkId)) { // Absence of chunkid in response means chunk is not present on the server TRACE_CPUPROFILER_EVENT_SCOPE(StorageServerRefreshCacheEntries::Invalidate); Owner.CacheStrategy->Invalidate(Request.ChunkId); } } #endif IsCompleted->Trigger(); return 0; } void FStorageServerConnection::BuildReadChunkRequestUrl(FAnsiStringBuilderBase& Builder, const FIoChunkId& ChunkId, const uint64 Offset, const uint64 Size) { Builder.Append(BaseURI) << "/" << ChunkId; bool HaveQuery = false; auto AppendQueryDelimiter = [&] { if (HaveQuery) { Builder.Append(ANSITEXTVIEW("&")); } else { Builder.Append(ANSITEXTVIEW("?")); HaveQuery = true; } }; if (Offset) { AppendQueryDelimiter(); Builder.Appendf("offset=%" UINT64_FMT, Offset); } if (Size != ~uint64(0)) { AppendQueryDelimiter(); Builder.Appendf("size=%" UINT64_FMT, Size); } } TIoStatusOr FStorageServerConnection::ReadChunkRequestProcessHttpResult( IStorageServerHttpClient::FResult ResultTuple, const uint64 Offset, const uint64 Size, const TOptional OptDestination, const bool bHardwareTargetBuffer ) { TIoStatusOr Result = ResultTuple.Get<0>(); EStorageServerContentType MimeType = ResultTuple.Get<1>(); if (!Result.IsOk()) { UE_LOG(LogStorageServerConnection, Warning, TEXT("Failed read chunk from storage server. '%s' Offset:%" UINT64_FMT " Size:%" UINT64_FMT), *Result.Status().ToString(), Offset, Size); return Result.Status(); } FIoBuffer Buffer = Result.ValueOrDie(); TRACE_COUNTER_ADD(ZenHttpClientSerializedBytes, Buffer.GetSize()); if (MimeType == EStorageServerContentType::Binary) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkRequest::Binary); if (OptDestination.IsSet()) { ensure(OptDestination->GetSize() >= Buffer.GetSize()); FIoBuffer Destination = OptDestination.GetValue(); FMemory::Memcpy(Destination.GetData(), Buffer.GetData(), Buffer.GetSize()); Destination.SetSize(Buffer.GetSize()); return Destination; } else { Buffer.MakeOwned(); return Buffer; } } else if (MimeType == EStorageServerContentType::CompressedBinary) { TRACE_CPUPROFILER_EVENT_SCOPE(FStorageServerConnection::ReadChunkRequest::CompressedBinary); FMemoryReaderView Reader(Buffer.GetView()); FCompressedBuffer CompressedBuffer = FCompressedBuffer::FromCompressed(FSharedBuffer::MakeView(Buffer.GetData(), Buffer.GetSize())); FCompressedBufferReader CompressedBufferReader(CompressedBuffer); const uint64 RawSize = CompressedBufferReader.GetRawSize(); if (RawSize > 0) { const uint64 CompressedOffset = GetCompressedOffset(CompressedBuffer, Offset); const uint64 BytesToReadNonTrimmed = Size > 0 ? FMath::Min(Size, RawSize) : RawSize; const uint64 BytesToRead = FMath::Min(BytesToReadNonTrimmed, RawSize - CompressedOffset); ensure(!OptDestination.IsSet() || OptDestination->GetSize() >= BytesToRead); FIoBuffer OutChunk = OptDestination.IsSet() ? OptDestination.GetValue() : FIoBuffer(BytesToRead); OutChunk.SetSize(BytesToRead); if (CompressedBufferReader.TryDecompressTo(OutChunk.GetMutableView(), CompressedOffset, bHardwareTargetBuffer ? ECompressedBufferDecompressFlags::IntermediateBuffer : ECompressedBufferDecompressFlags::None)) { return OutChunk; } } } return FIoStatus(EIoErrorCode::Unknown); } uint64 FStorageServerConnection::GetCompressedOffset(const FCompressedBuffer& Buffer, uint64 RawOffset) { if (RawOffset > 0) { uint64 BlockSize = 0; ECompressedBufferCompressor Compressor; ECompressedBufferCompressionLevel CompressionLevel; const bool bOk = Buffer.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize); check(bOk); return BlockSize > 0 ? RawOffset % BlockSize : 0; } return 0; } void FStorageServerConnection::AddTimingInstance(const double Duration, const uint64 Bytes) { if ((Duration >= 0.0)) { double tr = ((double)(Bytes * 8) / Duration) / 1000000.0; //Mbps AccumulatedBytes.fetch_add(Bytes, std::memory_order_relaxed); RequestCount.fetch_add(1, std::memory_order_relaxed); double MinTemp = MinRequestThroughput.load(std::memory_order_relaxed); while (!MinRequestThroughput.compare_exchange_weak(MinTemp, FMath::Min(MinTemp, tr), std::memory_order_relaxed)) { MinTemp = MinRequestThroughput.load(std::memory_order_relaxed); } double MaxTemp = MaxRequestThroughput.load(std::memory_order_relaxed); while (!MaxRequestThroughput.compare_exchange_weak(MaxTemp, FMath::Max(MaxTemp, tr), std::memory_order_relaxed)) { MaxTemp = MaxRequestThroughput.load(std::memory_order_relaxed); } } TRACE_COUNTER_ADD(ZenHttpClientThroughputBytes, Bytes); } // TODO revive FStorageServerChunkBatchRequest #if 0 class FStorageServerChunkBatchRequest : private FStorageServerRequest { public: FStorageServerChunkBatchRequest& AddChunk(const FIoChunkId& ChunkId, int64 Offset, int64 Size); bool Issue(TFunctionRef OnResponse); private: friend FStorageServerConnection; FStorageServerChunkBatchRequest(FStorageServerConnection& Owner, FAnsiStringView Resource, FAnsiStringView Hostname); FStorageServerConnection& Owner; int32 ChunkCountOffset = 0; }; FStorageServerChunkBatchRequest::FStorageServerChunkBatchRequest(FStorageServerConnection& InOwner, FAnsiStringView Resource, FAnsiStringView Hostname) : FStorageServerRequest("POST", Resource, Hostname) , Owner(InOwner) { uint32 Magic = 0xAAAA'77AC; uint32 ChunkCountPlaceHolder = 0; uint32 Reserved1 = 0; uint32 Reserved2 = 0; *this << Magic; ChunkCountOffset = BodyBuffer.Num(); *this << ChunkCountPlaceHolder << Reserved1 << Reserved2; } FStorageServerChunkBatchRequest& FStorageServerChunkBatchRequest::AddChunk(const FIoChunkId& ChunkId, int64 Offset, int64 Size) { uint32* ChunkCount = reinterpret_cast(BodyBuffer.GetData() + ChunkCountOffset); *this << const_cast(ChunkId) << *ChunkCount << Offset << Size; ++(*ChunkCount); return *this; } bool FStorageServerChunkBatchRequest::Issue(TFunctionRef OnResponse) { IStorageConnectionSocket* Socket = Send(Owner); if (!Socket) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Failed to send chunk batch request to storage server.")); return false; } FStorageServerResponse Response(Owner, *Socket); if (!Response.IsOk()) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Failed to read chunk batch from storage server. '%s'"), *Response.GetErrorMessage()); return false; } uint32 Magic; uint32 ChunkCount; uint32 Reserved1; uint32 Reserved2; Response << Magic; if (Magic != 0xbada'b00f) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Invalid magic in chunk batch response from storage server.")); return false; } Response << ChunkCount; if (ChunkCount > INT32_MAX) { UE_LOG(LogStorageServerConnection, Fatal, TEXT("Invalid chunk count in chunk batch response from storage server.")); return false; } Response << Reserved1; Response << Reserved2; TArray> ChunkIndices; ChunkIndices.Reserve(ChunkCount); TArray> ChunkSizes; ChunkSizes.Reserve(ChunkCount); for (uint32 Index = 0; Index < ChunkCount; ++Index) { uint32 ChunkIndex; uint32 Flags; int64 ChunkSize; Response << ChunkIndex; Response << Flags; Response << ChunkSize; ChunkIndices.Add(ChunkIndex); ChunkSizes.Emplace(ChunkSize); } OnResponse(ChunkCount, ChunkIndices.GetData(), ChunkSizes.GetData(), Response); Owner.AddTimingInstance(GetDuration(), (double)Response.Tell()); return true; } #endif #endif