diff options
Diffstat (limited to 'docs/research/105-growing-a-shared-board.md')
| -rw-r--r-- | docs/research/105-growing-a-shared-board.md | 119 |
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의 중단 기준). 업그레이드하는 동안만 드는 비용이다. |
