diff options
| author | eunhhu <[email protected]> | 2026-09-27 08:09:40 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 08:09:40 +0100 |
| commit | 5d3a7cefa27f3fe20a9c6991fb044815b38aac01 (patch) | |
| tree | b6e06c7976eddd846430cd809f7ace5e80553449 /docs | |
| parent | 01c87d2c204723905457f225b3113dac82eb0ced (diff) | |
| download | pure-5d3a7cefa27f3fe20a9c6991fb044815b38aac01.tar.gz pure-5d3a7cefa27f3fe20a9c6991fb044815b38aac01.zip | |
Deliver an app's saved sends over HTTP, in order and in batches (083)
pure-serve --send OUTPUT=http://host:port/path wires an output: after
every save the owner queues the new sends as JSON (fields and groups)
and a thread per output posts them in order with X-Pure-Source,
X-Pure-Sequence and Idempotency-Key. A 2xx, or a Pure receiver's
refusal, delivers; the owner then trims. 408, 429, 5xx and no answer
are sent again after a pause growing to 10 s, other answers every 10 s:
nothing is dropped. A Pure receiver applies a delivered message with
pure_app_deliver (now taking placed fields) and advertises
X-Pure-Batch: 256; the sender then posts up to that many numbers in a
row as one JSON array (X-Pure-Count), applied in order in one burst
with one save.
Delivery kept up with orders once batched (391 deliveries/s one at a
time; a backlog of 20,000 went at 60k/s). With four clients ordering,
killing the receiver and then the sender (-9) left the receiver's count
and total equal to what the sender saved, five times and under ASan.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/008-module-format.md | 6 | ||||
| -rw-r--r-- | docs/research/078-app-connections.md | 4 | ||||
| -rw-r--r-- | docs/research/083-delivery-over-http.md | 73 |
3 files changed, 81 insertions, 2 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index e67afa8..07cff23 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -289,6 +289,7 @@ text·리스트가 남을 때만 위치를 유지한다. | `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, text는 UTF-8 그대로(`text/plain`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) | | `GET /views/표?from=N&count=M` | 표의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` | | `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수)를 쿼리(`?points=3`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) | +| `POST /inputs/이름` + `X-Pure-Source`·`X-Pure-Sequence` | 다른 앱의 출력에서 전해진 메시지(`pure_app_deliver`): 번호마다 한 번 적용하고, 이미 들은 번호는 200. 답에 `X-Pure-Batch: 256`을 싣고, `X-Pure-Count: N`이면 본문은 N개 객체의 JSON 배열(연속 번호, 한 묶음에서 차례로 적용) | | `GET`·`PUT /objects/키` | 모듈에 키별 메시지 `show`·`edit`가 있으면 기존 문서 서버(`server.c`)와 같은 계약. `GET`은 `show`, `PUT`(`If-Match`/`If-None-Match`)은 `edit` | - **실패는 실패 종류로 답한다.** 이벤트가 실패하면 `X-Pure-Error`에 종류를 싣는다. `not_found` 404, `exists` 409, @@ -337,6 +338,11 @@ text·리스트가 남을 때만 위치를 유지한다. 이벤트 30 → 50, p50은 오히려 5.4 → 3.5 ms). 끊김은 답을 돌려준 뒤에 온 요청이 하나라도 있어야 세기 시작한다. 저장을 기다리는 동안 줄에 선 요청만으로 끊김을 세면 막 답을 받은 클라이언트가 다음 묶음을 놓치고, 클라이언트가 두 무리로 갈려 번갈아 저장한다(연결 스레드 둘, 64연결에서 묶음당 34개, 15.8–16.0k/s였고, 고친 뒤 61.6개, 22.9–23.9k/s). +- **보냄의 전달.** `--send 출력=http://호스트:포트/경로`로 출력을 받는 곳에 잇는다. 저장이 끝날 때마다 소유자가 새 보냄을 + JSON(필드와 그룹)으로 그 출력의 줄에 넣고, 출력마다 전달 스레드가 순서대로 보낸다(`X-Pure-Source: 이름.출력`, + `X-Pure-Sequence`, `Idempotency-Key`). 2xx 또는 Pure 받는 쪽의 거절이면 전해진 것이고, 소유자가 덜어 낸다. 그 밖에는 쉬었다가 + 다시 보낸다(버리지 않는다). 받는 쪽이 `X-Pure-Batch`를 알리면 밀린 것을 묶어 보낸다([083](../research/083-delivery-over-http.md)). + 출력이 있는 앱은 상태가 없어도 소유자를 둔다. - **보기는 스냅숏에서.** 소유자는 저장이 성공할 때마다, 답을 돌려주기 전에 저장된 상태의 스냅숏을 게시한다. 연결 스레드는 라운드마다(돌려받은 답을 처리한 뒤) 최신 스냅숏을 잡아 자기 읽기용 앱으로 그 라운드의 보기를 모두 평가하고 답을 모아 보낸다. 입력은 그보다 먼저 소유자에게 넘겨, 소유자가 그동안 적용하고 저장한다. 그래서 쓰기의 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 전체를 훑어 새 보냄을 찾는다. 받는 쪽이 오래 꺼져 수만 건이 밀리면 저장마다 그만큼 훑는다. + 마지막으로 넣은 자리에서 이어 훑게 해야 한다. +- 덜어 낸 것은 다음 저장에 지워진다. 요청이 없는 앱은 저장하지 않으므로, 다시 시작하면 전한 것을 한 번 더 보낸다(받는 + 쪽이 걸러 낸다). |
