Testing Retries in Quartz.NET

This is the second in a series of posts.

In the last post I showed you the moving parts for persistent retries with Quartz.NET. Now it’s time to bring them all together.

With a little trick it is quite easy to run the actual Quartz.NET scheduler in a unit test. My current testing framework of choice is xUnit. So that’s what I will use in my code.

[Fact]
public void Should_Try_3_Times_And_Then_Give_Up()
{
  ISchedulerFactory factory = new StdSchedulerFactory();
  IScheduler scheduler = factory.GetScheduler();

  AlwaysFails alwaysFails = new AlwaysFails();
  IJob decoratedJob = new EnsureJobExecutionExceptionDecorator(alwaysFails);
  var jobFactory = new Mock<IJobFactory>();
  jobFactory
    .Setup(
      jf => jf.NewJob(It.IsAny<TriggerFiredBundle>(), It.IsAny<IScheduler>()))
    .Returns(decoratedJob);

  ManualResetEvent reset = new ManualResetEvent(false);
  IRetrySettings settings = new InMemoryRetrySettings
    {
      BackoffBaseInterval = 250.Milliseconds(),
      MaxRetries = 2
    };
  IRetryStrategy sut = new ExponentialBackoffRetryStrategy(settings);
  IRetryStrategy retryStrategy = new UnfreezeWhenJobShouldNotRunAgain(sut, reset);
  IJobListener retryListener = new RetryJobListener(retryStrategy);

  scheduler.ListenerManager.AddJobListener(
    retryListener,
    GroupMatcher<JobKey>.AnyGroup());
  scheduler.JobFactory = jobFactory.Object;

  ITrigger trigger = TriggerBuilder
    .Create()
    .StartNow()
    .WithSimpleSchedule(
      x =>
      {
        x.WithIntervalInSeconds(1);
        x.WithRepeatCount(0);
      })
    .WithIdentity("always", "fails")
    .Build();

  IJobDetail job = JobBuilder
    .Create<AlwaysFails>()
    .WithIdentity("always", "fails")
    .Build();

  scheduler.ScheduleJob(job, trigger);
  scheduler.Start();
  scheduler.ResumeAll();

  reset.WaitOne(15.Seconds());

  Assert.Equal(3, alwaysFails.Counter);
}

We start by setting up the SchedulerFactory and use it to get us a live Quartz.NET scheduler.

For this test I want to be able to tell that scheduler how it should create instances of my job class. I usually use a DI container to implement that factory but for the purpose of this test I use a mock object created with Moq.

Whenever the scheduler asks for an instance of the AlwaysFails job the factory returns a canned instance I created as part of my test setup. I explain the EnsureJobExecutionExceptionDecorator in another post.

Next I plug the actual RetryJobListener together and set it up to listen to all jobs using a catch-all matcher.

Then I configure my job to run immediately. Note that you can instruct the scheduler to run your job repeatedly but that is not what I want here. Under ideal circumstances the job should run once and be done with it. Only in case of failure do I want to run it again. And as another note: You can set a flag on the JobExecutionException that will cause the scheduler to retry immediately. But again: Not what I want.

When you start the scheduler it will create a background thread and do all its work there. The test method will just continue and exit before anything meaningful happens. Thus I need to freeze the test execution thread for a little while. This is what the ManualResetEvent is used for. To allow the test to continue as soon as the retry strategy says we are done trying I use another decorator. This one wraps the retry strategy. Please see the aforementioned post for further details.

In this test I use a large part of the Quartz.NET infrastructure. It’s not quite a system test but close enough for me at the moment. It can be run along with my unit tests because everything is done in-memory and it really doesn’t take that long to finish.

In the final post of this series we will make an excursion into the realm of composable software systems. Stay tuned!

Advertisement

2 Responses to Testing Retries in Quartz.NET

  1. Pingback: Quartz and other gems | Outlawtrail - .NET Development

  2. Pingback: Quartz.NET meets Design Patterns | Outlawtrail - .NET Development

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: