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:

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:

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