IBM MQ
Queues, messages, and syncpoint.
Part of the BankTS language reference.
IBM MQ
record Payment {
accountId: string<16>;
amount: decimal<15, 2>;
idempotencyKey: string<36>;
}
queue paymentOut manager "CSQ1" name "PAYMENT.OUT" output
record Payment status outReason;
queue paymentIn manager "CSQ1" name "PAYMENT.IN" input
record Payment status inReason;
connectQueue paymentIn;
getMessage paymentIn into payment {
putMessage paymentOut from payment;
} else {
log "NOTHING TO DO";
};
disconnectQueue paymentIn;
A queue is not a file, and nothing about it goes through file control. The
program connects to a queue manager, opens the queue as an object
described by an MQOD, and every operation is a CALL with a completion code and
a reason code coming back:
01 PAYMENT-OUT-MQOD.
COPY CMQODV.
...
CALL "MQOPEN" USING BANK-MQM-1-HCONN, MQOD OF PAYMENT-OUT-MQOD,
PAYMENT-OUT-OPTIONS, PAYMENT-OUT-HOBJ,
PAYMENT-OUT-COMPCODE, PAYMENT-OUT-REASON
The manager and queue names live on the declaration because they go into the
object descriptor once, and each is 48 characters — MQ_Q_MGR_NAME_LENGTH and
MQ_Q_NAME_LENGTH are both that, which is what MQOD-OBJECTNAME and MQCONN's
first parameter are declared as. A longer one is truncated into a name the queue
manager has never heard of (BANK-MQ-001).
connectQueue is MQCONN then MQOPEN, and disconnectQueue is MQCLOSE
then MQDISC. Neither half is useful alone: a connection with nothing open
does no work, an open object with no connection cannot exist. Emitting them as
one statement each removes the two orderings that are wrong and the ending that
leaves a handle behind.
The connection belongs to the queue manager, not to the queue. Two queues on
CSQ1 share one MQCONN, one handle and one MQDISC, and the handle is
declared once as BANK-MQM-1-HCONN rather than per queue. This is not a saving.
IBM's Application Programming Reference is explicit about the second MQCONN
naming a manager the program is already connected to: "the handle returned is
the same as that returned by the previous MQCONN call, but with completion code
MQCC_WARNING and reason code MQRC_ALREADY_CONNECTED". MQCC-WARNING is 1 and
the generated check tests NOT = MQCC-OK, so connecting per queue would end the
step with RC 12 on the second queue of a program that is, in fact, connected.
One MQDISC matches, for the same reason in reverse: the queue manager does not
count connections, so the second would be issued against a handle the first had
already invalidated.
Which of the two statements does it is decided at run time, by a count of the open queues on that manager:
IF BANK-MQM-1-OPENS = 0
MOVE "CSQ1" TO BANK-MQM-1-MGRNAME
CALL "MQCONN" USING BANK-MQM-1-MGRNAME, BANK-MQM-1-HCONN,
PAYMENT-IN-COMPCODE, PAYMENT-IN-REASON
...
END-IF
A connectQueue cannot know statically whether another has already run — it may
sit behind a condition or inside a loop — so the first one to run connects and
the last one to close disconnects, in whatever order the program does them.
MQRC-ALREADY-CONNECTED is also the one warning the generated check forgives.
A generated program no longer provokes it; a caller that connected before
linking to this one does, and IBM's guidance for that case is to "use the
connection handle returned in this situation as normal". Every other warning
stays a failure: a truncated or converted message is exactly what the check
exists to stop.
Every structure is copied into a group of its own. CMQODV declares
10 MQOD. with 15 MQOD-... beneath it, and the other copybooks do the same, so
a program with two queues has two of every one of those names and each reference
has to be qualified. IBM's own samples copy each structure once and reference it
bare, which is why the ambiguity does not show up there.
A get has three outcomes, not two. A message, an empty queue, and a failure:
EVALUATE TRUE
WHEN PAYMENT-IN-COMPCODE = MQCC-OK
...
WHEN PAYMENT-IN-REASON = MQRC-NO-MSG-AVAILABLE
...
WHEN OTHER
DISPLAY "MQGET FAILED paymentIn COMPCODE " ...
An empty queue is the ordinary end of a drain, not a failure — MQ reports it as reason 2033. Folding it in with the failures stops a batch every time it finishes its work; folding it in with success processes the message area again, still holding the last message read. That is why both branches are required.
A put goes under MQPMO-SYNCPOINT and a get under MQGMO-SYNCPOINT, so
neither is visible outside the unit of work until it commits: a payment released
before its ledger posting is committed is one the downstream system acts on and
the bank has not recorded. A get also asks MQGMO-NO-WAIT, because a batch
that blocks on an empty queue never ends. Both set MQMI-NONE and MQCI-NONE
in the message descriptor — on a put that asks the queue manager for a new
message identifier rather than reusing the last one, and on a get it means any
message will do.
What commits it, since no MQCMIT is emitted
Nothing in the generated program calls MQCMIT, and that is deliberate. IBM MQ
Application Programming Reference, MQDISC usage note 2a: where the unit of work
is coordinated by the queue manager, "the queue manager issues the MQCMIT call
on behalf of the application" when the connection ends normally. disconnectQueue
is an MQCLOSE and, on the last queue open on that manager, an MQDISC — so the
puts and gets above commit there.
Under CICS or IMS the unit of work belongs to the transaction manager rather than
to the queue manager, and MQCMIT is not merely unnecessary but rejected: the
program syncpoints through EXEC CICS SYNCPOINT or its IMS equivalent. An
unconditional MQCMIT would therefore be wrong on two of the three environments
this compiler targets.
The direction on the declaration decides which calls are allowed, because it is
what the MQOPEN asked for. Reading a queue opened MQOO-OUTPUT fails at run
time with reason 2037 and putting to one opened MQOO-INPUT-AS-Q-DEF fails with
2039, so both are refused when the program is compiled (BANK-MQ-002).
The status field is a reason code, not a two-character status like a file's
or a PCB's, so it is a number: inReason holds 2033 for an empty queue and 2085
for a queue that is not there. The field is required for the same reason a
file's status is (BANK-MQ-001).
What a queue costs on z/OS
MQ needs no precompiler — the MQI is plain CALLs — but the job needs three
things it would not otherwise have, and the generated JCL asks for all of them:
MQM.SCSQCOBC on SYSLIB so COPY CMQV and the rest resolve at compile time,
MQM.SCSQLOAD on SYSLIB at link time so MQCONN and the others resolve to the
stub, and MQM.SCSQANLE with MQM.SCSQLOAD on STEPLIB so the stub reaches MQ
at run time.
Nothing outside z/OS supplies the MQI, so the local build cannot link a queue
program at all. runtime/BANKMQ.cbl stands in: the precompiler replaces the MQ
copybooks with a local declaration of the fields the compiler sets, and the stub
answers each call with the completion and reason codes IBM documents, holding
one message between a put and a get. It is not IBM MQ — no queue manager, no
persistence, no syncpoint, no channel. What running against it proves is that
the call sequence is one MQ accepts, that every operand resolves and is the
right type, and that all three outcomes of a get are reachable. What ships to
z/OS keeps its COPY CMQV and its calls exactly as written.