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
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# build artifacts
|
||||
*.o
|
||||
*.so
|
||||
build/
|
||||
bin/acq*
|
||||
# ecpg-generated C from .pgc (regenerated at build)
|
||||
**/gen/*.c
|
||||
# Enduro/X runtime state
|
||||
ULOG.*
|
||||
log/
|
||||
qdata/
|
||||
tmlogs/
|
||||
tmp/
|
||||
*.pid
|
||||
# ref (upstream Enduro/X test sample, vendored for guidance only)
|
||||
ref/
|
||||
# ecpg-generated C (sibling of *.pgc)
|
||||
app/src/acquire.c
|
||||
app/src/reconcile.c
|
||||
app/src/settle.c
|
||||
app/bin/
|
||||
app/conf/setapp.sh
|
||||
56
README.md
Normal file
56
README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# acquire-core-x — 카드 매입·정산 시스템 (실동작 Enduro/X)
|
||||
|
||||
실제 **Enduro/X**(오픈소스 Tuxedo/ATMI TP 모니터) + **ECPG/PostgreSQL** 위에서
|
||||
**실제로 기동·트랜잭션 처리되는** 카드 매입·정산 레거시 시스템. 포지(Klaro Forge)
|
||||
자율 마이그레이션 도구의 대형 전환 대상 픽스처 — 이 C/ATMI 시스템을 Java Spring Boot로
|
||||
전환한다.
|
||||
|
||||
> 앞서의 `acquire-core-full`(자체 TxCore 셔임, 컴파일만 되는 픽스처)과 달리, 이 프로젝트는
|
||||
> **진짜 TP 미들웨어에서 tpcall·XA 트랜잭션이 실제로 동작**한다. (사용자 요구 = B방식)
|
||||
|
||||
## 스택
|
||||
|
||||
| 계층 | 실체 |
|
||||
|---|---|
|
||||
| TP 모니터 | Enduro/X 7.0.12 (오픈 Tuxedo/ATMI: `tpservice`/`tpcall`/`tpadvertise`/`tpbegin`) — 소스빌드 |
|
||||
| 전문 버퍼 | UBF (≈FML32): `Bget`/`Bchg`, 필드테이블 `mkfldhdr` |
|
||||
| DB 접근 | ECPG (`EXEC SQL`) → PostgreSQL 15 |
|
||||
| 분산 트랜잭션 | XA 2PC — `libndrxxaecpg.so` ECPG XA 스위치, `tmsrv`가 조율 |
|
||||
| 오케스트레이션 | `ndrxd` + `ndrxconfig.xml` + `app.ini`(CCONFIG) |
|
||||
|
||||
## 실행 (docker-compose)
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yml up -d --build # postgres + endurox app
|
||||
```
|
||||
|
||||
### 필수 런타임 요건 (없으면 ndrxd 부팅 실패 — 실측으로 규명됨)
|
||||
POSIX 큐/세마포어 한도 때문에 app 컨테이너에 반드시 필요:
|
||||
```yaml
|
||||
sysctls:
|
||||
fs.mqueue.msg_max: "512"
|
||||
fs.mqueue.msgsize_max: "65536"
|
||||
ulimits:
|
||||
msgqueue: 536870912 # 큐 1개 = msg_max×msgsize_max ≈ 5.6MB, 기본 RLIMIT_MSGQUEUE(800KB) 초과
|
||||
nofile: 65536
|
||||
```
|
||||
PostgreSQL는 XA(2단계 커밋) 위해 `-c max_prepared_transactions=100`.
|
||||
|
||||
## 구성
|
||||
|
||||
- `docker/` — `endurox.Dockerfile`(Enduro/X 소스빌드, `-DENABLE_POSTGRES=ON`), `docker-compose.yml`
|
||||
- `db/` — `schema.sql`(merchant/purchase/approval/settlement/ledger …)
|
||||
- `app/` — `conf/`(ndrxconfig.xml, app.ini, 환경), `ubftab/`(UBF 필드테이블),
|
||||
서비스 `.pgc`/`.c`, `entrypoint.sh`
|
||||
- `docs/` — `architecture.md`, `service-catalog.md`
|
||||
- `ref/` — Enduro/X upstream PostgreSQL 통합테스트(참고용, .gitignore)
|
||||
- `slice/` — 최초 검증용 non-DB ATMI 슬라이스(부팅+tpcall 증명 완료)
|
||||
|
||||
## 검증 상태
|
||||
|
||||
- [x] Enduro/X 소스빌드 (PostgreSQL XA 포함) → 이미지 `acquire-x/endurox:7.0.12`
|
||||
- [x] `ndrxd` 부팅 (10 프로세스 runok)
|
||||
- [x] 커스텀 `tpservice` advertise + **실제 `tpcall` 왕복** (ACQUIRE: fee=2500 net=997500)
|
||||
- [x] ECPG XA 서비스 체인(매입→대사→정산) + 커밋 row psql 검증 + 롤백 검증 ✅
|
||||
- [ ] 2000본+ 확장 (Phase 2)
|
||||
- [ ] 포지 마이그레이션 → Spring Boot (Phase 3)
|
||||
31
app/conf/app.ini
Normal file
31
app/conf/app.ini
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[@global]
|
||||
NDRX_CLUSTERISED=1
|
||||
NDRX_CMDWAIT=1
|
||||
NDRX_CONFIG=${NDRX_APPHOME}/conf/ndrxconfig.xml
|
||||
NDRX_DMNLOG=${NDRX_APPHOME}/log/ndrxd.log
|
||||
NDRX_DPID=${NDRX_APPHOME}/tmp/ndrxd.pid
|
||||
NDRX_DQMAX=100
|
||||
NDRX_IPCKEY=44100
|
||||
NDRX_LDBAL=0
|
||||
NDRX_LEV=5
|
||||
NDRX_LOG=${NDRX_APPHOME}/log/xadmin.log
|
||||
NDRX_MSGMAX=100
|
||||
NDRX_MSGSIZEMAX=56000
|
||||
NDRX_NODEID=1
|
||||
NDRX_QPATH=/dev/mqueue
|
||||
NDRX_QPREFIX=/acq
|
||||
NDRX_RNDK=0myWI5nu
|
||||
NDRX_SRVMAX=10000
|
||||
NDRX_SVCMAX=20000
|
||||
NDRX_TOUT=90
|
||||
NDRX_UBFMAXFLDS=16000
|
||||
NDRX_LIBEXT=so
|
||||
NDRX_ULOG=${NDRX_APPHOME}/log
|
||||
FIELDTBLS=acq.fd
|
||||
FLDTBLDIR=${NDRX_APPHOME}/ubftab
|
||||
|
||||
[@debug]
|
||||
; * - goes for all binaries not listed below
|
||||
*= ndrx=5 ubf=1 tp=5 file=
|
||||
xadmin= ndrx=5 ubf=1 tp=5 file=${NDRX_APPHOME}/log/xadmin.log
|
||||
ndrxd= ndrx=5 ubf=1 tp=5 file=${NDRX_APPHOME}/log/ndrxd.log
|
||||
55
app/conf/ndrxconfig.xml
Normal file
55
app/conf/ndrxconfig.xml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" ?>
|
||||
<endurox>
|
||||
<appconfig>
|
||||
<sanity>1</sanity>
|
||||
<brrefresh>5</brrefresh>
|
||||
<restart_min>1</restart_min>
|
||||
<restart_step>1</restart_step>
|
||||
<restart_max>5</restart_max>
|
||||
<restart_to_check>20</restart_to_check>
|
||||
<gather_pq_stats>Y</gather_pq_stats>
|
||||
</appconfig>
|
||||
<defaults>
|
||||
<min>1</min>
|
||||
<max>1</max>
|
||||
<autokill>1</autokill>
|
||||
<start_max>20</start_max>
|
||||
<pingtime>100</pingtime>
|
||||
<ping_max>800</ping_max>
|
||||
<end_max>30</end_max>
|
||||
<killtime>1</killtime>
|
||||
</defaults>
|
||||
<servers>
|
||||
<!-- XA transaction manager for RM1 = the ECPG/PostgreSQL resource.
|
||||
XA env (NDRX_XA_*) is inherited from the shell (see setapp.sh). -->
|
||||
<server name="tmsrv">
|
||||
<srvid>40</srvid>
|
||||
<min>1</min>
|
||||
<max>1</max>
|
||||
<sysopt>-e ${NDRX_APPHOME}/log/tmsrv-rm1.log -r -- -t1 -l${NDRX_APPHOME}/tmlogs/rm1 -m10</sysopt>
|
||||
</server>
|
||||
|
||||
<!-- The three ECPG XA services. Separate binaries so the in-chain
|
||||
tpcall()s (ACQUIRE->RECONCILE->SETTLE) hit distinct server
|
||||
processes (avoids single-server self-call deadlock) while sharing
|
||||
one global transaction across their DB branches. -->
|
||||
<server name="acqacquire">
|
||||
<srvid>100</srvid>
|
||||
<min>1</min>
|
||||
<max>1</max>
|
||||
<sysopt>-e ${NDRX_APPHOME}/log/acqacquire.log -r</sysopt>
|
||||
</server>
|
||||
<server name="acqreconcile">
|
||||
<srvid>110</srvid>
|
||||
<min>1</min>
|
||||
<max>1</max>
|
||||
<sysopt>-e ${NDRX_APPHOME}/log/acqreconcile.log -r</sysopt>
|
||||
</server>
|
||||
<server name="acqsettle">
|
||||
<srvid>120</srvid>
|
||||
<min>1</min>
|
||||
<max>1</max>
|
||||
<sysopt>-e ${NDRX_APPHOME}/log/acqsettle.log -r</sysopt>
|
||||
</server>
|
||||
</servers>
|
||||
</endurox>
|
||||
75
app/entrypoint.sh
Executable file
75
app/entrypoint.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/bin/bash
|
||||
##
|
||||
## Build + boot the Enduro/X acquiring slice, then stay alive.
|
||||
## Runs inside the app container (image acquire-x/endurox:7.0.12).
|
||||
##
|
||||
set -e
|
||||
|
||||
. /app/conf/setapp.sh
|
||||
|
||||
echo "=================================================================="
|
||||
echo " acquire-core-x : build + boot"
|
||||
echo "=================================================================="
|
||||
|
||||
# runtime dirs
|
||||
mkdir -p /app/log /app/tmp /app/tmlogs/rm1 /app/bin
|
||||
rm -f /app/log/*.log 2>/dev/null || true
|
||||
|
||||
# ensure posix mqueue is available (compose provides the sysctls/ulimits)
|
||||
if [ ! -d /dev/mqueue ]; then
|
||||
mkdir -p /dev/mqueue
|
||||
mount -t mqueue none /dev/mqueue 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. UBF field header from the field table
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "--- mkfldhdr acq.fd ---"
|
||||
cd /app/ubftab
|
||||
FIELDTBLS=acq.fd FLDTBLDIR=/app/ubftab mkfldhdr acq.fd
|
||||
ls -l /app/ubftab/acq.fd.h
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. ECPG precompile + buildserver each .pgc (XA switch loaded via env, so
|
||||
# buildserver needs no -r; we just link ecpg + pq). buildclient the driver.
|
||||
# ---------------------------------------------------------------------------
|
||||
export CFLAGS="-I/app/ubftab -I/app/src -I$(pg_config --includedir)"
|
||||
PGLIB="-L$(pg_config --libdir) -lecpg -lpq"
|
||||
|
||||
cd /app/src
|
||||
build_svc () {
|
||||
local src="$1" bin="$2"
|
||||
echo "--- ecpg $src ---"
|
||||
ecpg -I/app/ubftab -o "/app/src/${src%.pgc}.c" "/app/src/$src"
|
||||
echo "--- buildserver -> $bin ---"
|
||||
buildserver -o "/app/bin/$bin" -f "/app/src/${src%.pgc}.c" -a "$PGLIB"
|
||||
}
|
||||
|
||||
build_svc acquire.pgc acqacquire
|
||||
build_svc reconcile.pgc acqreconcile
|
||||
build_svc settle.pgc acqsettle
|
||||
|
||||
echo "--- buildclient -> acqdrv ---"
|
||||
buildclient -o /app/bin/acqdrv -f /app/src/acqdrv.c
|
||||
|
||||
ls -l /app/bin
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Boot Enduro/X
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "--- xadmin down/start ---"
|
||||
xadmin down -y 2>/dev/null || true
|
||||
xadmin start -y
|
||||
|
||||
echo "--- xadmin psc (advertised services) ---"
|
||||
xadmin psc
|
||||
echo "--- xadmin ppm (processes) ---"
|
||||
xadmin ppm
|
||||
|
||||
echo "=================================================================="
|
||||
echo " acquire-core-x : UP. Run the driver with:"
|
||||
echo " docker compose -f docker/docker-compose.yml exec app /app/run-driver.sh"
|
||||
echo "=================================================================="
|
||||
|
||||
# keep the container (and the ndrxd daemon) alive
|
||||
tail -F /app/log/ndrxd.log
|
||||
16
app/run-driver.sh
Executable file
16
app/run-driver.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
## Run the acquiring driver under a real global XA transaction.
|
||||
## ./run-driver.sh -> one COMMIT run + one ROLLBACK run (demo)
|
||||
## ./run-driver.sh <merch> <amount> <bizdate> [abort] -> single custom run
|
||||
. /app/conf/setapp.sh
|
||||
|
||||
if [ "$#" -ge 1 ]; then
|
||||
exec /app/bin/acqdrv "$@"
|
||||
fi
|
||||
|
||||
echo "===== COMMIT run (ACQUIRE -> RECONCILE -> SETTLE, tpcommit) ====="
|
||||
/app/bin/acqdrv M0001 1000000 2026-07-19
|
||||
|
||||
echo
|
||||
echo "===== ROLLBACK run (same chain, tpabort) ====="
|
||||
/app/bin/acqdrv M0002 500000 2026-07-19 abort
|
||||
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: */
|
||||
13
app/ubftab/acq.fd
Normal file
13
app/ubftab/acq.fd
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
$/* @(#) Acquiring (카드 매입) UBF field table */
|
||||
*base 6000
|
||||
|
||||
# name id type flag comment
|
||||
T_MERCHANT 1 string - -
|
||||
T_AMOUNT 2 long - -
|
||||
T_FEE 3 long - -
|
||||
T_NET 4 long - -
|
||||
T_STATUS 5 string - -
|
||||
T_PURCHASE_ID 6 long - -
|
||||
T_BIZDATE 7 string - -
|
||||
T_SETTLE_ID 8 long - -
|
||||
T_MSG 9 string - -
|
||||
12
app/ubftab/acq.fd.h
Normal file
12
app/ubftab/acq.fd.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* @(#) Acquiring (카드 매입) UBF field table */
|
||||
/* fname bfldid */
|
||||
/* ----- ----- */
|
||||
#define T_MERCHANT ((BFLDID32)167778161) /* number: 6001 type: string */
|
||||
#define T_AMOUNT ((BFLDID32)33560434) /* number: 6002 type: long */
|
||||
#define T_FEE ((BFLDID32)33560435) /* number: 6003 type: long */
|
||||
#define T_NET ((BFLDID32)33560436) /* number: 6004 type: long */
|
||||
#define T_STATUS ((BFLDID32)167778165) /* number: 6005 type: string */
|
||||
#define T_PURCHASE_ID ((BFLDID32)33560438) /* number: 6006 type: long */
|
||||
#define T_BIZDATE ((BFLDID32)167778167) /* number: 6007 type: string */
|
||||
#define T_SETTLE_ID ((BFLDID32)33560440) /* number: 6008 type: long */
|
||||
#define T_MSG ((BFLDID32)167778169) /* number: 6009 type: string */
|
||||
74
db/schema.sql
Normal file
74
db/schema.sql
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
-- Card-acquiring vertical-slice schema (카드 매입/승인/정산/원장)
|
||||
-- Loaded into PostgreSQL on first boot (docker-entrypoint-initdb.d).
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 가맹점 (merchant)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS merchant (
|
||||
merchant_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
mdr_bps INTEGER NOT NULL DEFAULT 25, -- Merchant Discount Rate, basis points (0.25%)
|
||||
status TEXT NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 매입 (purchase) - the acquiring transaction
|
||||
-- purchase_id driven by an explicit sequence so ECPG can SELECT nextval first.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS purchase_seq;
|
||||
CREATE TABLE IF NOT EXISTS purchase (
|
||||
purchase_id BIGINT PRIMARY KEY,
|
||||
merchant_id TEXT NOT NULL,
|
||||
amount BIGINT NOT NULL, -- minor units (원, 정수)
|
||||
fee BIGINT NOT NULL, -- MDR fee
|
||||
net BIGINT NOT NULL, -- amount - fee
|
||||
status TEXT NOT NULL, -- A=접수, M=대사완료(matched), S=정산완료(settled)
|
||||
biz_date DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 승인 (approval)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS approval_seq;
|
||||
CREATE TABLE IF NOT EXISTS approval (
|
||||
approval_id BIGINT PRIMARY KEY,
|
||||
purchase_id BIGINT NOT NULL,
|
||||
approved BOOLEAN NOT NULL DEFAULT true,
|
||||
approved_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 원장 (ledger) - double-entry style recon check written during 대사
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS ledger_seq;
|
||||
CREATE TABLE IF NOT EXISTS ledger (
|
||||
ledger_id BIGINT PRIMARY KEY,
|
||||
purchase_id BIGINT NOT NULL,
|
||||
entry_type TEXT NOT NULL, -- RECON / SETTLE
|
||||
amount BIGINT NOT NULL,
|
||||
biz_date DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 정산 (settlement) - netting written during 정산
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS settlement_seq;
|
||||
CREATE TABLE IF NOT EXISTS settlement (
|
||||
settlement_id BIGINT PRIMARY KEY,
|
||||
purchase_id BIGINT NOT NULL,
|
||||
merchant_id TEXT NOT NULL,
|
||||
net BIGINT NOT NULL,
|
||||
biz_date DATE NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'SETTLED',
|
||||
settled_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Seed a couple of merchants so ACQUIRE has a valid counterparty.
|
||||
INSERT INTO merchant (merchant_id, name, mdr_bps) VALUES
|
||||
('M0001', '클라로 커피', 25),
|
||||
('M0002', '포지 마트', 25)
|
||||
ON CONFLICT (merchant_id) DO NOTHING;
|
||||
43
docker/docker-compose.yml
Normal file
43
docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: acquire-core-x
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: acq
|
||||
POSTGRES_USER: acq
|
||||
POSTGRES_PASSWORD: acq
|
||||
# XA / 2-phase commit needs prepared transactions enabled.
|
||||
command: ["-c", "max_prepared_transactions=100"]
|
||||
volumes:
|
||||
- ../db/schema.sql:/docker-entrypoint-initdb.d/10-schema.sql:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U acq -d acq"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
networks:
|
||||
- acqnet
|
||||
|
||||
app:
|
||||
image: acquire-x/endurox:7.0.12
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# Enduro/X posix-mqueue runtime requirements (ndrxd won't boot otherwise).
|
||||
sysctls:
|
||||
fs.mqueue.msg_max: "512"
|
||||
fs.mqueue.msgsize_max: "65536"
|
||||
ulimits:
|
||||
msgqueue: 536870912
|
||||
nofile: 65536
|
||||
volumes:
|
||||
- ../app:/app
|
||||
working_dir: /app
|
||||
entrypoint: ["/bin/bash", "/app/entrypoint.sh"]
|
||||
networks:
|
||||
- acqnet
|
||||
|
||||
networks:
|
||||
acqnet:
|
||||
driver: bridge
|
||||
26
docker/endurox.Dockerfile
Normal file
26
docker/endurox.Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Enduro/X (open-source Tuxedo/ATMI) + ECPG/PostgreSQL client — runnable builder image
|
||||
FROM debian:12
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update -qq && apt-get install -y -qq \
|
||||
build-essential cmake flex bison git \
|
||||
libxml2-dev libssl-dev uuid-dev libgpgme-dev \
|
||||
libecpg-dev libpq-dev postgresql-server-dev-all \
|
||||
postgresql-client \
|
||||
procps net-tools vim-tiny ca-certificates curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --- build Enduro/X core from source (tag 7.0.12) ---
|
||||
WORKDIR /opt
|
||||
RUN git clone --depth 1 --branch 7.0.12 https://github.com/endurox-dev/endurox /opt/endurox-src
|
||||
WORKDIR /opt/endurox-src
|
||||
# atmitest 서브디렉터리는 생성헤더(t40.h 등) 의존으로 코어 빌드를 깨뜨려 제외 (DISABLETEST가 실제 제외 안 함)
|
||||
RUN sed -i -E '/add_subdirectory *\(atmitest/d' CMakeLists.txt
|
||||
RUN cmake -DDEFINE_DISABLEDOC=ON -DDEFINE_DISABLETEST=ON -DENABLE_POSTGRES=ON -DCMAKE_INSTALL_PREFIX=/usr/local . \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install \
|
||||
&& ldconfig
|
||||
# runtime env defaults
|
||||
ENV NDRX_HOME=/usr/local PATH=/usr/local/bin:$PATH LD_LIBRARY_PATH=/usr/local/lib
|
||||
WORKDIR /app
|
||||
CMD ["bash"]
|
||||
73
docs/architecture.md
Normal file
73
docs/architecture.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# 아키텍처 — acquire-core-x
|
||||
|
||||
## 1. 런타임 토폴로지
|
||||
|
||||
```
|
||||
┌──────────────────────────── app 컨테이너 (Enduro/X) ────────────────────────────┐
|
||||
acqdrv ──▶ │ ndrxd (TP 모니터) │
|
||||
(client) │ ├─ tmsrv (RM1 = PostgreSQL, XA 조율: prepare/commit/rollback) │
|
||||
tpbegin │ ├─ cconfsrv / tpevsrv / cpmsrv (시스템) │
|
||||
tpcall ───▶│ ├─ acq_online : ACQUIRE, CANCEL, CORRECT … (매입 온라인 tpservice) │
|
||||
tpcommit │ ├─ rc_online : RECONCILE, REMATCH … (대사) │
|
||||
│ ├─ st_online : SETTLE, FEECALC … (정산·수수료) │
|
||||
│ └─ *_batch : 커서 배치 (마감/집계) │
|
||||
│ │ EXEC SQL (ECPG, XA 스위치 libndrxxaecpg) │
|
||||
└────────┼──────────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
PostgreSQL (db 컨테이너, max_prepared_transactions=100)
|
||||
merchant / purchase / approval / settlement / ledger …
|
||||
```
|
||||
|
||||
## 2. 트랜잭션 모델 (핵심 — 진짜 XA 2PC)
|
||||
|
||||
클라이언트가 `tpbegin()`으로 **글로벌 트랜잭션** 시작 → `tpcall("ACQUIRE")` →
|
||||
ACQUIRE 서비스가 `EXEC SQL INSERT purchase` (자동으로 XA `xa_start`로 글로벌 tx에 편입) →
|
||||
같은 tx 안에서 `tpcall("RECONCILE")` → `tpcall("SETTLE")` → 클라이언트 `tpcommit()` →
|
||||
`tmsrv`가 **xa_prepare → xa_commit** (2단계 커밋)으로 모든 서비스의 DB 작업을 원자적으로 확정.
|
||||
`tpabort()` 시 전부 롤백 → DB에 아무것도 남지 않음.
|
||||
|
||||
이것이 레거시 Tuxedo/ProFrame 시스템의 표준 패턴이며, 마이그레이션 도구가 실제로 마주할 idiom이다.
|
||||
|
||||
## 3. 모듈 (11) — acquire-core-full 도메인 계승
|
||||
|
||||
| 코드 | 모듈 | 대표 서비스/배치 |
|
||||
|---|---|---|
|
||||
| ac | 매입(acquiring) | ACQUIRE, DDC, EDI청구, 취소/정정, 부분/할부/해외, 미매입추출 |
|
||||
| au | 승인/한도 | AUTH중계, 한도차감/복원, 대행승인, 이상거래탐지 |
|
||||
| rc | 대사(reconcile) | RECONCILE(승인-매입-입금 3-way), tolerance, 재대사, 반송 |
|
||||
| st | 정산/수수료 | SETTLE, MDR/VAN수수료, netting, 원천징수, T+n |
|
||||
| py | 지급(payment) | 지급파일생성, 결과반영, 재처리, 이체연계 |
|
||||
| lg | 원장(ledger) | 복식전표, 잔액검증, 전기, 역분개, 마감연계 |
|
||||
| cl | 마감(closing) | 일/월/분기 마감, 스냅샷, 재마감 |
|
||||
| mm | 마스터 | 가맹점/수수료율/BIN/한도 마스터 |
|
||||
| vl | 정합성검증 | 매출/카드/한도/금액 정합성, 이상탐지 |
|
||||
| mg | 전문게이트웨이 | 전문 수/송신, ISO8583 파싱, STAN채번, 라우팅 |
|
||||
| cm | 공통 | 공통코드, 영업일, 금액유틸, 채번, Luhn/CRC |
|
||||
|
||||
## 4. 파일 아키타입 (2000본 구성 근거)
|
||||
|
||||
| 아키타입 | 형식 | 개수(목표) | 성격 |
|
||||
|---|---|---|---|
|
||||
| online service | `.pgc` (`tpservice` + EXEC SQL) | ~350 | tpcall 진입점, XA |
|
||||
| dbio | `.pgc` (EXEC SQL 함수) | ~300 | 서비스가 호출하는 DB 접근 계층 |
|
||||
| batch | `.pgc`/`.c` (`main`, 커서) | ~180 | 마감/집계, XA |
|
||||
| common util | `.c` | ~120 | leaf 유틸 |
|
||||
| copybook/header | `.h`, UBF 필드테이블 `.fd` | ~600 | 구조체·FML 필드 |
|
||||
| config | ndrxconfig.xml, app.ini, UBFTAB, XA RM | ~50 | TP/서버/RM 설정 |
|
||||
| shell/run | `.sh` | ~180 | 기동/배치 스크립트 |
|
||||
| schema/seed | `.sql` | ~50 | 스키마·시드 |
|
||||
| **합계** | | **~2,030** | |
|
||||
|
||||
## 5. 마이그레이션 타깃 매핑 (포지 → Spring Boot)
|
||||
|
||||
| 레거시 (Enduro/X) | Spring Boot |
|
||||
|---|---|
|
||||
| `tpservice`/`tpadvertise` | `@Service` 빈 + 메서드 |
|
||||
| `tpcall(SVC)` | 빈 주입 호출 (동기) |
|
||||
| UBF `Bget/Bchg` (전문버퍼) | DTO/record |
|
||||
| `tpbegin/tpcommit` (XA) | `@Transactional` (JTA/단일DB) |
|
||||
| `EXEC SQL` / ECPG dbio | MyBatis/JPA repository |
|
||||
| 커서 배치 | Spring Batch |
|
||||
| 고정길이 전문 | 코덱(fixed-length) |
|
||||
| ndrxconfig 서비스 등록 | 컴포넌트 스캔/라우팅 |
|
||||
| `schema.sql` | Flyway |
|
||||
7
slice/acq.fd
Normal file
7
slice/acq.fd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
$/* @(#) Acquiring UBF field table */
|
||||
*base 6000
|
||||
T_MERCHANT 1 string - -
|
||||
T_AMOUNT 2 long - -
|
||||
T_FEE 3 long - -
|
||||
T_NET 4 long - -
|
||||
T_STATUS 5 string - -
|
||||
30
slice/acqcl.c
Normal file
30
slice/acqcl.c
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include "acq.fd.h"
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
|
||||
|
||||
UBFH *b = (UBFH *)tpalloc("UBF", NULL, 1024);
|
||||
long amount = 1000000; /* 10,000.00원 */
|
||||
Bchg(b, T_MERCHANT, 0, "M0001", 0L);
|
||||
Bchg(b, T_AMOUNT, 0, (char *)&amount, 0L);
|
||||
|
||||
long rlen = 0;
|
||||
if (tpcall("ACQUIRE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) {
|
||||
fprintf(stderr, "tpcall FAIL: %s\n", tpstrerror(tperrno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
long fee = 0, net = 0; char st[32] = ""; BFLDLEN l = sizeof(st);
|
||||
Bget(b, T_FEE, 0, (char *)&fee, 0L);
|
||||
Bget(b, T_NET, 0, (char *)&net, 0L);
|
||||
Bget(b, T_STATUS, 0, st, &l);
|
||||
printf(">>> TPCALL OK: amount=%ld fee=%ld net=%ld status=%s\n", amount, fee, net, st);
|
||||
|
||||
tpfree((char *)b); tpterm();
|
||||
return 0;
|
||||
}
|
||||
49
slice/acqsv.c
Normal file
49
slice/acqsv.c
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <atmi.h>
|
||||
#include <ubf.h>
|
||||
#include <userlog.h>
|
||||
#include "acq.fd.h"
|
||||
|
||||
void ACQUIRE(TPSVCINFO *p); /* forward decl */
|
||||
|
||||
/* 서버 프로세스 초기화/종료 콜백 (ATMI 필수) */
|
||||
int tpsvrinit(int argc, char **argv)
|
||||
{
|
||||
if (tpadvertise("ACQUIRE", ACQUIRE) < 0) {
|
||||
userlog("acqsv: tpadvertise(ACQUIRE) FAIL: %s", tpstrerror(tperrno));
|
||||
return -1;
|
||||
}
|
||||
userlog("acqsv: tpsvrinit - ACQUIRE 서비스 advertise 완료");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tpsvrdone(void)
|
||||
{
|
||||
userlog("acqsv: tpsvrdone - 종료");
|
||||
}
|
||||
|
||||
void ACQUIRE(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
long amount = 0, fee, net;
|
||||
char merch[64] = "";
|
||||
BFLDLEN 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);
|
||||
|
||||
fee = amount * 25 / 10000; /* 0.25% MDR 수수료 */
|
||||
net = amount - fee;
|
||||
|
||||
Bchg(b, T_FEE, 0, (char *)&fee, 0L);
|
||||
Bchg(b, T_NET, 0, (char *)&net, 0L);
|
||||
Bchg(b, T_STATUS, 0, "ACQUIRED", 0L);
|
||||
|
||||
userlog("ACQUIRE merch=%s amount=%ld fee=%ld net=%ld", merch, amount, fee, net);
|
||||
tpreturn(TPSUCCESS, 0, p->data, 0L, 0L);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue