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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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의 중단 기준). 업그레이드하는 동안만 드는 비용이다.
|