From 5ce7655d613689223050715ff3f8fe5c456ebf11 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Wed, 30 Sep 2026 05:58:38 +0100 Subject: Take a delivery's fields by name, dropping none A message from another node was taken only in its input's exact shape (the link's STREAM shape, fields and order alike; over HTTP, every field). So a board whose nodes are upgraded one after the other stopped both ways until both were, and an output and its input whose fields differ only in order were refused for good (docs/design/014). An input now takes a message when it has every field the sender sends, by name, of the same type or one that holds its values (a u32 in a number, text in bytes); a field only the input has is 0 or empty. One that sends a field the input lacks is refused: taking it would drop the value, and with its number heard the value would never come again (two boards apart for good). So add fields to receivers first and take them from senders first, and nothing stops or is lost. The link carries a sender's fields in a new frame, FIELDS (0x0d: names, shape, u8 count, each {type, name length, name}): after a REFUSED for another shape the sender puts it in its packets (after OFFER, before the records, so a receiver that knows no FIELDS reads what it read before) until the stream is taken, the first time at once. A receiver seeing another shape waits for the packet's FIELDS before refusing, and asks its host with them (PureLinkHost.input takes them); REFUSED says 4 when the input lacks a field. Deliveries carry the fields and the host lays each record out anew (pure_module_takes, pure_module_recast, pure_module_fields, over pure_send_recast's rule). A stream whose sender starts again in another shape lets go what it held; the sender sends it again laid out anew. A refusal already queued for a stream now says the last reason judged, not the first. Over HTTP a delivery (X-Pure-Source) may lack fields; an unknown member is still refused. test-link named: a stream of another shape naming its fields is taken, 200 stored in 47 ms with every delivery naming them; an input that does not take them refuses (4) and the sender backs off. check.py server_fields: five outputs, three linked and two over HTTP, fields in another order taken, an input with a field more taking messages without it, one lacking a field refusing until it starts again with it, then taking all. server_upgrade's new receiver declares its fields in another order. 012 names the frame and the rules. --- docs/design/012-link-protocol.md | 20 ++++++++-- docs/design/014-fields-by-name.md | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 docs/design/014-fields-by-name.md (limited to 'docs') diff --git a/docs/design/012-link-protocol.md b/docs/design/012-link-protocol.md index 99d7a91..373c618 100644 --- a/docs/design/012-link-protocol.md +++ b/docs/design/012-link-protocol.md @@ -19,7 +19,8 @@ run은 0이 아닌 무작위 u64다. - **받는 쪽**: 받아들일 보내는 쪽을 (source 이름, 공개 키) 목록으로 가진다. - **스트림**: source 이름과 input 이름의 쌍이다. 각 이름은 1–64 B다. 메시지의 shape(u32)가 함께 간다. 받는 쪽 입력의 - shape와 같아야 한다. 링크에게 shape는 비교만 하는 값이고, 뜻은 모듈 형식([008](008-module-format.md))이 정한다. + shape와 같거나, 보내는 쪽이 FIELDS로 이름을 댄 필드를 입력이 모두 가져야 한다([014](014-fields-by-name.md)). 링크에게 + shape와 필드 목록은 비교하고 나르기만 하는 값이고, 뜻은 모듈 형식([008](008-module-format.md))이 정한다. - **기록**: 스트림 안에서 1부터 연속된 번호(u64)를 가진 바이트열이다. 0–16 MiB다. 바이트는 보내는 앱이 저장한 그대로이고, 링크는 읽지 않는다. - **들은 번호**: 받는 쪽 앱이 적용하고 저장한 마지막 기록 번호다. @@ -110,7 +111,8 @@ PADDING도 그렇게 끝을 채운다. 이름은 u8 길이(1–64)와 그 바이 | 0x09 | OFFER | 보냄 | u32 에폭, 공개 키 32 B | | 0x0a | ACCEPT | 답 | u32 에폭, 공개 키 32 B, 답하는 OFFER 공개 키의 앞 4 B | | 0x0b | LOST | 답 | u64 패킷 번호 | -| 0x0c | REFUSED | 답 | names, u8 이유(1: 그 이름의 메시지 입력이 없다, 2: 입력의 shape가 다르다, 3: 이 키는 그 source로 보낼 수 없다) | +| 0x0c | REFUSED | 답 | names, u8 이유(1: 그 이름의 메시지 입력이 없다, 2: 입력의 shape가 다르다, 3: 이 키는 그 source로 보낼 수 없다, 4: 입력이 보낸 필드를 다 갖지 않는다) | +| 0x0d | FIELDS | 보냄 | names, u32 shape, u8 필드 수, 필드마다 {u8 타입, u8 이름 길이, 이름} | - RECORD와 RECORDS는 그 앞의 가장 가까운 STREAM의 스트림에 속한다. 앞에 STREAM이 없으면 버린다. - RECORDS는 번호가 이어진 온전한 기록들이다. LEB128은 7비트씩, 낮은 것부터, 10 B까지다. 기록 하나가 패킷 끝을 넘으면 그 @@ -171,11 +173,16 @@ STORED의 들은 번호 이하인 기록은 덜어 낸다. 보내는 앱은 그 **거절.** 자기 스트림에 대한 REFUSED를 들으면 기록을 보내지 않는다. +- 이유가 2(다른 shape)나 4이고 필드를 댈 수 있으면, 그 뒤 패킷마다 FIELDS를 싣는다. 자리는 OFFER 다음, 기록 앞이다. 처음 + 이유 2를 들었을 때는 기다리지 않고 곧바로 FIELDS를 실은 패킷 하나로 시도한다. - 기다린 뒤 기록을 실은 패킷 하나로 다시 시도한다. - 기다림은 1초에서 시작해 거절될 때마다 두 배로 늘고, 64초에서 멈춘다. - 시도한 패킷을 확인한 답에 REFUSED가 없으면 거절이 끝난다. - STORED를 들어도 끝나며, 이때 기다림도 처음으로 돌아간다. - 시도한 패킷보다 앞선 패킷들에 대한 REFUSED는 무시한다. +- 시도한 패킷을 잃었다고 판단하거나 에폭을 잃어 날던 패킷을 모두 놓으면, 다음 패킷이 다시 시도한다. 받는 쪽이 다시 떠서 시도가 + 아무에게도 닿지 않은 경우다([105](../research/105-growing-a-shared-board.md)). +- 스트림이 받아들여지면 FIELDS를 그만 싣는다. **경로 MTU**(DPLPMTUD, RFC 8899). @@ -229,7 +236,12 @@ STORED의 들은 번호 이하인 기록은 덜어 낸다. 보내는 앱은 그 - 들은 번호 + 65,536보다 큰 기록은 버린다. 그 안의 기록도 메모리(16 MiB)를 넘으면 받지 않는다. 보내는 쪽은 저장 소식이 없으니 다시 보낸다. - 들은 번호 이하의 기록이 다시 오면 STORED를 다시 보낸다. -- 받는 쪽은 들은 번호 다음부터 번호 순서로 온전한 기록을 앱에 넘긴다. 앱이 적용하고 저장한 뒤에만 STORED의 들은 번호를 올린다. +- STREAM의 shape가 입력과 다르면 곧바로 거절하지 않고 그 패킷의 FIELDS를 본다. 같은 스트림의 FIELDS가 있고 입력이 그 필드를 + 모두 같은 이름, 같은 타입이나 넓은 타입(u32를 number로, text를 bytes로)으로 가지면 스트림을 받는다. 입력에만 있는 필드는 0이나 + 빈 값이 된다. 아니면 이유 4다. FIELDS가 없으면 이유 2다. 이미 있는 스트림에 다른 shape가 오면, 앱에 넘긴 전달이 없을 때 쥐던 + 기록을 놓고 새로 판단한다(보내는 쪽이 새 배치로 다시 보낸다). +- 받는 쪽은 들은 번호 다음부터 번호 순서로 온전한 기록을 앱에 넘긴다. FIELDS로 받은 스트림의 기록은 그 필드 목록과 함께 넘기고, + 앱이 입력의 배치로 옮겨 적용한다. 앱이 적용하고 저장한 뒤에만 STORED의 들은 번호를 올린다. 틈에서 멈추고, 틈 뒤의 기록을 버렸으면 깃발 1을 세운다. - 앱이 가득 차 더 받지 못하면(앱의 메모리 예산, [013](013-memory-budget.md)) 받는 쪽은 받지 못한 기록을 쥔 채 잠시 뒤 다시 넘긴다. 그동안 보내는 STORED에 깃발 2를 세운다. 다시 넘기는 간격은 지역 선택이지만, 보내는 쪽이 저장을 기다리는 1초보다 @@ -338,7 +350,7 @@ LOST는 그 패킷의 번호를 담아 에폭 0의 키로 봉인한다. 답 run ## 이 명세와 구현 - 구현은 `link/link-protocol.c`(상태, 소켓 없음)와 `link/link.c`(UDP, GSO/GRO, 스레드)다. -- 전송 코드는 C 3,161줄이다(프로토콜 2,574, UDP 587). 011의 한도는 5,000줄이다. +- 전송 코드는 C 3,272줄이다(프로토콜 2,670, UDP 602). 011의 한도는 5,000줄이다. - 이 명세를 쓰면서 구현의 구멍 셋을 찾아 고쳤다. 셋 다 `link/test-link.c`에 검사가 있다. 메모의 곁 자리와 허가의 크기까지 [102](../research/102-adoption-and-spec.md)에 있다. - 받는 쪽이 스트림을 받지 않을 때 보내는 쪽이 아무 말도 듣지 못해 매초 기록을 다시 보냈다. REFUSED를 더했다. diff --git a/docs/design/014-fields-by-name.md b/docs/design/014-fields-by-name.md new file mode 100644 index 0000000..6db07aa --- /dev/null +++ b/docs/design/014-fields-by-name.md @@ -0,0 +1,77 @@ +# 014 — 필드 이름으로 받는 전달 + +작성일: 2026-09-30. 상태: 구현. +[105](../research/105-growing-a-shared-board.md)(두 노드가 함께 쓰는 보드를 키우며)에서 시작한다. + +## 문제 + +다른 노드에서 온 메시지는 받는 입력과 모양이 똑같아야 받았다. 링크는 STREAM의 shape(필드의 이름과 타입, 그 순서까지)를 +입력의 것과 비교하고, 다르면 REFUSED(다른 모양)로 거절한다. HTTP 전달은 JSON이라 이름으로 읽지만, 빠진 필드가 있으면 400 +`missing_field`로 거절한다. + +- **한 노드씩 올리면 전달이 멈춘다.** 카드에 필드 하나(`who`)를 더한 보드에서, 먼저 올린 노드와 아직 옛 노드가 서로의 + 보내기를 모두 거절한다. 두 노드가 다 올라갈 때까지 아무 변경도 오가지 않는다. +- **순서만 달라도 영영 거절된다.** 출력과 입력을 따로 선언하므로(`output changed`, `input remote`) 필드 순서가 어긋나기 쉽다. + 같은 필드라도 순서가 다르면 거절되고, 이유는 보내는 쪽 stderr에 한 줄 남을 뿐이다. + +## 결정 + +**받는 입력은 보낸 필드를 모두 가지면 받는다.** 이름으로 맞춘다. + +- 보낸 쪽에 없는 입력의 필드는 0이나 빈 값이다. 새 입력이 옛 보내는 쪽의 메시지를 받는다. +- 입력에 없는 필드를 보내면 받지 않는다. 받으면 그 값을 버리게 된다. 옛 받는 쪽이 새 필드를 버린 채 적용하고 번호를 들으면, 나중에 + 올라가도 그 값은 다시 오지 않는다. 보드라면 두 노드가 영영 다른 판을 갖는다. 그래서 그 메시지는 받는 쪽이 올라갈 때까지 기다린다. +- 같은 이름은 같은 종류여야 한다. 넓히는 것은 된다(u32를 number로, text를 bytes로). 좁히는 것(number를 u32로, bytes를 text로)은 + 값을 잃을 수 있어 받지 않는다. +- 순서는 상관없다. + +그래서 필드를 **더할 때는 받는 쪽부터**, **뺄 때는 보내는 쪽부터** 올리면 멈추지도 잃지도 않는다. 한 노드가 보내고 받는 +보드에서는, 먼저 올린 노드가 옛 노드의 변경을 받는 동안 제 새 필드는 옛 노드가 올라갈 때까지 기다린다. + +이것은 대기 중인 보내기를 옮기는 규칙([105](../research/105-growing-a-shared-board.md), `pure_send_recast`)과 같은 규칙에서 "버리지 +않는다"를 더한 것이다. 보내는 앱이 제 출력에서 필드를 뺀 것은 그 앱의 결정이라 기다리는 보내기에서도 뺀다. 받는 쪽이 모르는 +필드를 버리는 것은 누구의 결정도 아니다. + +## 링크 + +프레임을 하나 더한다([012](012-link-protocol.md)). + +| 종류 | 이름 | 방향 | 배치 | +|---:|---|---|---| +| 0x0d | FIELDS | 보냄 | names, u32 shape, u8 필드 수, 필드마다 {u8 타입, u8 이름 길이, 이름} | + +- 보내는 쪽은 REFUSED(2: 다른 모양)를 들으면, 스트림이 받아들여질 때까지 패킷마다 FIELDS를 싣는다. 자리는 OFFER 다음, 기록 + 앞이다. 이 프레임을 모르는 받는 쪽은 거기서 읽기를 멈추므로, 그 앞의 RESPONSE와 OFFER는 읽고 기록은 읽지 않는다(지금처럼 + 거절된 스트림이다). +- 받는 쪽은 STREAM의 shape가 입력과 다를 때 곧바로 거절하지 않고 그 패킷의 FIELDS를 기다린다. 같은 스트림의 FIELDS가 오면 + 호스트가 결정의 규칙대로 판단한다. 받으면 스트림을 열고(뒤의 기록도 받는다), 아니면 REFUSED(4: 입력이 보낸 필드를 다 + 갖지 않는다)다. 패킷이 끝나도록 FIELDS가 없으면 REFUSED(2)다. +- 전달에는 스트림의 shape가 따라간다. 호스트는 입력과 다른 shape의 기록을 입력의 배치로 옮긴 뒤 적용한다. +- 이미 있는 스트림에 다른 shape가 오면(보내는 쪽이 새 모듈로 다시 떴다) 새 스트림처럼 판단한다. +- 옛 보내는 쪽은 FIELDS를 보내지 않으므로 지금처럼 거절된다. +- FIELDS는 한 패킷에 다른 프레임과 함께 들어갈 때만 싣는다. 필드가 아주 많거나 이름이 길어 1,200 B 패킷에 들어가지 않으면 + 전처럼 거절된다(필드 64개의 이름이 평균 15 B를 넘을 때다). +- 받은 스트림의 모양이 바뀌면(보내는 쪽이 새 모듈로 다시 떴다) 받는 쪽은 쥐던 기록을 놓는다. 보내는 쪽은 받았다는 답을 + 들었지만 저장 소식이 없으니 1초 뒤 새 배치로 다시 보낸다. 모양이 바뀔 때 한 번 1초가 걸린다. + +## HTTP + +`X-Pure-Source`가 있는 전달의 JSON은 빠진 필드를 0이나 빈 값으로 둔다. 모르는 멤버는 지금처럼 거절한다(버리지 않는다). +클라이언트의 요청은 지금처럼 모든 필드가 있어야 한다. + +## 검사 + +- test-link: 다른 모양의 스트림이 FIELDS 뒤에 받아들여져 모든 기록이 저장된다. 호스트가 받지 않으면 REFUSED(4)가 남는다. +- check.py: + - 필드 순서만 다른 출력과 입력이 이어진다. + - 새 입력이 옛 출력의 메시지를 받는다(빠진 필드는 빈 값). + - 옛 입력은 새 필드를 보내는 출력을 거절하고, 올라가면 받는다(보낸 값을 잃지 않는다). + - HTTP 전달도 같다. +- 보드: 한 노드씩 올리는 동안 옛 노드의 변경이 새 노드로 가고, 새 노드의 변경은 옛 노드가 올라간 뒤 간다. 끝에 두 판이 같다. + +## 중단 기준 + +| 조건 | 결정 | +|---|---| +| FIELDS가 들어간 패킷 때문에 옛 받는 쪽과의 링크가 전보다 나빠진다(거절 외의 것이 막힌다) | FIELDS를 따로 된 패킷으로 보낸다 | +| 옮기는 비용이 전달의 처리량을 눈에 띄게 줄인다 | 같은 모양이면 옮기지 않는다(이미 그렇다). 다른 모양의 비용을 잰다 | -- cgit v1.2.3