From demo to production: Handling the edge cases that aren't there

time to read 5 min | 854 words

There is a difference between getting a scenario to working and actually finishing it. The first part is just making sure that the tech stuff work, that is generally easy, the hard part is getting all the stuff that you need to take care of taken care of. The biggest problem is that often you don't really see those issues. Let us start with an example from today.

Let us have this user story:

As part of the nightly maintenance routines, two days before the event, the system will remind all the participants about the event using SMS.

Simple & easy, right?

Here is the code that we started with:

[Occurances(OccureEvery.Day)]
public class SendSmsTwoDaysBeforeEventStart : ScheduledTask
{
	public override void Execute()
	{
		ICollection<Event> eventsInTwoDays = Repository<Event>.FindAll(
			Where.Event.ScheduledDate == DateTime.Today.AddDays(2) &&
			Where.Event.Participants.With(FetchMode.Join)
		);
		foreach(Event e in eventsInTwoDays)
		{
			SendNotificationSmsToAllParticipantsIn(e);
		}
	}
}

Hm, that looks good, doesn't it? 8 lines of code, very readable, nothing can go wrong here, right?

As it turns out, no, there are a few very bad bugs here. Can you spot the first awful bug?

Hint: When does most scheduled tasks run?

So far, we didn't care, so we assume that they would run at night, when no one is using the system. The problem, this way we actually sending the SMS at night. I don't know about you, but I would be pissed off if someone sent me an SMS at 2:02 AM.

There are several ways to handle that, from queued SMS processes to scheduling another execution, etc. We decided that we aren't interested in solving this and just added this:

[Occurances(OccureEvery.Day, At = "08:00")]

So, that took care of the problem for now, at some point, we will add the ability to parse that to the scheduler. Now we can move on to the next story, right?

Nitpicker corner : You probably want to say that I shouldn't hard code the hour, but I don't care about that, actually, if it become a problem, it will change, but this is the simplest thing that would make the problem go away.

Take a look at the code above, can you see another problem there? A big serious one?

Hint; Saturday.

Here you need to understand something about Israel, Saturday is more than the rest day here. I'll be short and probably convey the wrong impression, but it is considered, by the religious people, as a day where no fire can be lit, or electricity be used. This is highly inaccurate, but it would suffice for this post. You can read more about it here.

So, what this basically means is that if you send an SMS to someone on Saturday, it is considered a desecration of the Sabbath. And that actually translate well, surprisingly. Not all people feels like this, I don't, for instance, but there are many who do. So, we absolutely must not send SMS on Saturday.

Okay, that is a simply fix, right?

public override void Execute() 
{ 
	if(DateTime.Today.DayOfWeek == DayOfWeek.Saturday)
		return;
	// ...

Okay, we are done now, right?

Hm, no. I don't really like this code, and we have a bigger problem. Can you see it?

Hint:  Holidays

In Israel, there are many holidays which have the same treatment as Sabbath, and some are even more strict than that. We just had Yom Kippur (day of forgiveness). The PR horror of sending an SMS to even a few hundreds people in Yom Kippur is quite huge. But other holidays should probably be considered as well, so we need to check for those as well. At that point, I said: "I am not dealing with it right now" and wrote the following:

[Occurances(OccureEvery.Day, At = "08:00", RunAtHolidays = false)]

Now it is someone's else job to fix this so it would work correctly. Mine, probably, but at some later date.

Are we done yet? Not yet, I am afraid. So, what has gone wrong now?

Hint: DateTime.Today.AddDays(2)

This does the calculation based on calendar days, but do we really want it to be this way? Considering that we are skipping holidays, we probably want to consider only business days. For that matter, at what time zone are those meeting being held?

In order to facilitate that, the ScheduledTask base class contains several properties that return the time at various interesting points. (InTwoWeek,InOneMonth, Tomorrow, etc).

That one is easy, we can just change the query to:

ICollection<Event> eventsInTwoDays = Repository<Event>.FindAll(
			Where.Event.ScheduledDate == InTwoDays &&
			Where.Event.Participants.With(FetchMode.Join)
		);		

That is good, right? Except that here we need to take into account events that occurs during non business days. So we probably want the intersection of the events from the next two days on the calendar and the next two business days.

The final code now looks like this:

[Occurances(OccureEvery.Day, At = "08:00", RunAtHolidays = false)]
public class SendSmsTwoDaysBeforeEventStart : ScheduledTask
{
	public override void Execute()
	{
		ICollection<Event> eventsInTwoDays = Repository<Event>.FindAll(
			Where.Event.ScheduledDate.Between(InTwoCalendarDays, InTwoDays) && 
			Where.Event.Participants.With(FetchMode.Join)
		);
		foreach(Event e in eventsInTwoDays)
		{
			SendNotificationSmsToAllParticipantsIn(e);
		}
	}
}

Now, I am getting tired of this post, but I am going to leave you with a cliffhanger.

There is at least one other bug here, can you spot it?