diff options
Diffstat (limited to 'docs/research/083-delivery-over-http.md')
| -rw-r--r-- | docs/research/083-delivery-over-http.md | 73 |
1 files changed, 73 insertions, 0 deletions
diff --git a/docs/research/083-delivery-over-http.md b/docs/research/083-delivery-over-http.md new file mode 100644 index 0000000..48da056 --- /dev/null +++ b/docs/research/083-delivery-over-http.md @@ -0,0 +1,73 @@ +# 083 — 앱의 보냄을 HTTP로: 저장된 것만, 순서대로, 받는 쪽은 한 번 + +2026-09-27. [로드맵](../roadmap.md) 중기 1 "비동기 아웃바운드 호출"의 첫 단계이자 [078](078-app-connections.md)의 3·4단계. + +착수 기준: + +- 바꿀 결정: 앱이 다른 서비스를 부를 때 핸들러 안에서 HTTP 클라이언트를 쓰고 재시도·중복 제거·outbox를 앱이 짤지, + 보냄을 이벤트의 일부로 저장하고 호스트가 저장 뒤에 전하며 받는 쪽 런타임이 번호로 한 번만 적용할지. +- 남은 질문: 보내는 서버와 받는 서버가 아무 순간에 죽어도, 보내는 앱이 저장한 메시지가 받는 앱에 정확히 한 번씩 + 적용되는가? 전달이 받는 쪽의 저장(동기화)에 묶이면 얼마나 느린가? +- 최소 확인: 두 `pure-serve`를 잇고, 클라이언트 넷이 주문하는 동안 받는 서버와 보내는 서버를 `kill -9`로 죽였다 살린 + 뒤, 받는 앱의 합계와 건수가 보내는 앱이 저장한 것과 같은지 본다. 전달 속도를 잰다. +- 중단 기준: 한 번이라도 합계가 다르면(잃거나 겹치면) 되돌린다. + +## 의심한 관례 + +| 관례 | 이 설계 | +|---|---| +| 외부 호출은 핸들러 안에서 한다. 이벤트가 실패하면 보상 호출을 짠다 | `send`는 이벤트의 일부로 저장되고, 호스트는 저장된 보냄만 전한다. 실패한 이벤트는 보낸 적이 없다 | +| 재시도, 중복 제거, outbox 테이블은 앱 코드다 | 번호, 순서, 재시도, 덜어 내기는 호스트가, 들은 번호는 받는 쪽 런타임이 맡는다. 앱에는 `send` 한 줄이다 | +| 서비스 사이에는 메시지 브로커가 있다 | 보내는 앱의 저장소가 곧 줄이다. 받는 쪽이 저장한 뒤에야 덜어 낸다 | +| 묶음 크기는 설정이다 | 받는 쪽이 한 요청에 받을 수를 알리고(`X-Pure-Batch`), 보내는 쪽은 밀린 만큼 묶는다 | + +## 한 것 + +- **배선.** `pure-serve --send 출력=http://호스트:포트/경로`. 보내는 쪽 이름은 `--name`(없으면 모듈 파일 이름)과 출력 + 이름을 이은 것(`orders.ordered`)이다. 출력이 있는 앱은 상태가 없어도 소유자를 둔다(보냄이 상태이므로). +- **전달.** 저장이 끝날 때마다(시작할 때 저장소에 남은 것도) 소유자가 배선된 출력의 새 보냄을 JSON으로 만들어 그 + 출력의 줄에 넣는다. 출력마다 전달 스레드 하나가 순서대로 `POST`한다(`X-Pure-Source`, `X-Pure-Sequence`, + `Idempotency-Key`). 2xx면 전해졌고, Pure 받는 쪽이 앱의 거절(실패 종류가 있는 4xx)로 답해도 전해졌다(들었고 거절했다). + 408·429·5xx·응답 없음은 0.1초에서 두 배씩 10초까지 쉬었다가, 다른 4xx는 기록하고 10초마다 다시 보낸다. 버리지 않는다. + 전해진 번호까지 소유자가 덜어 내고(`pure_app_trim`), 다음 저장이 저장소에서 지운다. 연결은 이어 쓰고, 끊긴 연결에 보낸 + 것은 새 연결로 한 번 더 보낸다(받는 쪽이 번호로 거른다). +- **받기.** `X-Pure-Source`와 `X-Pure-Sequence`가 붙은 `POST /inputs/이름`을 `pure_app_deliver`로 적용한다. 이미 들은 + 번호는 적용하지 않고 200이다. 앱이 거절하면 들은 것으로 치고 그 실패 종류로 답한다. 답에는 `X-Pure-Batch: 256`을 싣는다. +- **묶음.** 그 헤더를 본 보내는 쪽은 줄에 쌓인 연속 번호를 최대 256개까지 JSON 배열 하나로 보낸다(`X-Pure-Count`). + 받는 쪽은 한 요청의 메시지를 차례로 한 묶음에서 적용하고 한 번 저장한다. 거절한 것은 들은 것으로 치고 넘어가고, 다른 + 실패에서 멈추면 묶음 전체를 다시 받는다(앞부분은 번호로 걸러진다). 제3자 끝점은 이 헤더를 모르므로 하나씩 받는다. +- JSON 본문은 출력의 필드와 그룹 그대로다([082](082-json-in-place.md)의 입력과 같은 모양). 한 그룹의 필드가 떨어져 있는 + 출력, `https`는 시작할 때 거절한다. UTF-8이 아닌 bytes 필드는 JSON으로 만들 수 없어 그 출력이 거기서 멈춘다(보냄은 남는다). + +## 장치가 말한 것 + +Raspberry Pi 5. 받는 서버는 CPU 0–1(연결 스레드 2), 보내는 서버는 CPU 2, 클라이언트(파이썬 16스레드)는 나머지. + +| | 하나씩 | 묶음 | +|---|---:|---:| +| 주문 2,992건을 넣는 동안 | 2.49 s(1,200/s) | 1.04 s(2,864/s) | +| 받는 쪽이 다 받은 때 | 7.64 s(**391건/s**) | 1.10 s(주문을 따라감) | +| 받는 쪽이 꺼진 동안 쌓인 20,000건 | — | 첫 묶음 뒤 0.32 s(**60,215건/s**) | + +하나씩 보내면 전달 하나가 받는 쪽의 저장 한 번(SD의 동기화 1–2 ms)과 왕복을 기다린다. 묶으면 저장 한 번이 256건까지 +나눈다. 보내는 앱의 저장이 이미 주문을 묶어 저장하므로, 받는 앱도 같은 비율로 묶어 받는 것이 맞다. + +## 검증 + +- aup `make check`의 `server_links`: 클라이언트 넷이 보내는 앱에 주문하는 동안 받는 서버를, 그다음 보내는 서버를 + `kill -9`로 죽이고 같은 저장소로 다시 띄운다. 그 뒤 수량 150(받는 앱이 거절한다)과 7의 주문을 더 넣는다. 받는 앱의 건수와 + 합계가 보내는 앱이 저장한 것(거절한 한 건을 빼고)과 같아지고, 확인 응답한 주문은 모두 그 안에 있다. 다섯 번(각 약 + 7,300건)과 ASan·UBSan 빌드로 한 번 통과했다. 거절한 주문 뒤의 주문도 전해졌다(막히지 않는다). +- `test-module`의 링크 검사([078](078-app-connections.md), 008)가 런타임 쪽(이벤트의 보냄, 저장 실패, 재시작, 중복·거절)을 + 맡는다. + +## 다음 결정 + +- **TLS.** 제3자 API는 거의 `https`다. 문서 피어의 TLS([055](055-authenticated-peer-transport-cost.md))를 전달 클라이언트에 + 쓰는 것이 다음이다. +- **응답을 이벤트로.** 지금은 보내고 끝이다(알림, 웹훅). 결제처럼 답이 필요한 호출은 응답을 앱의 입력으로 돌려주는 + 배선(`--send 출력=URL --reply 입력`)이 필요하다. +- 저장이 끝날 때마다 outbox 전체를 훑어 새 보냄을 찾는다. 받는 쪽이 오래 꺼져 수만 건이 밀리면 저장마다 그만큼 훑는다. + 마지막으로 넣은 자리에서 이어 훑게 해야 한다. +- 덜어 낸 것은 다음 저장에 지워진다. 요청이 없는 앱은 저장하지 않으므로, 다시 시작하면 전한 것을 한 번 더 보낸다(받는 + 쪽이 걸러 낸다). |
