Building an EAI Middleware Layer
Build a Synchronous Relay Server — Say You Don't Know When You Don't
Goal
Build a synchronous relay server that receives an LH-STD message, calls core banking (HTTP JSON) and converts the result into a standard response code. Distinguish a read timeout (result unknown) from a connection failure (confirmed not sent).
Why it matters
The channel does not have to know the target system's conventions and should decide from a single standard response code. The relay layer takes on that translation. And since the relay adds one more point of failure, whether "it is safe to send again" depends on where it stopped. Getting this distinction wrong causes double transfers.
Steps
- Start the core banking fixture:
nohup python3 /opt/lab/fixtures/eaimw/partner.py core --log /root/eaimw/sync/core.log > /root/eaimw/sync/core.out 2>&1 &(port 9201). With curl, send one normal transfer (wdBankLHB,wdAcct11002003004005,amountan integer, and so on — see the API description at the top ofpartner.py) and save the response body as is to/root/eaimw/sync/core-ok.json. - Look at section 4 of the specification
/opt/lab/fixtures/eaimw/header/SPEC.mdand the core banking API and create/root/eaimw/sync/rspmap.csv. Headersource,rsp_code, and the nine sources areHTTP200,INSUFFICIENT_FUNDS,NO_ACCOUNT,LIMIT_EXCEEDED,HTTP500,READ_TIMEOUT,CONNECT_FAIL,UNKNOWN_TXandBAD_FRAME. - The skeleton of
/root/eaimw/sync/relay.py: it receives messages over TCP withpython3 relay.py --port <P> --core <계정계URL> --timeout <초>(port, core banking URL, seconds). It reads exactly 4 bytes of length and then reads that much again (even if it arrives in fragments). It answers a format error with E102, and if the transaction code is not BKTR0001, with E101 (same GUID, indicator R, institutions swapped). BKTR0001 is not yet connected to core banking, so it answers E902. - Relay BKTR0001 to core banking. Convert the body to JSON with
lhconv, addguidandPOST /v1/transfers, with the GUID in theX-GUIDheader as well. On 200, return 0000 and the response body (BKTR0001.rsp.layout, 45 bytes). Call core banking exactly once. - Convert business errors: according to the result of a 422, B201, B202 or B203, and E500 for a 500. Error responses have no body.
- If waiting for the response exceeds
--timeout, answer E901. Do not wait for core banking to the end. - If you cannot connect to core banking (connection refused, connect timeout), answer E902. Distinguish it from the E901 of step 6.
- Handle each connection separately. While one slow request is being processed, other requests must not wait.
Notes
- Common library:
import sys; sys.path.insert(0, "/opt/lab/fixtures/eaimw/lib"); import lhstd, lhconv—lhstd.read_frame(sock),parse,reply(req, code, body), andlhconv.load_layout,load_codemap,fixed_to_json,json_to_fixed. - Fault reproduction switch: if the body memo starts with
SLOW, core banking processes it after--delayseconds (it does process it), and if it starts withFAIL, it returns 500. - Core banking statistics:
curl -s localhost:9201/_stats(number of calls, counts per GUID), reset withcurl -s -XPOST localhost:9201/_reset. - Telling exceptions apart: a problem at the connection stage is
urllib.error.URLError, a timeout waiting for a response isTimeoutError, and an HTTP error status isurllib.error.HTTPError(a subclass of URLError, so catch it first). - Common mistake: converting everything to the same code with a single
except Exception. E901 and E902 get lumped together.
Call the other system directly
Start the core banking fixture on 9201 and save the response body of one normal transfer to /root/eaimw/sync/core-ok.json.
The API is in the header of partner.py. Call it like curl -s -XPOST -H 'Content-Type: application/json' -d '{...}' localhost:9201/v1/transfers. guid is 32 lowercase hexadecimal characters, and amount is an integer without quotes.
Write the response code conversion table
In /root/eaimw/sync/rspmap.csv, map the nine sources to standard response codes.
Read the meanings in section 4 of the specification. The key is READ_TIMEOUT and CONNECT_FAIL — one is "sent but unknown," and the other is "could not send."
Skeleton — read to the end, reject what you don't know
/root/eaimw/sync/relay.py reads even fragmented messages for their full length, and answers a format error with E102 and an unregistered transaction with E101.
lhstd.read_frame(sock) reads the 4 bytes of length and repeats recv until it has read the rest. For a message with a wrong format, parse fails, so build the response by reading directly the places in the original 80 bytes (4–12 transaction code, 12–44 GUID).
Relay a normal transfer to core banking once
Convert BKTR0001 to JSON, call core banking once, and return 0000 and the 45-byte response body (including the X-GUID header).
Convert with lhconv.fixed_to_json(REQ, h['BODY'], CODES) and add guid. Give urllib.request.Request headers={'X-GUID': ...}, and urlopen(req, timeout=ARGS.timeout). Build the response body with lhconv.json_to_fixed(RSP, res) and lhstd.reply(h, '0000', body).
Business errors into standard codes
Convert the result of a 422 into B201, B202 or B203, and a 500 into E500. Error responses have no body.
urllib.error.HTTPError carries the status code (e.code) and the body (e.read()). It is a subclass of URLError, so the except order matters.
When tired of waiting — answer that you don't know
If waiting for the response exceeds --timeout, answer E901. Do not wait for core banking to the end.
The timeout of urlopen applies to both connecting and waiting for the response. A timeout that occurs while waiting for the response comes up as an unwrapped TimeoutError. Remember that core banking processes the transfer even after that.
If you couldn't even send — say it was certainly not sent
If you cannot connect to core banking, answer E902 (distinguished from E901).
A failure at the connection stage (refused, connect timeout) comes wrapped in urllib.error.URLError. Catch HTTPError and TimeoutError first, and then URLError.
So that one slow request does not line everyone up
Handle each connection separately, so that other requests do not wait while one slow request is being processed.
socketserver.TCPServer handles only one connection at a time. The standard library has a server class that starts a thread per connection.