477개 서비스를 tpcall 체인으로 연결하고(2 → 477), 그 과정에서 드러난
런타임 결함들을 실행 검증으로 잡아냈다.
체인 오케스트레이션 (Wave E)
- 매입/승인/정산/지급/대사/원장/마감/마스터/전문 흐름을 모듈 간 tpcall 로 연결
- 호출그래프 553노드 494엣지, 순환 0, SELFCALL 0
- 핵심 경로: ACQUIRE→RECONCILE→SETTLE→ST_MDR→LG_SETTLEPOST (6단, XA 2PC)
런타임 결함 수정 (Wave F) — 정적 게이트로는 잡히지 않던 것들
1. 모듈내 체인 자기교착: 모듈 서버 카피가 1이면 SETTLE→ST_MDR 처럼 같은
서버로 되돌아오는 호출이 자기 요청을 못 꺼내 TPETIME. 카피 4로 상향.
2. 크로스브랜치 핫로우 교착: 한 글로벌 트랜잭션의 형제 XA 브랜치가 같은 행을
UPDATE 하면 같은 트랜잭션이라 락이 안 풀려 타임아웃까지 블록된다.
- mg_channel: 비소유자 96개 서비스의 UPDATE 제거, STAN 은 시퀀스 채번으로
(실제 전문 스위치 방식). 생애주기 소유자 7개만 UPDATE 유지.
- lg_merch_bal: ACQ_SETTLELINK 의 LG_SETTLEPOST 다이아몬드 호출 제거.
교착일 뿐 아니라 같은 정산건이 원장에 이중 전기되는 회계 오류였다.
- cl_close_log / py_payout / lg_account_bal: CHAIN 마커로 쓰기 소유자 일원화.
py_payout 건은 피호출자가 호출자의 미커밋 승인을 볼 수 없어 기능적으로도
동작 불능이었다.
3. cm_log PK 충돌: st 131개 파일이 max(log_id)+1 로 채번해 nextval 을 쓰는
피호출자와 동일 PK 를 INSERT, 미커밋 인덱스 엔트리에 걸려 블록. 전부
nextval('cm_log_seq') 로 전환.
4. strncpy 스택 파괴: cmdb_get_param 이 strncpy(out, h_val, 127) 로 항상 128
바이트를 써서, bizdate[16] 을 넘기는 호출부 133곳이 매번 호출자 스택 112
바이트를 훼손했다. 인접 ecpg 호스트 변수가 오염돼 서버 카피가 세그폴트.
DB 락 대기가 0인데 60초 블록되는 증상의 원인. cm_code/py_payout_reject 동일.
5. ACQ_CANCEL→LIMIT_RST 입력계약 불일치: 카드번호를 안 실어 보내 항상 거절.
승인원장에서 카드를 끌어오고, 무승인 매입은 한도원복을 생략하도록 수정.
6. mgdb_channel_bump_stan: 활동 채널 전체를 한 번에 잠그는 다중행 UPDATE
(호출자 0인 잠복 지뢰). 시퀀스 채번 + 집계 조회로 교체.
화면
- 기본값을 실존 시드 키로 부팅 시 조회해 덮어쓴다. 기존 플레이스홀더
(card_limit 에 없는 카드, 8자리 영업일)로는 처리성 화면이 전부 거절당했다.
- 게이트웨이가 단일 스레드 직렬 루프라 기본값 조회도 순차로 수행.
검증 (실측)
- 통합 빌드: modules=11 servers=11 batches=506, error 0
- 도메인: 45 프로세스, 1,466 서비스 AVAIL
- 체인 전수 스모크 477/477 응답, TPETIME/TPEABORT 0건, 최대 530ms
- AUTH: 수정 전 60초 블록→TPEABORT, 수정 후 0.215초 ok:true
- 6단 매입 체인 COMMIT OK, 미결 2PC 0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
222 lines
8.2 KiB
Bash
222 lines
8.2 KiB
Bash
#!/bin/bash
|
|
##
|
|
## build.sh - REUSABLE build + Enduro/X config generator for acquire-core-x.
|
|
##
|
|
## Discovers every module under app/src/<mod>/, ecpg-precompiles its .pgc, builds
|
|
## - libacqcommon.a (from app/src/common/*.c) [once]
|
|
## - lib<mod>dbio.a (from app/src/<mod>/dbio/*.pgc) [per module]
|
|
## - <mod>_svr (from app/src/<mod>/<mod>_svr.pgc) [per module]
|
|
## - <mod> batches (from app/src/<mod>/batch/*.pgc) [per module]
|
|
## - standalone clients (from app/src/clients/*.c)
|
|
## then GENERATES app/conf/ndrxconfig.xml with one <server> entry per <mod>_svr.
|
|
##
|
|
## Adding a new module later = drop app/src/<mod>/{<mod>_svr.pgc,dbio,batch} and
|
|
## re-run build.sh. No hand-editing of config or this script is required: modules,
|
|
## servers, dbio libs, batches, and srvids are all DISCOVERED.
|
|
##
|
|
set -euo pipefail
|
|
|
|
# App root = this script's own directory (normally /app in the container, but
|
|
# relocatable so an isolated copy — e.g. CI building a throwaway /b — works too).
|
|
APP="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SRC=$APP/src
|
|
BIN=$APP/bin
|
|
LIB=$APP/lib
|
|
OBJ=$APP/obj
|
|
UBF=$APP/ubftab
|
|
CONF=$APP/conf
|
|
EXINC=/usr/local/include
|
|
PGINC="$(pg_config --includedir)"
|
|
PGLIB="$(pg_config --libdir)"
|
|
|
|
mkdir -p "$BIN" "$LIB" "$OBJ"
|
|
|
|
# Includes for every compile (CFLAGS is honored by buildserver/buildclient and
|
|
# by our direct gcc calls). Add every module's dbio AND svc dir so headers resolve
|
|
# (svc/*.pgc copybooks include the module's dbio + common headers).
|
|
CFLAGS="-I$UBF -I$EXINC -I$PGINC -I$SRC/common"
|
|
for d in "$SRC"/*/dbio; do [ -d "$d" ] && CFLAGS="$CFLAGS -I$d"; done
|
|
for d in "$SRC"/*/svc; do [ -d "$d" ] && CFLAGS="$CFLAGS -I$d"; done
|
|
export CFLAGS
|
|
PGLINK="-L$PGLIB -lecpg -lpq"
|
|
|
|
# ecpg needs the same header search path as gcc (its preprocessor opens the
|
|
# #include'd copybooks to find EXEC SQL host-var decls). Mirror CFLAGS' -I set.
|
|
ECPG_INC="-I$UBF -I$SRC/common"
|
|
for d in "$SRC"/*/dbio; do [ -d "$d" ] && ECPG_INC="$ECPG_INC -I$d"; done
|
|
for d in "$SRC"/*/svc; do [ -d "$d" ] && ECPG_INC="$ECPG_INC -I$d"; done
|
|
|
|
log() { echo "[build] $*"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# discover modules = dirs app/src/<mod>/ that contain <mod>_svr.pgc
|
|
# ---------------------------------------------------------------------------
|
|
MODS=()
|
|
for d in "$SRC"/*/; do
|
|
m="$(basename "$d")"
|
|
[ -f "$d/${m}_svr.pgc" ] && MODS+=("$m")
|
|
done
|
|
IFS=$'\n' MODS=($(printf '%s\n' "${MODS[@]}" | sort)); unset IFS
|
|
log "modules discovered: ${MODS[*]}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 0. UBF field header
|
|
# ---------------------------------------------------------------------------
|
|
log "mkfldhdr acq.fd"
|
|
( cd "$UBF" && FIELDTBLS=acq.fd FLDTBLDIR="$UBF" mkfldhdr acq.fd >/dev/null )
|
|
|
|
# ecpg precompile helper: $1 = .pgc path -> echoes generated .c path
|
|
ecpg_gen() {
|
|
local pgc="$1" base out
|
|
base="$(basename "${pgc%.pgc}")"
|
|
out="$OBJ/${base}.c"
|
|
ecpg $ECPG_INC -I"$(dirname "$pgc")" -o "$out" "$pgc"
|
|
echo "$out"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. common lib -> libacqcommon.a
|
|
# ---------------------------------------------------------------------------
|
|
log "libacqcommon.a"
|
|
COMMON_OBJS=()
|
|
for c in "$SRC"/common/*.c; do
|
|
o="$OBJ/$(basename "${c%.c}").o"
|
|
gcc -c $CFLAGS "$c" -o "$o"
|
|
COMMON_OBJS+=("$o")
|
|
done
|
|
ar rcs "$LIB/libacqcommon.a" "${COMMON_OBJS[@]}"
|
|
NSVR=0; NDBIO=0; NBATCH=0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2..4 per module: dbio lib, server, batches
|
|
# ---------------------------------------------------------------------------
|
|
NSVC=0
|
|
for m in "${MODS[@]}"; do
|
|
MDIR="$SRC/$m"
|
|
DBIO_LIB=""
|
|
SVC_LIB=""
|
|
|
|
# -- dbio static lib (optional) --
|
|
if compgen -G "$MDIR/dbio/*.pgc" >/dev/null; then
|
|
log "lib${m}dbio.a"
|
|
DB_OBJS=()
|
|
for pgc in "$MDIR"/dbio/*.pgc; do
|
|
c="$(ecpg_gen "$pgc")"
|
|
o="${c%.c}.o"
|
|
gcc -c $CFLAGS "$c" -o "$o"
|
|
DB_OBJS+=("$o")
|
|
done
|
|
ar rcs "$LIB/lib${m}dbio.a" "${DB_OBJS[@]}"
|
|
DBIO_LIB="$LIB/lib${m}dbio.a"
|
|
NDBIO=$((NDBIO+1))
|
|
fi
|
|
|
|
# -- per-service static lib (NEW legacy layout, optional) --
|
|
# A module may keep its services INLINE in <mod>_svr.pgc (old style) OR split
|
|
# them into svc/*.pgc, one service per file (new style). Both are supported;
|
|
# when svc/*.pgc exist they are archived into lib<mod>svc.a and linked in.
|
|
if compgen -G "$MDIR/svc/*.pgc" >/dev/null; then
|
|
log "lib${m}svc.a"
|
|
SVC_OBJS=()
|
|
for pgc in "$MDIR"/svc/*.pgc; do
|
|
c="$(ecpg_gen "$pgc")"
|
|
o="${c%.c}.o"
|
|
gcc -c $CFLAGS "$c" -o "$o"
|
|
SVC_OBJS+=("$o")
|
|
done
|
|
ar rcs "$LIB/lib${m}svc.a" "${SVC_OBJS[@]}"
|
|
SVC_LIB="$LIB/lib${m}svc.a"
|
|
NSVC=$((NSVC+1))
|
|
fi
|
|
|
|
# -- module server <mod>_svr (links svc lib + dbio lib + common) --
|
|
log "${m}_svr"
|
|
SVR_C="$(ecpg_gen "$MDIR/${m}_svr.pgc")"
|
|
buildserver -o "$BIN/${m}_svr" -f "$SVR_C" \
|
|
-a "$SVC_LIB $DBIO_LIB $LIB/libacqcommon.a $PGLINK"
|
|
NSVR=$((NSVR+1))
|
|
|
|
# -- batches (optional): each *.pgc -> its own client executable --
|
|
if compgen -G "$MDIR/batch/*.pgc" >/dev/null; then
|
|
for pgc in "$MDIR"/batch/*.pgc; do
|
|
name="$(basename "${pgc%.pgc}")"
|
|
log "batch $name"
|
|
BC="$(ecpg_gen "$pgc")"
|
|
buildclient -o "$BIN/$name" -f "$BC" \
|
|
-a "$DBIO_LIB $LIB/libacqcommon.a $PGLINK"
|
|
NBATCH=$((NBATCH+1))
|
|
done
|
|
fi
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. standalone clients (plain C, no EXEC SQL)
|
|
# ---------------------------------------------------------------------------
|
|
NCLI=0
|
|
for c in "$SRC"/clients/*.c; do
|
|
name="$(basename "${c%.c}")"
|
|
log "client $name"
|
|
buildclient -o "$BIN/$name" -f "$c" -a "-L$PGLIB -lpq"
|
|
NCLI=$((NCLI+1))
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. GENERATE ndrxconfig.xml (tmsrv fixed at srvid 40; module servers 100 step 10)
|
|
# ---------------------------------------------------------------------------
|
|
log "generating ndrxconfig.xml for servers: ${MODS[*]/%/_svr}"
|
|
gen_servers() {
|
|
local srvid=100
|
|
for m in "${MODS[@]}"; do
|
|
cat <<EOF
|
|
<server name="${m}_svr">
|
|
<srvid>${srvid}</srvid>
|
|
<!-- 모듈 내부 tpcall 체인(예: SETTLE→ST_MDR)이 같은 서버로 되돌아오므로
|
|
복수 카피 필수. 카피가 1이면 호출자가 블록된 채 자기 요청을 못 꺼내
|
|
자기교착(TPETIME)이 난다. 모듈내 최대 체인 깊이 3 + 동시성 여유 = 4. -->
|
|
<min>4</min>
|
|
<max>4</max>
|
|
<sysopt>-e \${NDRX_APPHOME}/log/${m}_svr.log -r</sysopt>
|
|
</server>
|
|
EOF
|
|
srvid=$((srvid+10))
|
|
done
|
|
}
|
|
cat > "$CONF/ndrxconfig.xml" <<EOF
|
|
<?xml version="1.0" ?>
|
|
<!-- GENERATED by app/build.sh - do not hand-edit. One <server> per discovered
|
|
<mod>_svr module server; each advertises its module's services at tpsvrinit. -->
|
|
<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 (ECPG/PostgreSQL). -->
|
|
<server name="tmsrv">
|
|
<srvid>40</srvid>
|
|
<min>1</min>
|
|
<max>1</max>
|
|
<sysopt>-e \${NDRX_APPHOME}/log/tmsrv-rm1.log -r -- -t3 -l\${NDRX_APPHOME}/tmlogs/rm1 -m20</sysopt>
|
|
</server>
|
|
$(gen_servers)
|
|
</servers>
|
|
</endurox>
|
|
EOF
|
|
|
|
log "DONE: modules=${#MODS[@]} servers=$NSVR svc_libs=$NSVC dbio_libs=$NDBIO batches=$NBATCH clients=$NCLI"
|
|
echo "[build] server binaries: ${MODS[*]/%/_svr}"
|