2
\$\begingroup\$

A simple design for task scheduling using the following classes/interfaces.

/// <summary>
///     Represents an executable action.
/// </summary>
public interface IExecutableTask {
    Task ExecuteAsync();
}

/// <summary>
///     A task which can be planned for execution.
/// </summary>
public interface IPlannedTask : IExecutableTask {
    TimeSpan? GetExecutionDelay( DateTimeOffset? lastExecutedAt );
    DateTimeOffset? GetExecutionTime( DateTimeOffset? lastExecutedAt );
}

/// <summary>
///     A task which can be scheduled to run periodically.
/// </summary>
public interface IPeriodicTask : IPlannedTask {
    TimeSpan Frequency { get; }
}

/// <summary>
///     A task which can be planned to run at specific times of the day.
/// </summary>
public interface IScheduledTask : IPlannedTask {
    IReadOnlyList<TimeSpan> ExecutionTimes { get; }
}

And as for the execution mechanism

public class PlannedTaskRegistration {
    public PlannedTaskRegistration( IPlannedTask task, Timer timer ) {
        Task = task;
        Timer = timer;
    }
    public IPlannedTask Task { get; set; }
    public Timer Timer { get; set; }
    public DateTimeOffset? LastExecutedAt { get; set; }
    public DateTimeOffset? NextExecutionAt { get; set; }
    public bool IsExecuting { get; set; }
}

public class TaskManager {
    private readonly List<PlannedTaskRegistration> _registrations = new List<PlannedTaskRegistration>();
    private readonly object _lock = new object();

    public bool IsEnabled { get; private set; }

    public void Add( IPlannedTask task ) {
        var registration = new PlannedTaskRegistration(task, null!);
        var timer = new Timer(TimerTick, registration, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
        registration.Timer = timer;

        lock( _lock ) {
            _registrations.Add(registration);
            RescheduleTask(registration, false);
        }
    }
    public void Start() {
        lock( _lock ) {
            if( IsEnabled ) { return; }
            IsEnabled = true;
            foreach( var registration in _registrations.ToList() ) {
                RescheduleTask(registration, false);
            }
        }
    }
    public void Stop() {
        lock( _lock ) {
            if( !IsEnabled ) { return; }
            IsEnabled = false;
            foreach( var registration in _registrations ) {
                RescheduleTask(registration, false);
            }
        }
    }
    private async void TimerTick( object state ) {
        var registration = (PlannedTaskRegistration)state;
        try {
            lock( registration ) {
                if( registration.IsExecuting ) { return; }
                registration.IsExecuting = true;
            }
            registration.Timer.Change(Timeout.Infinite, Timeout.Infinite);
            await registration.Task.ExecuteAsync();
        } catch {
            // catch/log exceptions
        } finally {
            // reschedule the task
            lock( _lock ) {
                RescheduleTask(registration, true);
            }
        }
    }

    private void RescheduleTask( PlannedTaskRegistration registration, bool isPostExecutionCall ) {
        lock( registration ) {
            if( registration.IsExecuting && !isPostExecutionCall ) {
                // the task is executing right now, do not reschedule
                return;
            }

            var now = DateTimeOffset.Now;
            if( isPostExecutionCall ) {
                registration.LastExecutedAt = now;
            }

            if( IsEnabled ) {
                var executionDelay = registration.Task.GetExecutionDelay(now);
                if( executionDelay is null ) {
                    _registrations.Remove(registration);
                } else {
                    registration.NextExecutionAt = now.Add(executionDelay.Value);
                    registration.Timer.Change(executionDelay.Value, Timeout.InfiniteTimeSpan);
                    registration.IsExecuting = false;
                }
            } else {
                registration.NextExecutionAt = null;
                registration.Timer.Change(Timeout.Infinite, Timeout.Infinite);
            }
        }
    }
}

This design allows the planning of two types of tasks that appear in the system

  • Periodic tasks that should execute with a given frequency, no matter the time of day (e.g. deferred mail sending)
  • Scheduled tasks that should execute at certain times of the day (e.g. system clean-ups at 3 am)

Few of my observations/concerns

  1. I lock the whole collection processing, but I believe the time spent in the locked region is not excessive, plus I remove elements from the list. Lastly, I think it's very plausible given the use case for the class tip-toeing around locks could worsen performance (not too many tasks executing and their intersections are improbable).
  2. I think this design should protect me from unwanted multi-executions by, e.g. rapidly starting/stopping the TaskManager.
  3. I'm aware that stopping/starting the TaskManager while a task is running won't restart the task immediately and will reschedule it according to the "previous" schedule once it's done.

Can you spot any bugs/possible edge-case unwanted behaviours? Any other flaws, such as unclear naming, etc.?

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.