RPG programs reach a data queue by calling the data-queue APIs in QSYS - there is no F-spec device and no opcode for them. Triton RPG services three of those APIs directly in the runtime, so a CALL 'QSNDDTAQ' in migrated source works without a compiled program of that name anywhere on the classpath:
| API | Purpose |
|---|---|
QSNDDTAQ |
Send an entry to a data queue |
QRCVDTAQ |
Receive an entry, optionally waiting for one to arrive |
QCLRDTAQ |
Discard the entries on a data queue |
Both source formats are supported, because both arrive by the same dynamic call. A fixed-format CALL/PARM sequence:
C EVAL DQName = 'ORDERQ'
C EVAL DQLib = '*LIBL'
C EVAL DQLen = 10
C EVAL DQData = 'ALPHA'
C CALL 'QSNDDTAQ'
C PARM DQName
C PARM DQLib
C PARM DQLen
C PARM DQData
and a free-format prototyped call are equivalent:
dcl-pr QSNDDTAQ extpgm('QSNDDTAQ');
dqName char(10);
dqLib char(10);
dqLen packed(5:0);
dqData char(64) options(*varsize);
end-pr;
QSNDDTAQ('ORDERQ' : '*LIBL' : 10 : 'ALPHA');
Parameters are positional, exactly as they are for any other external program call: the names in your prototype are documentation, and only the order and type matter. A library qualifier on the program name ('QSYS/QSNDDTAQ') is accepted and ignored.
Parameters
The parameter groups match the IBM API definitions.
QSNDDTAQ - the required group is parameters 1-4; supply 6 parameters for a keyed queue.
| # | Parameter | Use | Type |
|---|---|---|---|
| 1 | Data queue name | Input | Char(10) |
| 2 | Library name | Input | Char(10) |
| 3 | Length of data | Input | Packed(5:0) |
| 4 | Data | Input | Char(*) |
| 5 | Length of key | Input | Packed(3:0) |
| 6 | Key data | Input | Char(*) |
QRCVDTAQ - the required group is parameters 1-5; supply 10 for a keyed queue.
| # | Parameter | Use | Type |
|---|---|---|---|
| 1 | Data queue name | Input | Char(10) |
| 2 | Library name | Input | Char(10) |
| 3 | Length of data | Output | Packed(5:0) |
| 4 | Data | Output | Char(*) |
| 5 | Wait time | Input | Packed(5:0) |
| 6 | Key order | Input | Char(2) |
| 7 | Length of key | Input | Packed(3:0) |
| 8 | Key data | I/O | Char(*) |
| 9 | Length of sender information | Input | Packed(3:0) |
| 10 | Sender information | Output | Char(*) |
QCLRDTAQ - the required group is parameters 1-2; supply 5 to clear by key.
| # | Parameter | Use | Type |
|---|---|---|---|
| 1 | Data queue name | Input | Char(10) |
| 2 | Library name | Input | Char(10) |
| 3 | Key order | Input | Char(2) |
| 4 | Length of key | Input | Packed(3:0) |
| 5 | Key data | Input | Char(*) |
Exactly the number of bytes named by the length parameter is sent - the rest of your data field is not part of the entry. On a receive, the entry's bytes overwrite the front of the data receiver and the rest of that field is left alone, and the received length tells you how many bytes arrived.
Waiting
The wait time on QRCVDTAQ behaves as it does on IBM i:
| Wait time | Behavior |
|---|---|
| Negative | Wait until an entry arrives |
| Zero | Return immediately |
| Positive | Wait that many seconds |
When the wait expires with nothing to receive, the length receiver comes back 0 and your data and key receivers are left exactly as you set them. Test the length, not the data, to detect a timeout.
Keyed queues
Set the key order to EQ, NE, LT, LE, GT or GE. The comparison decides which entries are eligible; among the eligible entries the one with the lowest key is dequeued, and arrival order only breaks ties between equal keys. So a queue holding keys K001, K002 and K003 answers a search key of K002 this way:
| Key order | Entry received |
|---|---|
EQ |
K002 |
GE |
K002 |
GT |
K003 |
LT |
K001 |
LE |
K001 |
NE |
K001 |
On a hit, the key parameter is replaced with the dequeued entry's key, so you can read which entry you got. On a timeout it keeps the search key you passed in.
Your key field is usually wider than the queue's key length; only the leading bytes up to the key length participate, blank-padded if your field is shorter.
Sender information
Parameters 9 and 10 of QRCVDTAQ report who sent the entry. That depends on how the queue was created: CRTDTAQ SENDERID defaults to *NO, and a queue that does not record senders returns no identity. Asking for it anyway is not an error - you get the header and nothing more, and your receiver keeps whatever it already held.
When the queue does record senders, the 44-byte receiver comes back laid out as IBM documents it:
| Bytes | Field |
|---|---|
| 1-8 | Binary header - bytes returned, then bytes available |
| 9-18 | Job name |
| 19-28 | User profile |
| 29-34 | Job number |
| 35-44 | Current user profile |
The user profile comes from the activation group's user, the same value the PSDS reports. The job name and number have no JVM equivalent to derive them from, so they are blank unless your host supplies a logical job identity - set alongside the user profile:
group.setJobName("ORDERSRV");
group.setUserProfile("APPUSER");
group.setJobNumber("123456");
Unset subfields are blank rather than invented, so a program reading them sees *BLANKS instead of a fabricated job.
To create a queue that records senders, define it up front with the last argument set:
provider.define("PRODLIB", "ORDERQ", 256, RpgDataQueue.Sequence.FIFO, 0, true);
Where queues live
On IBM i a data queue is an object in a library, created by CRTDTAQ before any program uses it, and shared by every job on the system. Triton RPG has no CL, so there is nothing to run CRTDTAQ:
- A queue is created the first time a program names it. It takes its attributes from that first reference - keyed with your key length if you passed a key, first-in-first-out otherwise, and a maximum entry length of 64512 bytes. Migrated source therefore needs no change.
- By default a queue is scoped to the activation group, the same reach a named data area has. Programs bound to one activation group exchange entries freely; a program with its own activation group gets its own queues. See "Reaching across jobs" below to give queues the reach they have on IBM i.
- The library name is not used to distinguish queues.
*LIBL,*CURLIBand a blank library resolve on the queue name alone. Naming a real library records it, and a later reference that names a different library is rejected rather than answered with the wrong queue.
Reaching across jobs
Inter-job messaging is the main reason data queues exist, and for that the in-memory default is not enough: a job only sees its own queues. Point the activation groups at a database instead - every job naming a queue is talking to the same one, and entries outlive the job that enqueued them:
DataSource queueStore = /* your DataSource */;
RpgActivationGroup group = new RpgActivationGroup();
group.setDataQueueProvider(new JdbcDataQueueProvider(queueStore));
program.setActivationGroup(group);
Each activation group gets its own provider; they meet in the database, so no state is shared inside the JVM. JdbcDataQueueProvider creates the two tables it needs (RPG_DATA_QUEUE and RPG_DATA_QUEUE_ENTRY) the first time it is used, so there is no schema step; pass your own table names to the three-argument constructor if you need them elsewhere.
Two properties are worth knowing:
- Queues are not under commitment control. The provider uses its own connections from the
DataSource, so aROLBKof your program's unit of work never un-sends an entry. Give it aDataSourcerather than the connection your files use. - A blocking receive polls, because a database cannot signal a waiting job. A blocking receive returns within roughly a tenth of a second of the entry arriving rather than instantly. Results are unaffected - only latency.
An entry still goes to exactly one receiver: several jobs can drain one queue concurrently and no entry is delivered twice.
Other stores, and non-default attributes
To give a queue attributes other than the inferred defaults - LIFO, a smaller maximum entry length - define it up front through whichever provider you are using:
queues.define("PRODLIB", "ORDERQ", 256, RpgDataQueue.Sequence.LIFO, 0);
To back queues with something else entirely - a message broker, an existing work-queue service - implement ltd.whitehorn.rpg.runtime.DataQueueProvider and inject that instead; the two methods are find and define. Handing one InMemoryDataQueueProvider to several activation groups also works when the jobs all live in one JVM and the entries need not survive it.
Reporting errors
QRCVDTAQ and QCLRDTAQ accept an optional trailing error-code structure, which selects how a failure is reported:
| Bytes provided | Behavior |
|---|---|
| Zero, or the parameter omitted | The failure is signalled as an escape: the CALL fails, and your program halts unless a MONITOR or a CALL error indicator traps it |
| Positive | The call returns normally and the message ID is left in the structure for you to test |
Test bytes available, not the message ID: a successful call leaves the structure exactly as you set it, so a zeroed structure comes back zeroed.
The message IDs are the ones IBM raises:
| Condition | Message ID |
|---|---|
| Queue not found in the library you named | CPF9801 |
| Keyed receive against a queue that is not keyed | CPF9502 |
| Key order not one of EQ, NE, LT, LE, GT, GE | CPF9504 |
QSNDDTAQ has no error-code parameter - its parameter 8 is the journal-entry flag, not an error structure - so a send failure always escapes. An entry longer than the queue's maximum is the common case.
Limits
- The asynchronous-request and journal-entry parameters of
QSNDDTAQare not read. Sends are already immediate. - The message text accompanying a reported error is not reproduced - the message ID is, but no substitution data follows it, so bytes available is always 16.