From 448c5ab943fb322fe58e76e21e7800d92f67efbb Mon Sep 17 00:00:00 2001 From: Michael Brunner Date: Wed, 16 Dec 2020 17:13:50 +0100 Subject: [PATCH] Added ShutdownJob interface for methods executed after scheduler --- AsyncScheduler/IShutdownJob.cs | 19 +++++++ AsyncScheduler/Scheduler.cs | 49 ++++++++++++++++++ AsyncSchedulerTest/ShutdownJobTest.cs | 50 +++++++++++++++++++ AsyncSchedulerTest/TestData/ShutdownJob.cs | 22 ++++++++ AsyncSchedulerTest/TestUtils/TestActivator.cs | 9 +++- 5 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 AsyncScheduler/IShutdownJob.cs create mode 100644 AsyncSchedulerTest/ShutdownJobTest.cs create mode 100644 AsyncSchedulerTest/TestData/ShutdownJob.cs diff --git a/AsyncScheduler/IShutdownJob.cs b/AsyncScheduler/IShutdownJob.cs new file mode 100644 index 0000000..9749482 --- /dev/null +++ b/AsyncScheduler/IShutdownJob.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace AsyncScheduler +{ + /// + /// Interface which can be implemented by a Job, + /// so its shutdown method is executed, when scheduler is stopped by CancellationToken. + /// + public interface IShutdownJob : IJob + { + + /// + /// Method is executed on shutdown of Scheduler/Job independent whether this job was started before or not. + /// It is allowed to throw an exception as they are caught. + /// + /// message for Log + Task Shutdown(); + } +} \ No newline at end of file diff --git a/AsyncScheduler/Scheduler.cs b/AsyncScheduler/Scheduler.cs index f0ca03c..5672f68 100644 --- a/AsyncScheduler/Scheduler.cs +++ b/AsyncScheduler/Scheduler.cs @@ -161,6 +161,55 @@ public async Task Start(CancellationToken cancellationToken) } } } + + await ExecuteShutdownMethods(); + } + + private async Task ExecuteShutdownMethods() + { + var shutdownTasksWithErrorHandler = JobManager.Jobs + .Where(j => typeof(IShutdownJob).IsAssignableFrom(j.Value)) + .Select(pair => new Tuple>(pair.Key, ExecuteShutdownJob(pair))) + .Select(t => t.Item2.ContinueWith(task => HandleShutdownFinished(t.Item1, task))); + + await Task.WhenAll(shutdownTasksWithErrorHandler); + } + + private async Task HandleShutdownFinished(string jobKey, Task shutdownTask) + { + try + { + var result = await shutdownTask; + _logger.LogInformation("Shutdown job for {jobKey} finished with result: {result}", jobKey, result); + } + catch (Exception e) + { + _logger.LogWarning(e, "Unable to shutdown job {jobKey}", jobKey); + } + } + + private async Task ExecuteShutdownJob(KeyValuePair job) + { + using (_logger.BeginScope("job", job.Key)) + { + var jobInstance = CreateJobInstance(job); + if (jobInstance == null) + { + _logger.LogWarning("Cannot create service! Skipping job {jobKey}", job.Key); + return "Job cannot be initialized"; + } + + try + { + var shutdown = ((IShutdownJob) jobInstance).Shutdown(); + return await shutdown; + } + catch (Exception e) + { + _logger.LogWarning(e, "Unable to shutdown job {jobKey}", job.Key); + return "Shutdown of job not executed"; + } + } } private void ExecuteQuickStarts(CancellationToken cancellationToken) diff --git a/AsyncSchedulerTest/ShutdownJobTest.cs b/AsyncSchedulerTest/ShutdownJobTest.cs new file mode 100644 index 0000000..d177b8c --- /dev/null +++ b/AsyncSchedulerTest/ShutdownJobTest.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AsyncScheduler; +using AsyncScheduler.JobStorage; +using AsyncScheduler.Schedules; +using AsyncSchedulerTest.TestData; +using AsyncSchedulerTest.TestUtils; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace AsyncSchedulerTest +{ + public class ShutdownJobTest + { + private readonly Scheduler _scheduler; + private readonly ShutdownJob _shutdownJobInstancee; + + public ShutdownJobTest(ITestOutputHelper testOutputHelper) + { + _shutdownJobInstancee = new ShutdownJob(); + var serviceProvider = new TestActivator(null, _shutdownJobInstancee); + var jobManager = new JobManager(serviceProvider, new XUnitLogger(testOutputHelper), + new InMemoryStorage()); + _scheduler = new Scheduler(serviceProvider, new XUnitLogger(testOutputHelper), + new UtcSchedulerClock(), jobManager); + } + + [Fact] + public async Task TestShutdown() + { + _scheduler.JobManager.AddJob(); + await RunScheduler(TimeSpan.FromSeconds(1)); + _shutdownJobInstancee.ShutdownCount.Should().Be(1); + } + + private async Task RunScheduler(TimeSpan schedulerTime) + { + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + var schedulerTask = _scheduler.Start(cancellationTokenSource.Token); + // ReSharper disable MethodSupportsCancellation + await Task.Delay(schedulerTime).ContinueWith((t) => cancellationTokenSource.Cancel()); + var schedulerFinishTimeout = TimeSpan.FromSeconds(1); + await Task.WhenAny(schedulerTask, Task.Delay(schedulerFinishTimeout)); + cancellationTokenSource.Cancel(); + await schedulerTask; + } + } +} \ No newline at end of file diff --git a/AsyncSchedulerTest/TestData/ShutdownJob.cs b/AsyncSchedulerTest/TestData/ShutdownJob.cs new file mode 100644 index 0000000..27b5336 --- /dev/null +++ b/AsyncSchedulerTest/TestData/ShutdownJob.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using AsyncScheduler; + +namespace AsyncSchedulerTest.TestData +{ + public class ShutdownJob : IShutdownJob + { + public int ShutdownCount { get; private set; } = 0; + + public async Task Start(CancellationToken cancellationToken) + { + return await Task.FromResult("Done"); + } + + public Task Shutdown() + { + ShutdownCount++; + return Task.FromResult("Shutdown executed"); + } + } +} \ No newline at end of file diff --git a/AsyncSchedulerTest/TestUtils/TestActivator.cs b/AsyncSchedulerTest/TestUtils/TestActivator.cs index 964dc18..121d87a 100644 --- a/AsyncSchedulerTest/TestUtils/TestActivator.cs +++ b/AsyncSchedulerTest/TestUtils/TestActivator.cs @@ -6,10 +6,12 @@ namespace AsyncSchedulerTest.TestUtils public class TestActivator : IServiceProvider { private readonly SimpleJob _simpleJobInstance; + private readonly ShutdownJob _shutdownJobInstance; - public TestActivator(SimpleJob simpleJobInstance = null) + public TestActivator(SimpleJob simpleJobInstance = null, ShutdownJob shutdownJobInstance = null) { _simpleJobInstance = simpleJobInstance; + _shutdownJobInstance = shutdownJobInstance; } public object GetService(Type serviceType) @@ -25,6 +27,11 @@ public object GetService(Type serviceType) { return _simpleJobInstance; } + + if (serviceType == typeof(ShutdownJob) && _shutdownJobInstance != null) + { + return _shutdownJobInstance; + } return Activator.CreateInstance(serviceType); } }