w w w . P r o v a t o S y s . c o m
M i c h a e l   C a t a l a n i
9 0 1 . 5 8 1 . 8 7 9 1

 

               


The TIME_DelayJobUntil() Procedure

You probably have several jobs on your system that run as batch jobs, and they basically sit and wait until a certain time of the day before they wake up and begin processing.  Many times, these are CL programs which use the DLYJOB command to sit out until they need to run.  For example, the code to the program may look like this:

----------------------------------------------------------------
PGM

/*  Wait until 11pm and then resume processing
DLYJOB RSMTIME('23:00:00')  

(do some processing)

ENDPGM
----------------------------------------------------------------

A problem with this program code is that if we need to shutdown the job for some reason (via the ENDJOB, ENDSBS, or PWRDWNSYS), the job will not be able to end normally. That's because the job will hang on the DLYJOB statement until the "resume time" is reached.  So one of the following things will happen:

  1. The controlled shutdown will not complete until after the resume time is reached, which may be a significant delay.
  2. The maximum time for a controlled shutdown will reach and the program will be forced to end abnormally
  3. You will have to end the job immediately, causing it to abnormally end.

In order to address these issues, I created a procedure called TIME_DelayJobUntil.  This procedure basically allows me to do two things:

Here's the structure of the procedure:

 TIME_DelayJobUntil( resume time  : resolution time optional);

The function requires 1 parameter, which is the time we want the job to resume processing.  The second parameter is optional, and is a resolution time. This time is how often the function "wakes up" to see if a shutdown has been requested.  If we do not pass the resolution time, then it defaults to 10 seconds.

Let's say that we wanted to use this function to delay until 11pm like our original example.  We would simply code:

TIME_DelayJobUntil( %time( '23.00.00' ) );

And that's it.  This code will act just like running a delay job command with a resume time, except that it will check every 10 seconds for a shutdown request.  

A big benefit of using the TIME_DelayJobUntil procedure is that the first parameter is a time field.  This means that not only can we hard code a time, we can also calculate a time within the parameter field.  Let's say that we want to delay a job until an hour and a half from now.  We could simply code the following:

TIME_DelayJobUntil(  %time + %minutes (90)  );

Notice how this code is more understandable than using the DLYJOB command, in which would look like this:

DLYJOB DLY(5400);

Sure, the delay job says to delay 5400 seconds, but unless we've been exposed continuously to what 5400 seconds is, we probably wouldn't know that it meant an hour and a half.

We can also use a function for the time parameter.  Let's say we've created a function called TIME_TopOfTheHour. (Which we have, you can read about it in the TimeTopOfTheHour section.) This function will return us the next top of the hour time. For example, if the system time is currently 22.16.12, then the TIME_TopOfTheHour function returns 23.00.00. So if we wanted to delay a job until the top of the hour, we could code:

TIME_DelayJobUntil(  TIME_TopOfTheHour()  );
 

Application Scenario:
Let's look at an actual application scenario, where we could benefit from using the TIME_DelayJobUntil and the TIME_TopOfTheHour function. An online nursery/greenhouse business ships live plants to customers across the US.  Certain plants are sensitive to very hot and very cold temperature, and their shipment should be held up until the temperatures moderate for their shipping destination. An application is needed to retrieve the forecasted high and low temperatures for every order's delivery location. This application should run every hour to ensure the most up-to-date weather information has been retrieved.  The application is a subsystem auto-start batch job. As soon as the subsystem starts, this application is launched and gets temperature data for the upcoming week for all orders in the online system.  Then, every hour on the hour, the application runs to keep any weather updates current. The application should check for a shutdown request intermittently in case the subsystem it is running in is told to end.

Here's the application that performs the above task:

 
     h dftactgrp(*no) bnddir('CF')

 
      /copy *libl/qrpglesrc,time_p

      /free

        dou %shtdn; 
 
          callp GetWeatherUpdate();

          TIME_DelayJobUntil( TIME_TopOfTheHour() );
          

        enddo;
        
        return;

 

 

When this application is first started, it calls the GetWeatherUpdate procedure and immediately retrieves weather information. Then, it will delay until the top of the hour via the call to the TIME_DelayJobUntil procedure.  While waiting for the resume time, which will occur at the top of the hour, the TIME_DelayJobUntil function will check every 10 seconds to see if a shutdown request has been received.  ( The default resolution time is 10 seconds since we didnt pass the resolution time as a parameter to the TIME_DelayJobUntil procedure) . If the function detects a shutdown request, it will return processing immediately to our program, where the code hits the enddo statement.  At this point, the %shtdn value is true, and the code drops out of the loop and the program ends.

 

Naming Convention

Before we move on, I want to mention quickly how my naming convention works.  When we look at the function name TIME_DelayJobUntil, the first section of the name "TIME_" tells me the following about this function:

So if I choose to use TIME_DelayJobUntil, then I also need to included the prototypes for it.  That's as easy as:

/copy *libl/qrpglesrc, time_p

By keeping a naming standard like this, there's a lot of information I dont need to look up, such as the prototype member which I have to copy into any program that uses the functions and subprocedures. I already know the name of the prototype member by looking at the function's prefix name. 

All of my custom function service programs are located in a binding directory call CF, which stands for custom functions.  I also have a CF library where the binding directory is located, as well as the source files where the prototype and procedure code members are located. So when you see BNDDIR('CF') in the program's "H" specs, that's what it's for. It's my binding directory.

Now, let's take a look at the code behind the TIME_DelayJobUntil procedure. First, here's the prototype:

Prototype  TIME_DelayJobUntil()

 
    
     d TIME_DelayJobUntil...
     d                 pr
     d  PassedTime                     t   const
     d  Resolution                    6p 0 const options(*nopass)    
      

 

Procedure TIME_DelayJobUntil()

 
    
      * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
      *
      *  T i m e _ D e l a y J o b U n t i l
      *
      *          This procedure will sleep until the time specified, while
      *          checking perioically for a %shutdn condition
      *
      *          Input Field - Resume Time ( Time Field )
      *
      * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     p Time_DelayJobUntil...
     p                 b                   export
     d Time_DelayJobUntil...
     d                 pi
     d  PassedTime                     t   const
     d  Resolution                    6p 0 const options(*nopass)

     d  WaitTime       s             20i 0
     d  ResumeStamp    s               z
     d  UntilTime      s             20i 0
     d  MicroTime      s             10u 0

      /free

        if %parms < 2;
          WaitTime = 10;
        else;
          WaitTime = Resolution;
        endif;

        ResumeStamp = %date + passedtime;

        if ResumeStamp < %timestamp;
          ResumeStamp = ResumeStamp + %days( 1 );
        endif;

        dou %timestamp > ResumeStamp ;

          UntilTime = %diff( ResumeStamp : %timestamp : *mseconds );

          if UntilTime < ( WaitTime * 1000000);
            MicroTime = UntilTime;
            TIME_MicroSleep( MicroTime );
          else;
            TIME_Sleep( WaitTime );
          endif;

          if %shtdn;
             return;
          endif;

        enddo;

        return;

      /end-free
     p                 e
      


We pass the "resume time" as the first parameter. This is a time field.   The second parameter is optional, and tells the procedure how often to "wake up" and chec for a shutdown request.

The first item of business in the calculations is to see if the resolution time was passed. (parameter 2). If it was, then we use the passed parameter for the resolution time field "WaitTime".  If parameter 2 was not passed, the we set WaitTime to 10, meaning 10 seconds.

Next, we populate a timestamp field called ResumeStamp with the current date and the resume time.   Because we are only being passed a time field to the procedure, it is very possible that the resume time is less than the current time.  For example, if the current time is 17.00.00 (5pm) and the resume time passed to us in the PassedTime field is 01.00.00 (1am), then the resume time has already been passed.  What we want the procedure to do is to wait until 1am tomorrow.  So we check this condition by comparing our ResumeStamp field with the current timestamp. If our resume stamp is less than the current timestamp, then that means we are wanting to resume for a time that has already passed today, which really means that we are wanting to resume at that time for tomorrow. So if ResumeStamp is less than timestamp, we add 1 day to our ResumeStamp field.  Now, the procedure gets down to sleeping and waking up.

Then, we calculate how much time is left between the current time and the time we want to resume processing.  We do this with the following statement:

UntilTime = %diff( ResumeStamp : %timestamp : *mseconds );

The value of UntilTime will be the number of microseconds before we reach our resume time. I'll explain why we are dealing in microseconds in just a moment. Next, the program checks to see if the UntilTime field is greater than or less than the WaitTime field, which is the resolution time.  More about this in just a second, but here's the code snippet I am referring to:

     if UntilTime < ( WaitTime * 1000000);
         MicroTime = UntilTime;
        TIME_MicroSleep( MicroTime );
     else;
        TIME_Sleep( WaitTime );
     endif;

Notice that we have to check to see if UntilTime is less than WaitTime multiplied by one million. This is because UntilTime is in microseconds, and we need to convert our Wait Time to microseconds. If WaitTime was the default of 10 seconds, then we would compare the number of UntilTime to 10 million.  I'll explain a bit more why we do this in a second, lets get thru the code first.

If the until time is less than our resolution time, then we perform the TIME_microsleep function, because we want to wait until the exact amount of time (to the microsecond) thas left before we need to exit this function.  If Until Time is equal to or more than the Wait Time, then we can simply sleep for the number of seconds of our resolution time (WaitTime.)

After this code, we have the following code:

     if %shtdn;
       return;
    endif;

   enddo;

\

This code snippet simply checks to see if a shutdown request has been made.

Ok, so let's hash thru some of the above code.  What's happening is that we want this procedure to intermittently check for shutdown.  We check for shutdown as often as the resolution time says to (WaitTime field), which defaults to 10 seconds. But we cant simply delay for 10 seconds each time. If we do, then there's a chance that our resume time is only a second away, and we end up delaying for 10 seconds before checking again.  It would be better if our procedure delayed 10 seconds, unless there is less than 10 seconds left, in which case we would want to delay the remaining time only.  So lets say that we check and we are 2 seconds away from reaching our resume time. Instead of waiting 10 seconds, let's just wait 2 seconds.

If our resume time is further away than our resolution time, we simply delay the job by the resolution time.  This is accomplished with the TIME_Sleep(WaitTime) statement.  Time_Sleep is a procedure that calls a "c" library function called "sleep". Sleep is basically a Delay Job function.  You pass it the number of seconds you want to sleep (delay).  Please refer to the section on the Sleep Api's for the description and prototypes for these api's.

 So, if we are calling TIME_Sleep to delay the maximum amount of our resolution time, why aren't we using this same procedure to delay the remaining time (UntilTime) if the remaining time is less than our resolution time?  Why are we using TIME_MicroSleep?  TIME_MicroSleep calls a "C" library function that says to sleep an amount of time in microseconds.  If we simply used the TIME_Sleep function, which only deals in sleeping in whole seconds, then our resume time may be exceeded by nearly a second before our calling program gets returned to. While this is probably trivial, there may be cases where you need a job to attempt to resume processing at practically the exact resume time.  By using the TIME_MicroSleep procedure, and passing it the number of microseconds left before our resume time is reached, we basically sleep until the very microsecond that we should resume.   In tests performed, the calling program is usually returned to in just a few milliseconds past the resume time.

So, in summary, TIME_DelayJobUntil is a procedure that delays our job, and resumes the exact microsecond that our resume time is reached.  It also intermittently checks for a shutdown request, which allows our controlled ends of the job, subsystem, or system to end quickly and normally.