Retry downloading in batches for Outlook
This commit is contained in:
@@ -4,7 +4,6 @@ using System.Diagnostics;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -21,8 +20,6 @@ using Microsoft.Graph.Models.ODataErrors;
|
|||||||
using Microsoft.Kiota.Abstractions;
|
using Microsoft.Kiota.Abstractions;
|
||||||
using Microsoft.Kiota.Abstractions.Authentication;
|
using Microsoft.Kiota.Abstractions.Authentication;
|
||||||
using Microsoft.Kiota.Abstractions.Serialization;
|
using Microsoft.Kiota.Abstractions.Serialization;
|
||||||
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
|
|
||||||
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
|
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MoreLinq.Extensions;
|
using MoreLinq.Extensions;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -122,14 +119,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
handlers.Add(GetMicrosoftImmutableIdHandler());
|
handlers.Add(GetMicrosoftImmutableIdHandler());
|
||||||
|
|
||||||
// Remove existing RetryHandler and add a new one with custom options.
|
|
||||||
var existingRetryHandler = handlers.FirstOrDefault(a => a is RetryHandler);
|
|
||||||
if (existingRetryHandler != null)
|
|
||||||
handlers.Remove(existingRetryHandler);
|
|
||||||
|
|
||||||
// Add custom one.
|
|
||||||
handlers.Add(GetRetryHandler());
|
|
||||||
|
|
||||||
var httpClient = GraphClientFactory.Create(handlers);
|
var httpClient = GraphClientFactory.Create(handlers);
|
||||||
_graphClient = new GraphServiceClient(httpClient, new BaseBearerTokenAuthenticationProvider(tokenProvider));
|
_graphClient = new GraphServiceClient(httpClient, new BaseBearerTokenAuthenticationProvider(tokenProvider));
|
||||||
|
|
||||||
@@ -141,30 +130,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
private MicrosoftImmutableIdHandler GetMicrosoftImmutableIdHandler() => new();
|
private MicrosoftImmutableIdHandler GetMicrosoftImmutableIdHandler() => new();
|
||||||
|
|
||||||
private RetryHandler GetRetryHandler()
|
|
||||||
{
|
|
||||||
var options = new RetryHandlerOption()
|
|
||||||
{
|
|
||||||
ShouldRetry = (delay, attempt, httpResponse) =>
|
|
||||||
{
|
|
||||||
var statusCode = httpResponse.StatusCode;
|
|
||||||
|
|
||||||
return statusCode switch
|
|
||||||
{
|
|
||||||
HttpStatusCode.ServiceUnavailable => true,
|
|
||||||
HttpStatusCode.GatewayTimeout => true,
|
|
||||||
(HttpStatusCode)429 => true,
|
|
||||||
HttpStatusCode.Unauthorized => true,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Delay = 3,
|
|
||||||
MaxRetry = 3
|
|
||||||
};
|
|
||||||
|
|
||||||
return new RetryHandler(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -193,7 +158,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
for (int i = 0; i < totalFolders; i++)
|
for (int i = 0; i < totalFolders; i++)
|
||||||
{
|
{
|
||||||
var folder = synchronizationFolders[i];
|
var folder = synchronizationFolders[i];
|
||||||
|
|
||||||
// Update progress based on folder completion
|
// Update progress based on folder completion
|
||||||
UpdateSyncProgress(totalFolders, totalFolders - (i + 1), $"Syncing {folder.FolderName}...");
|
UpdateSyncProgress(totalFolders, totalFolders - (i + 1), $"Syncing {folder.FolderName}...");
|
||||||
|
|
||||||
@@ -316,7 +281,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
};
|
};
|
||||||
|
|
||||||
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||||
|
|
||||||
if (handled)
|
if (handled)
|
||||||
{
|
{
|
||||||
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
|
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
|
||||||
@@ -333,7 +298,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
// No handler could process this error, log and re-throw
|
// No handler could process this error, log and re-throw
|
||||||
_logger.Error(apiException, "Unhandled API error during initial sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
|
_logger.Error(apiException, "Unhandled API error during initial sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-throw the exception so the synchronization can be retried
|
// Re-throw the exception so the synchronization can be retried
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -482,7 +447,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Download all messages in this chunk concurrently
|
// Download all messages in this chunk concurrently
|
||||||
var chunkDownloadedIds = await DownloadMessageMetadataBatchAsync(messageIdsToDownload, folder, cancellationToken).ConfigureAwait(false);
|
var chunkDownloadedIds = await DownloadMessageMetadataBatchAsync(messageIdsToDownload, folder, true, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
downloadedMessageIds.AddRange(chunkDownloadedIds);
|
downloadedMessageIds.AddRange(chunkDownloadedIds);
|
||||||
|
|
||||||
@@ -533,7 +498,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
/// Downloads metadata for a batch of messages using Graph SDK batch API (no MIME content).
|
/// Downloads metadata for a batch of messages using Graph SDK batch API (no MIME content).
|
||||||
/// Processes up to 20 messages per batch request as per MaximumAllowedBatchRequestSize.
|
/// Processes up to 20 messages per batch request as per MaximumAllowedBatchRequestSize.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<string>> DownloadMessageMetadataBatchAsync(List<string> messageIds, MailItemFolder folder, CancellationToken cancellationToken)
|
private async Task<List<string>> DownloadMessageMetadataBatchAsync(List<string> messageIds, MailItemFolder folder, bool retryFailedOnce, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (messageIds == null || messageIds.Count == 0)
|
if (messageIds == null || messageIds.Count == 0)
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
@@ -561,6 +526,10 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
return downloadedIds;
|
return downloadedIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store failed message ids to retry after.
|
||||||
|
|
||||||
|
List<string> failedMessageIds = new();
|
||||||
|
|
||||||
// Process in batches of MaximumAllowedBatchRequestSize (20)
|
// Process in batches of MaximumAllowedBatchRequestSize (20)
|
||||||
var batches = messagesToDownload.Batch((int)MaximumAllowedBatchRequestSize);
|
var batches = messagesToDownload.Batch((int)MaximumAllowedBatchRequestSize);
|
||||||
|
|
||||||
@@ -623,6 +592,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.Warning("Failed to deserialize message {MailId} for folder {FolderName}", messageId, folder.FolderName);
|
_logger.Warning("Failed to deserialize message {MailId} for folder {FolderName}", messageId, folder.FolderName);
|
||||||
|
failedMessageIds.Add(messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ODataError odataError)
|
catch (ODataError odataError)
|
||||||
@@ -634,6 +604,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
failedMessageIds.Add(messageId);
|
||||||
_logger.Error("OData error while downloading mail {MailId} for folder {FolderName}. Error: {Error}", messageId, folder.FolderName, odataError.Error?.Message);
|
_logger.Error("OData error while downloading mail {MailId} for folder {FolderName}. Error: {Error}", messageId, folder.FolderName, odataError.Error?.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,28 +616,44 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
Account = Account,
|
Account = Account,
|
||||||
ErrorCode = (int?)serviceException.ResponseStatusCode,
|
ErrorCode = (int?)serviceException.ResponseStatusCode,
|
||||||
ErrorMessage = $"Service error during batch mail download: {serviceException.Message}",
|
ErrorMessage = $"Service error during batch mail download: {serviceException.Message}",
|
||||||
Exception = serviceException
|
Exception = serviceException,
|
||||||
};
|
};
|
||||||
|
|
||||||
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!handled)
|
if (!handled)
|
||||||
{
|
{
|
||||||
|
failedMessageIds.Add(messageId);
|
||||||
_logger.Error(serviceException, "Unhandled service error while downloading mail {MailId} for folder {FolderName}. Error: {ErrorCode}", messageId, folder.FolderName, serviceException.ResponseStatusCode);
|
_logger.Error(serviceException, "Unhandled service error while downloading mail {MailId} for folder {FolderName}. Error: {ErrorCode}", messageId, folder.FolderName, serviceException.ResponseStatusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
failedMessageIds.Add(messageId);
|
||||||
_logger.Error(ex, "Error occurred while processing message {MailId} for folder {FolderName}", messageId, folder.FolderName);
|
_logger.Error(ex, "Error occurred while processing message {MailId} for folder {FolderName}", messageId, folder.FolderName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
failedMessageIds.AddRange(batch);
|
||||||
|
|
||||||
_logger.Error(ex, "Error occurred during batch download for folder {FolderName}", folder.FolderName);
|
_logger.Error(ex, "Error occurred during batch download for folder {FolderName}", folder.FolderName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (retryFailedOnce && failedMessageIds.Any())
|
||||||
|
{
|
||||||
|
// For a good cause wait a little bit.
|
||||||
|
|
||||||
|
await Task.Delay(3000);
|
||||||
|
|
||||||
|
// Do not retry here once again.
|
||||||
|
var failedDownloadedMessagIds = await DownloadMessageMetadataBatchAsync(failedMessageIds, folder, false, cancellationToken);
|
||||||
|
|
||||||
|
downloadedIds.Concat(failedDownloadedMessagIds);
|
||||||
|
}
|
||||||
|
|
||||||
return downloadedIds;
|
return downloadedIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,7 +709,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
};
|
};
|
||||||
|
|
||||||
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!handled)
|
if (!handled)
|
||||||
{
|
{
|
||||||
// No handler could process this error, log and handle appropriately
|
// No handler could process this error, log and handle appropriately
|
||||||
@@ -799,7 +786,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
if (newMailIds.Any())
|
if (newMailIds.Any())
|
||||||
{
|
{
|
||||||
_logger.Information("Downloading {Count} new mails from delta sync for folder {FolderName} (metadata only)", newMailIds.Count, folder.FolderName);
|
_logger.Information("Downloading {Count} new mails from delta sync for folder {FolderName} (metadata only)", newMailIds.Count, folder.FolderName);
|
||||||
var deltaDownloadedIds = await DownloadMessageMetadataBatchAsync(newMailIds, folder, cancellationToken).ConfigureAwait(false);
|
var deltaDownloadedIds = await DownloadMessageMetadataBatchAsync(newMailIds, folder, true, cancellationToken).ConfigureAwait(false);
|
||||||
downloadedMessageIds.AddRange(deltaDownloadedIds);
|
downloadedMessageIds.AddRange(deltaDownloadedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +811,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
};
|
};
|
||||||
|
|
||||||
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||||
|
|
||||||
if (handled)
|
if (handled)
|
||||||
{
|
{
|
||||||
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
|
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
|
||||||
@@ -915,7 +902,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
};
|
};
|
||||||
|
|
||||||
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!handled)
|
if (!handled)
|
||||||
{
|
{
|
||||||
// No handler could process this error, log and re-throw
|
// No handler could process this error, log and re-throw
|
||||||
@@ -1162,7 +1149,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
};
|
};
|
||||||
|
|
||||||
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||||
|
|
||||||
if (handled)
|
if (handled)
|
||||||
{
|
{
|
||||||
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
|
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
|
||||||
@@ -1179,13 +1166,13 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
_logger.Error(apiException, "Unhandled API error during folder synchronization for account {AccountName}. Error: {ErrorCode}", Account.Name, apiException.ResponseStatusCode);
|
_logger.Error(apiException, "Unhandled API error during folder synchronization for account {AccountName}. Error: {ErrorCode}", Account.Name, apiException.ResponseStatusCode);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a handler processed the error and it was 410, retry with fresh token
|
// If a handler processed the error and it was 410, retry with fresh token
|
||||||
if (apiException.ResponseStatusCode == 410)
|
if (apiException.ResponseStatusCode == 410)
|
||||||
{
|
{
|
||||||
return await GetDeltaFoldersAsync(cancellationToken);
|
return await GetDeltaFoldersAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other handled errors, we still need to throw since we can't return a meaningful response
|
// For other handled errors, we still need to throw since we can't return a meaningful response
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user