/// <summary> /// It's recommended to use this method (or another TryAwaitOrTimeout or AwaitOrTimeout method) /// instead of just Task.WhenAny(taskToAwait, Task.Delay(timeout)) /// because this method cancels the timer for timeout while <c>Task.Delay(timeout)</c>. /// If the number of “zombie” timer jobs starts becoming significant, performance could suffer. /// For more detailed explanation see https://devblogs.microsoft.com/pfxteam/crafting-a-task-timeoutafter-method/ /// </summary> /// <returns><c>true</c> if <c>taskToAwait</c> completed before the timeout, <c>false</c> otherwise</returns> internal static async Task <bool> TryAwaitOrTimeout(this IAgentTimer agentTimer, Task taskToAwait , AgentTimeInstant until, CancellationToken cancellationToken = default ) { var timeoutDelayCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var timeoutDelayTask = agentTimer.Delay(until, timeoutDelayCts.Token); try { var completedTask = await Task.WhenAny(taskToAwait, timeoutDelayTask); if (completedTask == taskToAwait) { // await taskToAwait to make it throw if taskToAwait is faulted or cancelled so it will throw as it should await taskToAwait; return(true); } Assertion.IfEnabled?.That(completedTask == timeoutDelayTask , $"{nameof(completedTask)}: {completedTask}, {nameof(timeoutDelayTask)}: timeOutTask, {nameof(taskToAwait)}: taskToAwait"); // await timeout task in case it is cancelled and did not timed out so it will throw as it should await timeoutDelayTask; return(false); } finally { if (!timeoutDelayTask.IsCompleted) { timeoutDelayCts.Cancel(); } timeoutDelayCts.Dispose(); } }
protected override async Task WorkLoopIteration() { ++_dbgIterationsCount; var waitingLogSeverity = LogLevel.Trace; WaitInfoS waitInfo; HttpRequestMessage httpRequest = null; HttpResponseMessage httpResponse = null; string httpResponseBody = null; try { httpRequest = BuildHttpRequest(_eTag); (httpResponse, httpResponseBody) = await FetchConfigHttpResponseAsync(httpRequest); ConfigDelta configDelta; (configDelta, waitInfo) = ProcessHttpResponse(httpResponse, httpResponseBody); if (configDelta != null) { UpdateConfigStore(configDelta); _eTag = httpResponse.Headers.ETag; } } catch (OperationCanceledException) { throw; } catch (Exception ex) { var severity = LogLevel.Error; waitInfo = new WaitInfoS(WaitTimeIfAnyError, "Default wait time is used because exception was thrown" + " while fetching configuration from APM Server and parsing it."); if (ex is FailedToFetchConfigException fEx) { severity = fEx.Severity; fEx.WaitInfo?.Let(it => { waitInfo = it; }); } if (severity == LogLevel.Error) { waitingLogSeverity = LogLevel.Information; } _logger.IfLevel(severity) ?.LogException(ex, "Exception was thrown while fetching configuration from APM Server and parsing it." + " ETag: `{ETag}'. URL: `{Url}'. Apm Server base URL: `{ApmServerUrl}'. WaitInterval: {WaitInterval}." + " dbgIterationsCount: {dbgIterationsCount}." + Environment.NewLine + "+-> Request:{HttpRequest}" + Environment.NewLine + "+-> Response:{HttpResponse}" + Environment.NewLine + "+-> Response body [length: {HttpResponseBodyLength}]:{HttpResponseBody}" , _eTag.AsNullableToString(), _getConfigAbsoluteUrl, HttpClientInstance.BaseAddress, waitInfo.Interval.ToHms(), _dbgIterationsCount , httpRequest == null ? " N/A" : Environment.NewLine + TextUtils.Indent(httpRequest.ToString()) , httpResponse == null ? " N/A" : Environment.NewLine + TextUtils.Indent(httpResponse.ToString()) , httpResponseBody == null ? "N/A" : httpResponseBody.Length.ToString() , httpResponseBody == null ? " N/A" : Environment.NewLine + TextUtils.Indent(httpResponseBody)); } finally { httpRequest?.Dispose(); httpResponse?.Dispose(); } _logger.IfLevel(waitingLogSeverity) ?.Log("Waiting {WaitInterval}... {WaitReason}. dbgIterationsCount: {dbgIterationsCount}." , waitInfo.Interval.ToHms(), waitInfo.Reason, _dbgIterationsCount); await _agentTimer.Delay(_agentTimer.Now + waitInfo.Interval, CtsInstance.Token); }