TT Lab
Get started
Learn Learning paths Courses

Building an EAI Middleware Layer

Build a Synchronous Relay Server — Say You Don't Know When You Don't

Continue in TT Lab

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

  1. 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 (wdBank LHB, wdAcct 11002003004005, amount an integer, and so on — see the API description at the top of partner.py) and save the response body as is to /root/eaimw/sync/core-ok.json.
  2. Look at section 4 of the specification /opt/lab/fixtures/eaimw/header/SPEC.md and the core banking API and create /root/eaimw/sync/rspmap.csv. Header source,rsp_code, and the nine sources are HTTP200, INSUFFICIENT_FUNDS, NO_ACCOUNT, LIMIT_EXCEEDED, HTTP500, READ_TIMEOUT, CONNECT_FAIL, UNKNOWN_TX and BAD_FRAME.
  3. The skeleton of /root/eaimw/sync/relay.py: it receives messages over TCP with python3 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.
  4. Relay BKTR0001 to core banking. Convert the body to JSON with lhconv, add guid and POST /v1/transfers, with the GUID in the X-GUID header as well. On 200, return 0000 and the response body (BKTR0001.rsp.layout, 45 bytes). Call core banking exactly once.
  5. Convert business errors: according to the result of a 422, B201, B202 or B203, and E500 for a 500. Error responses have no body.
  6. If waiting for the response exceeds --timeout, answer E901. Do not wait for core banking to the end.
  7. If you cannot connect to core banking (connection refused, connect timeout), answer E902. Distinguish it from the E901 of step 6.
  8. Handle each connection separately. While one slow request is being processed, other requests must not wait.

Notes

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.