aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/105-growing-a-shared-board.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research/105-growing-a-shared-board.md')
-rw-r--r--docs/research/105-growing-a-shared-board.md119
1 files changed, 119 insertions, 0 deletions
diff --git a/docs/research/105-growing-a-shared-board.md b/docs/research/105-growing-a-shared-board.md
new file mode 100644
index 0000000..f0b8b97
--- /dev/null
+++ b/docs/research/105-growing-a-shared-board.md
@@ -0,0 +1,119 @@
+# 105 — 두 노드가 함께 쓰는 보드를 키우며
+
+2026-09-30. [103](103-review-004-defects.md)의 "다음" 둘째(리뷰 004의 권고)다. 보드·메신저 계열의 실제 앱 하나를 키우면서
+필드를 더하고 재배치하고, 연결을 끊고, 다시 띄우고 맞추며, 무엇에 걸리는지 적는다.
+
+착수 기준:
+
+- **바꿀 결정.** 앱을 키울 때 걸리는 것을 언어(aup), 런타임, 링크, 진단으로 가르고, 앱을 멈추게 하거나 데이터를 잃게 하는 것부터
+ 고친다.
+- **최소 확인.** Vega의 `examples/board.aup`에서 시작해, 두 노드가 서로에게 바뀐 카드를 보내는 보드를 키운다. 한 노드씩 올리는
+ 업그레이드를 거친 뒤 두 판이 같아야 하고, 그동안 잃은 변경이 없어야 한다.
+- **중단 기준.** 고치려면 런타임에 보드 전용 개념(카드, 판)을 넣어야 한다면 멈춘다.
+
+## 쉬운 말로
+
+- 알아낸 것:
+ - **필드 하나를 더하는 업그레이드가 두 노드를 영영 멈췄다.** 한 노드를 새 카드(`who`)로 올리면 두 노드가 서로의 보내기를 "다른
+ 모양"이라며 거절했다. 옛 노드는 옛 보내기가 기다린다는 이유로 새 모듈로 시작하지도 못했다. 나오는 말은 `Cannot start
+ the module on the store`뿐이었다.
+ - 그 밑에 결함이 둘 더 있었다. 거절된 스트림의 시험 패킷이 사라지면 링크가 영영 멈췄다. 링크는 필드의 순서까지 같아야 받았다.
+ - 모두 고쳤다(아래 "고친 것"). 지금은 한 노드를 올리는 동안에도 옛 노드의 변경이 새 노드로 가고, 새 노드의 새 필드는 옛 노드가
+ 올라갈 때까지 기다린다. 올린 뒤 1초 안에 두 판이 같다.
+- 곁에서 찾아 고친 것: 스냅숏 등록부의 참조 수 경쟁과 앱을 멈출 때의 누수(`eebdeea`가 남긴 것). AddressSanitizer로 누수를 세니
+ 보였다. `make check`가 이제 그 빌드도 돌린다.
+- 필드 순서를 바꾼 판(v4)은 한 노드만 올린 채로 0.1초 만에 같아진다. 두 노드를 갈라 같은 카드를 양쪽에서 고치면, 다시 이을 때
+ 나중 변경이 두 판 모두에서 이긴다. 두 노드를 다 멈췄다 띄워도 이어진다.
+- 남은 걸림: 보내기와 받은 것을 적용하는 곳은 여전히 필드마다 적는다(언어). 출력과 입력의 필드 목록은 `message like`로 한 번만
+ 적는다(aup `aa279fb`). 노드 이름은 시작 줄이 알린다(`f695e9a`).
+
+## 앱
+
+두 노드가 같은 모듈을 돈다. 카드를 더하거나 옮기거나 누구의 것으로 정하면 그 카드를 상대에게 보내고(`send changed`),
+상대에게서 온 변경(`on remote`)은 더 늦은 것이 이긴다(시각, 같으면 노드 이름). 노드는 서로를 `pure://`로 봉인해 잇는다.
+
+| 판 | 바뀐 것 |
+|---|---|
+| v2 | 공유: 카드 {title, lane, since, node}, `output changed`, `input remote` |
+| v3 | 카드가 누구의 것인가: `who` 필드와 `input assign` |
+| v4 | 카드와 메시지의 필드 순서를 바꿨다 |
+| v5 | 받는 입력을 `input remote = message like changed`로 적었다 |
+
+## 걸린 것
+
+| # | 걸림 | 어디 | 어떻게 됐나 |
+|---|---|---|---|
+| 1 | 카드의 "누가 바꿨나" 필드를 `by`로 쓸 수 없다(`list of Card by title`의 예약어) | aup | 진단은 분명하다. `node`로 썼다 |
+| 2 | 노드 이름을 주지 않으면 모듈 파일 이름(`board2`)이 된다. 링크의 source가 `board2.changed`가 되어 받는 쪽(`--sender a.changed`)이 거절했다 | pure-serve | `--name`으로 풀었다. 이제 시작 줄이 이름을 알리고 사용법이 기본값을 말한다(`f695e9a`) |
+| 3 | 필드 하나(`who`)를 더하려고 레코드, 출력, 입력, 보내기 셋, 받은 것을 적용하는 곳을 고쳤다 | aup | 출력과 입력은 `message like`로 한 번 적는다(aup `aa279fb`). 보내기와 적용은 여전히 필드마다다(남음) |
+| 4 | 한 노드씩 올리면 교착: 새 노드는 옛 보내기를, 옛 노드는 새 보내기를 거절하고, 옛 노드는 옛 보내기가 기다려 새 모듈로 시작할 수 없다 | 런타임, 링크 | 고쳤다: 기다리는 보내기를 이름으로 옮긴다(`290cbdb`), 필드를 이름으로 받는다(014) |
+| 5 | 시작이 거절될 때 이유를 말하지 않는다 | 런타임 | 고쳤다: `PureApp.why`, pure-serve와 Vega가 찍는다 |
+| 6 | 옛 노드가 새 노드의 보내기를 거절하는 동안 옛 노드가 다시 뜨면, 새 노드의 링크가 영영 멈췄다 | 링크 | 고쳤다(`d6f4eb8`) |
+| 7 | 출력과 입력의 필드가 같아도 순서가 다르면 영영 거절된다 | 링크 | 고쳤다(014) |
+
+### 4. 업그레이드의 교착
+
+리뷰 004의 F3는 필드가 바뀐 출력에 보내기가 기다리면 모듈을 거절하게 했다. 그 바이트의 뜻이 바뀌기 때문이다. 안전하지만,
+한 노드씩 올리는 업그레이드에서는 빠져나갈 길이 없었다. 링크도 입력과 모양이 똑같아야 받았다.
+
+- **기다리는 보내기는 이름으로 옮긴다.** 목록의 항목이 모듈이 바뀔 때 옮겨지는 규칙과 같다. 새 타입이 담을 수 있는 값은 가고,
+ 나머지는 0이나 빈 값이다(`pure_send_recast`). 시작과 교체 모두 한다.
+- **옮긴 뜻은 죽어도 남아야 한다.** 옮긴 보내기를 outbox 이미지에 쓴 뒤, 새 필드를 links에 적는 저장 전에 죽으면, 다음
+ 시작이 한 번 더 옮겨 뜻이 뒤섞인다. 그래서 outbox 이미지가 제 보내기의 필드 이름을 스스로 적는다("PLO1"). 목록 이미지가
+ 레코드의 필드 이름을 적는 것과 같다. 모듈의 출력이 이미지와 다르면 보내기를 기록하기 전에 이미지를 다시 쓴다.
+ test-module `output_meaning`이 교체 뒤 저장 없이 멈췄다가 다시 떠도 뜻이 맞는지 본다.
+- **필드는 이름으로 받는다**([설계 014](../design/014-fields-by-name.md)). 받는 입력이 보낸 필드를 모두 가지면 받는다. 입력에만
+ 있는 필드는 0이나 빈 값이다. 입력에 없는 필드를 보내면 받지 않는다. 받으면 그 값을 버리게 되고, 버린 채 번호를 들으면 그
+ 값은 다시 오지 않아 두 판이 영영 달라진다. 그래서 필드를 더할 때는 받는 쪽부터, 뺄 때는 보내는 쪽부터 올리면 멈추지도 잃지도
+ 않는다.
+
+### 6. 사라진 시험 패킷
+
+거절된 스트림의 보내는 쪽은 기다림마다 기록을 실은 패킷 하나로 시도하고, 그 패킷이 REFUSED 없이 확인되면 거절이 끝난다. 시도가
+아무에게도 닿지 않으면(받는 쪽이 다시 뜨는 중이었다) 확인이 오지 않는다. 보내는 쪽은 에폭을 잃었다고 보고 새로 협상한 뒤에도
+그 시도를 기다리며 아무것도 보내지 않았다. 날고 있는 기록이 없으니 탐침도 없었다. 시도가 사라졌다고 판단되면(손실, 에폭을
+잃음) 다음 패킷이 다시 시도한다. test-link `refused_restarted`: 고치기 전 120초 동안 0건 저장, 고친 뒤 18.2초에 200건.
+
+### 갈라짐과 재동기화
+
+v4 두 노드에서, B를 아무도 듣지 않는 곳으로 떼어 놓고(듣는 포트와 보내는 곳을 둘 다 바꿔 띄운다) 같은 카드를 B가 먼저 `kim`에게,
+A가 나중에 `lee`에게 준다. B는 떨어진 채 카드 하나를 더한다. 떨어진 동안 두 판은 다르다. B를 제자리로 다시 띄우면 서로 기다리던
+변경이 가고, 두 판 모두 A의 나중 배정(`lee`)과 B가 만든 카드를 갖는다. 앱의 규칙(더 늦은 변경이 이긴다)과 링크의 "번호마다 한
+번"이 합쳐진 결과다. 런타임에 보드 전용 코드는 없다.
+
+## 결과
+
+한 노드씩 올리는 업그레이드(v2에서 v3). A를 먼저 올리고, A는 카드 하나를 `eun`의 것으로 정하고, 아직 v2인 B는 카드 하나를
+더한다. 3초 뒤 B를 올린다.
+
+| | 고치기 전 | 기다리는 보내기를 옮긴 뒤 | 필드를 이름으로 받은 뒤 |
+|---|---|---|---|
+| B가 v3로 시작 | 안 됨(교착) | 됨 | 됨 |
+| B의 새 카드가 A에 닿음 | 아니오 | B를 올린 뒤 | B가 v2인 동안 |
+| A의 배정이 B에 닿음 | 아니오 | 아니오(링크가 멈춤, 6) | B를 올린 뒤 |
+| 두 판이 같아짐 | 아니오 | 아니오 | B를 올리고 1.0초 뒤 |
+
+검사:
+
+- check.py `server_upgrade`: 받는 쪽이 꺼진 채 옛 출력으로 보낸 5건이 기다린다. 보내는 쪽이 새 출력으로 다시 뜨면 이름으로 옮긴다.
+ 출력이 없는 모듈은 그 이름을 대며 거절된다. 옛 받는 쪽은 `note`가 없어 거절하고(버리지 않는다), 필드 순서가 다른 새 받는
+ 쪽이 8건을 한 번씩 받는다.
+- check.py `server_fields`: 한 노드의 출력 다섯(링크 셋, HTTP 둘)이 한 받는 쪽으로 간다. 순서만 다른 필드를 받고, 필드가 하나
+ 더 있는 입력은 없는 메시지를 빈 값으로 받는다. 필드가 모자란 입력은 링크(REFUSED 4)와 HTTP(`json_field`) 모두에서 거절하고,
+ 받는 쪽이 그 필드를 갖고 다시 뜨면 기다리던 것이 모두 온다(값을 잃지 않는다).
+- test-link `named`: FIELDS를 대는 다른 모양의 스트림이 받아들여져 200건이 저장되고, 전달마다 필드 목록이 따라간다. 받지 않는
+ 입력은 이유 4로 거절한다.
+
+이어서(같은 두 노드, 같은 저장소):
+
+| 단계 | 결과 |
+|---|---|
+| v4를 A에만(필드 순서가 다르다) | 두 방향 모두 곧바로 간다. 0.1초 만에 같다 |
+| B도 v4 | 같다 |
+| 갈라 놓고 양쪽에서 같은 카드를 고침 | 떨어진 동안 다르다. 다시 이으면 나중 변경(`lee`)으로 같다 |
+| 둘 다 멈췄다 띄움, 카드 하나 더함 | 같다 |
+
+## 다음
+
+- 걸림 3의 나머지: 보내기와 받은 것의 적용을 필드마다 적지 않게 하는 것(레코드를 펼쳐 보내기 같은 것)은 aup의 일이다.
+- 다른 모양의 기록을 옮기는 비용은 재지 않았다(014의 중단 기준). 업그레이드하는 동안만 드는 비용이다.