// ================================================================================================ // DTFluxRequestManager.cpp - Implémentation du gestionnaire de requêtes // ================================================================================================ #include "DTFluxQueuedManager.h" #include "DTFluxAsyncParser.h" #include "DTFluxNetworkModule.h" #include "Struct/DTFluxServerResponseStruct.h" #include "Struct/DTFluxRequestStructs.h" #include "JsonObjectConverter.h" bool FDTFluxTrackedRequest::HasTimedOut() const { if (State != EDTFluxRequestState::Pending && State != EDTFluxRequestState::Sent) return false; return (FDateTime::Now() - CreatedAt).GetTotalSeconds() > Config.TimeoutSeconds; } bool FDTFluxTrackedRequest::CanRetry() const { return CurrentRetries < Config.MaxRetries && (State == EDTFluxRequestState::Failed || State == EDTFluxRequestState::TimedOut); } float FDTFluxTrackedRequest::GetRetryDelay() const { return FMath::Pow(Config.RetryBackoffMultiplier, CurrentRetries); } bool FDTFluxTrackedRequest::Matches(EDTFluxApiDataType InType, int32 InContestId, int32 InStageId, int32 InSplitId) const { return RequestType == InType && ContestId == InContestId && StageId == InStageId && SplitId == InSplitId; } void FDTFluxTrackedRequest::SetRawResponse(const FString& RawData) { RawResponseData = RawData; ParsedResponse.Reset(); bIsResponseParsed = false; } FString FDTFluxTrackedRequest::Serialize() const { FString JSONString; switch (RequestType) { case EDTFluxApiDataType::RaceData: { FDTFluxRaceDataRequest RaceData; FJsonObjectConverter::UStructToJsonObjectString(RaceData, JSONString); break; } case EDTFluxApiDataType::TeamList: { const FDTFluxTeamListRequest TeamList; FJsonObjectConverter::UStructToJsonObjectString(TeamList, JSONString); break; } case EDTFluxApiDataType::ContestRanking: { FDTFluxContestRankingRequest ContestRanking(ContestId); FJsonObjectConverter::UStructToJsonObjectString(ContestRanking, JSONString); break; } case EDTFluxApiDataType::StageRanking: { FDTFluxStageRankingRequest StageRanking(ContestId, StageId); FJsonObjectConverter::UStructToJsonObjectString(StageRanking, JSONString); break; } case EDTFluxApiDataType::SplitRanking: { FDTFluxSplitRankingRequest SplitRanking(ContestId, StageId, SplitId); FJsonObjectConverter::UStructToJsonObjectString(SplitRanking, JSONString); break; } default: JSONString = ""; break; } return JSONString; } FDTFluxQueuedRequestManager::FDTFluxQueuedRequestManager() { AsyncParser = MakeUnique(); UE_LOG(logDTFluxNetwork, Verbose, TEXT("RequestManager created")); } FDTFluxQueuedRequestManager::~FDTFluxQueuedRequestManager() { Shutdown(); UE_LOG(logDTFluxNetwork, Verbose, TEXT("RequestManager destroyed")); } void FDTFluxQueuedRequestManager::Initialize(const FDTFluxRequestConfig& InDefaultConfig) { if (bIsInitialized.load()) { UE_LOG(logDTFluxNetwork, Warning, TEXT("RequestManager already initialized")); return; } DefaultConfig = InDefaultConfig; bIsInitialized.store(true); UE_LOG(logDTFluxNetwork, Log, TEXT("RequestManager initialized with timeout=%.1fs"), DefaultConfig.TimeoutSeconds); } void FDTFluxQueuedRequestManager::Shutdown() { if (!bIsInitialized.load()) return; bIsInitialized.store(false); // Nettoyer toutes les données { FScopeLock RequestsLock_Local(&RequestsLock); FScopeLock CallbacksLock_Local(&CallbacksLock); AllRequests.Empty(); SuccessCallbacks.Empty(); ErrorCallbacks.Empty(); } UE_LOG(logDTFluxNetwork, Log, TEXT("RequestManager shutdown")); } FGuid FDTFluxQueuedRequestManager::CreateTrackedRequest( EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, int32 SplitId, const FDTFluxRequestConfig& CustomConfig) { if (!bIsInitialized.load()) { UE_LOG(logDTFluxNetwork, Error, TEXT("RequestManager not initialized")); return FGuid(); } // Create new request auto NewRequest = MakeShared(); NewRequest->RequestType = RequestType; NewRequest->ContestId = ContestId; NewRequest->StageId = StageId; NewRequest->SplitId = SplitId; NewRequest->Config = (CustomConfig.TimeoutSeconds > 0) ? CustomConfig : DefaultConfig; FGuid RequestId = NewRequest->RequestId; { FScopeLock Lock(&RequestsLock); AllRequests.Add(RequestId, NewRequest); TotalRequests++; } UE_LOG(logDTFluxNetwork, Log, TEXT("Created tracked request %s: Type=%s, Contest=%d, Stage=%d, Split=%d"), *RequestId.ToString(), *UEnum::GetValueAsString(RequestType), ContestId, StageId, SplitId); return RequestId; } FGuid FDTFluxQueuedRequestManager::CreateTrackedRequestWithCallbacks( EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, int32 SplitId, FOnDTFluxRequestSuccess OnSuccess, FOnDTFluxRequestError OnError, const FDTFluxRequestConfig& CustomConfig) { FGuid RequestId = CreateTrackedRequest(RequestType, ContestId, StageId, SplitId, CustomConfig); if (RequestId.IsValid()) { FScopeLock Lock(&CallbacksLock); if (OnSuccess.IsBound()) { SuccessCallbacks.Add(RequestId, OnSuccess); } if (OnError.IsBound()) { ErrorCallbacks.Add(RequestId, OnError); } } return RequestId; } bool FDTFluxQueuedRequestManager::MarkRequestAsSent(const FGuid& RequestId) { FScopeLock Lock(&RequestsLock); if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { TSharedPtr Request = *RequestPtr; Request->SentAt = FDateTime::Now(); Request->LastAttemptTime = FDateTime::Now(); ChangeRequestState(Request, EDTFluxRequestState::Sent); return true; } return false; } bool FDTFluxQueuedRequestManager::CompleteRequest(const FGuid& RequestId, const FString& RawResponseData, bool bUseAsyncParsing) { UE_LOG(logDTFluxNetwork, Log, TEXT("FDTFluxQueuedRequestManager::CompleteRequest() %s"), *RequestId.ToString()); TSharedPtr Request; { FScopeLock Lock(&RequestsLock); if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { Request = *RequestPtr; } } if (!Request.IsValid()) { UE_LOG(logDTFluxNetwork, Warning, TEXT("Request %s not found"), *RequestId.ToString()); return false; } // Store RawResponse Request->SetRawResponse(RawResponseData); Request->CompletedAt = FDateTime::Now(); UE_LOG(logDTFluxNetwork, Log, TEXT("Request %s completed at %s"), *RequestId.ToString(), *Request->CompletedAt.ToString()); // Decide to parse based upon config bool bHasCallbacks = false; { FScopeLock Lock(&CallbacksLock); bHasCallbacks = SuccessCallbacks.Contains(RequestId) || ErrorCallbacks.Contains(RequestId); } if (bHasCallbacks && bUseAsyncParsing && !RawResponseData.IsEmpty()) { UE_LOG(logDTFluxNetwork, Log, TEXT("Request %s [bHasCallbacks=%s], [bUseAsyncParsing=%s], [bIsRawResponseEmpty=%s]"), *RequestId.ToString(), bHasCallbacks ? TEXT("true") : TEXT("false"), bUseAsyncParsing ? TEXT("true") : TEXT("false"), RawResponseData.IsEmpty() ? TEXT("true") : TEXT("false")); // Async parsing for Cb FOnParsingCompleted OnCompleted = FOnParsingCompleted::CreateRaw( this, &FDTFluxQueuedRequestManager::OnParsingCompleted ); FOnParsingFailed OnFailed = FOnParsingFailed::CreateRaw( this, &FDTFluxQueuedRequestManager::OnParsingFailed ); // Maybe send to parser in another place AsyncParser->ParseResponseAsync(RequestId, RawResponseData, OnCompleted, OnFailed); UE_LOG(logDTFluxNetwork, Verbose, TEXT("Started async parsing for request %s"), *RequestId.ToString()); return true; } else { UE_LOG(logDTFluxNetwork, Warning, TEXT("request %s completed without sync"), *RequestId.ToString()); // Compléter immédiatement sans parsing ou avec parsing sync EDTFluxRequestState NewState = EDTFluxRequestState::Completed; ChangeRequestState(Request, NewState); // Déclencher les callbacks avec les données brutes TriggerCallbacks(*Request); CleanupCallbacks(RequestId); return true; } } /** * @todo Check protocol errors ??? * @param RequestId * @param ErrorMessage * @return */ bool FDTFluxQueuedRequestManager::FailRequest(const FGuid& RequestId, const FString& ErrorMessage) { TSharedPtr Request; { FScopeLock Lock(&RequestsLock); if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { Request = *RequestPtr; } } if (!Request.IsValid()) { return false; } Request->LastErrorMessage = ErrorMessage; ChangeRequestState(Request, EDTFluxRequestState::Failed); TriggerCallbacks(*Request); CleanupCallbacks(RequestId); UE_LOG(logDTFluxNetwork, Error, TEXT("Failed request %s: %s"), *RequestId.ToString(), *ErrorMessage); return true; } bool FDTFluxQueuedRequestManager::RetryRequest(const FGuid& RequestId) { TSharedPtr Request; { FScopeLock Lock(&RequestsLock); if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { Request = *RequestPtr; } } if (!Request.IsValid() || !Request->CanRetry()) { return false; } Request->CurrentRetries++; Request->LastAttemptTime = FDateTime::Now(); ChangeRequestState(Request, EDTFluxRequestState::Retrying); UE_LOG(logDTFluxNetwork, Log, TEXT("Retrying request %s (attempt %d/%d)"), *RequestId.ToString(), Request->CurrentRetries, Request->Config.MaxRetries); return true; } bool FDTFluxQueuedRequestManager::FindPendingRequest( FGuid& OutRequestId, EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, int32 SplitId) const { FScopeLock Lock(&RequestsLock); for (const auto& [RequestId, Request] : AllRequests) { if ((Request->State == EDTFluxRequestState::Pending || Request->State == EDTFluxRequestState::Sent) && Request->Matches(RequestType, ContestId, StageId, SplitId)) { OutRequestId = RequestId; return true; } } return false; } // === ACCESSEURS === bool FDTFluxQueuedRequestManager::GetRequest(const FGuid& RequestId, FDTFluxTrackedRequest& OutRequest) const { FScopeLock Lock(&RequestsLock); if (const TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { OutRequest = **RequestPtr; return true; } return false; } const FDTFluxTrackedRequest* FDTFluxQueuedRequestManager::GetRequestPtr(const FGuid& RequestId) const { FScopeLock Lock(&RequestsLock); if (const TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { return RequestPtr->Get(); } return nullptr; } TArray FDTFluxQueuedRequestManager::GetRequestsByState(EDTFluxRequestState State) const { FScopeLock Lock(&RequestsLock); TArray Results; for (const auto& [RequestId, Request] : AllRequests) { if (Request->State == State) { Results.Add(*Request); } } return Results; } int32 FDTFluxQueuedRequestManager::GetRequestCount(EDTFluxRequestState State) const { FScopeLock Lock(&RequestsLock); int32 Count = 0; for (const auto& [RequestId, Request] : AllRequests) { if (Request->State == State) { Count++; } } return Count; } FDTFluxQueuedRequestManager::FRequestStatistics FDTFluxQueuedRequestManager::GetStatistics() const { FScopeLock RequestsLock_Local(&RequestsLock); FScopeLock MetricsLock_Local(&MetricsLock); FRequestStatistics Stats; for (const auto& [RequestId, Request] : AllRequests) { switch (Request->State) { case EDTFluxRequestState::Pending: case EDTFluxRequestState::Sent: case EDTFluxRequestState::Retrying: Stats.Pending++; break; case EDTFluxRequestState::Completed: Stats.Completed++; break; case EDTFluxRequestState::Failed: case EDTFluxRequestState::TimedOut: Stats.Failed++; break; } } Stats.TotalRequests = TotalRequests; return Stats; } // === NETTOYAGE === int32 FDTFluxQueuedRequestManager::CleanupCompletedRequests(float OlderThanSeconds) { FScopeLock Lock(&RequestsLock); TArray OldRequests; const FDateTime Threshold = FDateTime::Now() - FTimespan::FromSeconds(OlderThanSeconds); for (const auto& [RequestId, Request] : AllRequests) { if ((Request->State == EDTFluxRequestState::Completed || Request->State == EDTFluxRequestState::Failed || Request->State == EDTFluxRequestState::TimedOut) && Request->CompletedAt < Threshold) { OldRequests.Add(RequestId); } } for (const FGuid& RequestId : OldRequests) { AllRequests.Remove(RequestId); } return OldRequests.Num(); } void FDTFluxQueuedRequestManager::ClearAllRequests() { FScopeLock RequestsLock_Local(&RequestsLock); FScopeLock CallbacksLock_Local(&CallbacksLock); AllRequests.Empty(); SuccessCallbacks.Empty(); ErrorCallbacks.Empty(); UE_LOG(logDTFluxNetwork, Log, TEXT("Cleared all requests")); } void FDTFluxQueuedRequestManager::Tick(float DeltaTime) { if (!bIsInitialized.load()) return; // Mise à jour des timers TimeSinceLastTimeoutCheck += DeltaTime; TimeSinceLastRetryCheck += DeltaTime; // Vérifier les timeouts if (TimeSinceLastTimeoutCheck >= TimeoutCheckInterval) { ProcessTimeouts(); TimeSinceLastTimeoutCheck = 0.0f; } // Vérifier les retries if (TimeSinceLastRetryCheck >= RetryCheckInterval) { ProcessRetries(); TimeSinceLastRetryCheck = 0.0f; } } void FDTFluxQueuedRequestManager::ChangeRequestState(TSharedPtr Request, EDTFluxRequestState NewState) { if (!Request.IsValid()) return; const EDTFluxRequestState OldState = Request->State; Request->State = NewState; // Déclencher l'événement de changement d'état OnRequestStateChanged.Broadcast(Request->RequestId, NewState); UE_LOG(logDTFluxNetwork, VeryVerbose, TEXT("DTFluxQueuedRequestManager: Request %s state changed from %s to %s"), *Request->RequestId.ToString(), *UEnum::GetValueAsString(OldState), *UEnum::GetValueAsString(NewState)); } void FDTFluxQueuedRequestManager::ProcessTimeouts() { FScopeLock Lock(&RequestsLock); TArray> TimedOutRequests; for (const auto& Pair : AllRequests) { const auto& Request = Pair.Value; if (Request->HasTimedOut()) { TimedOutRequests.Add(Request); } } for (const auto& Request : TimedOutRequests) { Request->LastErrorMessage = FString::Printf( TEXT("Request timed out after %.1f seconds"), Request->Config.TimeoutSeconds); if (Request->CanRetry()) { Request->CurrentRetries++; ChangeRequestState(Request, EDTFluxRequestState::Retrying); } else { ChangeRequestState(Request, EDTFluxRequestState::TimedOut); TriggerCallbacks(*Request); OnRequestFailed.Broadcast(*Request); } } } void FDTFluxQueuedRequestManager::ProcessRetries() { FScopeLock Lock(&RequestsLock); const FDateTime Now = FDateTime::Now(); TArray> ReadyToRetry; for (const auto& Pair : AllRequests) { const auto& Request = Pair.Value; if (Request->State == EDTFluxRequestState::Retrying) { const float ElapsedSinceLastAttempt = (Now - Request->LastAttemptTime).GetTotalSeconds(); if (ElapsedSinceLastAttempt >= Request->GetRetryDelay()) { ReadyToRetry.Add(Request); } } } for (const auto& Request : ReadyToRetry) { Request->LastAttemptTime = Now; ChangeRequestState(Request, EDTFluxRequestState::Pending); } } void FDTFluxQueuedRequestManager::TriggerCallbacks(const FDTFluxTrackedRequest& Request) { FScopeLock Lock(&CallbacksLock); if (Request.State == EDTFluxRequestState::Completed) { // Success Cb const FOnDTFluxRequestSuccess* SuccessCallback = SuccessCallbacks.Find(Request.RequestId); if (SuccessCallback && SuccessCallback->IsBound()) { SuccessCallback->Execute(Request); } } else if (Request.State == EDTFluxRequestState::Failed || Request.State == EDTFluxRequestState::TimedOut) { // Error Cb const FOnDTFluxRequestError* ErrorCallback = ErrorCallbacks.Find(Request.RequestId); if (ErrorCallback && ErrorCallback->IsBound()) { ErrorCallback->Execute(Request, Request.LastErrorMessage); } } } void FDTFluxQueuedRequestManager::CleanupCallbacks(const FGuid& RequestId) { FScopeLock Lock(&CallbacksLock); SuccessCallbacks.Remove(RequestId); ErrorCallbacks.Remove(RequestId); } void FDTFluxQueuedRequestManager::OnParsingCompleted(const FGuid& RequestId, TSharedPtr ParsedResponse, bool bSuccess) { UE_LOG(logDTFluxNetwork, Log, TEXT("FDTFluxQueuedRequestManager::OnParsingCompleted() request %s"), *RequestId.ToString()) TSharedPtr Request; { FScopeLock Lock(&RequestsLock); auto* RequestPtr = AllRequests.Find(RequestId); if (!RequestPtr || !RequestPtr->IsValid()) { UE_LOG(logDTFluxNetwork, Error, TEXT( "DTFluxQueuedRequestManager::OnParsingCompleted() RequestId%s [InvalidRequestId=%s], [RequestPtrValid=%s]" ), *RequestId.ToString(), RequestPtr ? TEXT("true") : TEXT("false"), RequestPtr->IsValid() ? TEXT("true") : TEXT("false")); return; } Request = *RequestPtr; } if (bSuccess && ParsedResponse.IsValid()) { Request->ParsedResponse = ParsedResponse; Request->bIsResponseParsed = true; EDTFluxRequestState NewState = EDTFluxRequestState::Completed; ChangeRequestState(Request, NewState); UE_LOG(logDTFluxNetwork, Log, TEXT("DTFluxQueuedRequestManager: Async parsing completed for request %s"), *RequestId.ToString()); } else { Request->LastErrorMessage = TEXT("Async parsing failed"); ChangeRequestState(Request, EDTFluxRequestState::Failed); UE_LOG(logDTFluxNetwork, Error, TEXT("Async parsing failed for request %s"), *RequestId.ToString()); } TriggerCallbacks(*Request); CleanupCallbacks(RequestId); } void FDTFluxQueuedRequestManager::OnParsingFailed(const FGuid& RequestId, const FString& ErrorMessage) { UE_LOG(logDTFluxNetwork, Error, TEXT("DTFluxQueuedRequestManager: Async parsing failed for request %s: %s"), *RequestId.ToString(), *ErrorMessage); FailRequest(RequestId, FString::Printf(TEXT("Parsing failed: %s"), *ErrorMessage)); }