Timestamps support flexible date arithmetic, which you can see in this practical example of a task scheduler.
Today's article is going to be very practical indeed! We're going to use timestamps to perform some common business functions and, in so doing, learn a number of simple but effective techniques for working with these versatile variables. This is a real-world example; I am currently running a scheduler using exactly this design.
Do I Really Need to Rewrite the IBM i Job Scheduler?
No, of course you don't, and if that's what I was doing here, I wouldn't waste your time. But powerful though it may be, the default job scheduler provided with the IBM i (using command WRKJOBSCDE) just doesn't have the features that I need.
In fact, before we go any further, let's identify my actual business requirements here. What I'm looking for is the ability to run a specific program on a schedule. Simple enough, but there's an immediate twist: I need two different types of scheduling. First, I need to be able to schedule a program that runs multiple times a day, say every 30 minutes or every four hours. Next, I need to schedule a program that runs once a day at a specific time, but only on certain days of the month.
That's the project. I could try to do this a couple of ways. First, for the looping programs, I'd just set up a CL program. You've probably done one or two of these yourself. It looks like this:
LOOP: CALL PROCESS &ENDJOB
IF (&ENDJOB = '1') RETURN
Done! The program named PROCESS will run once every 60 minutes! Of course, this is a bit of a problem because I'd have to have different programs for different delays. But there's a more subtle problem as well. This doesn't run once an hour. It runs once every 60 minutes plus the time it takes to run the called program. That is, the delay doesn't start until after the program runs. So if it takes 10 minutes to run the process, then it will actually run again 70 minutes after the time it was previously called. This is an inherent problem with a hard delay.
What We Need Is a "Next Run Time"
Let's say the program ran at 9:00 a.m. We'd want to store the fact that the next runtime would be at 10:00 AM. Then, when it was time to delay, we'd only delay until 10:00 AM. Now, there is a way to do this using DLYJOB; you can delay until a specific time. But remember, we want to be able to schedule multiple jobs, and they may have different delay times. I think we're going to have to go to a database file. Here is file AUTCFG:
R AUTCFGR TEXT('Autorun Configuration')
ACRUTP 10 COLHDG('Run Type')
ACRUN1 6S 0 COLHDG('Run Value 1')
ACRUN2 32 COLHDG('Run Value 2')
ACPGM 10 COLHDG('Program')
ACLRTS Z COLHDG('Last Run')
ACNRTS Z COLHDG('Next Run')
ACFORC 1 COLHDG('Force')
It's a simple file. It has three values that determine the loop type: run type, run value 1, and run value 2. It has a program name that simply specifies the program to call. And finally, it has the last run and next run timestamps. Because you’re sharp-eyed, I'm sure you also noticed the force field; we'll get into that at the end of all of this.
Anyway, the way this file works is simple. There are two types: *LOOP and *DAILY. Feel free to add more as needed, but this is what we're starting with. *LOOP uses only ACRUN1, which is the number of minutes to delay between calls to ACPGM. *DAILY uses both fields. ACRUN1 is the 6-digit time of day the program will run, while ACRUN2 is an array of Ys and Ns; a Y in position 15 indicates that ACPGM should run on the 15th of every month. Simple enough, right?
So what are the other fields? Ah, this is where we solve the business problem! The Last Run field simply gets updated whenever ACPGM is run; this is just an informational field used to tell us the last run time. That's important when you want to track down the last time a scheduled job actually ran. But it's the Next Run field, ACNRTS, that does the magic. You see, what we're going to do is write a master scheduler that runs through this file once a minute. That program will compare the current timestamp to the next run timestamp, and if it's greater, we're going to run the program! But here's where we get geeky: as soon as we run the program, we're going to then bump the time to what should be the next run. We don't just wait another hour (or whatever); we bump the next run field by an hour, thus making sure that we continue to run consistently.
This Is How We Rock Around the Clock
dow not %eof(AUTCFGP);
if ACFORC = 'Y';
elseif %timestamp() > ACNRTS;
This is the code that is called every minute. We read every record in the file and see if it needs to be run. Notice the first IF statement that checks ACFORC (I bet you were wondering about that field!). This field allows you to force an immediate run of a scheduled job without disrupting the normal scheduling. It will run the scheduled program but not bump it to the next time field. So say you want to run something every hour, but you need to run it halfway in. You can just set ACFORC to Y for that entry and the scheduler will run it, but the regularly scheduled iteration will still run 30 minutes later.
Now we finally come down to the fun of timestamps. First, you'll see that we decide whether or not to run the entry with a very simple comparison: We just compare the current timestamp to the next run timestamp. The beauty of timestamps is that they allow comparison of a complete date and time; this gets around the issues of comparing 12:01 a.m. to 11:59 p.m. of the previous day. Because the date is included in the comparison, it always works.
So we have two cases: If it's a force, then just run the program, but if we've passed the next scheduled run time, then run the program and bump the next scheduled run time.
I won't include the code for the run procedure. You don't need it. In fact, you'll end up writing your own that may submit a job, or send a message to a log file, or create a command string with parameters, or check for and report errors. What the run procedure does is actually irrelevant to the scheduling process!
Things That Go Bump in the Job
The bump procedure, on the other hand, is very important. That's actually the heart of the scheduler, the part that shows just how versatile timestamps can be. It has two pieces: first is a bit of code that will initialize a new or stale record. Records may get stale if the scheduler is held for some length of time. Anyway, the code is straightforward:
// If next run is in past, then initialize to 12AM today
if %date(ACNRTS) < %date();
ACNRTS = %timestamp(%date());
All we're doing is checking the date portion of the next run time. You can easily extract either the date portion or the time portion from a timestamp as shown. If the extracted is earlier than today's date, the timestamp is stale and we initialize it. That's the next cool thing: You can initialize a timestamp using a date. The timestamp will be initialized to 12:00 a.m. (time 00:00:00) of the specified date.
Now we have a valid next run timestamp; we simply need to bump that timestamp until it's past the current time. Why are we doing this? Well, let's take the situation before, where it takes 10 minutes to run the job. If we last ran at 9:00 a.m. and the job took 10 minutes, and then we added 60 minutes to the current time, we'd end up with the next run being at 10:10 a.m. This way, we add 60 minutes to the last scheduled time, so the next scheduled time is 10:00 a.m. But here's the tricky part: What if it takes an hour and 30 minutes, and it's already 10:30 a.m.? Well, if we just add 60 minutes, we'll get 10:00 a.m., so we'll run it immediately. Is that what we want? I don't think so, since we've already just run. So let's see how I handle this for type *LOOP:
// Increment next until past current
if ACRUTP = '*LOOP';
dow ACNRTS < ACLRTS;
ACNRTS += %minutes(ACRUN1);
In the run procedure, I set ACLRTS, the last run timestamp, to the current timestamp. Now we want to set ACNRTS to some value later than that. What I do is add the number of minutes in ACRUN1 to the next run timestamp until it's greater than the last run timestamp. The += syntax is very nice here; it allows me to get to the timestamp field directly. And even if the process took long enough to skip an entire iteration, this loop will get me to the next valid run time. Work it out yourself: If my loop is 30 minutes and the next run time was 9:00 a.m., and if the program for whatever reason took an hour and 45 minutes, then ACLRTS would be 10:45. If I add 30 minutes to 9:00 a.m., I get 9:30 a.m. Still not big enough. I add 30 more minutes, I get 10:00 a.m. Then 10:30 a.m. Finally, I get to 11:00 a.m. and that becomes the next run time.
And please note that since this is a timestamp, not just a time field, it will automatically work as you cross over the midnight boundary. I'll leave that exercise to you. Instead, I want to focus on the other scheduler function: schedule days of the month. In this case, the second run parameter is a list of the days that something should run. It would look like this:
This says that the process should run on the 1st and 15th day of every month. The ACRUN1 parameter is the time of day the process should run on those days, in HHMMSS format. Here's the code:
elseif ACRUTP = '*DAILY';
nextDate = %date;
dow ACNRTS < ACLRTS;
nextDate += %days(1);
if %subst( ACRUN2: %subdt(nextDate:*days): 1) = 'Y';
ACNRTS = nextDate + %time(ACRUN1:*iso);
The field nextDate is just a work field of type date. We start with today's date. Then we sit in a loop once again, waiting for the value ACNRTS to be greater than the last run stored in ACLRTS. What we do is add a day to the nextDate field and then see if it's a day that we run the process. We do that by checking the position within ACRUN2 for that day. This is where date arithmetic gets fun; you can easily extract the day number (or any other part of the date) as an integer by using the %subdt BIF. In this case, I get the day number by specifying *days and use that as the offset into the ACRUN2 variable. If that position is a Y, then it's a potential date.
Finally, I build the timestamp by combining the date selected and the time from ACRUN1. Note that you can always create a timestamp just by adding a date and a time together. I use the %time BIF to convert the decimal value in ACRUN1 into an actual time data type, and then I add that to nextDate to get the next timestamp. If it's not big enough, just keep adding a day until it is.
Clearly, there are plenty of other features that could be added here. We could definitely support blackout periods when the process cannot run (for example, from 11:00 p.m. until 2:00 a.m.). We could add a special flag for "the last day of the month". We could add support for days of the week rather than days of the month. But the point is, this design with just one file and a couple of dozen lines of code can provide you a pretty full-featured scheduler.
Have fun with it!