From demo to production: Handling the edge cases that aren't there
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?
Comments
This post also explains why functional requirements documents are almost always woefully inadequate.
I hate to say this, but you didn't fix "bugs" you refined your user story. If this had been done up front you would have written easily been able to write the code.
BTW: The "edge case that weren't there" were there.... you just didn't know about them.
I know you can't think about everything up front, but I think some common questions that apply to refining all users storys would help here.. espesially for agent like tasks:
When should this be done? Everyday...
What time everyday? Oh, how about 8AM
Will you ever want to change that?
What about people in other time zones?
Ok, really everyday? What about weekends and holidays?
What if the message fails, bad phone number or is bounced?
How much after 8AM is ok?
etc.
In the last query, if you use between, wouldn't you get only hits that were /more/ than two (real) days ahead, but less than two business days ?
Also, the last code doesn't check for Saturdays, does it?
And with the "if (!Saturday)" check, wouldn't that mean that the SMS for any event on a Monday would never be sent?
/Mats
Oh, and wouldn't you be sending multiple SMSs whenever InTwoDays > InTwoCalenderDays ?
/Mats
Is this custom code or is it apart of a framework?
ICollection<Event> eventsInTwoDays = Repository<Event>.FindAll(
Why use attributes? It seems weird to have two styles in the same class. Why not have a method to override which specifies the schedule (and maybe could even be injected or something)
public Occurances GetOccurances()
{
}
or maybe:
public Occurances GetOccurances()
{
return Occurs.EveryDay.AtConfiguredTime().ExceptOnHolidays()
}
I found the bug! It should be "Occurrences", not "Occurrances". =)
Really, though, I have to admire your attention to detail. I'd say a lot of people wouldn't consider these bugs and just say "Eh, that's how the system works. Tough patooties."
Are you sure we don't work for the same company? This story is amazingly similar to the stories I run into in which one of the business managers pops into my office with the infamous intro: "I've got a project I need you do and it's really simple..."
Oren,
I think this is a cool example of a typical problem. I personally would actually like to see more of the solution (for example how/where do you get/maintain the information about what is a holiday or not). I feel the intial problem/solution(s) are great but bordering on academic as they don't give the full picture. Like someone mentioned about the user story, this would be a great full blown user story -> specs -> tdd code sample (although if you're like me you use real-world examples in your code so maybe you can't go further with this because of client IP which I would understand).
No, between is inclusive. It is at least two real days ahead, and at most two business days ahead. Often enough, those those would be the same, so I don't care about that.
Note that ScheduledDate is a date only, not date time.
The last code doesn't need to check for Saturdays. I am afraid that this is using the Saturday and Holidays convention around here, where Saturday is considered a Holiday.
With the if(!Saturday) check, you will get the SMS for Monday on Friday.
Mats,
Yes, that is the next problem.
Adam,
This is Rhino Commons repository + NHibernate Query Generator code.
Chad,
The scheduling options and the action itself are two different things, and I want them in two different ways.
And I want it declarative. I am scanning all tasks in assemblies and show them to the user, that is easier to do as attributes than code.
Bob,
Yes, absolutely. Except that some common questions will not really help. This is the first scheduled task that will actually interact with humans in a synchronous way. There isn't any meaning to all the rest of the tasks, because most of those do internal house keeping stuff, and the other send email, which is perfectly fine to do.
I wouldn't like to try and answer a whole lot of check list questions for each requirement, in advance.
I mostly wouldn't know what questions to ask, or how to answer them
Bil,
Holidays are usually maintained in a BusinessDays table, {Date (PK), DayType { Normal, HolidayEvening, Holiday }, Remark {Name, if a Holiday} }. I don't have it yet, and I am going to find what sources exists that can give it to me automatically, but that is the basic idea.
I tend to use real use cases, but not the real scenario, so we did have a case that we needed to send an SMS in two days, but it is not about events, for example.
What would you like to see?
I've got the extra bug - the guy/gal might no longer be alive.
We should check with the local citizen registry (hebrew: מרשם אוכלוסין).
Also, he might had a child born in the last week or a 1st degree relative killed - we should check for that too.
(What? That's a real concern where I'm from... I've actually got that code written somewhere)
For that matter, sending SMS abroad cost a lot more, we need to check with the internal affairs office to see if he is out of the country.
What are you using to schedule the tasks with? I've been looking for a strong .NET scheduler.
I will probably use Castle.Scheduler, but right now I am running them off a web page manually.
I didn't want to have to handle that right now.
I mostly wouldn't know what questions to ask, or how to answer them
Wow. Just wow. So basically you admit you have no expertise in your business domain, or perhaps you do, but you're too dumb to put two and two together and think that your knowledge of the real world could somehow impact your time in front of the keyboard.
Amazing. Keep up with the Agile thing, and keep slagging the "Morts". It seems to be the only way to keep yourself afloat and looking good! God forbid you actually improve your analysis skills.
Foobar,
is there a reason you are insulting?
You are allowed to expressed your own opinions here, but they will be expressed in a polite manner of they will be removed.
Now, to the actual content in your post.
No, I make sure that I understand, at least in a superficial level, what is going on in the business level.
And yes, I do believe that getting down and writing the simplest code possible to meet the requirement, and then expanding on that is the way to go.
After I have wrote the first task of this type, I can try to come up with a list of questions, and hopefully I'll also have some input from the customer about what those answers should be.
But no, I wouldn't try to think of those in advance, doing so is fruitless. What I think up ahead and what I end up with are very different animals, so there isn't much point to it.
And the key part here is _first task of type_. Go above and look at the scenario, this i s the first task that touched humans in a synchronous way, out of several dozens that do house keeping or send mails.
Comment preview