Phase 1: 실동작 Enduro/X + PostgreSQL XA 매입 슬라이스
- Enduro/X 7.0.12 소스빌드 이미지(-DENABLE_POSTGRES=ON, libndrxxaecpg XA 스위치) - docker-compose: postgres(max_prepared_transactions=100) + endurox app - ECPG XA 서비스 3종: ACQUIRE→RECONCILE→SETTLE (tpservice + EXEC SQL) - 클라이언트 tpbegin/tpcall체인/tpcommit → tmsrv 2PC(prepare/commit) 원자 커밋 - 검증: 서비스 AVAIL, 커밋 row(status=S) psql 확인, 롤백 시 무잔존, pg_prepared_xacts=0 - 규명된 런타임 요건: sysctl fs.mqueue.*, ulimit msgqueue/nofile, XA open-str JSON - docs/architecture.md, README, service-catalog 설계 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
e6e29e6b5d
19 changed files with 1005 additions and 0 deletions
86
app/src/acqdrv.c
Normal file
86
app/src/acqdrv.c
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* acqdrv - acquiring driver client.
|
||||
*
|
||||
* Starts ONE global transaction (tpbegin), fires the ACQUIRE -> RECONCILE ->
|
||||
* SETTLE chain via a single tpcall("ACQUIRE"), then tpcommit (or tpabort when
|
||||
* the 4th arg is "abort", to prove XA rollback).
|
||||
*
|
||||
* Usage: acqdrv [merchant] [amount] [bizdate YYYY-MM-DD] [abort]
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include "acq.fd.h"
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *merch = (argc > 1) ? argv[1] : "M0001";
|
||||
long amount = (argc > 2) ? atol(argv[2]) : 1000000; /* 10,000.00 */
|
||||
const char *bizdate = (argc > 3) ? argv[3] : "2026-07-19";
|
||||
int do_abort = (argc > 4 && strcmp(argv[4], "abort") == 0);
|
||||
|
||||
UBFH *b;
|
||||
long rlen = 0;
|
||||
long pid = 0, fee = 0, net = 0, sid = 0;
|
||||
char st[16] = "";
|
||||
BFLDLEN l;
|
||||
|
||||
if (tpinit(NULL) < 0) {
|
||||
fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno));
|
||||
return 1;
|
||||
}
|
||||
if (tpopen() < 0) {
|
||||
fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
b = (UBFH *)tpalloc("UBF", NULL, 4096);
|
||||
if (b == NULL) {
|
||||
fprintf(stderr, "tpalloc FAIL: %s\n", tpstrerror(tperrno));
|
||||
return 1;
|
||||
}
|
||||
Bchg(b, T_MERCHANT, 0, (char *)merch, 0L);
|
||||
Bchg(b, T_AMOUNT, 0, (char *)&amount, 0L);
|
||||
Bchg(b, T_BIZDATE, 0, (char *)bizdate, 0L);
|
||||
|
||||
if (tpbegin(60, 0) < 0) {
|
||||
fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (tpcall("ACQUIRE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) {
|
||||
fprintf(stderr, "tpcall(ACQUIRE) FAIL: %s\n", tpstrerror(tperrno));
|
||||
tpabort(0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Bget(b, T_PURCHASE_ID, 0, (char *)&pid, 0L);
|
||||
Bget(b, T_FEE, 0, (char *)&fee, 0L);
|
||||
Bget(b, T_NET, 0, (char *)&net, 0L);
|
||||
Bget(b, T_SETTLE_ID, 0, (char *)&sid, 0L);
|
||||
l = sizeof(st); Bget(b, T_STATUS, 0, st, &l);
|
||||
|
||||
if (do_abort) {
|
||||
if (tpabort(0) < 0) {
|
||||
fprintf(stderr, "tpabort FAIL: %s\n", tpstrerror(tperrno));
|
||||
return 1;
|
||||
}
|
||||
printf(">>> ROLLBACK: global tx aborted. purchase_id=%ld should NOT persist "
|
||||
"(merch=%s amount=%ld)\n", pid, merch, amount);
|
||||
} else {
|
||||
if (tpcommit(0) < 0) {
|
||||
fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno));
|
||||
tpabort(0);
|
||||
return 1;
|
||||
}
|
||||
printf(">>> COMMIT OK: purchase_id=%ld merch=%s amount=%ld fee=%ld net=%ld "
|
||||
"settlement_id=%ld status=%s\n", pid, merch, amount, fee, net, sid, st);
|
||||
}
|
||||
|
||||
tpfree((char *)b);
|
||||
tpclose();
|
||||
tpterm();
|
||||
return 0;
|
||||
}
|
||||
142
app/src/acquire.pgc
Normal file
142
app/src/acquire.pgc
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* ACQUIRE (매입 접수) - ECPG XA service.
|
||||
*
|
||||
* Inserts a `purchase` row (fee = amount * 25/10000 MDR, net = amount - fee),
|
||||
* then within the SAME global transaction tpcall()s RECONCILE.
|
||||
*
|
||||
* XA connection is opened by the ECPG XA switch (libndrxxaecpg.so) via tpopen();
|
||||
* there is deliberately NO `EXEC SQL CONNECT` (matches ref/atmisv67.pgc).
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include <userlog.h>
|
||||
#include <ndebug.h>
|
||||
#include "acq.fd.h"
|
||||
|
||||
void ACQUIRE(TPSVCINFO *p);
|
||||
|
||||
int tpsvrinit(int argc, char **argv)
|
||||
{
|
||||
if (tpopen() < 0) {
|
||||
userlog("acquire: tpopen FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
if (tpadvertise("ACQUIRE", ACQUIRE) < 0) {
|
||||
userlog("acquire: tpadvertise(ACQUIRE) FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
userlog("acquire: ACQUIRE advertised, RM opened");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tpsvrdone(void)
|
||||
{
|
||||
tpclose();
|
||||
userlog("acquire: tpsvrdone");
|
||||
}
|
||||
|
||||
void ACQUIRE(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
long amount = 0, fee = 0, net = 0;
|
||||
char merch[64] = "";
|
||||
char bizdate[16] = "";
|
||||
BFLDLEN len;
|
||||
long rlen = 0;
|
||||
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid;
|
||||
long h_amount;
|
||||
long h_fee;
|
||||
long h_net;
|
||||
char h_merch[64];
|
||||
char h_bizdate[16];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
len = sizeof(merch);
|
||||
if (Bget(b, T_MERCHANT, 0, merch, &len) < 0) {
|
||||
userlog("ACQUIRE: missing T_MERCHANT: %s", Bstrerror(Berror));
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
Bget(b, T_AMOUNT, 0, (char *)&amount, 0L);
|
||||
len = sizeof(bizdate);
|
||||
Bget(b, T_BIZDATE, 0, bizdate, &len);
|
||||
|
||||
fee = amount * 25 / 10000; /* 0.25% MDR */
|
||||
net = amount - fee;
|
||||
|
||||
/* obtain a purchase id (sequence is non-transactional by design) */
|
||||
EXEC SQL SELECT nextval('purchase_seq') INTO :h_pid;
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("ACQUIRE: nextval FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
h_amount = amount;
|
||||
h_fee = fee;
|
||||
h_net = net;
|
||||
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch) - 1] = 0;
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate) - 1] = 0;
|
||||
|
||||
EXEC SQL INSERT INTO purchase
|
||||
(purchase_id, merchant_id, amount, fee, net, status, biz_date)
|
||||
VALUES (:h_pid, :h_merch, :h_amount, :h_fee, :h_net, 'A', :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("ACQUIRE: INSERT purchase FAIL [%d] %s",
|
||||
sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
/* record the approval as well (all under the same global tx) */
|
||||
EXEC SQL INSERT INTO approval (approval_id, purchase_id, approved)
|
||||
VALUES (nextval('approval_seq'), :h_pid, true);
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("ACQUIRE: INSERT approval FAIL [%d] %s",
|
||||
sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
Bchg(b, T_PURCHASE_ID, 0, (char *)&h_pid, 0L);
|
||||
Bchg(b, T_FEE, 0, (char *)&fee, 0L);
|
||||
Bchg(b, T_NET, 0, (char *)&net, 0L);
|
||||
Bchg(b, T_STATUS, 0, "A", 0L);
|
||||
|
||||
userlog("ACQUIRE pid=%ld merch=%s amount=%ld fee=%ld net=%ld -> RECONCILE",
|
||||
h_pid, merch, amount, fee, net);
|
||||
|
||||
/* Continue the chain under the SAME global transaction. RECONCILE (and, in
|
||||
* turn, SETTLE) run as separate XA branches (separate DB connections) and
|
||||
* therefore CANNOT see this not-yet-committed purchase row - so they write
|
||||
* only their OWN rows (ledger/settlement). This owner branch performs the
|
||||
* purchase status transition itself once the chain returns, because its
|
||||
* connection is resumed on the same branch and can see its own INSERT. */
|
||||
if (tpcall("RECONCILE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) {
|
||||
userlog("ACQUIRE: tpcall(RECONCILE) FAIL: %s", tpstrerror(tperrno));
|
||||
tpreturn(TPFAIL, 0, (char *)b, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
/* branch A resumed here: settle-complete the purchase we own */
|
||||
EXEC SQL UPDATE purchase
|
||||
SET status = 'S', updated_at = now()
|
||||
WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
|
||||
userlog("ACQUIRE: UPDATE purchase status FAIL [%d] %s",
|
||||
sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, (char *)b, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
Bchg(b, T_STATUS, 0, "S", 0L);
|
||||
|
||||
tpreturn(TPSUCCESS, 0, (char *)b, 0L, 0L);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
88
app/src/reconcile.pgc
Normal file
88
app/src/reconcile.pgc
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* RECONCILE (대사) - ECPG XA service.
|
||||
*
|
||||
* Marks the purchase matched (status='M'), writes a ledger recon entry, then
|
||||
* tpcall()s SETTLE - all inside the caller's global transaction.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include <userlog.h>
|
||||
#include <ndebug.h>
|
||||
#include "acq.fd.h"
|
||||
|
||||
void RECONCILE(TPSVCINFO *p);
|
||||
|
||||
int tpsvrinit(int argc, char **argv)
|
||||
{
|
||||
if (tpopen() < 0) {
|
||||
userlog("reconcile: tpopen FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
if (tpadvertise("RECONCILE", RECONCILE) < 0) {
|
||||
userlog("reconcile: tpadvertise(RECONCILE) FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
userlog("reconcile: RECONCILE advertised, RM opened");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tpsvrdone(void)
|
||||
{
|
||||
tpclose();
|
||||
userlog("reconcile: tpsvrdone");
|
||||
}
|
||||
|
||||
void RECONCILE(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
long net = 0;
|
||||
char bizdate[16] = "";
|
||||
BFLDLEN len;
|
||||
long rlen = 0;
|
||||
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid;
|
||||
long h_net;
|
||||
char h_bizdate[16];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
if (Bget(b, T_PURCHASE_ID, 0, (char *)&h_pid, 0L) < 0) {
|
||||
userlog("RECONCILE: missing T_PURCHASE_ID: %s", Bstrerror(Berror));
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
Bget(b, T_NET, 0, (char *)&net, 0L);
|
||||
len = sizeof(bizdate);
|
||||
Bget(b, T_BIZDATE, 0, bizdate, &len);
|
||||
h_net = net;
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate) - 1] = 0;
|
||||
|
||||
/* This is a distinct XA branch (own DB connection); it writes only its own
|
||||
* recon evidence row and does NOT touch the purchase row (which belongs to
|
||||
* the ACQUIRE branch and is not yet committed / not visible here). */
|
||||
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
|
||||
VALUES (nextval('ledger_seq'), :h_pid, 'RECON', :h_net, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("RECONCILE: INSERT ledger FAIL [%d] %s",
|
||||
sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
Bchg(b, T_STATUS, 0, "M", 0L);
|
||||
|
||||
userlog("RECONCILE pid=%ld matched -> SETTLE", h_pid);
|
||||
|
||||
if (tpcall("SETTLE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) {
|
||||
userlog("RECONCILE: tpcall(SETTLE) FAIL: %s", tpstrerror(tperrno));
|
||||
tpreturn(TPFAIL, 0, (char *)b, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
tpreturn(TPSUCCESS, 0, (char *)b, 0L, 0L);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
107
app/src/settle.pgc
Normal file
107
app/src/settle.pgc
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* SETTLE (정산) - ECPG XA service.
|
||||
*
|
||||
* Inserts a `settlement` (netting) row, writes a ledger settle entry, and marks
|
||||
* the purchase settled (status='S'). Terminal service of the chain; still runs
|
||||
* inside the caller's global transaction.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include <userlog.h>
|
||||
#include <ndebug.h>
|
||||
#include "acq.fd.h"
|
||||
|
||||
void SETTLE(TPSVCINFO *p);
|
||||
|
||||
int tpsvrinit(int argc, char **argv)
|
||||
{
|
||||
if (tpopen() < 0) {
|
||||
userlog("settle: tpopen FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
if (tpadvertise("SETTLE", SETTLE) < 0) {
|
||||
userlog("settle: tpadvertise(SETTLE) FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
userlog("settle: SETTLE advertised, RM opened");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tpsvrdone(void)
|
||||
{
|
||||
tpclose();
|
||||
userlog("settle: tpsvrdone");
|
||||
}
|
||||
|
||||
void SETTLE(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
long net = 0;
|
||||
char merch[64] = "";
|
||||
char bizdate[16] = "";
|
||||
BFLDLEN len;
|
||||
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid;
|
||||
long h_sid;
|
||||
long h_net;
|
||||
char h_merch[64];
|
||||
char h_bizdate[16];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
if (Bget(b, T_PURCHASE_ID, 0, (char *)&h_pid, 0L) < 0) {
|
||||
userlog("SETTLE: missing T_PURCHASE_ID: %s", Bstrerror(Berror));
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
Bget(b, T_NET, 0, (char *)&net, 0L);
|
||||
len = sizeof(merch);
|
||||
Bget(b, T_MERCHANT, 0, merch, &len);
|
||||
len = sizeof(bizdate);
|
||||
Bget(b, T_BIZDATE, 0, bizdate, &len);
|
||||
|
||||
h_net = net;
|
||||
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch) - 1] = 0;
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate) - 1] = 0;
|
||||
|
||||
EXEC SQL SELECT nextval('settlement_seq') INTO :h_sid;
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("SETTLE: nextval FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
EXEC SQL INSERT INTO settlement
|
||||
(settlement_id, purchase_id, merchant_id, net, biz_date, status)
|
||||
VALUES (:h_sid, :h_pid, :h_merch, :h_net, :h_bizdate, 'SETTLED');
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("SETTLE: INSERT settlement FAIL [%d] %s",
|
||||
sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
/* SETTLE is its own XA branch: it writes the settlement + a settle ledger
|
||||
* entry (its own rows). The purchase status transition is performed by the
|
||||
* ACQUIRE branch that owns that row. */
|
||||
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
|
||||
VALUES (nextval('ledger_seq'), :h_pid, 'SETTLE', :h_net, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("SETTLE: INSERT ledger FAIL [%d] %s",
|
||||
sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
tpreturn(TPFAIL, 0, p->data, 0L, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
Bchg(b, T_SETTLE_ID, 0, (char *)&h_sid, 0L);
|
||||
Bchg(b, T_STATUS, 0, "S", 0L);
|
||||
|
||||
userlog("SETTLE pid=%ld settlement_id=%ld net=%ld status=S", h_pid, h_sid, net);
|
||||
|
||||
tpreturn(TPSUCCESS, 0, (char *)b, 0L, 0L);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
Loading…
Add table
Add a link
Reference in a new issue