diff --git a/NCrontab.Scheduler.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/NCrontab.Scheduler.AspNetCore/Extensions/ServiceCollectionExtensions.cs index 40e6109..a221e16 100644 --- a/NCrontab.Scheduler.AspNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/NCrontab.Scheduler.AspNetCore/Extensions/ServiceCollectionExtensions.cs @@ -7,22 +7,26 @@ namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static void AddHostedScheduler(this IServiceCollection serviceCollection, IConfiguration configuration) + public static IServiceCollection AddHostedScheduler(this IServiceCollection serviceCollection, IConfiguration configuration) { // Register services serviceCollection.AddScheduler(configuration); // Add hosted service serviceCollection.AddHostedService(); + + return serviceCollection; } - public static void AddHostedScheduler(this IServiceCollection serviceCollection, Action options = null) + public static IServiceCollection AddHostedScheduler(this IServiceCollection serviceCollection, Action options = null) { // Register services serviceCollection.AddScheduler(options); // Add hosted service serviceCollection.AddHostedService(); + + return serviceCollection; } } } \ No newline at end of file diff --git a/NCrontab.Scheduler/AsyncScheduledTask.cs b/NCrontab.Scheduler/AsyncScheduledTask.cs index 2948f5a..3d2dd34 100644 --- a/NCrontab.Scheduler/AsyncScheduledTask.cs +++ b/NCrontab.Scheduler/AsyncScheduledTask.cs @@ -4,29 +4,41 @@ namespace NCrontab.Scheduler { - public class AsyncScheduledTask : IAsyncScheduledTask + public class AsyncScheduledTask : TaskBase, IAsyncScheduledTask { - private readonly Func action; + private readonly Func task; - public AsyncScheduledTask(CrontabSchedule cronExpression, Func action) - : this(Guid.NewGuid(), cronExpression, action) + public AsyncScheduledTask(string cronExpression, Func task = null) + : this(CrontabSchedule.Parse(cronExpression), task) { } - public AsyncScheduledTask(Guid id, CrontabSchedule crontabSchedule, Func action) + public AsyncScheduledTask(CrontabSchedule crontabSchedule, Func task = null) + : this(Guid.NewGuid(), crontabSchedule, task) { - this.Id = id; - this.CrontabSchedule = crontabSchedule; - this.action = action; } - public Guid Id { get; } + public AsyncScheduledTask(Guid id, CrontabSchedule crontabSchedule, Func task = null) + : this(id, null, crontabSchedule, task) + { + this.task = task; + } - public CrontabSchedule CrontabSchedule { get; set; } + public AsyncScheduledTask(string name, CrontabSchedule crontabSchedule, Func task = null) + : this(Guid.NewGuid(), name, crontabSchedule, task) + { + this.task = task; + } + + public AsyncScheduledTask(Guid id, string name, CrontabSchedule crontabSchedule, Func task = null) + : base(id, name, crontabSchedule) + { + this.task = task; + } public Task RunAsync(CancellationToken cancellationToken) { - return this.action(cancellationToken); + return this.task != null ? this.task(cancellationToken) : Task.CompletedTask; } } } diff --git a/NCrontab.Scheduler/Extensions/SchedulerExtensions.cs b/NCrontab.Scheduler/Extensions/SchedulerExtensions.cs index ad02301..49935c1 100644 --- a/NCrontab.Scheduler/Extensions/SchedulerExtensions.cs +++ b/NCrontab.Scheduler/Extensions/SchedulerExtensions.cs @@ -20,6 +20,16 @@ public static void Start(this IScheduler scheduler, CancellationToken cancellati Task.Run(() => scheduler.StartAsync(cancellationToken)); } + /// + /// Adds a task to the scheduler. + /// + /// The cron expression. + /// The task identifier. + public static Guid AddTask(this IScheduler scheduler, string cronExpression) + { + return scheduler.AddTask(cronExpression, action: null); + } + /// /// Adds a task to the scheduler. /// @@ -32,6 +42,16 @@ public static Guid AddTask(this IScheduler scheduler, string cronExpression, Act return scheduler.AddTask(crontabSchedule, action); } + /// + /// Adds a task to the scheduler. + /// + /// The crontab schedule. + /// The task identifier. + public static Guid AddTask(this IScheduler scheduler, CrontabSchedule crontabSchedule) + { + return scheduler.AddTask(crontabSchedule, action: null); + } + /// /// Adds a task to the scheduler. /// @@ -47,18 +67,38 @@ public static Guid AddTask(this IScheduler scheduler, CrontabSchedule crontabSch return taskId; } + /// + /// Adds a task to the scheduler. + /// + /// The task identifier. + /// The cron expression. + public static void AddTask(this IScheduler scheduler, Guid taskId, string cronExpression) + { + scheduler.AddTask(taskId, cronExpression, action: null); + } + /// /// Adds a task to the scheduler. /// /// The task identifier. /// The cron expression. /// The callback action which is called whenever the is planned to execute. - public static void AddTask(this IScheduler scheduler, Guid taskId, string cronExpression, Action action) + public static void AddTask(this IScheduler scheduler, Guid taskId, string cronExpression, Action action = null) { var crontabSchedule = CrontabSchedule.Parse(cronExpression); scheduler.AddTask(taskId, crontabSchedule, action); } + /// + /// Adds a task to the scheduler. + /// + /// The task identifier. + /// The crontab schedule. + public static void AddTask(this IScheduler scheduler, Guid taskId, CrontabSchedule crontabSchedule) + { + scheduler.AddTask(taskId, crontabSchedule, action: null); + } + /// /// Adds a task to the scheduler. /// @@ -75,25 +115,25 @@ public static void AddTask(this IScheduler scheduler, Guid taskId, CrontabSchedu /// Adds a task to the scheduler. /// /// The cron expression. - /// The callback action which is called whenever the is planned to execute. + /// The callback action which is called whenever the is planned to execute. /// The task identifier. - public static Guid AddTask(this IScheduler scheduler, string cronExpression, Func action) + public static Guid AddTask(this IScheduler scheduler, string cronExpression, Func task) { var crontabSchedule = CrontabSchedule.Parse(cronExpression); - return scheduler.AddTask(crontabSchedule, action); + return scheduler.AddTask(crontabSchedule, task); } /// /// Adds a task to the scheduler. /// /// The crontab schedule. - /// The callback action which is called whenever the is planned to execute. + /// The callback action which is called whenever the is planned to execute. /// The task identifier. - public static Guid AddTask(this IScheduler scheduler, CrontabSchedule crontabSchedule, Func action) + public static Guid AddTask(this IScheduler scheduler, CrontabSchedule crontabSchedule, Func task) { var taskId = Guid.NewGuid(); - scheduler.AddTask(taskId, crontabSchedule, action); + scheduler.AddTask(taskId, crontabSchedule, task); return taskId; } @@ -103,11 +143,11 @@ public static Guid AddTask(this IScheduler scheduler, CrontabSchedule crontabSch /// /// The task identifier. /// The cron expression. - /// The callback action which is called whenever the is planned to execute. - public static void AddTask(this IScheduler scheduler, Guid taskId, string cronExpression, Func action) + /// The callback action which is called whenever the is planned to execute. + public static void AddTask(this IScheduler scheduler, Guid taskId, string cronExpression, Func task) { var crontabSchedule = CrontabSchedule.Parse(cronExpression); - scheduler.AddTask(taskId, crontabSchedule, action); + scheduler.AddTask(taskId, crontabSchedule, task); } /// @@ -115,10 +155,10 @@ public static void AddTask(this IScheduler scheduler, Guid taskId, string cronEx /// /// The task identifier. /// The crontab schedule. - /// The callback action which is called whenever the is planned to execute. - public static void AddTask(this IScheduler scheduler, Guid taskId, CrontabSchedule crontabSchedule, Func action) + /// The callback action which is called whenever the is planned to execute. + public static void AddTask(this IScheduler scheduler, Guid taskId, CrontabSchedule crontabSchedule, Func task) { - var asyncScheduledTask = new AsyncScheduledTask(taskId, crontabSchedule, action); + var asyncScheduledTask = new AsyncScheduledTask(taskId, crontabSchedule, task); scheduler.AddTask(asyncScheduledTask); } diff --git a/NCrontab.Scheduler/Extensions/ServiceCollectionExtensions.cs b/NCrontab.Scheduler/Extensions/ServiceCollectionExtensions.cs index 38429ca..0b37e89 100644 --- a/NCrontab.Scheduler/Extensions/ServiceCollectionExtensions.cs +++ b/NCrontab.Scheduler/Extensions/ServiceCollectionExtensions.cs @@ -8,16 +8,18 @@ namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static void AddScheduler(this IServiceCollection serviceCollection, IConfiguration configuration) + public static IServiceCollection AddScheduler(this IServiceCollection serviceCollection, IConfiguration configuration) { // Configuration serviceCollection.Configure(configuration); // Register services serviceCollection.AddScheduler(); + + return serviceCollection; } - public static void AddScheduler(this IServiceCollection serviceCollection, Action options = null) + public static IServiceCollection AddScheduler(this IServiceCollection serviceCollection, Action options = null) { // Configuration if (options != null) @@ -33,6 +35,8 @@ public static void AddScheduler(this IServiceCollection serviceCollection, Actio x.GetRequiredService>(), x.GetRequiredService>()); }); + + return serviceCollection; } } } \ No newline at end of file diff --git a/NCrontab.Scheduler/ISchedulerOptions.cs b/NCrontab.Scheduler/ISchedulerOptions.cs index 10827a8..f0ad9f4 100644 --- a/NCrontab.Scheduler/ISchedulerOptions.cs +++ b/NCrontab.Scheduler/ISchedulerOptions.cs @@ -12,5 +12,7 @@ public interface ISchedulerOptions /// consistent behavior across daylight savings time changes. /// Log messages are formatted with UTC timestamps too. public DateTimeKind DateTimeKind { get; set; } + + public LoggingOptions Logging { get; set; } } } \ No newline at end of file diff --git a/NCrontab.Scheduler/ITask.cs b/NCrontab.Scheduler/ITask.cs index a3e85e0..fb004a7 100644 --- a/NCrontab.Scheduler/ITask.cs +++ b/NCrontab.Scheduler/ITask.cs @@ -2,10 +2,27 @@ namespace NCrontab.Scheduler { + /// + /// Abstraction for any kind of task that can be scheduled with a instance. + /// See also: + /// + /// + /// public interface ITask { + /// + /// Unique identifier of the task. + /// public Guid Id { get; } + /// + /// Display name of the task. + /// + public string Name { get; set; } + + /// + /// The cron schedule expressions. + /// CrontabSchedule CrontabSchedule { get; set; } } } diff --git a/NCrontab.Scheduler/LogIdentifier.cs b/NCrontab.Scheduler/LogIdentifier.cs new file mode 100644 index 0000000..7e0defd --- /dev/null +++ b/NCrontab.Scheduler/LogIdentifier.cs @@ -0,0 +1,25 @@ +namespace NCrontab.Scheduler +{ + public enum LogIdentifier + { + /// + /// Use in the log messages. + /// + TaskId, + + /// + /// Use in the log messages. + /// + TaskName, + + /// + /// Use and (if available) in the log messages. + /// + TaskIdAndName, + + /// + /// Use (if available) and in the log messages. + /// + TaskNameAndId, + } +} \ No newline at end of file diff --git a/NCrontab.Scheduler/LoggingOptions.cs b/NCrontab.Scheduler/LoggingOptions.cs new file mode 100644 index 0000000..d371bdc --- /dev/null +++ b/NCrontab.Scheduler/LoggingOptions.cs @@ -0,0 +1,34 @@ +using System; + +namespace NCrontab.Scheduler +{ + public class LoggingOptions + { + public LoggingOptions() + { + this.DateTimeKind = DateTimeKind.Utc; + this.LogIdentifier = LogIdentifier.TaskName; + this.TaskIdFormatter = "B"; + } + + public virtual DateTimeKind DateTimeKind { get; set; } + + /// + /// Sets the formatting rule for a task identifier when written to the log output. + /// This option can improve the readability of the scheduler log output. + /// It has no impact on functionaly and/or performance of the scheduler. + /// Default is LogIdentifier.TaskName. + /// + /// + /// Since is optional, + /// is used if is null or empty. + /// + public virtual LogIdentifier LogIdentifier { get; set; } + + /// + /// Formatter used when logging . + /// Default is "B". + /// + public virtual string TaskIdFormatter { get; set; } + } +} \ No newline at end of file diff --git a/NCrontab.Scheduler/NCrontab.Scheduler.csproj b/NCrontab.Scheduler/NCrontab.Scheduler.csproj index 60babd5..5a51a31 100644 --- a/NCrontab.Scheduler/NCrontab.Scheduler.csproj +++ b/NCrontab.Scheduler/NCrontab.Scheduler.csproj @@ -35,6 +35,7 @@ - Initial release Copyright 2023 + True @@ -48,8 +49,8 @@ - + diff --git a/NCrontab.Scheduler/NullTask.cs b/NCrontab.Scheduler/NullTask.cs index 6022fd7..0dbb343 100644 --- a/NCrontab.Scheduler/NullTask.cs +++ b/NCrontab.Scheduler/NullTask.cs @@ -13,6 +13,8 @@ public NullTask(Guid taskId) public Guid Id => this.taskId; + public string Name { get; set; } + public CrontabSchedule CrontabSchedule { get; set; } } } diff --git a/NCrontab.Scheduler/ScheduledTask.cs b/NCrontab.Scheduler/ScheduledTask.cs index 5c14f34..1f7a7a9 100644 --- a/NCrontab.Scheduler/ScheduledTask.cs +++ b/NCrontab.Scheduler/ScheduledTask.cs @@ -3,34 +3,41 @@ namespace NCrontab.Scheduler { - public class ScheduledTask : IScheduledTask + public class ScheduledTask : TaskBase, IScheduledTask { private readonly Action action; - public ScheduledTask(string cronExpression, Action action) - : this(Guid.NewGuid(), CrontabSchedule.Parse(cronExpression), action) + public ScheduledTask(string cronExpression, Action action = null) + : this(CrontabSchedule.Parse(cronExpression), action) { } - - public ScheduledTask(CrontabSchedule cronExpression, Action action) - : this(Guid.NewGuid(), cronExpression, action) + + public ScheduledTask(CrontabSchedule crontabSchedule, Action action = null) + : this(Guid.NewGuid(), crontabSchedule, action) { } - public ScheduledTask(Guid id, CrontabSchedule cronExpression, Action action) + public ScheduledTask(Guid id, CrontabSchedule crontabSchedule, Action action = null) + : this(id, null, crontabSchedule, action) + { + this.action = action; + } + + public ScheduledTask(string name, CrontabSchedule crontabSchedule, Action action = null) + : this(Guid.NewGuid(), name, crontabSchedule, action) + { + this.action = action; + } + + public ScheduledTask(Guid id, string name, CrontabSchedule crontabSchedule, Action action = null) + : base(id, name, crontabSchedule) { - this.Id = id; - this.CrontabSchedule = cronExpression; this.action = action; } - - public Guid Id { get; } - - public CrontabSchedule CrontabSchedule { get; set; } public void Run(CancellationToken cancellationToken) { - this.action(cancellationToken); + this.action?.Invoke(cancellationToken); } } } diff --git a/NCrontab.Scheduler/Scheduler.cs b/NCrontab.Scheduler/Scheduler.cs index 4025a2e..c54807f 100644 --- a/NCrontab.Scheduler/Scheduler.cs +++ b/NCrontab.Scheduler/Scheduler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -90,21 +91,21 @@ internal Scheduler( } /// - public void AddTask(IScheduledTask scheduledTask) + public void AddTask(IScheduledTask task) { this.logger.LogDebug( - $"AddTask: taskId={scheduledTask.Id:B}, crontabSchedule={scheduledTask.CrontabSchedule}"); + $"AddTask: task={FormatTask(task, this.schedulerOptions.Logging)}, crontabSchedule={task.CrontabSchedule}"); - this.AddTaskInternal(scheduledTask); + this.AddTaskInternal(task); } /// - public void AddTask(IAsyncScheduledTask scheduledTask) + public void AddTask(IAsyncScheduledTask task) { this.logger.LogDebug( - $"AddTask: taskId={scheduledTask.Id:B}, crontabSchedule={scheduledTask.CrontabSchedule}"); + $"AddTask: task={FormatTask(task, this.schedulerOptions.Logging)}, crontabSchedule={task.CrontabSchedule}"); - this.AddTaskInternal(scheduledTask); + this.AddTaskInternal(task); } private void AddTaskInternal(ITask scheduledTask) @@ -147,7 +148,7 @@ public void UpdateTask(ITask scheduledTask) /// public void UpdateTask(Guid taskId, CrontabSchedule crontabSchedule) { - this.logger.LogDebug($"UpdateTask: taskId={taskId:B}, crontabSchedule={crontabSchedule}"); + this.logger.LogDebug($"UpdateTask: taskId={taskId.ToString(this.schedulerOptions.Logging.TaskIdFormatter)}, crontabSchedule={crontabSchedule}"); lock (this.threadLock) { @@ -244,12 +245,14 @@ public async Task StartAsync(CancellationToken cancellationToken = default) return; } - var now = this.GetCurrentDate(); + var loggingOptions = this.schedulerOptions.Logging; + + var now = this.GetCurrentDate(this.schedulerOptions.DateTimeKind); var utcNow = now.ToUniversalTime(); - var (startDateUtc, taskIds) = this.GetScheduledTasksToRunAndHowLongToWait(now); + var (startDateUtc, nextTasks) = this.GetScheduledTasksToRunAndHowLongToWait(now); TimeSpan timeToWait; - if (taskIds.Count == 0) + if (nextTasks.Count == 0) { timeToWait = TaskHelper.InfiniteTimeSpan; this.logger.LogInformation( @@ -260,11 +263,15 @@ public async Task StartAsync(CancellationToken cancellationToken = default) { timeToWait = startDateUtc.Subtract(utcNow).RoundUp(MaxDelayRounding); + var displayStartDate = this.schedulerOptions.Logging.DateTimeKind == DateTimeKind.Utc + ? startDateUtc + : startDateUtc.ToLocalTime(); + this.logger.LogInformation( $"Scheduling next event:{Environment.NewLine}" + - $" --> nextOccurrence: {startDateUtc:O}{Environment.NewLine}" + + $" --> nextOccurrence: {displayStartDate:O}{Environment.NewLine}" + $" --> timeToWait: {timeToWait}{Environment.NewLine}" + - $" --> taskIds ({taskIds.Count}): {string.Join(", ", taskIds.Select(id => $"{id:B}"))}"); + $" --> nextTasks ({nextTasks.Count}): {string.Join(", ", nextTasks.Select(t => FormatTask(t, loggingOptions)))}"); } var isCancellationRequested = await TaskHelper.LongDelay(this.dateTime, timeToWait, this.localCancellationTokenSource.Token) @@ -285,24 +292,33 @@ public async Task StartAsync(CancellationToken cancellationToken = default) return; } - ITask[] scheduledTasksToRun; + // Re-evaluate list of tasks to run. This is necessary since + // the original list of scheduled tasks may be changed in the meantime. lock (this.threadLock) { - scheduledTasksToRun = this.scheduledTasks.Where(m => taskIds.Contains(m.Id)).ToArray(); + var newNextTasks = this.scheduledTasks.Where(m => nextTasks.Select(t => t.Id).Contains(m.Id)).ToArray(); + //if (newNextTasks.Length != nextTasks.Count) + //{ + // Debugger.Break(); + //} + + nextTasks = newNextTasks; } - if (scheduledTasksToRun.Length > 0) + if (nextTasks.Count > 0) { var signalTime = this.dateTime.UtcNow; var timingInaccuracy = signalTime - startDateUtc; + this.logger.LogInformation( $"Starting scheduled event:{Environment.NewLine}" + $" --> signalTime: {signalTime:O} (deviation: {timingInaccuracy.TotalMilliseconds}ms){Environment.NewLine}" + - $" --> scheduledTasksToRun ({scheduledTasksToRun.Length}): {string.Join(", ", scheduledTasksToRun.Select(t => $"{t.Id:B}"))}"); + $" --> nextTasks ({nextTasks.Count}): {string.Join(", ", nextTasks.Select(t => FormatTask(t, loggingOptions)))}"); - this.RaiseNextEvent(signalTime, scheduledTasksToRun); + var nextTaskIds = nextTasks.Select(t => t.Id).ToArray(); + this.RaiseNextEvent(signalTime, nextTaskIds); - foreach (var task in scheduledTasksToRun) + foreach (var task in nextTasks) { if (this.localCancellationTokenSource.IsCancellationRequested) { @@ -310,7 +326,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) break; } - this.logger.LogDebug($"Starting task with Id={task.Id:B}..."); + this.logger.LogDebug($"Starting task {FormatTask(task, loggingOptions)}..."); try { @@ -326,7 +342,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) } catch (Exception e) { - this.logger.LogError(e, $"Task with Id={task.Id:B} failed with exception"); + this.logger.LogError(e, $"Task {FormatTask(task, loggingOptions)} failed with exception"); } } @@ -344,13 +360,39 @@ public async Task StartAsync(CancellationToken cancellationToken = default) } } - private DateTime GetCurrentDate() + private static string FormatTask(ITask t, LoggingOptions loggingOptions) + { + switch (loggingOptions.LogIdentifier) + { + case LogIdentifier.TaskName: + return !string.IsNullOrEmpty(t.Name) + ? t.Name + : t.Id.ToString(loggingOptions.TaskIdFormatter); + + case LogIdentifier.TaskIdAndName: + return !string.IsNullOrEmpty(t.Name) + ? $"{t.Id.ToString(loggingOptions.TaskIdFormatter)} {t.Name}" + : t.Id.ToString(loggingOptions.TaskIdFormatter); + + case LogIdentifier.TaskNameAndId: + return !string.IsNullOrEmpty(t.Name) + ? $"{t.Name} {t.Id.ToString(loggingOptions.TaskIdFormatter)}" + : t.Id.ToString(loggingOptions.TaskIdFormatter); + + case LogIdentifier.TaskId: + default: + return t.Id.ToString(loggingOptions.TaskIdFormatter); + } + } + + private DateTime GetCurrentDate(DateTimeKind dateTimeKind) { - return this.schedulerOptions.DateTimeKind == DateTimeKind.Local - ? this.dateTime.Now //new DateTime(2023, 10, 29, 0, 0, 0, DateTimeKind.Local) // this.dateTime.Now + return dateTimeKind == DateTimeKind.Local + ? this.dateTime.Now : this.dateTime.UtcNow; } + /// public bool IsRunning { get => this.isRunning; @@ -364,10 +406,10 @@ private set } } - private (DateTime StartDateUtc, IReadOnlyCollection TaskIds) GetScheduledTasksToRunAndHowLongToWait(DateTime now) + private (DateTime StartDateUtc, IReadOnlyCollection Tasks) GetScheduledTasksToRunAndHowLongToWait(DateTime now) { var lowestNextTimeToRun = DateTime.MaxValue; - var lowestIds = new List(); + var lowestTasks = new List(); lock (this.threadLock) { @@ -381,18 +423,18 @@ private set if (nextTimeToRun < lowestNextTimeToRun) { - lowestIds.Clear(); - lowestIds.Add(scheduledTask.Id); + lowestTasks.Clear(); + lowestTasks.Add(scheduledTask); lowestNextTimeToRun = nextTimeToRun; } else if (nextTimeToRun == lowestNextTimeToRun) { - lowestIds.Add(scheduledTask.Id); + lowestTasks.Add(scheduledTask); } } } - return (lowestNextTimeToRun, lowestIds); + return (lowestNextTimeToRun, lowestTasks); } private void ResetScheduler() @@ -414,13 +456,14 @@ private void RegisterLocalCancellationToken() } } + /// public event EventHandler Next; - private void RaiseNextEvent(DateTime signalTime, params ITask[] tasks) + private void RaiseNextEvent(DateTime signalTime, params Guid[] taskIds) { try { - this.Next?.Invoke(this, new ScheduledEventArgs(signalTime, tasks.Select(t => t.Id).ToArray())); + this.Next?.Invoke(this, new ScheduledEventArgs(signalTime, taskIds)); } catch (Exception e) { @@ -428,6 +471,7 @@ private void RaiseNextEvent(DateTime signalTime, params ITask[] tasks) } } + /// public void Stop() { this.logger.LogInformation("Stopping..."); diff --git a/NCrontab.Scheduler/SchedulerOptions.cs b/NCrontab.Scheduler/SchedulerOptions.cs index 02fe5e8..5d4fa0d 100644 --- a/NCrontab.Scheduler/SchedulerOptions.cs +++ b/NCrontab.Scheduler/SchedulerOptions.cs @@ -7,9 +7,13 @@ public class SchedulerOptions : ISchedulerOptions public SchedulerOptions() { this.DateTimeKind = DateTimeKind.Utc; + this.Logging = new LoggingOptions(); } /// - public DateTimeKind DateTimeKind { get; set; } + public virtual DateTimeKind DateTimeKind { get; set; } + + /// + public virtual LoggingOptions Logging { get; set; } } } \ No newline at end of file diff --git a/NCrontab.Scheduler/TaskBase.cs b/NCrontab.Scheduler/TaskBase.cs new file mode 100644 index 0000000..2d9ec06 --- /dev/null +++ b/NCrontab.Scheduler/TaskBase.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System; + +namespace NCrontab.Scheduler +{ + /// + /// Base class for . + /// + public abstract class TaskBase : ITask + { + protected TaskBase(Guid id, CrontabSchedule crontabSchedule) + : this(id, null, crontabSchedule) + { + } + + protected TaskBase(string name, CrontabSchedule crontabSchedule) + : this(Guid.NewGuid(), name, crontabSchedule) + { + } + + protected TaskBase(Guid id, string name, CrontabSchedule crontabSchedule) + { + this.Id = id; + this.Name = name; + this.CrontabSchedule = crontabSchedule; + } + + public Guid Id { get; } + + public string Name { get; set; } + + public CrontabSchedule CrontabSchedule { get; set; } + + public override string ToString() + { + return $"{this.GetType().Name}: {this.Name ?? this.Id.ToString()}"; + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 685d378..f487f9e 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,6 @@ Following options are available: Use method `AddTask` with all the provided convenience overloads to add tasks to the scheduler. A task is composed of a cron pattern which specifies the recurrance interval and an action (for synchronous callbacks) or a task (for asynchronous callbacks). -Tasks can be added to the scheduler either before or after the scheduler has been started. - ```C# scheduler.AddTask( cronExpression: CrontabSchedule.Parse("* * * * *"), @@ -96,6 +94,11 @@ scheduler.AddTask( cronExpression: CrontabSchedule.Parse("0 0 1 1 *"), action: ct => { Console.WriteLine($"{DateTime.Now:O} -> Task runs on Januar 1 every year"); }); ``` + +A task is identified by its Id or its Name. The Id is a Guid which can be user-defined. If it's not set, a rand Guid is used. The Name property is optional and will remain unset if it's not set. The Name property is used in log messages, if the appropriate option is enabled. + +Tasks can be added to the scheduler either before or after the scheduler has been started. + A very helpful resource for creating cron expression is https://crontab.guru. ### Starting and Stopping diff --git a/Samples/NCrontab.Scheduler.AspNetCoreSample/Controllers/SchedulerDemoController.cs b/Samples/NCrontab.Scheduler.AspNetCoreSample/Controllers/SchedulerDemoController.cs index 321894f..9ba64d6 100644 --- a/Samples/NCrontab.Scheduler.AspNetCoreSample/Controllers/SchedulerDemoController.cs +++ b/Samples/NCrontab.Scheduler.AspNetCoreSample/Controllers/SchedulerDemoController.cs @@ -17,12 +17,35 @@ public SchedulerDemoController( this.scheduler = scheduler; } + [HttpGet("start")] + public void Start() + { + this.scheduler.Start(); + } + + [HttpGet("stop")] + public void Stop() + { + this.scheduler.Stop(); + } + [HttpPost("addtask")] - public void AddTaskEveryMinute(string cronExpression = "* * * * *") + public Guid AddTask(string name, string cronExpression = "* * * * *") + { + var scheduledTask = new ScheduledTask( + name, + CrontabSchedule.Parse(cronExpression), + action: ct => { this.logger.LogInformation($"Action executed!"); }); + + this.scheduler.AddTask(scheduledTask); + + return scheduledTask.Id; + } + + [HttpDelete("removetask")] + public bool RemoveTask(Guid taskId) { - this.scheduler.AddTask( - crontabSchedule: CrontabSchedule.Parse("* * * * *"), - action: ct => { this.logger.LogInformation($"{DateTime.Now:O} -> Task runs every minutes"); }); + return this.scheduler.RemoveTask(taskId); } [HttpDelete("removealltasks")] diff --git a/Samples/NCrontab.Scheduler.AspNetCoreSample/NCrontab.Scheduler.AspNetCoreSample.csproj b/Samples/NCrontab.Scheduler.AspNetCoreSample/NCrontab.Scheduler.AspNetCoreSample.csproj index 9a91034..4e69384 100644 --- a/Samples/NCrontab.Scheduler.AspNetCoreSample/NCrontab.Scheduler.AspNetCoreSample.csproj +++ b/Samples/NCrontab.Scheduler.AspNetCoreSample/NCrontab.Scheduler.AspNetCoreSample.csproj @@ -2,7 +2,7 @@ net6.0 - enable + disable enable diff --git a/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyAsyncTask.cs b/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyAsyncTask.cs index 558a9db..a791a2d 100644 --- a/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyAsyncTask.cs +++ b/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyAsyncTask.cs @@ -1,18 +1,15 @@ namespace NCrontab.Scheduler.AspNetCoreSample.Tasks { - public class NightlyAsyncTask : IAsyncScheduledTask + public class NightlyAsyncTask : TaskBase, IAsyncScheduledTask { private readonly ILogger logger; public NightlyAsyncTask(ILogger logger) + : base("NightlyAsyncTask", CrontabSchedule.Parse("0 0 * * *")) { this.logger = logger; } - public CrontabSchedule CrontabSchedule { get; set; } = CrontabSchedule.Parse("0 0 * * *"); - - public Guid Id { get; } = Guid.NewGuid(); - public Task RunAsync(CancellationToken cancellationToken) { this.logger.LogInformation($"{DateTime.Now:O} -> RunAsync (Id={this.Id:B})"); diff --git a/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyTask.cs b/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyTask.cs index f5b9ad4..59e6a24 100644 --- a/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyTask.cs +++ b/Samples/NCrontab.Scheduler.AspNetCoreSample/Tasks/NightlyTask.cs @@ -1,18 +1,15 @@ namespace NCrontab.Scheduler.AspNetCoreSample.Tasks { - public class NightlyTask : IScheduledTask + public class NightlyTask : TaskBase, IScheduledTask { private readonly ILogger logger; public NightlyTask(ILogger logger) + : base("NightlyTask", CrontabSchedule.Parse("0 0 * * *")) { this.logger = logger; } - public Guid Id { get; } = Guid.NewGuid(); - - public CrontabSchedule CrontabSchedule { get; set; } = CrontabSchedule.Parse("0 0 * * *"); - public void Run(CancellationToken cancellationToken) { this.logger.LogInformation($"{DateTime.Now:O} -> Run (Id={this.Id:B})"); diff --git a/Samples/NCrontab.Scheduler.ConsoleApp/Program.cs b/Samples/NCrontab.Scheduler.ConsoleApp/Program.cs index e8b8a63..837a452 100644 --- a/Samples/NCrontab.Scheduler.ConsoleApp/Program.cs +++ b/Samples/NCrontab.Scheduler.ConsoleApp/Program.cs @@ -11,6 +11,14 @@ public static async Task Main(string[] args) $"Scheduler ConsoleApp version {typeof(Program).Assembly.GetName().Version} {Environment.NewLine}" + $"Copyright(C) superdev GmbH. All rights reserved.{Environment.NewLine}"); + var cancellationSource = new CancellationTokenSource(); + + Console.CancelKeyPress += (_, eventArgs) => + { + eventArgs.Cancel = true; + cancellationSource.Cancel(); + }; + var dateTimeFormat = CultureInfo.CurrentCulture.DateTimeFormat; using var loggerFactory = LoggerFactory.Create(builder => { @@ -31,7 +39,12 @@ public static async Task Main(string[] args) ILogger logger = loggerFactory.CreateLogger(); ISchedulerOptions schedulerOptions = new SchedulerOptions { - DateTimeKind = DateTimeKind.Utc + DateTimeKind = DateTimeKind.Utc, + Logging = new LoggingOptions + { + LogIdentifier = LogIdentifier.TaskName, + DateTimeKind = DateTimeKind.Local + }, }; IScheduler scheduler = new Scheduler(logger, schedulerOptions); @@ -39,10 +52,13 @@ public static async Task Main(string[] args) // for all tasks that are executed. scheduler.Next += OnSchedulerNext; - // Add tasks with different cron schedules and actions. - scheduler.AddTask( + // Add tasks with different cron schedules and actions. + + var scheduleTask = new ScheduledTask( + name: "MinutelyTask", crontabSchedule: CrontabSchedule.Parse("* * * * *"), action: ct => { Console.WriteLine($"{DateTime.Now:O} -> Task runs every minutes"); }); + scheduler.AddTask(scheduleTask); scheduler.AddTask( crontabSchedule: CrontabSchedule.Parse("*/2 * * * *"), @@ -66,9 +82,7 @@ public static async Task Main(string[] args) // Finally, start the scheduler and observe the action callbacks // as well as the Next event handler. - await scheduler.StartAsync(); - - Console.ReadLine(); + await scheduler.StartAsync(cancellationSource.Token); } private static void OnSchedulerNext(object? sender, ScheduledEventArgs e) diff --git a/Tests/NCrontab.Scheduler.Tests/SchedulerTests.cs b/Tests/NCrontab.Scheduler.Tests/SchedulerTests.cs index 29d8eaf..ff7571b 100644 --- a/Tests/NCrontab.Scheduler.Tests/SchedulerTests.cs +++ b/Tests/NCrontab.Scheduler.Tests/SchedulerTests.cs @@ -29,9 +29,17 @@ public SchedulerTests(ITestOutputHelper testOutputHelper) this.autoMocker = new AutoMocker(); this.autoMocker.Use>(new TestOutputHelperLogger(testOutputHelper)); + var schedulerOptions = this.autoMocker.GetMock(); + schedulerOptions.SetupGet(o => o.DateTimeKind) + .Returns(DateTimeKind.Utc); + schedulerOptions.SetupGet(o => o.Logging) + .Returns(new LoggingOptions()); + var schedulerOptionsMock = this.autoMocker.GetMock(); schedulerOptionsMock.SetupGet(o => o.DateTimeKind) .Returns(DateTimeKind.Utc); + schedulerOptionsMock.SetupGet(o => o.Logging) + .Returns(new LoggingOptions()); } [Fact] @@ -124,7 +132,7 @@ public async Task ShouldAddTask_SingleTask_Synchronous_WithLocalTime() dateTimeMock.SetupSequence(d => d.Now, referenceDate.ToLocalTime(), (n) => n.AddSeconds(1)); dateTimeMock.SetupSequence(d => d.UtcNow, referenceDate, (n) => n.AddSeconds(1)); - var schedulerOptionsMock = this.autoMocker.GetMock(); + var schedulerOptionsMock = this.autoMocker.GetMock(); schedulerOptionsMock.SetupGet(o => o.DateTimeKind) .Returns(DateTimeKind.Local); @@ -470,15 +478,19 @@ public async Task ShouldRemoveTask() { // Arrange var nextCount = 0; + var dateTimeMock = this.autoMocker.GetMock(); dateTimeMock.SetupSequence(d => d.UtcNow) - .Returns(new DateTime(2019, 11, 06, 14, 43, 59)) - .Returns(new DateTime(2019, 11, 06, 14, 43, 59)) - .Returns(new DateTime(2019, 11, 06, 14, 43, 59)) - .Returns(new DateTime(2019, 11, 06, 14, 44, 00)); + .Returns(new DateTime(2019, 11, 06, 14, 43, 59, DateTimeKind.Utc)) + .Returns(new DateTime(2019, 11, 06, 14, 43, 59, DateTimeKind.Utc)) + .Returns(new DateTime(2019, 11, 06, 14, 44, 00, DateTimeKind.Utc)) + .Returns(new DateTime(2019, 11, 06, 14, 44, 01, DateTimeKind.Utc)) + .Returns(new DateTime(2019, 11, 06, 14, 44, 02, DateTimeKind.Utc)) + .Returns(new DateTime(2019, 11, 06, 14, 44, 03, DateTimeKind.Utc)) + ; IScheduler scheduler = this.autoMocker.CreateInstance(enablePrivate: true); - scheduler.Next += (sender, args) => { nextCount++; }; + scheduler.Next += (sender, args) => { Interlocked.Increment(ref nextCount); }; // Act using (var cancellationTokenSource = new CancellationTokenSource(4000)) @@ -667,7 +679,7 @@ public async Task ScheduleAsyncJobsAndOneWillFailTheOtherWillStillRunAndLogWillB .Returns(new DateTime(2019, 11, 06, 14, 44, 00)); var logger = new Mock>(); - var schedulerOptionsMock = this.autoMocker.GetMock(); + var schedulerOptionsMock = this.autoMocker.GetMock(); IScheduler scheduler = new Scheduler(logger.Object, dateTimeMock.Object, schedulerOptionsMock.Object); @@ -693,7 +705,7 @@ public async Task ScheduleAsyncJobsAndOneWillFailTheOtherWillStillRunAndLogWillB // Arrange logger.Verify(x => x.Log(LogLevel.Error, It.IsAny(), - It.Is((o, t) => o.ToString().Contains($"Task with Id={failingTaskId:B} failed with exception")), + It.Is((o, t) => o.ToString().Contains($"Task {failingTaskId:B} failed with exception")), It.IsAny(), (Func)It.IsAny()), Times.Once); diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 21ef5d2..351d6a5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,8 +25,8 @@ variables: solution: 'NCrontab.Scheduler.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' - majorVersion: 1 - minorVersion: 2 + majorVersion: 2 + minorVersion: 0 patchVersion: $[counter(format('{0}.{1}', variables.majorVersion, variables.minorVersion), 0)] ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/master') }}: # Versioning: 1.0.0