Code Monkey home page Code Monkey logo

tempus's Introduction

Tempus

NuGet

This is a simple async/await, unit-test friendly timer implementation.

Why we are here

It appears that the timers .net platform provides out of the box are slightly outdated. The API of these timers never changed since the very beginning.

There are 2 major issues with the API:

  1. It is event-based

    In the era of the asynchronous programming, when nearly all the codebase top to bottom is asynchronous, event handlers do not fit this paradigm. Async void is not an option!

  2. There is no interface abstraction

    Yeah, there is no way to unit test our periodic logic.

Hence, here we are. This timer is to address these issues.

What it does

  • It ticks! More or less precisely. Underneath Task.Delay() is used.
  • Provides a unit-test friendly IScheduler abstraction and its impementation TestScheduler which allows us to time travel

What it does not do

  • It does not persist any kind of state
  • It does not do exclusive inter process/system/machine execution

Examples

Let's configure a periodic task which will be pinging an external service every 5 seconds:

public class EchoService
{
    private readonly IScheduledTask _periodTask;

    public EchoService(IScheduler scheduler, IExternalService externalService)
    {
        _periodTask = scheduler.Schedule(TimeSpan.FromSeconds(5), async token =>
            {
                await externalService.Ping(token);
            },
            (context, token) =>
            {
                Console.WriteLine(DateTime.Now + " " +
                                  $"Exception: {context.Exception}. " +
                                  $"First failure at: {context.FirstFailureDateTime} " +
                                  $"Fail count: {context.FailCount} " +
                                  $"Period: {context.Period} "+
                                  $"Current period: {context.CurrentPeriod} "+
                                  $"Max period: {context.MaxPeriod}");
                return Task.CompletedTask;
            });
    }

    public async Task Stop()
    {
        await _periodTask.Cancel();
    }
}

Here we don't know what can go wrong with our external service, hence no specific exception handling logic - print every exception.

In case of unhandled exception the scheduler will keep executing the task with the period specified:

04/07/2018 12:35:17 Normal. Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 1 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:22 Normal. Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 2 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:27 Normal. Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 3 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:32 Normal. Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 4 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:37 Normal. Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 5 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:42 Normal. Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 6 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05

Seems like a known situation - Service unavailable - no need to print the stack trace!

public class EchoService
{
    private readonly IScheduledTask _periodTask;

    public EchoService(IScheduler scheduler, IExternalService externalService)
    {
        _periodTask = scheduler.Schedule(TimeSpan.FromSeconds(5), async token =>
            {
                await externalService.Ping(token);
            },
            (context, token) =>
            {
                if (context.Exception is ExternalServiceUnavailableException)
                {
                    Console.WriteLine(DateTime.Now + " " +
                                      $"Warning - Service unavailable. " +
                                      $"First failure at: {context.FirstFailureDateTime} " +
                                      $"Fail count: {context.FailCount} " +
                                      $"Period: {context.Period} "+
                                      $"Current period: {context.CurrentPeriod} "+
                                      $"Max period: {context.MaxPeriod}");
                }
                else
                {
                    Console.WriteLine(DateTime.Now + " " +
                                      $"Exception: {context.Exception.Message}. " +
                                      $"First failure at: {context.FirstFailureDateTime} " +
                                      $"Fail count: {context.FailCount} " +
                                      $"Period: {context.Period} "+
                                      $"Current period: {context.CurrentPeriod} "+
                                      $"Max period: {context.MaxPeriod}");
                }
            });
    }

    public async Task Stop()
    {
        await _periodTask.Cancel();
    }
}

Again in case of ExternalServiceUnavailableException the scheduler will keep executing the task with the period specified:

04/07/2018 12:35:17 Warning - Service unavailable. First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 1 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:22 Warning - Service unavailable. First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 2 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:27 Warning - Service unavailable. First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 3 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:32 Warning - Service unavailable. First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 4 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:37 Warning - Service unavailable. First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 5 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05
04/07/2018 12:35:42 Warning - Service unavailable. First failure at: 04/07/2018 12:35:17 +02:00 Fail count: 6 Period: 00:00:05 Current period: 00:00:05 Max period: 00:00:05

Important part here is that ExternalServiceUnavailableException is still considered as an unhandled exception, cause we let it flow into the exception logging part. It is logged differently from any other exceptions, but the exception context is collecting the information about all the consequent exceptions happened recently.

If we would like to avoid this behaviour, we can actually handle the ExternalServiceUnavailableException in the body of the scheduled action:

public class EchoService
{
    private readonly IScheduledTask _periodTask;

    public EchoService(IScheduler scheduler, IExternalService externalService)
    {
        _periodTask = scheduler.Schedule(TimeSpan.FromSeconds(5), async token =>
            {
                try
                {
                    await externalService.Ping(token);
                }
                catch (ExternalServiceUnavailableException)
                {
                    Console.WriteLine(DateTime.Now + " Warning - Service unavailable");
                }
            },
            (context, token) =>
            {
                Console.WriteLine(DateTime.Now + " " +
                                  $"Exception: {context.Exception}. " +
                                  $"First failure at: {context.FirstFailureDateTime} " +
                                  $"Fail count: {context.FailCount} " +
                                  $"Period: {context.Period} "+
                                  $"Current period: {context.CurrentPeriod} "+
                                  $"Max period: {context.MaxPeriod}");
                return Task.CompletedTask;
            });
    }

    public async Task Stop()
    {
        await _periodTask.Cancel();
    }
}

In case of ExternalServiceUnavailableException no unhadled exceptions happened and the output will be like this:

04/07/2018 12:35:17 Warning - Service unavailable
04/07/2018 12:35:22 Warning - Service unavailable
04/07/2018 12:35:27 Warning - Service unavailable
04/07/2018 12:35:32 Warning - Service unavailable
04/07/2018 12:35:37 Warning - Service unavailable
04/07/2018 12:35:42 Warning - Service unavailable

There is another option for unhandled exceptions - exponential backoff. Let's take the first example and configure it to backoff exponentially to max 75 seconds:

public class EchoService
{
    private readonly IScheduledTask _periodTask;

    public EchoService(IScheduler scheduler, IExternalService externalService)
    {
        _periodTask = scheduler.Schedule(TimeSpan.FromSeconds(5), async token =>
            {
                await externalService.Ping(token);
            },
            (context, token) =>
            {
                Console.WriteLine(DateTime.Now + " " +
                                  $"Exception: {context.Exception}. " +
                                  $"First failure at: {context.FirstFailureDateTime} " +
                                  $"Fail count: {context.FailCount} " +
                                  $"Period: {context.Period} "+
                                  $"Current period: {context.CurrentPeriod} "+
                                  $"Max period: {context.MaxPeriod}");
                return Task.CompletedTask;
            },
            TimeSpan.FromSeconds(75));
    }

    public async Task Stop()
    {
        await _periodTask.Cancel();
    }
}

In case of unhandled exception the scheduler will keep executing the task but with ever increasing period up to the maximum period specified. Look at the timings here - execution periods here are: 5, 10, 20, 40, 75, 75 seconds:

04/07/2018 13:39:11 Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 13:39:11 +02:00 Fail count: 1 Period: 00:00:05 Current period: 00:00:05 Max period: 00:01:15
04/07/2018 13:39:16 Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 13:39:11 +02:00 Fail count: 2 Period: 00:00:05 Current period: 00:00:10 Max period: 00:01:15
04/07/2018 13:39:26 Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 13:39:11 +02:00 Fail count: 3 Period: 00:00:05 Current period: 00:00:20 Max period: 00:01:15
04/07/2018 13:39:46 Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 13:39:11 +02:00 Fail count: 4 Period: 00:00:05 Current period: 00:00:40 Max period: 00:01:15
04/07/2018 13:40:26 Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 13:39:11 +02:00 Fail count: 5 Period: 00:00:05 Current period: 00:01:15 Max period: 00:01:15
04/07/2018 13:41:41 Exception: Service unavailable(stack trace here). First failure at: 04/07/2018 13:39:11 +02:00 Fail count: 6 Period: 00:00:05 Current period: 00:01:15 Max period: 00:01:15

Unit testing

[Fact]
public async Task EchoServiceOnceConstructedShouldPingExternalServicePeriodically()
{
    // setup
    var testScheduler = new TestScheduler();

    var externalServiceMock = new Mock<IExternalService>();


    // call
    var echoService = new EchoService(testScheduler, externalServiceMock.Object);

    await testScheduler.AdvanceBy(TimeSpan.FromSeconds(16));


    // check
    externalServiceMock.Verify(service => service.Ping(It.IsAny<CancellationToken>()), Times.Exactly(3));

    await echoService.Stop();
}

tempus's People

Contributors

maxcherednik avatar

Watchers

James Cloos avatar Kalib.Gao avatar  avatar

Forkers

adlanelm

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.