From 8a0917fb0ecdcf7012e2fc616dca024657cbffdc Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Thu, 22 Feb 2024 15:20:33 +0300 Subject: [PATCH 1/6] net8.0 only --- .../RecurrentTasks.Sample.csproj | 7 ++-- src/RecurrentTasks/RecurrentTasks.csproj | 23 ++----------- .../RecurrentTasks.Tests.csproj | 32 +++++-------------- 3 files changed, 13 insertions(+), 49 deletions(-) diff --git a/sample/RecurrentTasks.Sample/RecurrentTasks.Sample.csproj b/sample/RecurrentTasks.Sample/RecurrentTasks.Sample.csproj index 832c5d6..288ac71 100644 --- a/sample/RecurrentTasks.Sample/RecurrentTasks.Sample.csproj +++ b/sample/RecurrentTasks.Sample/RecurrentTasks.Sample.csproj @@ -1,6 +1,6 @@  - netcoreapp2.1;netcoreapp3.1;net6.0 + net8.0 true RecurrentTasks.Sample Exe @@ -15,13 +15,10 @@ - + All - - - diff --git a/src/RecurrentTasks/RecurrentTasks.csproj b/src/RecurrentTasks/RecurrentTasks.csproj index bf0ca55..2d798ec 100644 --- a/src/RecurrentTasks/RecurrentTasks.csproj +++ b/src/RecurrentTasks/RecurrentTasks.csproj @@ -2,8 +2,9 @@ Dmitry Popov, 2016-2022 RecurrentTasks - netstandard2.0;netcoreapp3.1;net6.0;net8.0 + net8.0 RecurrentTasks + enable RecurrentTasks task;job;recurrent;recurring;aspnetcore https://github.com/justdmitry/RecurrentTasks/releases/tag/v6.6 @@ -29,24 +30,6 @@ - - - - - - - - - - - - - - - - - - @@ -61,7 +44,7 @@ - + All diff --git a/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj b/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj index da14e72..ad75aa7 100644 --- a/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj +++ b/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp2.1;netcoreapp3.1;net6.0;net8.0 + net8.0 RecurrentTasks.Tests RecurrentTasks.Tests true @@ -10,36 +10,20 @@ - - - - - + + + + + All - - + + all runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - - - From ac64df7b0acd626644def67324e92070c24166bf Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Tue, 5 Mar 2024 09:59:22 +0300 Subject: [PATCH 2/6] feat: remove Task.Run, use async only --- src/RecurrentTasks/RecurrentTasks.csproj | 6 +- src/RecurrentTasks/TaskRunner.cs | 132 +++++++++++++---------- 2 files changed, 78 insertions(+), 60 deletions(-) diff --git a/src/RecurrentTasks/RecurrentTasks.csproj b/src/RecurrentTasks/RecurrentTasks.csproj index 2d798ec..009acd0 100644 --- a/src/RecurrentTasks/RecurrentTasks.csproj +++ b/src/RecurrentTasks/RecurrentTasks.csproj @@ -1,18 +1,18 @@  - Dmitry Popov, 2016-2022 + Dmitry Popov, 2016-2024 RecurrentTasks net8.0 RecurrentTasks enable RecurrentTasks task;job;recurrent;recurring;aspnetcore - https://github.com/justdmitry/RecurrentTasks/releases/tag/v6.6 + https://github.com/justdmitry/RecurrentTasks/releases/tag/v7.0 https://github.com/justdmitry/RecurrentTasks git https://github.com/justdmitry/RecurrentTasks.git - 6.6.0 + 7.0.0-beta RecurrentTasks for .NET allows you to run simple recurrent background tasks with specific intervals, without complex frameworks, persistance, etc... just_dmitry diff --git a/src/RecurrentTasks/TaskRunner.cs b/src/RecurrentTasks/TaskRunner.cs index 70f5774..82ea4d6 100644 --- a/src/RecurrentTasks/TaskRunner.cs +++ b/src/RecurrentTasks/TaskRunner.cs @@ -11,13 +11,12 @@ public class TaskRunner : ITask where TRunnable : IRunnable { - private readonly EventWaitHandle runImmediately = new AutoResetEvent(false); - private readonly ILogger logger; + private readonly IServiceScopeFactory serviceScopeFactory; - private Task mainTask; - - private CancellationTokenSource cancellationTokenSource; + private Task? mainTask; + private CancellationTokenSource? stopTaskSource; + private CancellationTokenSource? waitForNextRunSource; /// /// Initializes a new instance of the class. @@ -27,24 +26,13 @@ public class TaskRunner : ITask /// Фабрика для создания Scope (при запуске задачи) public TaskRunner(ILoggerFactory loggerFactory, TaskOptions options, IServiceScopeFactory serviceScopeFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (serviceScopeFactory == null) - { - throw new ArgumentNullException(nameof(serviceScopeFactory)); - } + ArgumentNullException.ThrowIfNull(loggerFactory); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(serviceScopeFactory); this.logger = options.Logger ?? loggerFactory.CreateLogger($"{this.GetType().Namespace}.{nameof(TaskRunner)}<{typeof(TRunnable).FullName}>"); Options = options; - ServiceScopeFactory = serviceScopeFactory; + this.serviceScopeFactory = serviceScopeFactory; RunStatus = new TaskRunStatus(); } @@ -69,8 +57,6 @@ public bool IsStarted /// public Type RunnableType => typeof(TRunnable); - private IServiceScopeFactory ServiceScopeFactory { get; set; } - Task IHostedService.StartAsync(CancellationToken cancellationToken) { if (!IsStarted && Options.AutoStart) @@ -116,56 +102,69 @@ public void Start(CancellationToken cancellationToken) throw new InvalidOperationException("Already started"); } - cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - mainTask = Task.Run(() => MainLoop(Options.FirstRunDelay, cancellationTokenSource.Token)); + mainTask = MainLoop(Options.FirstRunDelay, cancellationToken); } /// public void Stop() { logger.LogInformation("Stop() called..."); - if (mainTask == null) + if (stopTaskSource == null) { throw new InvalidOperationException("Can't stop without start"); } - cancellationTokenSource.Cancel(); + stopTaskSource.Cancel(); } /// public void TryRunImmediately() { - if (mainTask == null) + if (waitForNextRunSource == null) { throw new InvalidOperationException("Can't run without Start"); } - runImmediately.Set(); + waitForNextRunSource.Cancel(); } - protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationToken) + protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationToken) { logger.LogInformation("MainLoop() started. Running..."); + + stopTaskSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var stopToken = stopTaskSource.Token; + + waitForNextRunSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken); + var waitForNextRunToken = waitForNextRunSource.Token; + + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var sleepInterval = firstRunDelay; - var handles = new[] { cancellationToken.WaitHandle, runImmediately }; while (true) { logger.LogDebug("Sleeping for {0}...", sleepInterval); RunStatus.NextRunTime = DateTimeOffset.Now.Add(sleepInterval); - WaitHandle.WaitAny(handles, sleepInterval); - if (cancellationToken.IsCancellationRequested) + await Task.Delay(sleepInterval, waitForNextRunToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + + if (stopToken.IsCancellationRequested) { // must stop and quit logger.LogWarning("CancellationToken signaled, stopping..."); - mainTask = null; - cancellationTokenSource = null; break; } + if (waitForNextRunToken.IsCancellationRequested) + { + // token and token source have been used, recreate them. + waitForNextRunSource?.Dispose(); + waitForNextRunSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken); + waitForNextRunToken = waitForNextRunSource.Token; + } + logger.LogDebug("It is time! Creating scope..."); - using (var scope = ServiceScopeFactory.CreateScope()) + using (var scope = serviceScopeFactory.CreateScope()) { if (Options.RunCulture != null) { @@ -176,7 +175,7 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo try { - var beforeRunResponse = OnBeforeRun(scope.ServiceProvider); + var beforeRunResponse = await OnBeforeRun(scope.ServiceProvider); if (!beforeRunResponse) { @@ -191,7 +190,7 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo var runnable = (TRunnable)scope.ServiceProvider.GetRequiredService(typeof(TRunnable)); logger.LogInformation("Calling Run()..."); - runnable.RunAsync(this, scope.ServiceProvider, cancellationToken).GetAwaiter().GetResult(); + await runnable.RunAsync(this, scope.ServiceProvider, stopToken); logger.LogInformation("Done."); RunStatus.LastRunTime = startTime; @@ -202,7 +201,7 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo RunStatus.LastException = null; IsRunningRightNow = false; - OnAfterRunSuccess(scope.ServiceProvider); + await OnAfterRunSuccess(scope.ServiceProvider); } } catch (Exception ex) @@ -218,7 +217,7 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo RunStatus.FailsCount++; IsRunningRightNow = false; - OnAfterRunFail(scope.ServiceProvider, ex); + await OnAfterRunFail(scope.ServiceProvider, ex); } finally { @@ -229,7 +228,7 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo if (Options.Interval.Ticks == 0) { logger.LogWarning("Interval equal to zero. Stopping..."); - cancellationTokenSource.Cancel(); + stopTaskSource.Cancel(); } else { @@ -237,6 +236,14 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo } } + waitForNextRunSource?.Dispose(); + waitForNextRunSource = null; + + stopTaskSource?.Dispose(); + stopTaskSource = null; + + mainTask = null; + logger.LogInformation("MainLoop() finished."); } @@ -245,9 +252,14 @@ protected void MainLoop(TimeSpan firstRunDelay, CancellationToken cancellationTo /// /// to be passed in event args /// Return falce to cancel/skip task run - protected virtual bool OnBeforeRun(IServiceProvider serviceProvider) + protected virtual async Task OnBeforeRun(IServiceProvider serviceProvider) { - return Options.BeforeRun?.Invoke(serviceProvider, this).GetAwaiter().GetResult() ?? true; + if (Options.BeforeRun == null) + { + return true; + } + + return await Options.BeforeRun(serviceProvider, this); } /// @@ -257,15 +269,18 @@ protected virtual bool OnBeforeRun(IServiceProvider serviceProvider) /// /// Attention! Any exception, catched during AfterRunSuccess.Invoke, is written to error log and ignored. /// - protected virtual void OnAfterRunSuccess(IServiceProvider serviceProvider) + protected virtual async Task OnAfterRunSuccess(IServiceProvider serviceProvider) { - try + if (Options.AfterRunSuccess != null) { - Options.AfterRunSuccess?.Invoke(serviceProvider, this).GetAwaiter().GetResult(); - } - catch (Exception ex2) - { - logger.LogError(0, ex2, "Error while processing AfterRunSuccess event (ignored)"); + try + { + await Options.AfterRunSuccess(serviceProvider, this); + } + catch (Exception ex2) + { + logger.LogError(0, ex2, "Error while processing AfterRunSuccess event (ignored)"); + } } } @@ -277,15 +292,18 @@ protected virtual void OnAfterRunSuccess(IServiceProvider serviceProvider) /// /// Attention! Any exception, catched during AfterRunFail.Invoke, is written to error log and ignored. /// - protected virtual void OnAfterRunFail(IServiceProvider serviceProvider, Exception ex) + protected virtual async Task OnAfterRunFail(IServiceProvider serviceProvider, Exception ex) { - try - { - Options.AfterRunFail?.Invoke(serviceProvider, this, ex).GetAwaiter().GetResult(); - } - catch (Exception ex2) + if (Options.AfterRunFail != null) { - logger.LogError(0, ex2, "Error while processing AfterRunFail event (ignored)"); + try + { + await Options.AfterRunFail(serviceProvider, this, ex); + } + catch (Exception ex2) + { + logger.LogError(0, ex2, "Error while processing AfterRunFail event (ignored)"); + } } } } From 55b8a5dc4c2c8f3d86952a85711625cbb6a5eb7f Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Tue, 5 Mar 2024 11:02:42 +0300 Subject: [PATCH 3/6] fix: codecov results --- .../RecurrentTasks.Tests.csproj | 4 ++-- testpublishreport.ps1 | 16 +++------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj b/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj index ad75aa7..75287c0 100644 --- a/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj +++ b/test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj @@ -10,10 +10,10 @@ - + - + All diff --git a/testpublishreport.ps1 b/testpublishreport.ps1 index a9a5497..58b0f9a 100644 --- a/testpublishreport.ps1 +++ b/testpublishreport.ps1 @@ -1,14 +1,4 @@ -# General configurations, such as location of the code coverage report, location of the OpenCover and location of .NET -$resultsFile = 'opencover-result.xml' -$openCoverConsole = $ENV:USERPROFILE + '\.nuget\packages\OpenCover\4.7.922\tools\OpenCover.Console.exe' -$codecovUploader = $ENV:USERPROFILE + '\.nuget\packages\codecov\1.9.0\tools\codecov.exe' -$target = '-target: C:\Program Files\dotnet\dotnet.exe' +$codecovUploader = $ENV:USERPROFILE + '\.nuget\packages\codecovuploader\0.7.2\tools\codecov.exe' -# Configuration and execution of the tests -$targetArgs = '-targetargs: test test/RecurrentTasks.Tests/RecurrentTasks.Tests.csproj' -$filter = '-filter: +[RecurrentTasks*]* -[RecurrentTasks.Tests*]*' -$output = '-output:' + $resultsFile -& $openCoverConsole $target $targetArgs '-register:user' $filter $output '-oldStyle' - -# Upload to codecov.io -& $codecovUploader -f $resultsFile -t 554bb7e1-09d2-4f3a-9b72-c2818e370f94 \ No newline at end of file +& dotnet test /p:AltCover=true +& $codecovUploader -t 554bb7e1-09d2-4f3a-9b72-c2818e370f94 \ No newline at end of file From e7c4b117ee4f12f23f1396b9295f2783394da46e Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Tue, 23 Apr 2024 15:14:25 +0300 Subject: [PATCH 4/6] chore: nullables, syntax etc --- ...currentTasksServiceCollectionExtensions.cs | 7 ++--- src/RecurrentTasks/TaskOptions.cs | 17 ++++++------ src/RecurrentTasks/TaskRunStatus.cs | 2 +- src/RecurrentTasks/TaskRunner.cs | 26 +++++++++---------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/RecurrentTasks/RecurrentTasksServiceCollectionExtensions.cs b/src/RecurrentTasks/RecurrentTasksServiceCollectionExtensions.cs index 0694a5b..9af22f5 100644 --- a/src/RecurrentTasks/RecurrentTasksServiceCollectionExtensions.cs +++ b/src/RecurrentTasks/RecurrentTasksServiceCollectionExtensions.cs @@ -9,14 +9,11 @@ public static class RecurrentTasksServiceCollectionExtensions { public static IServiceCollection AddTask( this IServiceCollection services, - Action> optionsAction = null, + Action>? optionsAction = null, ServiceLifetime runnableLifetime = ServiceLifetime.Transient) where TRunnable : IRunnable { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + ArgumentNullException.ThrowIfNull(services); var runnableType = typeof(TRunnable); diff --git a/src/RecurrentTasks/TaskOptions.cs b/src/RecurrentTasks/TaskOptions.cs index 2c63511..b3d1e5c 100644 --- a/src/RecurrentTasks/TaskOptions.cs +++ b/src/RecurrentTasks/TaskOptions.cs @@ -18,15 +18,15 @@ public class TaskOptions /// /// Custom logger (to use instead of calling loggerFactory.CreateLogger()). /// - public ILogger Logger { get; set; } + public ILogger? Logger { get; set; } /// - /// If non-null, current thread culture will be set to this value before is called + /// If non-null, current thread culture will be set to this value before is called. /// - public CultureInfo RunCulture { get; set; } + public CultureInfo? RunCulture { get; set; } /// - /// Auto-start task when is called (default true) + /// Auto-start task when is called (default true). /// public bool AutoStart { get; set; } = true; @@ -73,14 +73,13 @@ public TimeSpan FirstRunDelay } } - /// - /// Return false to cancel/skip task run + /// Return false to cancel/skip task run. /// - public Func> BeforeRun { get; set; } + public Func>? BeforeRun { get; set; } - public Func AfterRunSuccess { get; set; } + public Func? AfterRunSuccess { get; set; } - public Func AfterRunFail { get; set; } + public Func? AfterRunFail { get; set; } } } diff --git a/src/RecurrentTasks/TaskRunStatus.cs b/src/RecurrentTasks/TaskRunStatus.cs index c231ac1..f4e6fc2 100644 --- a/src/RecurrentTasks/TaskRunStatus.cs +++ b/src/RecurrentTasks/TaskRunStatus.cs @@ -14,7 +14,7 @@ public class TaskRunStatus public int FailsCount { get; set; } - public Exception LastException { get; set; } + public Exception? LastException { get; set; } public DateTimeOffset NextRunTime { get; set; } } diff --git a/src/RecurrentTasks/TaskRunner.cs b/src/RecurrentTasks/TaskRunner.cs index 82ea4d6..6ab7340 100644 --- a/src/RecurrentTasks/TaskRunner.cs +++ b/src/RecurrentTasks/TaskRunner.cs @@ -21,9 +21,9 @@ public class TaskRunner : ITask /// /// Initializes a new instance of the class. /// - /// Фабрика для создания логгера - /// TaskOptions - /// Фабрика для создания Scope (при запуске задачи) + /// Logger factory. + /// TaskOptions instance. + /// Service scope factory. public TaskRunner(ILoggerFactory loggerFactory, TaskOptions options, IServiceScopeFactory serviceScopeFactory) { ArgumentNullException.ThrowIfNull(loggerFactory); @@ -143,7 +143,7 @@ protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancella var sleepInterval = firstRunDelay; while (true) { - logger.LogDebug("Sleeping for {0}...", sleepInterval); + logger.LogDebug("Sleeping for {Interval}...", sleepInterval); RunStatus.NextRunTime = DateTimeOffset.Now.Add(sleepInterval); await Task.Delay(sleepInterval, waitForNextRunToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); @@ -168,7 +168,7 @@ protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancella { if (Options.RunCulture != null) { - logger.LogDebug("Switching to {0} CultureInfo...", Options.RunCulture.Name); + logger.LogDebug("Switching to {Name} CultureInfo...", Options.RunCulture.Name); CultureInfo.CurrentCulture = Options.RunCulture; CultureInfo.CurrentUICulture = Options.RunCulture; } @@ -248,10 +248,10 @@ protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancella } /// - /// Invokes handler (don't forget to call base.OnBeforeRun in override) + /// Invokes handler (don't forget to call base.OnBeforeRun in override). /// - /// to be passed in event args - /// Return falce to cancel/skip task run + /// to be passed in event args. + /// Return false to cancel/skip task run. protected virtual async Task OnBeforeRun(IServiceProvider serviceProvider) { if (Options.BeforeRun == null) @@ -263,9 +263,9 @@ protected virtual async Task OnBeforeRun(IServiceProvider serviceProvider) } /// - /// Invokes handler (don't forget to call base.OnAfterRunSuccess in override) + /// Invokes handler (don't forget to call base.OnAfterRunSuccess in override). /// - /// to be passed in event args + /// to be passed in event args. /// /// Attention! Any exception, catched during AfterRunSuccess.Invoke, is written to error log and ignored. /// @@ -285,10 +285,10 @@ protected virtual async Task OnAfterRunSuccess(IServiceProvider serviceProvider) } /// - /// Invokes handler - don't forget to call base.OnAfterRunSuccess in override + /// Invokes handler - don't forget to call base.OnAfterRunSuccess in override. /// - /// to be passed in event args - /// to be passes in event args + /// to be passed in event args. + /// to be passes in event args. /// /// Attention! Any exception, catched during AfterRunFail.Invoke, is written to error log and ignored. /// From ce1e86178e85a640bb1ba42a8342b3dca902f420 Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Tue, 23 Apr 2024 15:15:12 +0300 Subject: [PATCH 5/6] fix: throw correct exception in Start() --- src/RecurrentTasks/TaskRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RecurrentTasks/TaskRunner.cs b/src/RecurrentTasks/TaskRunner.cs index 6ab7340..2577a4d 100644 --- a/src/RecurrentTasks/TaskRunner.cs +++ b/src/RecurrentTasks/TaskRunner.cs @@ -88,7 +88,7 @@ public void Start(CancellationToken cancellationToken) { if (Options.FirstRunDelay < TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(Options.FirstRunDelay), "First run delay can't be negative"); + throw new InvalidOperationException("First run delay can't be negative"); } if (Options.Interval < TimeSpan.Zero) From f5859e950b273ced038c82c88058167546cdce1f Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Tue, 23 Apr 2024 15:18:36 +0300 Subject: [PATCH 6/6] fix: RunStatus.LastRunTime updates after task failure --- src/RecurrentTasks/TaskRunner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/RecurrentTasks/TaskRunner.cs b/src/RecurrentTasks/TaskRunner.cs index 2577a4d..2945c13 100644 --- a/src/RecurrentTasks/TaskRunner.cs +++ b/src/RecurrentTasks/TaskRunner.cs @@ -173,6 +173,8 @@ protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancella CultureInfo.CurrentUICulture = Options.RunCulture; } + var startTime = DateTimeOffset.Now; + try { var beforeRunResponse = await OnBeforeRun(scope.ServiceProvider); @@ -185,8 +187,6 @@ protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancella { IsRunningRightNow = true; - var startTime = DateTimeOffset.Now; - var runnable = (TRunnable)scope.ServiceProvider.GetRequiredService(typeof(TRunnable)); logger.LogInformation("Calling Run()..."); @@ -207,6 +207,7 @@ protected async Task MainLoop(TimeSpan firstRunDelay, CancellationToken cancella catch (Exception ex) { logger.LogWarning(0, ex, "Ooops, error (ignoring, see RunStatus.LastException or handle AfterRunFail event)"); + RunStatus.LastRunTime = startTime; RunStatus.LastResult = TaskRunResult.Fail; RunStatus.LastException = ex; if (RunStatus.FailsCount == 0)