aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/105-growing-a-shared-board.md
blob: 8f683b0605056c96e3ac32da342541a5d39a3134 (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
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
120
121
122
123
124
125
126
127
128
129
130
131
# 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`), 보내기와 적용은 항목이나 메시지를
  펼쳐 쓴다(`send changed { ..at }`, `cards[k] = Card { ..remote }`, aup `c69682e`). 필드 하나를 더할 때 고칠 곳이 다섯에서
  둘(레코드와 출력)로 줄었다. 노드 이름은 시작 줄이 알린다(`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`로 적었다 |
| v6 | 카드를 통째로 보내고(`send changed { ..at }`) 통째로 적용한다(`cards[k] = Card { ..remote }`) |

## 걸린 것

| # | 걸림 | 어디 | 어떻게 됐나 |
|---|---|---|---|
| 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`), 보내기와 적용은 펼친다(`..at`, `cards[k] = Card { ..remote }`, aup `c69682e`). 이제 레코드와 출력 두 곳이다 |
| 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`)으로 같다 |
| 둘 다 멈췄다 띄움, 카드 하나 더함 | 같다 |
| v6를 둘 다(통째로 보내고 적용), 갈라 놓고 같은 카드를 고침 | 다시 이으면 나중 변경으로 같다 |

v6의 모듈은 펼치기를 필드마다 적은 v6와 바이트로 같다. 펼치기는 적는 방법일 뿐이고, 실행은 그대로다.

## 남긴 것

- 보드는 Vega의 예제가 됐다: `examples/shared-board.aup`(Vega `b3c01b3`). Vega의 검사 `shared_board`가 두 노드를 이어 카드를
  더하고 옮기고 배정한 뒤 두 판이 같은지, 한 노드를 떼어 놓고 같은 카드를 양쪽에서 고친 뒤 다시 이으면 나중 변경으로 같은지 본다.

## 다음

- 다른 모양의 기록을 옮기는 비용은 기록 하나에 310 ns였다(014). 필드 목록을 전달마다 한 번만 읽으면 더 줄지만, 업그레이드하는
  동안에만 드는 비용이라 그 때문에 API를 늘리지 않는다.
- 리뷰 004의 권고 가운데 남은 것은 [103](103-review-004-defects.md)의 셋째, 조건(저장 완료 기준, 지연, 메모리)을 먼저 정한 성능
  비교를 넓히는 일이다.