From cb23514a5a2b00f635444d0e98a429419e6a67b4 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sat, 26 Sep 2026 21:23:53 +0100 Subject: Design connections between apps (078), not yet built A sent message belongs to its event's transaction in a runtime-owned outbox; hosts deliver committed messages in order, and a receiver keeps the last sequence per source in the same commit, so the effect is exactly once. The note records the design and its order of work; it waits behind the roadmap's short-term items and goes with outbound calls, which stand on the same outbox. --- docs/research/078-app-connections.md | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/research/078-app-connections.md (limited to 'docs/research/078-app-connections.md') diff --git a/docs/research/078-app-connections.md b/docs/research/078-app-connections.md new file mode 100644 index 0000000..fbe7f34 --- /dev/null +++ b/docs/research/078-app-connections.md @@ -0,0 +1,80 @@ +# 078 — 앱 사이의 연결: 런타임이 소유하는 outbox와 정확히 한 번의 효과 + +2026-09-26. 설계 노트이며 구현 순서를 포함한다. 상태: 설계만 했다. [로드맵](../roadmap.md)의 단기 과제(대여 완성, 저장과 +적용의 겹치기, 읽기 독립화, JSON 확장)를 먼저 하고, 중기 1(외부로 나가는 호출)과 함께 구현한다. 외부 API 호출도 같은 +outbox 위에 선다(출력을 HTTP 끝점에 잇는 배선). + +착수 기준: + +- 바꿀 결정: 앱이 다른 앱에 일을 넘길 때, 앱이 HTTP 클라이언트·재시도·중복 제거·outbox 테이블을 직접 짜는지, + 런타임이 "보냄"을 이벤트 트랜잭션의 일부로 소유하고 전달과 중복 제거를 책임지는지. +- 남은 질문: 보내는 쪽 이벤트가 실패하면 아무것도 보내지 않고, 받는 쪽이 같은 메시지를 두 번 받아도 효과는 한 번이며, + 어느 쪽이 어느 순간에 죽어도 확인된 일은 사라지지 않는가? 그 비용이 이벤트 하나에 무엇을 더하는가? +- 최소 확인: 두 앱을 한 호스트에서 잇고, 보내는 앱·받는 앱·호스트를 무작위 순간에 죽였다 살리며 받는 앱의 상태가 + "보낸 앱이 확정한 메시지를 정확히 한 번씩 적용한 것"과 같은지 모델과 대조한다. +- 중단 기준: 중복 적용이나 유실이 한 번이라도 나오면 다시 설계한다. + +## 의심한 관례 + +| 관례 | 이 설계 | +|---|---| +| 서비스는 다른 서비스의 주소를 안다 | 앱은 출력 포트만 선언한다. 어느 출력을 어느 입력에 잇는지는 앱 밖(호스트의 배선)이 정한다 | +| 메시지를 보내는 것은 부수 효과다(트랜잭션 밖) | `send`는 이벤트의 일부다. 이벤트가 실패하면 보내지 않았고, 저장된 이벤트의 메시지만 나간다 | +| 정확히 한 번은 불가능하니 앱이 멱등성을 설계한다 | 전달은 최소 한 번이고, 받는 쪽 런타임이 보낸 쪽별 마지막 순번을 자기 커밋에 함께 저장해 두 번째를 버린다. 효과는 정확히 한 번이다 | +| outbox 테이블과 전달 워커는 앱 코드다 | outbox는 런타임이 소유하는 상태다. 앱에는 `send` 한 줄이 있다 | + +## 언어 + +```text +# 주문 앱 +output ordered = message { + item: text + count: u32 +} +on buy { send ordered { item: buy.item, count: 1 } } + +# 창고 앱 +input order = message { + item: text + count: u32 +} +on order { stock[order.item].count = stock[order.item].count - order.count } +``` + +배선(호스트 설정): `orders.ordered -> warehouse.order`. 입력의 필드마다 출력에 이름과 타입이 같은 필드가 있어야 하고, +출력에만 있는 필드는 버린다(모듈 교체의 "이름으로 옮긴다"와 같은 규칙). 배선할 때 확인한다. + +## 모듈 형식 + +- 출력은 입력 표의 새 종류(5 출력)다. 모양은 메시지와 같다(필드 범위). 출력에는 핸들러가 없다. +- `SEND`(명령 72, `인자`는 출력): 출력의 필드 값을 필드 순서대로 받아 메시지 배치(008의 접두부와 가변 필드)로 만들고 + 앱의 outbox에 붙인다. 핸들러 전용. + +## 런타임 + +- **outbox는 상태다.** 앱마다 순번이 붙은 메시지 줄 하나를 둔다(출력 번호, 순번, 페이로드). `send`는 줄에 붙이고 이벤트의 + 되돌리기 기록에 남긴다. 이벤트가 실패하면 떼어낸다. 저장은 outbox를 다른 상태와 함께 한 커밋에 쓴다(`outbox` 문서). +- **확정된 것만 나간다.** 호스트는 마지막 저장까지의 메시지만 본다(`pure_app_outbox`). 지연 저장 중인 이벤트의 메시지는 + 그 묶음이 저장된 뒤에 나간다. 답이 저장 뒤에 나가는 것과 같은 규칙이다. +- **받는 쪽의 중복 제거.** `pure_app_deliver(app, input, source, seq, payload)`는 메시지 이벤트다. 앱은 보낸 쪽(배선의 + 이름)마다 마지막으로 적용한 순번을 가지며(`inbox` 문서), 순번이 그 이하면 적용하지 않고 성공을 답한다. 적용한 이벤트는 + 순번을 같은 커밋에 남긴다. +- **보낸 쪽의 정리.** 받는 쪽이 저장까지 마치면 호스트가 보낸 쪽의 outbox를 그 순번까지 잘라낸다(`pure_app_trim`, + 다음 저장에 함께 쓰인다). 자르기 전에 죽으면 다시 보내고, 받는 쪽이 버린다. +- **거절과 재시도.** 받는 쪽 핸들러가 실패하면(`exists`, `not_found` 같은 앱의 거절) 그 메시지는 전달을 마친 것으로 치고 + 호스트가 기록한다. 순서가 막히지 않게 하기 위해서다. 저장소 오류처럼 일시적인 실패는 나중에 다시 보낸다. + +## 호스트 + +1. `pure-run`에 여러 모듈과 배선을 준다(스크립트로 재현하는 검사용). +2. `pure-serve`가 한 프로세스에서 여러 앱을 돌리며 배선대로 전달한다. 앱마다 저장소와 소유자가 따로다. +3. 원격: 보내는 호스트가 받는 호스트에 HTTP로 보낸다(`POST /inputs/NAME`에 `X-Pure-Source`, `X-Pure-Sequence`). 받는 + `pure-serve`가 같은 중복 제거로 적용하고, 보내는 쪽은 저장된 답을 받은 뒤에 자른다. + +## 구현 순서 + +1. Pure: 출력 종류와 `SEND`, outbox(메모리, 되돌리기, 저장·열기), `pure_app_outbox`·`pure_app_trim`·`pure_app_deliver`, + `test-module`에서 모든 경계(실패한 이벤트의 `send`, 지연 저장, 저장 실패, 재시작, 중복 전달)를 검사한다. +2. aup: `output`, `send`, 진단, 두 앱을 잇는 참조 모델 대조. +3. `pure-run`의 여러 앱과 배선, 무작위 순간의 중단을 끼운 정확히-한-번 검사. +4. `pure-serve`의 한 프로세스 안 전달, 그다음 원격 전달. -- cgit v1.2.3