aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/078-app-connections.md
blob: fbe7f3481e9e32eec1ec07197f933872bbdcf702 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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`의 한 프로세스 안 전달, 그다음 원격 전달.