Synchronizer error factory implementation (#645)

* Added sync error factories for outlook and gmail.

* Implement ObjectCannotBeDeletedHandler for OutlookSynchronizer.

* Remove debug code.

* Implement del key to delete on mail list.

* Revert debug code.
This commit is contained in:
Burak Kaan Köse
2025-04-26 10:49:55 +02:00
committed by GitHub
parent 5b44cf03ce
commit 9feb3f35c3
13 changed files with 289 additions and 39 deletions

View File

@@ -4,6 +4,7 @@ using Wino.Authentication;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration.Processors;
using Wino.Core.Services;
using Wino.Core.Synchronizers.Errors.Outlook;
using Wino.Core.Synchronizers.ImapSync;
namespace Wino.Core;
@@ -34,5 +35,11 @@ public static class CoreContainerSetup
services.AddTransient<CondstoreSynchronizer>();
services.AddTransient<QResyncSynchronizer>();
services.AddTransient<UidBasedSynchronizer>();
// Register error factory handlers
services.AddTransient<ObjectCannotBeDeletedHandler>();
services.AddTransient<IOutlookSynchronizerErrorHandlerFactory, OutlookSynchronizerErrorHandlingFactory>();
services.AddTransient<IGmailSynchronizerErrorHandlerFactory, GmailSynchronizerErrorHandlingFactory>();
}
}

View File

@@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Wino.Core.Domain.Models.Errors;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Interface for handling specific synchronizer errors
/// </summary>
public interface ISynchronizerErrorHandler
{
/// <summary>
/// Determines if this handler can handle the specified error
/// </summary>
/// <param name="error">The error to check</param>
/// <returns>True if this handler can handle the error, false otherwise</returns>
bool CanHandle(SynchronizerErrorContext error);
/// <summary>
/// Handles the specified error
/// </summary>
/// <param name="error">The error to handle</param>
/// <returns>A task that completes when the error is handled</returns>
Task<bool> HandleAsync(SynchronizerErrorContext error);
}
public interface ISynchronizerErrorHandlerFactory
{
Task<bool> HandleErrorAsync(SynchronizerErrorContext error);
}
public interface IOutlookSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory;
public interface IGmailSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory;

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Errors;
/// <summary>
/// Contains context information about a synchronizer error
/// </summary>
public class SynchronizerErrorContext
{
/// <summary>
/// Account associated with the error
/// </summary>
public MailAccount Account { get; set; }
/// <summary>
/// Gets or sets the error code
/// </summary>
public int? ErrorCode { get; set; }
/// <summary>
/// Gets or sets the error message
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets the request bundle associated with the error
/// </summary>
public IRequestBundle RequestBundle { get; set; }
/// <summary>
/// Gets or sets additional data associated with the error
/// </summary>
public Dictionary<string, object> AdditionalData { get; set; } = new Dictionary<string, object>();
/// <summary>
/// Gets or sets the exception associated with the error
/// </summary>
public Exception Exception { get; set; }
}

View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Errors;
namespace Wino.Core.Services;
public class GmailSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IGmailSynchronizerErrorHandlerFactory
{
public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error);
public Task HandleAsync(SynchronizerErrorContext error) => HandleErrorAsync(error);
}

View File

@@ -0,0 +1,18 @@
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Errors;
using Wino.Core.Synchronizers.Errors.Outlook;
namespace Wino.Core.Services;
public class OutlookSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IOutlookSynchronizerErrorHandlerFactory
{
public OutlookSynchronizerErrorHandlingFactory(ObjectCannotBeDeletedHandler objectCannotBeDeleted)
{
RegisterHandler(objectCannotBeDeleted);
}
public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error);
public Task HandleAsync(SynchronizerErrorContext error) => HandleErrorAsync(error);
}

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Serilog;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Errors;
namespace Wino.Core.Services;
/// <summary>
/// Factory for handling synchronizer errors
/// </summary>
public class SynchronizerErrorHandlingFactory
{
private readonly ILogger _logger = Log.ForContext<SynchronizerErrorHandlingFactory>();
private readonly List<ISynchronizerErrorHandler> _handlers = new();
/// <summary>
/// Registers an error handler
/// </summary>
/// <param name="handler">The handler to register</param>
public void RegisterHandler(ISynchronizerErrorHandler handler)
{
_handlers.Add(handler);
}
/// <summary>
/// Handles an error using the registered handlers
/// </summary>
/// <param name="error">The error to handle</param>
/// <returns>True if the error was handled, false otherwise</returns>
public async Task<bool> HandleErrorAsync(SynchronizerErrorContext error)
{
foreach (var handler in _handlers)
{
if (handler.CanHandle(error))
{
_logger.Debug("Found handler {HandlerType} for error code {ErrorCode} message {ErrorMessage}",
handler.GetType().Name, error.ErrorCode, error.ErrorMessage);
return await handler.HandleAsync(error);
}
}
_logger.Debug("No handler found for error code {ErrorCode} message {ErrorMessage}",
error.ErrorCode, error.ErrorMessage);
return false;
}
}

View File

@@ -15,6 +15,8 @@ public class SynchronizerFactory : ISynchronizerFactory
private readonly IAccountService _accountService;
private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider;
private readonly IApplicationConfiguration _applicationConfiguration;
private readonly IOutlookSynchronizerErrorHandlerFactory _outlookSynchronizerErrorHandlerFactory;
private readonly IGmailSynchronizerErrorHandlerFactory _gmailSynchronizerErrorHandlerFactory;
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly IImapChangeProcessor _imapChangeProcessor;
@@ -30,7 +32,9 @@ public class SynchronizerFactory : ISynchronizerFactory
IGmailAuthenticator gmailAuthenticator,
IAccountService accountService,
IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider,
IApplicationConfiguration applicationConfiguration)
IApplicationConfiguration applicationConfiguration,
IOutlookSynchronizerErrorHandlerFactory outlookSynchronizerErrorHandlerFactory,
IGmailSynchronizerErrorHandlerFactory gmailSynchronizerErrorHandlerFactory)
{
_outlookChangeProcessor = outlookChangeProcessor;
_gmailChangeProcessor = gmailChangeProcessor;
@@ -40,6 +44,8 @@ public class SynchronizerFactory : ISynchronizerFactory
_accountService = accountService;
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
_applicationConfiguration = applicationConfiguration;
_outlookSynchronizerErrorHandlerFactory = outlookSynchronizerErrorHandlerFactory;
_gmailSynchronizerErrorHandlerFactory = gmailSynchronizerErrorHandlerFactory;
}
public async Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId)
@@ -69,9 +75,9 @@ public class SynchronizerFactory : ISynchronizerFactory
switch (providerType)
{
case Domain.Enums.MailProviderType.Outlook:
return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor);
return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor, _outlookSynchronizerErrorHandlerFactory);
case Domain.Enums.MailProviderType.Gmail:
return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor);
return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor, _gmailSynchronizerErrorHandlerFactory);
case Domain.Enums.MailProviderType.IMAP4:
return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _imapSynchronizationStrategyProvider, _applicationConfiguration);
default:

View File

@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Errors;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Requests.Bundles;
namespace Wino.Core.Synchronizers.Errors.Outlook;
public class ObjectCannotBeDeletedHandler : ISynchronizerErrorHandler
{
private readonly IMailService _mailService;
public ObjectCannotBeDeletedHandler(IMailService mailService)
{
_mailService = mailService;
}
public bool CanHandle(SynchronizerErrorContext error)
{
return error.ErrorMessage.Contains("ErrorCannotDeleteObject") && error.RequestBundle is HttpRequestBundle<RequestInformation>;
}
public async Task<bool> HandleAsync(SynchronizerErrorContext error)
{
var castedBundle = error.RequestBundle as HttpRequestBundle<RequestInformation>;
if (castedBundle?.Request is MailRequestBase mailRequest)
{
var request = castedBundle.Request;
await _mailService.DeleteMailAsync(error.Account.Id, mailRequest.Item.Id);
return true;
}
return false;
}
}

View File

@@ -25,6 +25,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Errors;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
@@ -57,6 +58,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
private readonly PeopleServiceService _peopleService;
private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly IGmailSynchronizerErrorHandlerFactory _gmailSynchronizerErrorHandlerFactory;
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
// Keeping a reference for quick access to the virtual archive folder.
@@ -64,7 +66,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
public GmailSynchronizer(MailAccount account,
IGmailAuthenticator authenticator,
IGmailChangeProcessor gmailChangeProcessor) : base(account)
IGmailChangeProcessor gmailChangeProcessor,
IGmailSynchronizerErrorHandlerFactory gmailSynchronizerErrorHandlerFactory) : base(account)
{
var messageHandler = new GmailClientMessageHandler(authenticator, account);
@@ -79,6 +82,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
_calendarService = new CalendarService(initializer);
_gmailChangeProcessor = gmailChangeProcessor;
_gmailSynchronizerErrorHandlerFactory = gmailSynchronizerErrorHandlerFactory;
}
public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient;
@@ -1106,30 +1110,50 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}
}
private void ProcessGmailRequestError(RequestError error, IRequestBundle<IClientServiceRequest> bundle)
private async Task ProcessGmailRequestErrorAsync(RequestError error, IRequestBundle<IClientServiceRequest> bundle)
{
if (error == null) return;
// OutOfMemoryException is a known bug in Gmail SDK.
if (error.Code == 0)
// Create error context
var errorContext = new SynchronizerErrorContext
{
bundle?.UIChangeRequest?.RevertUIChanges();
throw new OutOfMemoryException(error.Message);
}
ErrorCode = error.Code,
ErrorMessage = error.Message,
RequestBundle = bundle,
AdditionalData = new Dictionary<string, object>
{
{ "Account", Account },
{ "Error", error }
}
};
// Entity not found.
if (error.Code == 404)
// Try to handle the error with registered handlers
var handled = await _gmailSynchronizerErrorHandlerFactory.HandleErrorAsync(errorContext);
// If not handled by any specific handler, apply default error handling
if (!handled)
{
bundle?.UIChangeRequest?.RevertUIChanges();
throw new SynchronizerEntityNotFoundException(error.Message);
}
// OutOfMemoryException is a known bug in Gmail SDK.
if (error.Code == 0)
{
bundle?.UIChangeRequest?.RevertUIChanges();
throw new OutOfMemoryException(error.Message);
}
if (!string.IsNullOrEmpty(error.Message))
{
bundle?.UIChangeRequest?.RevertUIChanges();
error.Errors?.ForEach(error => _logger.Error("Unknown Gmail SDK error for {Name}\n{Error}", Account.Name, error));
// Entity not found.
if (error.Code == 404)
{
bundle?.UIChangeRequest?.RevertUIChanges();
throw new SynchronizerEntityNotFoundException(error.Message);
}
throw new SynchronizerException(error.Message);
if (!string.IsNullOrEmpty(error.Message))
{
bundle?.UIChangeRequest?.RevertUIChanges();
error.Errors?.ForEach(error => _logger.Error("Unknown Gmail SDK error for {Name}\n{Error}", Account.Name, error));
throw new SynchronizerException(error.Message);
}
}
}
@@ -1148,7 +1172,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
{
try
{
ProcessGmailRequestError(error, null);
await ProcessGmailRequestErrorAsync(error, null);
}
catch (OutOfMemoryException)
{
@@ -1209,7 +1233,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default)
{
ProcessGmailRequestError(error, bundle);
await ProcessGmailRequestErrorAsync(error, bundle);
if (bundle is HttpRequestBundle<IClientServiceRequest, Message> messageBundle)
{

View File

@@ -30,6 +30,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Errors;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
@@ -83,9 +84,12 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
private readonly ILogger _logger = Log.ForContext<OutlookSynchronizer>();
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
private readonly GraphServiceClient _graphClient;
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
public OutlookSynchronizer(MailAccount account,
IAuthenticator authenticator,
IOutlookChangeProcessor outlookChangeProcessor) : base(account)
IOutlookChangeProcessor outlookChangeProcessor,
IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory) : base(account)
{
var tokenProvider = new MicrosoftTokenProvider(Account, authenticator);
@@ -106,6 +110,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
_graphClient = new GraphServiceClient(httpClient, new BaseBearerTokenAuthenticationProvider(tokenProvider));
_outlookChangeProcessor = outlookChangeProcessor;
_errorHandlingFactory = errorHandlingFactory;
}
#region MS Graph Handlers
@@ -990,14 +995,37 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
HttpResponseMessage response,
List<string> errors)
{
bundle.UIChangeRequest?.RevertUIChanges();
var content = await response.Content.ReadAsStringAsync();
var errorJson = JsonNode.Parse(content);
var errorString = $"[{response.StatusCode}] {errorJson["error"]["code"]} - {errorJson["error"]["message"]}\n";
var errorCode = errorJson["error"]["code"].GetValue<string>();
var errorMessage = errorJson["error"]["message"].GetValue<string>();
var errorString = $"[{response.StatusCode}] {errorCode} - {errorMessage}\n";
Debug.WriteLine(errorString);
errors.Add(errorString);
// Create error context
var errorContext = new SynchronizerErrorContext
{
Account = Account,
ErrorCode = (int)response.StatusCode,
ErrorMessage = errorMessage,
RequestBundle = bundle,
AdditionalData = new Dictionary<string, object>
{
{ "ErrorCode", errorCode },
{ "HttpResponse", response },
{ "Content", content }
}
};
// Try to handle the error with registered handlers
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext);
// If not handled by any specific handler, revert UI changes and add to error list
if (!handled)
{
bundle.UIChangeRequest?.RevertUIChanges();
Debug.WriteLine(errorString);
errors.Add(errorString);
}
}
private void ThrowBatchExecutionException(List<string> errors)

View File

@@ -203,6 +203,8 @@
Key="A"
Invoked="SelectAllInvoked"
Modifiers="Control" />
<KeyboardAccelerator Key="Delete" Invoked="DeleteAllInvoked" />
</Page.KeyboardAccelerators>
<wino:BasePage.ShellContent>
<Grid>

View File

@@ -495,4 +495,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
=> MailListView.SelectAllWino();
private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
=> ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete);
}

View File

@@ -24,17 +24,6 @@ public class UserActionRequestHandler : ServerMessageHandler<ServerRequestPackag
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(package.AccountId);
synchronizer.QueueRequest(package.Request);
//if (package.QueueSynchronization)
//{
// var options = new SynchronizationOptions
// {
// AccountId = package.AccountId,
// Type = Wino.Core.Domain.Enums.SynchronizationType.ExecuteRequests
// };
// WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options));
//}
return WinoServerResponse<bool>.CreateSuccessResponse(true);
}
}