Skip to main content

TransactionManager Tutorial

Where to work

The code lives under tutorials/transaction-manager. Use the QMUX or LogonManager tutorials as a client — they connect to port 10000 by default.

# Terminal 1 — TransactionManager server
./gradlew :tutorials:transaction-manager:installApp
./tutorials/transaction-manager/build/install/transaction-manager/bin/q2

# Terminal 2 — any client tutorial (e.g. LogonManager)
./gradlew :tutorials:logon-manager:installApp
./tutorials/logon-manager/build/install/logon-manager/bin/q2

Why TransactionManager?

The QServer tutorial used a hand-written ISORequestListener to process messages. That works for simple cases, but production systems need:

  • Reliable response delivery — even if processing fails, the client must get a response.
  • Auditable steps — each processing stage is an identifiable participant with clear outcome.
  • Recoverability — crash recovery when using persistent space.
  • Observability — built-in TPS counters, per-participant timing metrics.

TransactionManager provides all of this through a two-phase commit protocol applied to a chain of TransactionParticipants.

Full stack with TransactionManager

IncomingListener is the bridge between the channel layer and the TransactionManager:

  1. QServer receives a message and calls IncomingListener.process(src, m).
  2. IncomingListener wraps src and m in a Context and puts it on the TM queue.
  3. TransactionManager picks up the Context and runs the participant chain.
  4. SendResponse reads the RESPONSE from the Context and sends it back through src.

QServer descriptor

The only change from the plain QServer tutorial is replacing the Handle2800 class with IncomingListener:

<qserver name="server" logger="Q2">
<attr name="port" type="java.lang.Integer">${server.port}</attr>
<channel class="org.jpos.iso.channel.XMLChannel" logger="Q2">
<property name="packager" value="org.jpos.iso.packager.XMLPackager"/>
</channel>

<request-listener class="org.jpos.iso.IncomingListener" logger="Q2" realm="incoming">
<property name="queue" value="TXN" />
<property name="timeout" value="30000" />
</request-listener>
</qserver>

IncomingListener puts the message on the TXN queue with a 30-second TTL. If the TransactionManager is overloaded and can't pick it up in time, the context expires rather than building an unbounded backlog.

TransactionManager descriptor

<txnmgr class="org.jpos.transaction.TransactionManager" logger="Q2" name="txnmgr">
<property name="queue" value="TXN" />
<property name="sessions" value="2" />
<property name="persistent-space" value="" />

<participant class="org.jpos.tutorial.HandleNMM"
logger="Q2" realm="handle-nmm" />

<participant class="org.jpos.transaction.participant.SendResponse"
logger="Q2" realm="send-response" />
</txnmgr>

Key properties

PropertyDefaultDescription
queueRequired. Space queue name. Must match IncomingListener's queue.
sessions1Number of concurrent worker threads. Each thread handles one transaction at a time.
max-sessions= sessionsUpper bound for auto-scaling. Workers are added up to this limit when load exceeds threshold.
thresholdsessions / 2When active sessions exceed this, a new worker is spawned (up to max-sessions).
persistent-space(TM name)Space used to journal transaction state for crash recovery. Empty string uses the default in-memory TSpace (no recovery).

The <participant> elements define the chain executed for every transaction. Participants are called in declaration order during prepare, and in reverse order during commit and abort. See Participants for the full protocol.

Deploy file ordering

30_txnmgr.xml   ← TransactionManager  (30_xxx per convention)
50_server.xml ← QServer (50_xxx per convention)

TM deploys before QServer so that the TXN queue is ready before any messages can arrive.

Build and run

./gradlew :tutorials:transaction-manager:installApp
./tutorials/transaction-manager/build/install/transaction-manager/bin/q2

With a LogonManager client connected, you'll see TM log output for each transaction:

<log realm="txnmgr" at="...">
<txn id="1" name="" rc="PREPARED" elapsed="3ms">
<handle-nmm>approve mti=2810 f70=001</handle-nmm>
</txn>
</log>

A declined message (unrecognised function code) produces:

<log realm="txnmgr" at="...">
<txn id="2" name="" rc="ABORTED" elapsed="2ms">
<handle-nmm>decline mti=2810 f70=999 (unknown function code)</handle-nmm>
</txn>
</log>

In both cases the client receives a response — the 0000 on commit, or the 9100 on abort — because SendResponse is an AbortParticipant. The next sections explain exactly how that works.