diff --git a/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs b/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs index 0dada4070..1d76e2715 100644 --- a/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs +++ b/BTCPayServer/HostedServices/BackgroundJobSchedulerHostedService.cs @@ -61,10 +61,10 @@ namespace BTCPayServer.HostedServices { class BackgroundJob { - public Func Action; + public Func Action; public TimeSpan Delay; public IDelay DelayImplementation; - public BackgroundJob(Func action, TimeSpan delay, IDelay delayImplementation) + public BackgroundJob(Func action, TimeSpan delay, IDelay delayImplementation) { this.Action = action; this.Delay = delay; @@ -74,7 +74,7 @@ namespace BTCPayServer.HostedServices public async Task Run(CancellationToken cancellationToken) { await DelayImplementation.Wait(Delay, cancellationToken); - await Action(); + await Action(cancellationToken); } } @@ -89,9 +89,9 @@ namespace BTCPayServer.HostedServices private Channel _Jobs = Channel.CreateUnbounded(); HashSet _Processing = new HashSet(); - public void Schedule(Func action, TimeSpan delay) + public void Schedule(Func act, TimeSpan scheduledIn) { - _Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay)); + _Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay)); } public async Task WaitAllRunning(CancellationToken cancellationToken) @@ -124,8 +124,7 @@ namespace BTCPayServer.HostedServices { _Processing.Add(processing); } -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - processing.ContinueWith(t => + _ = processing.ContinueWith(t => { if (t.IsFaulted) { @@ -136,7 +135,6 @@ namespace BTCPayServer.HostedServices _Processing.Remove(processing); } }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } } } diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 4e3fa6e4a..288066bbb 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -137,23 +137,29 @@ namespace BTCPayServer.HostedServices return; var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification }); if (!string.IsNullOrEmpty(invoice.NotificationURL)) - _JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero); + _JobClient.Schedule((cancellation) => NotifyHttp(invoiceStr, cancellation), TimeSpan.Zero); } - public async Task NotifyHttp(string invoiceData) + public async Task NotifyHttp(string invoiceData, CancellationToken cancellationToken) { var job = NBitcoin.JsonConverters.Serializer.ToObject(invoiceData); bool reschedule = false; var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name); - CancellationTokenSource cts = new CancellationTokenSource(10000); try { - HttpResponseMessage response = await SendNotification(job.Notification, cts.Token); + HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken); reschedule = !response.IsSuccessStatusCode; aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null; _EventAggregator.Publish(aggregatorEvent); } - catch (OperationCanceledException) when (cts.IsCancellationRequested) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // When the JobClient will be persistent, this will reschedule the job for after reboot + invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job); + _JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0)); + return; + } + catch (OperationCanceledException) { aggregatorEvent.Error = "Timeout"; _EventAggregator.Publish(aggregatorEvent); @@ -174,14 +180,13 @@ namespace BTCPayServer.HostedServices aggregatorEvent.Error = $"Unexpected error: {message}"; _EventAggregator.Publish(aggregatorEvent); } - finally { cts?.Dispose(); } job.TryCount++; if (job.TryCount < MaxTry && reschedule) { invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job); - _JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0)); + _JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0)); } } @@ -205,7 +210,7 @@ namespace BTCPayServer.HostedServices } Encoding UTF8 = new UTF8Encoding(false); - private async Task SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation) + private async Task SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellationToken) { var request = new HttpRequestMessage(); request.Method = HttpMethod.Post; @@ -226,7 +231,14 @@ namespace BTCPayServer.HostedServices request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute); request.Content = new StringContent(notificationString, UTF8, "application/json"); - var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation)); + var response = await Enqueue(notification.Data.Id, async () => + { + using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + cts.CancelAfter(TimeSpan.FromMinutes(1.0)); + return await _Client.SendAsync(request, cts.Token); + } + }); return response; } diff --git a/BTCPayServer/Services/IBackgroundJobClient.cs b/BTCPayServer/Services/IBackgroundJobClient.cs index 36debc6ca..24675210d 100644 --- a/BTCPayServer/Services/IBackgroundJobClient.cs +++ b/BTCPayServer/Services/IBackgroundJobClient.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace BTCPayServer.Services { public interface IBackgroundJobClient { - void Schedule(Func act, TimeSpan zero); + void Schedule(Func act, TimeSpan scheduledIn); } } diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index 801f525c8..148abb76d 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -1,4 +1,5 @@ using BTCPayServer.Logging; +using NBitcoin; using Microsoft.Extensions.Logging; using System; using System.Net.Mail; @@ -17,7 +18,7 @@ namespace BTCPayServer.Services.Mails public void SendEmail(string email, string subject, string message) { - _JobClient.Schedule(async () => + _JobClient.Schedule(async (cancellationToken) => { var emailSettings = await GetEmailSettings(); if (emailSettings?.IsComplete() != true) @@ -25,13 +26,21 @@ namespace BTCPayServer.Services.Mails Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); return; } - var smtp = emailSettings.CreateSmtpClient(); - var mail = new MailMessage(emailSettings.From, email, subject, message) + using (var smtp = emailSettings.CreateSmtpClient()) { - IsBodyHtml = true - }; - await smtp.SendMailAsync(mail); - + var mail = new MailMessage(emailSettings.From, email, subject, message) + { + IsBodyHtml = true + }; + try + { + await smtp.SendMailAsync(mail).WithCancellation(cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + smtp.SendAsyncCancel(); + } + } }, TimeSpan.Zero); }