public RetryStrategy(ExpectedPollException ex, RetryStrategy lastRetryStategy) { this.ErrorCategory = ex.ErrorCategory; if (this.ErrorCategory != lastRetryStategy?.ErrorCategory) { this.RetryCount = 0; } else { this.RetryCount = (lastRetryStategy?.RetryCount ?? 0) + 1; } switch (this.ErrorCategory) { case ExpectedErrorCategory.Unauthorised401: case ExpectedErrorCategory.DuplicateMessageDetected: this.DropImmediately = true; break; case ExpectedErrorCategory.InvalidRPDEPage: case ExpectedErrorCategory.PageFetchError: case ExpectedErrorCategory.UnexpectedErrorDuringDatabaseWrite: case ExpectedErrorCategory.UnexpectedError: // Exponential backoff if (this.RetryCount > 15) { this.DeadLetter = true; } else { this.DelaySeconds = (int)BigInteger.Pow(2, this.RetryCount); } break; case ExpectedErrorCategory.ForceClearProxyCache: this.DeadLetter = true; break; case ExpectedErrorCategory.SqlTransientError: this.DelaySeconds = SqlUtils.SqlRetrySecondsRecommendation; break; default: throw new InvalidOperationException("Unknown Retry Strategy"); } }
public static async Task Run([ServiceBusTrigger(Utils.FEED_STATE_QUEUE_NAME, Connection = "ServiceBusConnection")] Message message, MessageReceiver messageReceiver, string lockToken, [ServiceBus(Utils.FEED_STATE_QUEUE_NAME, Connection = "ServiceBusConnection", EntityType = EntityType.Queue)] IAsyncCollector <Message> queueCollector, ILogger log) { var feedStateItem = FeedState.DecodeFromMessage(message); log.LogInformation($"PollQueueHandler queue trigger function processed message: {feedStateItem?.nextUrl}"); // Increment poll requests before anything else feedStateItem.totalPollRequests++; feedStateItem.dateModified = DateTime.Now; SourcePage sourcePage; try { sourcePage = await ExecutePoll(feedStateItem.name, feedStateItem.nextUrl, feedStateItem.lastPageReads == 0, feedStateItem.deletedItemDaysToLive, log); } catch (Exception ex) { var expectedException = ExpectedPollException.ExpectTheUnexpected(ex); feedStateItem.retryStategy = new RetryStrategy(expectedException, feedStateItem?.retryStategy); feedStateItem.totalErrors++; if (feedStateItem.retryStategy.DeadLetter) { log.LogError(expectedException.RenderMessageWithFullContext(feedStateItem, $"DEAD-LETTERING: '{feedStateItem.name}'.")); await messageReceiver.DeadLetterAsync(lockToken); } if (feedStateItem.retryStategy.DropImmediately) { log.LogWarning(expectedException.RenderMessageWithFullContext(feedStateItem, $"Dropped message for '{feedStateItem.name}'.")); await messageReceiver.CompleteAsync(lockToken); } else { feedStateItem.lastError = expectedException.RenderMessageWithFullContext(feedStateItem, $"Retrying '{feedStateItem.name}' attempt {feedStateItem.retryStategy.RetryCount} in {feedStateItem.retryStategy.DelaySeconds} seconds."); log.LogWarning(feedStateItem.lastError); var retryMsg = feedStateItem.EncodeToMessage(feedStateItem.retryStategy.DelaySeconds); // Check lock exists, as close to a transaction as we can get if (await messageReceiver.RenewLockAsync(lockToken) != null) { await messageReceiver.CompleteAsync(lockToken); await queueCollector.AddAsync(retryMsg); } } return; } // Update counters on success if (sourcePage.IsLastPage) { feedStateItem.lastPageReads++; } else { feedStateItem.lastPageReads = 0; } feedStateItem.retryStategy = null; feedStateItem.lastError = null; feedStateItem.totalPagesRead++; feedStateItem.totalItemsRead += sourcePage.Content.items.Count; feedStateItem.nextUrl = sourcePage.Content.next; Message newMessage; // If immediate poll is specified for last page, respect any Expires header provided for throttling if (sourcePage.IsLastPage) { if (sourcePage.LastPageDetails?.Expires != null) { newMessage = feedStateItem.EncodeToMessage(sourcePage.LastPageDetails.Expires); } else if (sourcePage.LastPageDetails?.MaxAge != null) { newMessage = feedStateItem.EncodeToMessage((int)sourcePage.LastPageDetails.MaxAge?.TotalSeconds); } else { // Default last page polling interval newMessage = feedStateItem.EncodeToMessage(Utils.DEFAULT_POLL_INTERVAL); } } else { // If not last page, get the next page immediately newMessage = feedStateItem.EncodeToMessage(0); } // These two operations should be in a transaction, but to save cost they ordered so that a failure will result in the polling stopping, // and the ResyncDroppedFeeds timer trigger will hence be required to ensure the system is still robust // using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) // { // await messageReceiver.CompleteAsync(lockToken); // await queueCollector.AddAsync(newMessage); // scope.Complete(); // declare the transaction done // } // Check lock exists, as close to a transaction as we can get if (await messageReceiver.RenewLockAsync(lockToken) != null) { await messageReceiver.CompleteAsync(lockToken); await queueCollector.AddAsync(newMessage); } }