With the QUSRJOBI and QMHRCVM APIs and MONBCH2, you know exactly what the message is and how to reply.
In a previous article, "Keep Those Batch Jobs Running (Or How to Enjoy Your Off Time)," we saw how to detect when a batch job was waiting for a message to be replied to and how to inform a user of that situation. In that introductory article, we simply sent a generic message indicating that job X was waiting for a response to a message. In this article, we'll enhance the MONBCH program and inform the operator of which exact message the job is waiting on. Note that this enhancement to MONBCH will use a V6R1 feature that is not available on previous releases.
The new program, MONBCH2, is shown below with the functional changes highlighted.
h dftactgrp(*no)
dMonBch2 pr extpgm('MONBCH2')
dMonBch2 pi
dSetup pr *
dDelay pr
dProcessMsgW pr
dListJob pr extpgm('QSYS/QUSLJOB')
d SpcName 20 const
d Format 8 const
d JobName 26 const
d Status 10 const
d QUSEC likeds(QUSEC) options(*nopass)
d JobType 1 const options(*nopass)
d NbrKeyFlds 10i 0 const options(*nopass)
d KeyFlds likeds(KeyFlds) options(*nopass)
d ContinHdl 48 const options(*nopass)
dSndDtaQMsg pr extpgm('QSYS/QSNDDTAQ')
d DtaQName 10 const
d DtaQLib 10 const
d DataLen 5 0 const
d Data 65535 const options(*varsize)
d KeyLen 3 0 const options(*nopass)
d KeyValue 65535 const options(*varsize :*nopass)
d Asynch 10 const options(*nopass)
d JrnEntry 10 const options(*nopass)
/copy qsysinc/qrpglesrc,qusgen
/copy qsysinc/qrpglesrc,qusljob
/copy qsysinc/qrpglesrc,qusec
dGenHdr ds likeds(QUSH0100)
d based(GenHdrPtr)
dLstEntry ds likeds(QUSL020001)
d based(LstPtr)
dAttrEntry ds likeds(QUSLKF)
d based(AttrPtr)
dKeyFlds ds
d KeyValues 10i 0 dim(25)
dUsrSpcName ds
d Name 10 inz('MONBCHLJOB')
d Library 10 inz('QTEMP')
dJobsToList ds
d JobName 10 inz('*ALL')
d JobUser 10 inz('*ALL')
d JobNbr 6 inz('*ALL')
dDtaQMsg ds
d DtaQJobName 10
d 1
d DtaQJobUsr 10
d 1
d DtaQJobNbr 6
d 1 inz(':')
d DtaQMsgID 7
d 1
d DtaQMsgTxt 256
dPSDS sds 429 qualified
d Line 21 28
d MsgID 40 46
d ExcpDta 91 170
d JobName 244 253
d JobUser 254 263
d JobNbr 264 269
dLstCount s 10i 0
dAttrCount s 10i 0
dAttrValue s 100 based(AtrValPtr)
dNbrKeyFlds s 10i 0
dContinue s n inz(*on)
dMonBchCtl s 1 dtaara('VININGTST/MONBCHCTL')
dDtaQName c 'MESSAGES'
dDtaQLib c 'VININGTST'
/free
monitor;
GenHdrPtr = Setup();
dow Continue;
ListJob( UsrSpcName :'JOBL0200' :JobsToList :'*ACTIVE'
:QUSEC :'B' :NbrKeyFlds :KeyFlds :*blanks);
// Check to see if the list is complete
if (GenHdr.QUSIS = 'C') or (GenHdr.QUSIS = 'P');
// Get to the first list entry and process the list
LstPtr = GenHdrPtr + GenHdr.QUSOLD;
for LstCount = 1 to GenHdr.QUSNBRLE;
// Get first attribute and process all returned
if LstEntry.QUSJIS = ' ';
AttrPtr = LstPtr + %size(QUSL020001);
for AttrCount = 1 to LstEntry.QUSNBRFR;
AtrValPtr = AttrPtr + %size(QUSLKF);
select;
when AttrEntry.QUSKF = 0101;
if %subst(AttrValue :1 :AttrEntry.QUSLD00)
= 'MSGW';
ProcessMsgW();
endif;
endsl;
AttrPtr += AttrEntry.QUSLFIR;
endfor;
endif;
LstPtr += GenHdr.QUSSEE;
endfor;
Delay();
else;
DtaQJobName = PSDS.JobName;
DtaQJobUsr = PSDS.JobUser;
DtaQJobNbr = PSDS.JobNbr;
DtaQMsgID = *blanks;
DtaQMsgTxt =
'MONBCH is unable to access batch job information.';
SndDtaQMsg( DtaQName :DtaQLib :%len(%trimr(DtaQMsg)):DtaQMsg);
*inlr = *on;
return;
endif;
enddo;
*inlr = *on;
return;
on-error;
DtaQJobName = PSDS.JobName;
DtaQJobUsr = PSDS.JobUser;
DtaQJobNbr = PSDS.JobNbr;
DtaQMsgID = *blanks;
DtaQMsgTxt = 'Job failed at statement ' + PSDS.Line +
' with message ' + PSDS.MsgID +
': ' + PSDS.ExcpDta;
SndDtaQMsg( DtaQName :DtaQLib :%len(%trimr(DtaQMsg)) :DtaQMsg);
return;
endmon;
/end-free
*****************************************************************
pProcessMsgW b
dProcessMsgW pr
dProcessMsgW pi
dRJobI pr extpgm('QSYS/QUSRJOBI')
d RcvVar 1 options(*varsize)
d LenRcvVar 10i 0 const
d Format 8 const
d JobID 26 const
d IntJobID 16 const
d QUSEC likeds(QUSEC) options(*nopass)
d ResetStats 1 const options(*nopass)
dRcvMsg pr extpgm('QSYS/QMHRCVM')
d RcvVar 1 options(*varsize)
d LenRcvVar 10i 0 const
d Format 8 const
d QualMsgQ 20 const
d MsgType 10 const
d MsgKey 4 const
d WaitTime 10i 0 const
d MsgAction 10 const
d QUSEC likeds(QUSEC)
d CCSID 10i 0 const options(*nopass)
d AlwDftRspRjct 10 const options(*nopass)
/copy qsysinc/qrpglesrc,qmhrcvm
/copy qsysinc/qrpglesrc,qusrjobi
dMHRcvVar ds likeds(QMHM0200)
d based(MHRcvVarPtr)
dMsgTxt s 32767 based(MsgTxtPtr)
/free
monitor;
RJobI ( QUSI020000 :%size(QUSI020000) :'JOBI0200' :'*INT'
:LstEntry.QUSIJI00 :QUSEC);
if ((QUSMR01 = '1') and (QUSMK <> *loval));
DtaQJobName = QUSJN03;
DtaQJobUsr = QUSUN03;
DtaQJobNbr = QUSJNBR03;
RcvMsg( QMHM0200 :8 :'RCVM0200' :(QUSMQ + QUSMQLIB) :'*INQ'
:QUSMK :0 :'*SAME' :QUSEC);
MHRcvVarPtr = %alloc(QMHBAVL01);
RcvMsg( MHRcvVar :QMHBAVL01 :'RCVM0200' :(QUSMQ + QUSMQLIB)
:'*INQ' :QUSMK :0 :'*SAME' :QUSEC);
DtaQMsgID = MHRcvVar.QMHMI02;
MsgTxtPtr = (%addr(MHRcvVar) +%size(QMHM0200) +MHRcvVar.QMHLDRTN);
if MHRcvVar.QMHLMRTN <= %size(DtaQMsgTxt);
DtaQMsgTxt = *blanks;
DtaQMsgTxt = %subst(MsgTxt :1 :MHRcvVar.QMHLMRTN);
else;
DtaQMsgTxt = %subst(MsgTxt :1 :%size(DtaQMsgTxt));
endif;
SndDtaQMsg( DtaQName :DtaQLib :%len(%trimr(DtaQMsg)) :DtaQMsg);
dealloc MHRcvVarPtr;
endif;
on-error;
endmon;
return;
/end-free
pProcessMsgW e
*****************************************************************
pDelay b
dDelay pr
dDelay pi
dSleep pr 10u 0 extproc('sleep')
d Seconds 10u 0 value
dTimeToEnd c 'E'
dSecsPerMin c 60
dMinsToDly c 1
/free
in MonBchCtl;
if MonBchCtl = TimeToEnd;
Continue = *off;
return;
endif;
Sleep(SecsPerMin * MinsToDly);
in MonBchCtl;
if MonBchCtl = TimeToEnd;
Continue = *off;
endif;
return;
/end-free
pDelay e
*****************************************************************
pSetup b
dSetup pr *
dSetup pi *
dCrtUsrSpc pr extpgm('QSYS/QUSCRTUS')
d SpcName 20 const
d SpcAttr 10 const
d SpcSiz 10i 0 const
d SpcVal 1 const
d SpcAut 10 const
d SpcTxt 50 const
d SpcRpl 10 const options(*nopass)
d QUSEC likeds(QUSEC) options(*nopass)
d SpcDmn 10 const options(*nopass)
d SpcTfrSiz 10i 0 const options(*nopass)
d SpcOptAln 1 const options(*nopass)
dRtvSpcPtr pr extpgm('QSYS/QUSPTRUS')
d UsrSpcName 20
d UsrSpcPtr *
d QUSEC likeds(QUSEC) options(*nopass)
dUsrSpcPtr s *
/free
// Set API error code structure to return exception messages
QUSBPRV = 0;
// Set the job attribute key values
KeyValues(1) = 0101; // Active job status
NbrKeyFlds = 1;
// Create a *USRSPC to hold the list of jobs
CrtUsrSpc(UsrSpcName :' ' :1 :x'00' :'*EXCLUDE' :' ' :'*YES' :QUSEC);
// Retrieve a pointer to the *USRSPC
RtvSpcPtr(UsrSpcName :UsrSpcPtr :QUSEC);
// Return the pointer to the *USRSPC
return UsrSpcPtr;
/end-free
pSetup e
As you can see, the major functional change is in the ProcessMsgW procedure. There is, however, a change in the format of the message being sent by MONBCH2, and we'll discuss that first.
The original MONBCH program sent a message informing the operator that a given job was waiting on a message. As this message was of our own creation, we simply imbedded the simple job name within the message text. With MONBCH2, we are now sending the actual message text for the message needing a reply. We no longer have the luxury of simply imbedding the job name in the message text. To allow the operator to know what job needs a message reply, MONBCH2 has removed the original MsgTxt variable and replaced it with the data structure DtaQMsg. Within the DtaQMsg structure are the subfields DtaQJobName, DtaQJobUsr, and DtaQJobNbr, which will be used to identify what job is in a message wait condition. The fourth subfield, DtaQMsgID, will contain the message ID of the message needing a reply, and the fifth subfield, DtaQMsgTxt, will contain the first 256 bytes of the message text. Note that this size of 256 bytes is totally arbitrary. I chose this size as it should be sufficient to hold the first-level text of most messages. You should certainly feel free to increase this size.
Due to this change from MsgTxt to DtaQMsg, MONBCH2 needs minor changes in the way it sends messages related to two things: the QUSLJOB API returning incomplete or partial information, and internal failures within MONBCH2. The remaining changes to MONBCH2 are in the ProcessMsgQ procedure. In addition to using the Send Data Queue Message (QSNDDTAQ) API to send a message to the MESSAGES data queue, ProcessMsgW is now using two additional APIs.
The first, Retrieve Job Information, or QUSRJOBI, retrieves job information such as job attributes and performance data about a specific job. It is this API that has been enhanced in V6R1 so that it also now returns information on what message a job is waiting on. The V6R1 documentation for QUSRJOBI can be found here, with the parameter list provided below.
Retrieve Job Information (QUSRJOBI) API
Required Parameter Group:
1 |
Receiver variable |
Output |
Char(*) |
2 |
Length of receiver variable |
Input |
Binary(4) |
3 |
Format name |
Input |
Char(8) |
4 |
Qualified job name |
Input |
Char(26) |
5 |
Internal job identifier |
Input |
Char(16) |
Optional Parameter Group 1:
6 |
Error code |
I/O |
Char(*) |
Optional Parameter Group 2:
7 |
Reset performance statistics |
Input |
Char(1) |
Default Public Authority: *USE
Threadsafe: Conditional
As this API is a standard retrieve type API, we won't spend a lot of time discussing it. One important point is that format JOBI0200 now returns information on the message a job is waiting on. The additions to this format are shown below, where having the auxiliary storage pool containing the message queue library (QUSQLASP), a qualified message queue name (QUSMQ and QUSMQLIB), and a message key (QUSMK) provide MONBCH2 with direct access to the message.
DQUSI020000 DS
D* Qwc JOBI0200
....
D QUSMR01 191 191
D* Message Reply
D QUSMK 192 195
D* Message Key
D QUSMQ 196 205
D* Message Queue
D QUSMQLIB 206 215
D* Message Queue Library
D QUSQLASP 216 225
D* Message Queue Lib ASP
A second important point (or at least important if you are generally interested in performance) is the fifth parameter for QUSRJOBI. Rather than passing in a qualified job name to identify to QUSRJOBI the job you want to retrieve information about, you can instead use an internal job identifier. This internal job identifier provides faster access to a job's information and is a piece of information that the QUSLJOB API returns to us in field LstEntry.QUSIJI00. MONBCH2 uses this internal job identifier for demonstration purposes. In reality, you won't notice much difference between using a qualified job name and using an internal job identifier with MONBCH2 as the program is only using QUSRJOBI in the exception case of a batch job waiting on a message. But the performance advantage could make a difference if, in a different scenario, you were accessing information on every job on the system.
Prior to calling QUSRJOBI, ProcessMsgW sets a monitor. Since the list of jobs returned by QUSLJOB is static and potentially large, it is possible that the job waiting on the message is no longer active. The monitor is set to catch any error returned by QUSRJOBI and allows us to simply skip over the list entry.
After calling QUSRJOBI, ProcessMsgW also verifies that the job is still waiting on a message (QUSMR01 = '1') and that the wait is for a specific message (QUSMK <> *loval). This is done for two reasons. First, as mentioned before, the list of jobs returned by QUSLJOB is static and potentially large. A job that had been waiting for a message reply when QUSLJOB last ran may no longer be waiting for the reply by the time MONBCH2 processes the list entry. As we don't want to be notifying an operator about a condition that no longer exists, we check to see if the job is still waiting. Second, a job can be waiting for a specific message or any message to appear on a message queue. The QUSMK check is to make sure there is one specific message that the job is waiting for. If the job is waiting on a specific message, ProcessMsgW updates the appropriate subfields of the DtaQMsg data structure to identify the job to the operator and then calls the Receive Nonprogram Message (QMHRCVM) API using the message key and qualified message queue name returned by QUSRJOBI. The documentation for QMHRCVM can be found here with the parameter list repeated below.
Receive Nonprogram Message (QMHRCVM) API
Required Parameter Group:
1 |
Message information |
Output |
Char(*) |
2 |
Length of message information |
Input |
Binary(4) |
3 |
Format name |
Input |
Char(8) |
4 |
Qualified message queue name |
Input |
Char(20) |
5 |
Message type |
Input |
Char(10) |
6 |
Message key |
Input |
Char(4) |
7 |
Wait time |
Input |
Binary(4) |
8 |
Message action |
Input |
Char(10) |
9 |
Error code |
I/O |
Char(*) |
Optional Parameter Group 1:
10 |
Coded character set identifier |
Input |
Binary(4) |
Optional Parameter Group 2:
11 |
Allow default reply rejection |
Input |
Char(10) |
Default Public Authority: *USE
Threadsafe: Yes
As with QUSRJOBI, QMHRCVM is a standard retrieve type API, so we will not spend time going through the details of how the API works. ProcessMsgW is requesting that information about the inquiry message, which is identified by the Message key and Qualified message queue parameters, be returned using format RCVM0200. These are the pertinent subfields of format RCVM0200 from the QSYSINC include:
DQMHM0200 DS
D* Qmh Rcvm RCVM0200
D QMHBRTN00 1 4B 0
D* Bytes Returned
D QMHBAVL01 5 8B 0
D* Bytes Available
.....
D QMHMI02 13 19
D* Message Id
.....
D QMHLDRTN 153 156B 0
D* Length Data Returned
.....
D QMHLMRTN 161 164B 0
D* Length Message Returned
.....
D*QMHMT03 177 177
D* Varying length
D*QMHSSAGE 178 178
D* Varying length
As we don't know the size of the message to be returned when we call QMHRCVM, we call the API twice. We first call it with a Length of message information parameter value of 8. This is just sufficient for the API to return the Bytes returned and Bytes available fields of format RCVM0200. Bytes available tells us how many bytes are needed to receive all of the message-related information, and ProcessMsgW then allocates storage of this size and calls the QMHRCVM API a second time, using the newly allocated Message information parameter and the updated Length of message information parameter. We now have all of the information associated with the message.
ProcessMsgW first updates the DtaQMsgID subfield of DtaQMsg to the message ID of the message (QMHMI02). The procedure then calculates where in the returned information the text of the message can be found. With most system APIs, this would be a simple matter of taking the address of the Message information parameter and adding a provided Offset to message text value. Unfortunately, QMHRCVM is a rather old API (V2R1.1) and doesn't provide an offset. Instead, we need to calculate it by understanding the layout of format RCVM0200.
Format RCVM0200 starts with a series of fixed-length fields. These fields are QMHBRTN00 (Bytes returned) through QMHLHAVL (the length of help text available with the message). Following QMHLHAVL are several variable-length fields that are commented out in the QSYSINC include. The first variable-length field, QMHMT03, is the replacement data associated with the message. The actual length of the replacement data is returned in variable QMHLDRTN. Following QMHMT03 is QMHSSAGE, the actual message text. The length of the message text is returned in variable QMHLMRTN. Given this information, we can then calculate where the first byte of the message text is by taking the address of the Message information parameter (MHRcvVar), adding the size of the fixed-length fields (%size(QMHM0200)), and then adding the length of the replacement data (QMHLDRTN). This value is then stored in the pointer variable MsgTxtPtr, which is the basing pointer for the variable MsgTxt.
Having established addressability to the message text, we now move the text to the DtaQMsgTxt subfield of the DtaQMsg data structure and send the data structure to the MESSAGES data queue. When moving the message text returned by QMHRCVM, we do check to see if the length of the message text (QMHLMRTN) is less than the size of DtaQMsgTxt. If that is the case, we first set DtaQMsgTxt to *blanks in order to avoid having any residual data from a previously processed message appear in the current DtaQMsgTxt data structure. If the length of the returned message text is greater than the size of DtaQMsgTxt, then we truncate the message to the size of DtaQMsgTxt.
Two points of caution related to this truncation of the returned message text: the first is related to double-byte character set (DBCS) support. If your system is using translated DBCS national language versions, then it is possible that the truncation of the returned message text may cause a DBCS character string, imbedded in the message, to not be ended by a shift in control. In this situation, you should use the Truncate Character Data (QLGTRDTA) API to truncate the returned message to a properly formed string that is then stored in DtaQMsgTxt. Documentation for the QLGTRDTA API can be found here. The second area of concern is if your system is running in a visual bi-directional language environment. In this situation, you may want to use the Coded character set identifier parameter of the QMHRCVM API to request a conversion to a logical CCSID.
This wraps up our discussion of determining what message a given job is waiting on. Hopefully, this discussion will assist many of you in your day-to-day processing. Along the way, we've also looked at several APIs that could be used to enable a wide variety of applications, the monitoring of batch jobs being only one example. In most data processing endeavors, when it comes to solving your business problems, I believe it's more important to know what is available to you than to know a specific implementation.
Meanwhile, if you have other API questions, send them to me at
LATEST COMMENTS
MC Press Online