aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research')
-rw-r--r--docs/research/078-app-connections.md4
-rw-r--r--docs/research/083-delivery-over-http.md73
2 files changed, 75 insertions, 2 deletions
diff --git a/docs/research/078-app-connections.md b/docs/research/078-app-connections.md
index 5e89853..a75cdfd 100644
--- a/docs/research/078-app-connections.md
+++ b/docs/research/078-app-connections.md
@@ -2,8 +2,8 @@
2026-09-26. 설계 노트이며 구현 순서를 포함한다. 상태: 1단계(Pure 런타임: 출력, `SEND`, outbox·inbox와 그 저장)와
2단계(aup `output`·`send`)를 2026-09-27에 구현했다([008 앱 사이의 연결](../design/008-module-format.md)). 호스트의 배선과
-전달(3·4단계)은 [로드맵](../roadmap.md) 중기 1(외부로 나가는 호출)과 함께 한다. 외부 API 호출도 같은 outbox 위에 선다(출력을
-HTTP 끝점에 잇는 배선).
+전달은 한 프로세스 안이 아니라 곧바로 HTTP로 했다(`pure-serve --send`, [083](083-delivery-over-http.md)). 다른 앱에도 외부 API에도
+같은 outbox와 같은 전달이다.
착수 기준:
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 전체를 훑어 새 보냄을 찾는다. 받는 쪽이 오래 꺼져 수만 건이 밀리면 저장마다 그만큼 훑는다.
+ 마지막으로 넣은 자리에서 이어 훑게 해야 한다.
+- 덜어 낸 것은 다음 저장에 지워진다. 요청이 없는 앱은 저장하지 않으므로, 다시 시작하면 전한 것을 한 번 더 보낸다(받는
+ 쪽이 걸러 낸다).