Support for killing synchronizers.

This commit is contained in:
Burak Kaan Köse
2025-01-25 00:00:10 +01:00
parent 20010e77ae
commit 973ab1570d
19 changed files with 189 additions and 56 deletions

View File

@@ -25,8 +25,6 @@ namespace Wino.Core.Integration
/// Provides a pooling mechanism for ImapClient.
/// Makes sure that we don't have too many connections to the server.
/// Rents a connected & authenticated client from the pool all the time.
/// TODO: Keeps the clients alive by sending NOOP command periodically.
/// TODO: Listens to the Inbox folder for new messages.
/// </summary>
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
public class ImapClientPool : IDisposable
@@ -57,6 +55,7 @@ namespace Wino.Core.Integration
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
@@ -185,7 +184,7 @@ namespace Wino.Core.Integration
_clients.TryPop(out _);
item.Dispose();
}
else
else if (!_disposedValue)
{
_clients.Push(item);
}
@@ -332,24 +331,38 @@ namespace Wino.Core.Integration
};
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -109,5 +109,18 @@ namespace Wino.Core.Services
isInitialized = true;
}
public async Task DeleteSynchronizerAsync(Guid accountId)
{
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
if (synchronizer != null)
{
// Stop the current synchronization.
await synchronizer.KillSynchronizerAsync();
synchronizerCache.Remove(synchronizer);
}
}
}
}

View File

@@ -51,9 +51,6 @@ namespace Wino.Core.Synchronizers
/// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
// TODO: What if account is deleted during synchronization?
public bool CancelActiveSynchronization() => true;
/// <summary>
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.

View File

@@ -1193,5 +1193,15 @@ namespace Wino.Core.Synchronizers.Mail
}
#endregion
public override async Task KillSynchronizerAsync()
{
await base.KillSynchronizerAsync();
_gmailService.Dispose();
_peopleService.Dispose();
_calendarService.Dispose();
_googleHttpClient.Dispose();
}
}
}

View File

@@ -139,11 +139,14 @@ namespace Wino.Core.Synchronizers.ImapSync
}
finally
{
if (remoteFolder != null)
if (!cancellationToken.IsCancellationRequested)
{
if (remoteFolder.IsOpen)
if (remoteFolder != null)
{
await remoteFolder.CloseAsync().ConfigureAwait(false);
if (remoteFolder.IsOpen)
{
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
}

View File

@@ -57,7 +57,8 @@ namespace Wino.Core.Synchronizers.Mail
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
_applicationConfiguration = applicationConfiguration;
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, CreateAccountProtocolLogFileStream());
var protocolLogStream = CreateAccountProtocolLogFileStream();
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream);
_clientPool = new ImapClientPool(poolOptions);
}
@@ -66,7 +67,7 @@ namespace Wino.Core.Synchronizers.Mail
{
if (Account == null) throw new ArgumentNullException(nameof(Account));
var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}.log");
var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}_{Account.Id}.log");
// Each session should start a new log.
if (File.Exists(logFile)) File.Delete(logFile);
@@ -313,6 +314,8 @@ namespace Wino.Core.Synchronizers.Mail
var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled;
downloadedMessageIds.AddRange(folderDownloadedMessageIds);
}
}
@@ -524,6 +527,11 @@ namespace Wino.Core.Synchronizers.Mail
if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists)
continue;
// Check for NoSelect folders. These are not selectable folders.
// TODO: With new MailKit version 'CanOpen' will be implemented for ease of use. Use that one.
if (remoteFolder.Attributes.HasFlag(FolderAttributes.NoSelect))
continue;
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.FullName);
if (existingLocalFolder == null)
@@ -641,6 +649,10 @@ namespace Wino.Core.Synchronizers.Mail
goto retry;
}
catch (OperationCanceledException)
{
// Ignore cancellations.
}
catch (Exception)
{
@@ -716,6 +728,10 @@ namespace Wino.Core.Synchronizers.Mail
Log.Warning(ioException, "Idle client received IO exception.");
reconnect = true;
}
catch (OperationCanceledException)
{
reconnect = !IsDisposing;
}
catch (Exception ex)
{
Log.Warning(ex, "Idle client failed to start.");
@@ -782,5 +798,14 @@ namespace Wino.Core.Synchronizers.Mail
return Task.CompletedTask;
}
public override async Task KillSynchronizerAsync()
{
await base.KillSynchronizerAsync();
await StopIdleClientAsync();
// Make sure the client pool safely disconnects all ImapClients.
_clientPool.Dispose();
}
}
}

View File

@@ -1163,5 +1163,12 @@ namespace Wino.Core.Synchronizers.Mail
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
}
public override async Task KillSynchronizerAsync()
{
await base.KillSynchronizerAsync();
_graphClient.Dispose();
}
}
}

View File

@@ -25,13 +25,13 @@ namespace Wino.Core.Synchronizers
{
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
{
protected Dictionary<MailSynchronizationOptions, CancellationToken> PendingSynchronizationRequest = new();
protected bool IsDisposing { get; private set; }
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
protected WinoSynchronizer(MailAccount account) : base(account)
{
}
protected WinoSynchronizer(MailAccount account) : base(account) { }
/// <summary>
/// How many items per single HTTP call can be modified.
@@ -91,9 +91,10 @@ namespace Wino.Core.Synchronizers
return MailSynchronizationResult.Canceled;
}
PendingSynchronizationRequest.Add(options, cancellationToken);
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
activeSynchronizationCancellationToken = cancellationToken;
PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
@@ -413,6 +414,20 @@ namespace Wino.Core.Synchronizers
return ret;
}
public virtual Task KillSynchronizerAsync()
{
IsDisposing = true;
CancelAllSynchronizations();
return Task.CompletedTask;
}
protected void CancelAllSynchronizations()
{
foreach (var request in PendingSynchronizationRequest)
{
request.Value.Cancel();
}
}
}
}