diff options
| -rw-r--r-- | core/module.c | 65 | ||||
| -rw-r--r-- | core/module.h | 18 | ||||
| -rw-r--r-- | docs/design/012-link-protocol.md | 20 | ||||
| -rw-r--r-- | docs/design/014-fields-by-name.md | 77 | ||||
| -rw-r--r-- | link/link-protocol.c | 136 | ||||
| -rw-r--r-- | link/link-protocol.h | 26 | ||||
| -rw-r--r-- | link/link.c | 10 | ||||
| -rw-r--r-- | link/link.h | 4 | ||||
| -rw-r--r-- | link/test-link.c | 84 | ||||
| -rw-r--r-- | serve/serve.c | 43 | ||||
| -rw-r--r-- | tests/check.py | 193 |
11 files changed, 629 insertions, 47 deletions
diff --git a/core/module.c b/core/module.c index 97270ef..ea6809e 100644 --- a/core/module.c +++ b/core/module.c @@ -1211,6 +1211,71 @@ uint32_t pure_module_shape(const PureModule *m, uint32_t input) { return h; } +size_t pure_module_fields(const PureModule *m, uint32_t input, unsigned char out[PURE_FIELDS_NAMED]) { + const PureInputShape *s = &m->input[input]; + size_t at = 0; + out[at++] = (unsigned char)s->field_count; + for (uint32_t f = 0; f < s->field_count; ++f) { + const PureField *field = &m->field[s->field_start + f]; + size_t length = strlen(field->name); + out[at++] = field->type; + out[at++] = (unsigned char)length; + memcpy(out + at, field->name, length); + at += length; + } + return at; +} + +/* Fields as a sender names them, laid out: their count, or -1 when they are + * not well named (no rows: a sender sends none). */ +static int named_fields(const unsigned char *bytes, size_t size, PureField *fields) { + if (!size || bytes[0] > PURE_MODULE_FIELDS) return -1; + unsigned count = bytes[0]; + size_t at = 1; + for (unsigned f = 0; f < count; ++f) { + size_t length = size - at >= 2 ? bytes[at + 1] : 0; + if (size - at < 2 || !length || length >= PURE_MODULE_NAME || size - at < 2 + length || !bytes[at] || + bytes[at] >= PURE_FIELD_ROWS) + return -1; + fields[f] = (PureField){.type = bytes[at]}; + memcpy(fields[f].name, bytes + at + 2, length); + for (unsigned k = 0; k < f; ++k) + if (!strcmp(fields[k].name, fields[f].name)) return -1; + at += 2 + length; + } + if (at != size) return -1; + pure_fields_lay_out(fields, count); + return (int)count; +} + +bool pure_module_takes(const PureModule *m, uint32_t input, const unsigned char *fields, size_t size) { + PureField sent[PURE_MODULE_FIELDS]; + int count = named_fields(fields, size, sent); + if (count < 0) return false; + const PureInputShape *s = &m->input[input]; + for (int k = 0; k < count; ++k) { + uint32_t f = 0; + while (f < s->field_count && strcmp(m->field[s->field_start + f].name, sent[k].name)) ++f; + if (f == s->field_count) return false; /* its value would be dropped */ + uint8_t type = m->field[s->field_start + f].type, from = sent[k].type; + if (type != from && !(type == PURE_FIELD_NUMBER && from == PURE_FIELD_U32) && + !(type == PURE_FIELD_BYTES && from == PURE_FIELD_TEXT)) + return false; + } + return true; +} + +PureResult pure_module_recast(const PureModule *m, uint32_t input, const unsigned char *fields, size_t size, + PureSpan payload, unsigned char **out, size_t *out_size) { + PureField sent[PURE_MODULE_FIELDS]; + int count = named_fields(fields, size, sent); + *out = NULL; + *out_size = 0; + if (count < 0) return PURE_STORE_ERROR; + const PureInputShape *s = &m->input[input]; + return pure_send_recast(sent, (uint32_t)count, &m->field[s->field_start], s->field_count, payload, out, out_size); +} + int pure_module_view(const PureModule *m, const char *name) { for (uint32_t i = 0; i < m->views; ++i) if (!m->view_table[i] && !strcmp(m->view_names[i], name)) return (int)i; diff --git a/core/module.h b/core/module.h index ad31821..2cac3b3 100644 --- a/core/module.h +++ b/core/module.h @@ -217,8 +217,24 @@ int pure_module_view(const PureModule *, const char *name); int pure_module_column(const PureModule *, uint32_t table, const char *name); /* What a message of an input looks like, as 32 bits: its fields' names, * types and places (FNV-1a). A sender's output and the input it sends to - * across nodes must agree on it (link/link.h). */ + * across nodes agree on it, or the input takes the output's fields by name + * (link/link.h, docs/design/014-fields-by-name.md). */ uint32_t pure_module_shape(const PureModule *, uint32_t input); +/* An input's (or output's) fields as a sender names them to a receiver: u8 + * count, each field's {u8 type, u8 name length, name}. Its size (at most + * PURE_FIELDS_NAMED), written to out. */ +enum { PURE_FIELDS_NAMED = 1 + PURE_MODULE_FIELDS * (2 + PURE_MODULE_NAME) }; +size_t pure_module_fields(const PureModule *, uint32_t input, unsigned char out[PURE_FIELDS_NAMED]); +/* Whether input takes messages laid out with the fields named (as + * pure_module_fields names them): it has every one by name, of the same type + * or one that holds all its values (a u32 in a number, text in bytes). A + * field it has that they lack is 0 or empty; none of theirs is dropped. */ +bool pure_module_takes(const PureModule *, uint32_t input, const unsigned char *fields, size_t size); +/* A payload laid out with the fields named, laid out anew for input by + * name (pure_module_takes says it can be; out is malloc'd). STORE_ERROR + * when payload is not laid out with them. */ +PureResult pure_module_recast(const PureModule *, uint32_t input, const unsigned char *fields, size_t size, + PureSpan payload, unsigned char **out, size_t *out_size); /* The index state a number view shows as its position (a view that is just * `at.position`), or -1: what a host moves with pure_app_point. */ int pure_module_position(const PureModule *, uint32_t view); 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를 따로 된 패킷으로 보낸다 | +| 옮기는 비용이 전달의 처리량을 눈에 띄게 줄인다 | 같은 모양이면 옮기지 않는다(이미 그렇다). 다른 모양의 비용을 잰다 | diff --git a/link/link-protocol.c b/link/link-protocol.c index 4fa5aab..169705f 100644 --- a/link/link-protocol.c +++ b/link/link-protocol.c @@ -19,7 +19,7 @@ enum { DATAGRAM_MOST = PURE_LINK_DATAGRAM_MOST, BODY_MOST = DATAGRAM_MOST - HEAD * at least this much of it fits. */ enum { PART_LEAST = 64 }; enum { F_PADDING = 0, F_STREAM = 1, F_RECORD = 2, F_ACK = 3, F_STORED = 4, F_PING = 5, F_RECORDS = 6, F_CHALLENGE = 7, - F_RESPONSE = 8, F_OFFER = 9, F_ACCEPT = 10, F_LOST = 11, F_REFUSED = 12 }; + F_RESPONSE = 8, F_OFFER = 9, F_ACCEPT = 10, F_LOST = 11, F_REFUSED = 12, F_FIELDS = 13 }; /* STORED's flags: what came after a gap was not kept; the app is full (no * room for more now), and what came is kept for when it has room. */ enum { STORED_GAP = 1, STORED_FULL = 2 }; @@ -307,6 +307,11 @@ struct PureLinkSender { * answer with no REFUSED. */ unsigned refusal, tried; uint64_t refused_until, refused_wait, tried_pn; + /* The fields its records are laid out with, named (FIELDS) in each + * packet from a refusal for another shape until the stream is taken. */ + unsigned char *fields; + size_t fields_size; + bool naming; PureLinkCounts counts; /* Epochs (forward secrecy). Keys come from the secret of the epoch it * sends in (epoch 0's is the two nodes' static secret), each made from @@ -696,6 +701,7 @@ static void on_stored(PureLinkSender *l, const unsigned char *p, size_t size, ui unsigned flags = p[names + 8]; uint32_t credit = get32(p + names + 9); l->refusal = l->tried = 0, l->refused_until = l->refused_wait = 0; /* the stream is taken, and stored */ + l->naming = false; if (heard > l->stored) { while (l->count && record_at(l, 0)->seq <= heard) { Record *r = record_at(l, 0); @@ -725,8 +731,12 @@ static void on_stored(PureLinkSender *l, const unsigned char *p, size_t size, ui static void refused(PureLinkSender *l, unsigned why, uint64_t now) { if (l->refusal && l->tried != TRIED) return; l->refusal = why ? why : PURE_LINK_NO_INPUT; - l->refused_wait = l->refused_wait ? least(2 * l->refused_wait, REFUSED_MOST) : REFUSED_FIRST; - l->refused_until = now + l->refused_wait; + /* Another shape, and its fields not named yet: named in a packet at once. */ + bool naming = (why == PURE_LINK_OTHER_SHAPE || why == PURE_LINK_FIELDS_LACKED) && l->fields; + bool at_once = naming && !l->naming; + l->naming = l->naming || naming; + if (!at_once) l->refused_wait = l->refused_wait ? least(2 * l->refused_wait, REFUSED_MOST) : REFUSED_FIRST; + l->refused_until = at_once ? 0 : now + l->refused_wait; l->tried = 0; ++l->counts.refused; for (size_t k = 0; k < l->count; ++k) { @@ -804,7 +814,8 @@ void pure_link_sender_take(PureLinkSender *l, const unsigned char *packet, size_ } else if (type != F_PING) break; /* padding, or what this side does not know: the rest */ } - if (l->refusal && l->tried == TRIED && !refusing) l->refusal = l->tried = 0, l->refused_until = 0; /* taken now */ + if (l->refusal && l->tried == TRIED && !refusing) /* taken now */ + l->refusal = l->tried = 0, l->refused_until = 0, l->naming = false; } /* A packet of what is not sent yet, in order, within the receiver's credit @@ -831,6 +842,17 @@ static size_t fill(PureLinkSender *l, unsigned char *body, Flight *f, bool pad, memcpy(body + at + 5, l->offer_public, PURE_SEAL_KEY); at += EPOCH_FRAME; } + /* The fields, for a receiver whose input takes another shape: after the + * frames one that knows no FIELDS reads, before the records. */ + size_t names = 2 + strlen(l->source) + strlen(l->input); + if (l->naming && at + 1 + names + 4 + l->fields_size <= most) { + body[at++] = F_FIELDS; + at += put_names(body + at, l->source, l->input); + put32(body + at, l->shape); + at += 4; + memcpy(body + at, l->fields, l->fields_size); + at += l->fields_size; + } if (l->renegotiating) { /* no record under the static keys */ l->responding = 0; return at; @@ -1196,6 +1218,15 @@ uint64_t pure_link_sender_stored(const PureLinkSender *l) { return l->stored; } size_t pure_link_sender_held(const PureLinkSender *l) { return l->held; } +bool pure_link_sender_fields(PureLinkSender *l, const unsigned char *fields, size_t size) { + unsigned char *kept = size ? malloc(size) : NULL; + if (size && !kept) return false; + if (size) memcpy(kept, fields, size); + free(l->fields); + l->fields = kept, l->fields_size = size; + return true; +} + void pure_link_sender_counts(const PureLinkSender *l, PureLinkCounts *out) { *out = l->counts; out->stored = l->stored; @@ -1213,6 +1244,7 @@ void pure_link_sender_free(PureLinkSender *l) { explicit_bzero(l->epoch_secret, sizeof(l->epoch_secret)); explicit_bzero(l->before_secret, sizeof(l->before_secret)); explicit_bzero(l->offer_private, sizeof(l->offer_private)); + free(l->fields); free(l); } @@ -1298,6 +1330,8 @@ typedef struct Stream { size_t held_first, held_count, held_capacity, held_bytes; Run *run; /* the run that sent to it last: where STORED goes */ uint64_t run_id; + unsigned char *fields; /* the fields its records are laid out with, when its input took them by name */ + size_t fields_size; } Stream; struct Active { uint32_t peer; /* its place among the peers */ @@ -1720,6 +1754,7 @@ static void let_go(PureLinkReceiver *s, Active *a) { while (a->streams[k]->held_count) drop_first(a->streams[k]); free(a->streams[k]->held); free(a->streams[k]->line); + free(a->streams[k]->fields); free(a->streams[k]); } free(a->streams); @@ -1845,30 +1880,56 @@ static bool allowed(const PureLinkReceiver *s, const Peer *peer, const char *sou return false; } +/* Whose fields a stream's records are laid out with, kept (NULL, 0: its + * input's). False when memory runs out. */ +static bool keep_fields(Stream *st, const unsigned char *fields, size_t size) { + unsigned char *kept = fields && size ? malloc(size) : NULL; + if (fields && size && !kept) return false; + if (kept) memcpy(kept, fields, size); + free(st->fields); + st->fields = kept, st->fields_size = kept ? size : 0; + return true; +} + /* The stream of a peer's source to input, made the first time: NULL when it - * is refused (why: the source is not allowed from the peer, or the host has - * no input that takes such messages), or memory runs out (why 0). */ + * is refused (why: the source is not allowed from the peer, the host has no + * input that takes such messages, one of another shape unless it takes the + * fields named, which come with FIELDS), or memory runs out (why 0). A + * stream the sender now sends in another shape (it started again with + * another module) starts over with what it holds let go: the sender sends + * those again, laid out anew. */ static Stream *stream_of(PureLinkReceiver *s, Active *a, const char *source, const char *input, uint32_t shape, - unsigned *why) { + const unsigned char *fields, size_t fields_size, unsigned *why) { *why = 0; - for (unsigned k = 0; k < a->stream_count; ++k) { - Stream *st = a->streams[k]; - if (strcmp(st->input_name, input) || strcmp(st->source, source)) continue; - if (st->shape != shape) *why = PURE_LINK_OTHER_SHAPE; - return *why ? NULL : st; - } - int index = allowed(s, &s->peers[a->peer], source) ? s->host.input(s->host.context, input, shape) - : -PURE_LINK_NOT_ALLOWED; + Stream *st = NULL; + for (unsigned k = 0; k < a->stream_count && !st; ++k) + if (!strcmp(a->streams[k]->input_name, input) && !strcmp(a->streams[k]->source, source)) st = a->streams[k]; + if (st && st->shape == shape) return st; + int index = !allowed(s, &s->peers[a->peer], source) ? -PURE_LINK_NOT_ALLOWED : + s->host.input(s->host.context, input, shape, fields, fields ? fields_size : 0); if (index < 0) { - *why = index == -PURE_LINK_OTHER_SHAPE || index == -PURE_LINK_NOT_ALLOWED ? (unsigned)-index : PURE_LINK_NO_INPUT; + *why = index == -PURE_LINK_OTHER_SHAPE || index == -PURE_LINK_NOT_ALLOWED || index == -PURE_LINK_FIELDS_LACKED + ? (unsigned)-index : PURE_LINK_NO_INPUT; return NULL; } + if (st) { /* another shape now */ + if (st->out) { *why = PURE_LINK_OTHER_SHAPE; return NULL; } /* its last delivery first */ + if (!keep_fields(st, fields, fields_size)) return NULL; + while (st->held_count) drop_first(st); + st->line_head = st->line_size = 0; + st->line_count = 0; + st->tail = st->known = false; + st->input = (uint32_t)index; + st->shape = shape; + return st; + } size_t capacity = a->stream_capacity; - Stream **streams = room(a->streams, &capacity, a->stream_count, sizeof(*streams), 2), *st; + Stream **streams = room(a->streams, &capacity, a->stream_count, sizeof(*streams), 2); if (!streams) return NULL; a->streams = streams; a->stream_capacity = (unsigned)capacity; if (!(st = calloc(1, sizeof(*st)))) return NULL; + if (!keep_fields(st, fields, fields_size)) { free(st); return NULL; } st->active = a; st->input = (uint32_t)index; st->shape = shape; @@ -2023,7 +2084,8 @@ static bool hand_over(PureLinkReceiver *s, Stream *st) { size_t bytes = st->line_size - st->line_head; /* the whole line, when it is a batch or less */ if (n > BATCH) for (n = 0, bytes = 0; n < BATCH; ++n) bytes += 4 + (size_t)get32(st->line + st->line_head + bytes); - PureLinkDelivery d = {st->input, st->source, st->line_first, n, st->line + st->line_head, bytes, st}; + PureLinkDelivery d = {st->input, st->source, st->line_first, n, st->line + st->line_head, bytes, st, + st->fields, st->fields_size}; if (!s->host.deliver(s->host.context, &d)) return false; st->out = true; st->out_count = n, st->out_bytes = bytes; @@ -2052,7 +2114,8 @@ static bool hand_over(PureLinkReceiver *s, Stream *st) { memcpy(s->batch + at + 4, held_at(st, k)->bytes, held_at(st, k)->size); at += 4 + held_at(st, k)->size; } - PureLinkDelivery d = {st->input, st->source, held_at(st, first)->seq, (unsigned)n, s->batch, bytes, st}; + PureLinkDelivery d = {st->input, st->source, held_at(st, first)->seq, (unsigned)n, s->batch, bytes, st, + st->fields, st->fields_size}; if (!s->host.deliver(s->host.context, &d)) return false; st->out = true; ++s->counts.deliveries; @@ -2215,8 +2278,10 @@ static void lost(PureLinkReceiver *s, const Peer *peer, const Active *a, uint64_ * to its run: once a turn. */ static void refuse(PureLinkReceiver *s, Run *r, const unsigned char *names, size_t size, unsigned why) { for (size_t k = 0; k < s->refusal_count; ++k) - if (s->refusals[k].run == r && s->refusals[k].size == size + 2 && !memcmp(s->refusals[k].frame + 1, names, size)) + if (s->refusals[k].run == r && s->refusals[k].size == size + 2 && !memcmp(s->refusals[k].frame + 1, names, size)) { + s->refusals[k].frame[1 + size] = (unsigned char)why; /* said once, as last judged (named fields, say) */ return; + } void *more = room(s->refusals, &s->refusal_capacity, s->refusal_count, sizeof(*s->refusals), 4); if (!more) return; s->refusals = more; @@ -2318,6 +2383,10 @@ static bool take_from(PureLinkReceiver *s, Peer *peer, const unsigned char *pack Stream *st = NULL; bool eliciting = false, records = false; const unsigned char *offer = NULL; /* taken after the frames: whether records came with it */ + /* A stream of another shape: refused at the end unless this packet's + * FIELDS names fields its input takes. */ + const unsigned char *other = NULL; + size_t other_names = 0; for (size_t at = 0; at < size;) { unsigned type = plain[at++]; if (type == F_STREAM) { @@ -2325,14 +2394,36 @@ static bool take_from(PureLinkReceiver *s, Peer *peer, const unsigned char *pack size_t names = get_names(plain + at, size - at, source, input); if (!names || at + names + 4 > size) break; unsigned why; - st = stream_of(s, a, source, input, get32(plain + at + names), &why); + st = stream_of(s, a, source, input, get32(plain + at + names), NULL, 0, &why); if (st) { st->run = r; st->run_id = r->run; - } else if (why) + } else if (why == PURE_LINK_OTHER_SHAPE) + other = plain + at, other_names = names; + else if (why) refuse(s, r, plain + at, names, why); at += names + 4; eliciting = true; + } else if (type == F_FIELDS) { + char source[PURE_LINK_NAME + 1], input[PURE_LINK_NAME + 1]; + size_t names = get_names(plain + at, size - at, source, input), length = 1; + if (!names || at + names + 5 > size) break; + const unsigned char *fields = plain + at + names + 4; + for (unsigned f = 0; f < fields[0] && length != SIZE_MAX; ++f) /* each {type, name length, name} */ + length = at + names + 4 + length + 2 <= size ? length + 2 + fields[length + 1] : SIZE_MAX; + if (length == SIZE_MAX || at + names + 4 + length > size) break; + if (other && names == other_names && !memcmp(plain + at, other, names + 4)) { + unsigned why; + st = stream_of(s, a, source, input, get32(plain + at + names), fields, length, &why); + if (st) { + st->run = r; + st->run_id = r->run; + } else if (why) + refuse(s, r, other, other_names, why); + other = NULL; + } + at += names + 4 + length; + eliciting = true; } else if (type == F_RECORD) { if (at + RECORD_HEAD - 1 > size) break; uint32_t length = get16(plain + at + 16); @@ -2384,6 +2475,7 @@ static bool take_from(PureLinkReceiver *s, Peer *peer, const unsigned char *pack } else break; /* padding, or what this side does not know: the rest */ } + if (other) refuse(s, r, other, other_names, PURE_LINK_OTHER_SHAPE); /* its fields not named */ if (offer) offer_taken(s, a, r, get32(offer), offer + 4, e, !records, now); if (eliciting) { r->ack_due = true; diff --git a/link/link-protocol.h b/link/link-protocol.h index e4d3080..feff543 100644 --- a/link/link-protocol.h +++ b/link/link-protocol.h @@ -111,6 +111,12 @@ * sender sends one packet of them a wait from then (a * second, twice as long each time, 64 s at most) until one * is stored + * 0x0d FIELDS source, input, u32 shape (as STREAM), then the fields the + * stream's records are laid out with (core/module.h's + * pure_module_fields): after a REFUSED for another shape, + * in every packet (after OFFER, before records) until the + * stream is taken. A receiver whose input has every field + * takes it by name (docs/design/014-fields-by-name.md) * 0x00 PADDING to the end of the packet: a full packet sent in a burst is * a whole datagram * Losses are detected as RFC 9002 says, with RACK's patience for packets @@ -129,9 +135,10 @@ enum { PURE_LINK_DATAGRAM = 1200, PURE_LINK_DATAGRAM_MOST = 1452, PURE_LINK_NAME = 64, PURE_LINK_ADDRESS = 48, PURE_LINK_RECORD_MOST = 16 << 20 }; /* Why a receiver refuses a stream (REFUSED): no input of that name takes - * messages, the input takes messages of another shape, or the sender's key - * may not send as that source. */ -enum { PURE_LINK_NO_INPUT = 1, PURE_LINK_OTHER_SHAPE = 2, PURE_LINK_NOT_ALLOWED = 3 }; + * messages, the input takes messages of another shape (send FIELDS), the + * sender's key may not send as that source, or the input lacks fields the + * stream sends (it would drop them). */ +enum { PURE_LINK_NO_INPUT = 1, PURE_LINK_OTHER_SHAPE = 2, PURE_LINK_NOT_ALLOWED = 3, PURE_LINK_FIELDS_LACKED = 4 }; /* Counts for a host's report: packets and records sent, records sent again, * packets lost and acknowledged, packets received, deliveries handed over, @@ -193,6 +200,9 @@ unsigned pure_link_sender_refusal(const PureLinkSender *); /* How often it moves on to a new epoch (ns), and after how many bytes sent * (an hour and a GiB at first). */ void pure_link_sender_epochs(PureLinkSender *, uint64_t every_ns, uint64_t bytes); +/* The fields its records are laid out with (pure_module_fields), for a + * receiver whose input takes another shape (FIELDS); none at first. */ +bool pure_link_sender_fields(PureLinkSender *, const unsigned char *fields, size_t size); /* The last number queued; the last the receiver stored. */ uint64_t pure_link_sender_queued(const PureLinkSender *); uint64_t pure_link_sender_stored(const PureLinkSender *); @@ -206,7 +216,9 @@ void pure_link_sender_free(PureLinkSender *); * pure_link_idle). A stream (a source to an input) goes to the host's input * its name and shape find: its number, or -PURE_LINK_OTHER_SHAPE when it * takes another shape, or -1 (PURE_LINK_NO_INPUT) when none takes messages - * by that name; a stream refused is said to be (REFUSED). Whole + * by that name; asked again with the fields the sender names (FIELDS), + * its number when it takes them by name, else -PURE_LINK_FIELDS_LACKED. A + * stream refused is said to be (REFUSED). Whole * records of a stream, in order, go to deliver as one delivery; the host * applies them from the one after the source's last heard number, stops at * a gap, saves, and then says so with a done (pure_link_receiver_turn). @@ -220,9 +232,13 @@ typedef struct { const unsigned char *records; /* count of: u32le size, bytes (the receiver's: the host's during the call) */ size_t size; void *token; /* for its done */ + /* The fields the records are laid out with when the input took them by + * name (the host lays each out anew for it), else NULL. */ + const unsigned char *fields; + size_t fields_size; } PureLinkDelivery; typedef struct { - int (*input)(void *context, const char *name, uint32_t shape); + int (*input)(void *context, const char *name, uint32_t shape, const unsigned char *fields, size_t fields_size); bool (*deliver)(void *context, const PureLinkDelivery *); void *context; } PureLinkHost; diff --git a/link/link.c b/link/link.c index ad9376c..8f36e21 100644 --- a/link/link.c +++ b/link/link.c @@ -210,6 +210,13 @@ uint64_t pure_link_delivered(PureLink *l) { return stored; } +bool pure_link_fields(PureLink *l, const unsigned char *fields, size_t size) { + pthread_mutex_lock(&l->lock); + bool kept = pure_link_sender_fields(l->sender, fields, size); + pthread_mutex_unlock(&l->lock); + return kept; +} + size_t pure_link_held(PureLink *l) { pthread_mutex_lock(&l->lock); size_t held = pure_link_sender_held(l->sender); @@ -380,7 +387,8 @@ static void *run_link(void *context) { "trying again\"}\n", l->host, l->port, l->input, l->source, refusal == PURE_LINK_NOT_ALLOWED ? "this node's key may not send as that source" : refusal == PURE_LINK_OTHER_SHAPE ? "its input takes messages of another shape" - : "it has no input of that name taking messages"); + : refusal == PURE_LINK_FIELDS_LACKED ? "its input lacks fields this output sends (it would drop them)" + : "it has no input of that name taking messages"); } return NULL; } diff --git a/link/link.h b/link/link.h index 64846ce..c8d8635 100644 --- a/link/link.h +++ b/link/link.h @@ -18,6 +18,10 @@ typedef struct PureLink PureLink; PureLink *pure_link_open(const char *url, const char *source, uint32_t shape, const PureSealKey *own, const unsigned char own_public[PURE_SEAL_KEY], char *error, size_t size); void pure_link_peer(PureLink *, const unsigned char peer[PURE_SEAL_KEY]); +/* The fields its sends are laid out with (core/module.h's + * pure_module_fields): a receiver whose input takes another shape may take + * them by name (docs/design/014-fields-by-name.md). */ +bool pure_link_fields(PureLink *, const unsigned char *fields, size_t size); bool pure_link_queue(PureLink *, uint64_t seq, const unsigned char *bytes, size_t size); uint64_t pure_link_queued(PureLink *); /* The last number the receiver saved; any thread. */ diff --git a/link/test-link.c b/link/test-link.c index 66899ff..2e7509a 100644 --- a/link/test-link.c +++ b/link/test-link.c @@ -102,6 +102,8 @@ typedef struct { * handed records meanwhile, and how long after it had room again. */ uint64_t full_at, full_ms, full_until, full_tries, full_late; bool later_open; /* the app has an input "later" (as "order") */ + bool takes; /* its input takes the fields a sender of another shape names (NAMED) */ + uint64_t named_deliveries; /* deliveries that came with those fields */ } World; static void put_packet(World *w, bool to_receiver, const unsigned char *bytes, size_t size) { @@ -146,10 +148,15 @@ static bool answer(void *context, const unsigned char *packet, size_t size, cons return true; } -static int input_of(void *context, const char *name, uint32_t shape) { +/* The fields a sender of another shape (0x5eee) names; opaque to the link. */ +static const unsigned char NAMED[] = {2, 4, 4, 'i', 't', 'e', 'm', 1, 5, 'c', 'o', 'u', 'n', 't'}; + +static int input_of(void *context, const char *name, uint32_t shape, const unsigned char *fields, size_t size) { const World *w = context; if (strcmp(name, "order") && !(w && w->later_open && !strcmp(name, "later"))) return -PURE_LINK_NO_INPUT; - return shape == 0x5eed ? 3 : -PURE_LINK_OTHER_SHAPE; + if (shape == 0x5eed) return 3; + if (!fields) return -PURE_LINK_OTHER_SHAPE; + return w && w->takes && size == sizeof(NAMED) && !memcmp(fields, NAMED, size) ? 3 : -PURE_LINK_FIELDS_LACKED; } /* Applied from the one after the last heard, in order; a gap stops it. */ @@ -158,6 +165,7 @@ static bool deliver(void *context, const PureLinkDelivery *d) { CHECK(d->input == 3 && !strcmp(d->source, "test.ordered"), "a delivery to input %u from %s", d->input, d->source); if (w->done_count == sizeof(w->done) / sizeof(w->done[0])) return false; /* busy: tried again */ ++w->deliveries; + w->named_deliveries += d->fields && d->fields_size == sizeof(NAMED) && !memcmp(d->fields, NAMED, sizeof(NAMED)); if (w->full_ms && !w->full_until && w->heard >= w->full_at) w->full_until = w->now + w->full_ms * MS; if (w->now < w->full_until) { ++w->full_tries; @@ -682,8 +690,8 @@ typedef struct { PureLinkDone done[64]; size_t done_count; } Outputs; -static int output_input(void *context, const char *name, uint32_t shape) { - (void)context; +static int output_input(void *context, const char *name, uint32_t shape, const unsigned char *fields, size_t size) { + (void)context, (void)fields, (void)size; return shape != 0x5eed ? -1 : !strcmp(name, "one") ? 0 : !strcmp(name, "two") ? 1 : !strcmp(name, "three") ? 2 : -1; } static bool output_deliver(void *context, const PureLinkDelivery *d) { @@ -958,6 +966,73 @@ static void refused_restarted(const char *dir) { free(w.packets); } +/* A stream of another shape whose sender names its fields (FIELDS): the + * receiver's input takes them by name and every record is stored, each + * delivery naming them for the host to lay the records out anew; an input + * that does not take them refuses the stream (it would drop fields), and the + * sender tries a packet a wait as for any refusal. One that names none stays + * refused for another shape (refusals, above). */ +static void named(const char *dir) { + for (int takes = 1; takes >= 0; --takes) { + unsigned char public_a[PURE_SEAL_KEY], public_b[PURE_SEAL_KEY], out[PURE_LINK_DATAGRAM_MOST]; + PureSealKey *a = key(dir, "a.key", public_a), *b = key(dir, "b.key", public_b); + char memo[512]; + snprintf(memo, sizeof(memo), "%s/b.key.links", dir); + unlink(memo); + World w = {.packets = malloc(PACKETS * sizeof(Packet)), .takes = takes}; + if (!w.packets) exit(1); + PureLinkReceiver *r = receiver_of(&w, b, public_b, public_a, memo, 120000); + PureLinkSender *s = pure_link_sender("test.ordered", "order", 0x5eee, a, public_a); + if (!s || !pure_link_sender_peer(s, public_b) || !pure_link_sender_fields(s, NAMED, sizeof(NAMED))) exit(1); + static unsigned char bytes[20200]; + for (uint64_t seq = 1; seq <= 200; ++seq) { + size_t size = record_size(seq); + for (size_t k = 0; k < size; ++k) bytes[k] = record_byte(seq, k); + if (!pure_link_sender_queue(s, seq, bytes, size)) exit(1); + } + uint64_t number, packets = 0, stored_at = 0; + for (; w.now < 30000 * MS; w.now += MS) { + for (size_t size; (size = pure_link_sender_packet(s, out, false, w.now, &number)); ++packets) + pure_link_receiver_take(r, out, size, "sender0", 7, w.now); + size_t told = w.done_count; + memcpy(w.told, w.done, told * sizeof(*w.done)); + w.done_count = 0; + pure_link_receiver_turn(r, w.told, told, w.now, answer, &w); + for (size_t k = 0; k < w.count;) { + if (w.packets[k].at > w.now) { + ++k; + continue; + } + pure_link_sender_take(s, w.packets[k].bytes, w.packets[k].size, w.now); + w.packets[k] = w.packets[--w.count]; + } + pure_link_sender_tick(s, w.now); + if (!stored_at && pure_link_sender_stored(s) == 200) stored_at = w.now; + } + PureLinkCounts c; + pure_link_sender_counts(s, &c); + unsigned why = pure_link_sender_refusal(s); + printf("{\"case\":\"named\",\"takes\":%d,\"why\":%u,\"refused\":%llu,\"packets\":%llu,\"stored\":%llu," + "\"stored_ms\":%llu,\"named_deliveries\":%llu}\n", takes, why, (unsigned long long)c.refused, + (unsigned long long)packets, (unsigned long long)pure_link_sender_stored(s), + (unsigned long long)(stored_at / MS), (unsigned long long)w.named_deliveries); + if (takes) + CHECK(!why && c.refused == 1 && stored_at && stored_at < 1000 * MS && w.heard == 200 && + w.named_deliveries == w.deliveries, + "named and taken: why %u, refused %llu, stored at %llu ms, %llu of %llu deliveries named", why, + (unsigned long long)c.refused, (unsigned long long)(stored_at / MS), + (unsigned long long)w.named_deliveries, (unsigned long long)w.deliveries); + else /* refused at once, named at once, then at 1, 3, 7 and 15 s */ + CHECK(why == PURE_LINK_FIELDS_LACKED && c.refused == 6 && !pure_link_sender_stored(s) && packets <= 20, + "named and not taken: why %u, refused %llu, %llu packets", why, (unsigned long long)c.refused, + (unsigned long long)packets); + pure_link_sender_free(s); + pure_link_receiver_free(r); + pure_seal_key_free(a), pure_seal_key_free(b); + free(w.packets); + } +} + /* The largest send (PURE_LINK_RECORD_MOST) goes whole, over a receiver that * never heard of the stream before; one byte more is not taken to send. */ typedef struct { World *w; PureLinkSender *s; } Largest; @@ -1089,6 +1164,7 @@ int main(void) { outputs(dir); refusals(dir); refused_restarted(dir); + named(dir); largest(dir); char command[600]; snprintf(command, sizeof(command), "rm -rf %s", dir); diff --git a/serve/serve.c b/serve/serve.c index d06f430..319f165 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -843,8 +843,17 @@ static const char *convert(Connection *c) { memset(b.seen, 0, sizeof(b.seen)); if (!why) why = pure_json_message(&b, json, size, &at); at = pure_json_space(json, at, size); - for (uint32_t f = 0; !why && f < shape->field_count; ++f) - if (!b.seen[f] && !c->given[f]) why = "missing_field"; + /* A delivery from another node may lack fields its sender's module has + * not (docs/design/014-fields-by-name.md): they are 0 or empty. A + * member naming no field is refused all the same: it would be dropped. */ + for (uint32_t f = 0; !why && f < shape->field_count; ++f) { + if (b.seen[f] || c->given[f]) continue; + const PureField *field = &c->host->module->field[shape->field_start + f]; + if (!delivered || field->type == PURE_FIELD_ROWS) why = "missing_field"; + else if (field->type == PURE_FIELD_U32 || field->type == PURE_FIELD_NUMBER) + memset(b.payload + field->offset, 0, field->type == PURE_FIELD_NUMBER ? 8 : 4); + else put_u32(b.table + 8 * f, 0), put_u32(b.table + 8 * f + 4, 0); + } if (!why && c->batch > 1 && b.rows_size) why = "delivery"; /* an output sends no rows */ } if (!why && c->batch > 1 && (at == size || json[at++] != ']')) why = "json"; @@ -1374,6 +1383,9 @@ static PureResult apply_input(PureApp *app, const Route *route, const PureEffect uint64_t first = get_u64(delivery), heard = pure_app_heard(app, source); unsigned count = get_u32(delivery + 8); const unsigned char *at = effect->value.data + 4 + DELIVERY; + /* Records laid out with other fields (named after them) are laid out anew for the input. */ + size_t records = header & (STORED_RECORDS - 1), named = effect->value.size - 4 - DELIVERY - records; + const unsigned char *fields = named ? at + records : NULL; out->gap = false; out->fault = PURE_FAULT_NONE; for (unsigned i = 0; i < count; ++i) { @@ -1385,7 +1397,14 @@ static PureResult apply_input(PureApp *app, const Route *route, const PureEffect out->gap = true; break; } - PureResult status = pure_app_deliver(app, route->target, source, first + i, record, NULL); + unsigned char *recast = NULL; + size_t recast_size = 0; + if (fields && pure_module_recast(app->module, route->target, fields, named, record, &recast, &recast_size) != + PURE_OK) + break; /* not laid out as its fields were named: not applied, sent again */ + PureResult status = pure_app_deliver(app, route->target, source, first + i, + recast ? (PureSpan){recast, recast_size} : record, NULL); + free(recast); if (status != PURE_OK && !app_refused(app)) { /* not applied: sent again */ out->fault = app->fault; break; @@ -2375,12 +2394,15 @@ static void stop_shard(Host *host) { } /* The input a link's stream goes to: one taking messages (not keyed) of the - * sender's shape. */ -static int link_input(void *context, const char *name, uint32_t shape) { + * sender's shape, or, named its fields, one that takes them by name + * (pure_module_takes: each record is laid out anew for it). */ +static int link_input(void *context, const char *name, uint32_t shape, const unsigned char *fields, size_t size) { const PureModule *m = ((Owner *)context)->executor.app.module; int index = pure_module_input(m, name); if (index < 0 || m->input[index].kind != PURE_KIND_MESSAGE || m->input[index].keyed) return -PURE_LINK_NO_INPUT; - return pure_module_shape(m, (uint32_t)index) == shape ? index : -PURE_LINK_OTHER_SHAPE; + if (pure_module_shape(m, (uint32_t)index) == shape) return index; + if (!fields) return -PURE_LINK_OTHER_SHAPE; + return pure_module_takes(m, (uint32_t)index, fields, size) ? index : -PURE_LINK_FIELDS_LACKED; } /* Records a link delivered, queued at the owner as a request is: applied @@ -2388,7 +2410,7 @@ static int link_input(void *context, const char *name, uint32_t shape) { static PureLinkListener *link_listener; static bool link_deliver(void *context, const PureLinkDelivery *d) { Owner *owner = context; - size_t size = 4 + DELIVERY + d->size, length = strlen(d->source); + size_t size = 4 + DELIVERY + d->size + d->fields_size, length = strlen(d->source); unsigned char *ingress = malloc(size); LinkJob *link = malloc(sizeof(*link)); if (!ingress || !link || d->size > (STORED_RECORDS - 1) || length > PURE_SOURCE) { @@ -2403,6 +2425,7 @@ static bool link_deliver(void *context, const PureLinkDelivery *d) { ingress[16] = (unsigned char)length; memcpy(ingress + 17, d->source, length); memcpy(ingress + 4 + DELIVERY, d->records, d->size); + if (d->fields_size) memcpy(ingress + 4 + DELIVERY + d->size, d->fields, d->fields_size); /* laid out with those */ *link = (LinkJob){link_listener, d->token, ingress}; Job job = {.route = {.kind = ROUTE_INPUT, .target = d->input}, .link = link, .effect = {.input = PURE_INPUT_WRITE, .key = "", .expected = -1, .value = {ingress, size}}}; @@ -2451,6 +2474,12 @@ static bool open_wires(Executor *ex, const PureModule *m, const char *const *sen ? pure_link_open(equals + 1, source, pure_module_shape(m, (uint32_t)index), seal_key, own_public, error, sizeof(error)) : NULL; + unsigned char named[PURE_FIELDS_NAMED]; + if (link && !pure_link_fields(link, named, pure_module_fields(m, (uint32_t)index, named))) { + pure_link_close(link); + link = NULL; + snprintf(error, sizeof(error), "no memory"); + } if (!link) { fprintf(stderr, "--send %s: %s\n", sends[i], written > PURE_SOURCE ? "the name and output are longer than 64 bytes" : error); return false; diff --git a/tests/check.py b/tests/check.py index b377e20..7ccafc6 100644 --- a/tests/check.py +++ b/tests/check.py @@ -1339,9 +1339,9 @@ UPGRADED_RECEIVER = """state orders = 0 state noted = 0 state stock = 0 input order = message { - note: text item: text count: u32 + note: text } on order { orders = orders + 1 @@ -1360,8 +1360,9 @@ def server_upgrade(aup, pure, work): to.city}; started again with ordered {note, item, count} it recasts them by name (note empty, to.city gone) where it was refused before (review 004's F3), and a module without the output is refused naming it. The old - receiver refuses the new shape, so the sends wait; the upgraded receiver - takes all eight, once each.""" + receiver lacks note, so it refuses them (it would drop the field) and + they wait; the upgraded receiver, its fields in another order, takes all + eight by name, once each.""" import select import socket import time @@ -1439,6 +1440,191 @@ def server_upgrade(aup, pure, work): assert stats['a']['link_refused'] >= 1, stats['a'] return {'recast_waiting': 5, 'delivered': 8, 'refused_while_old': stats['a']['link_refused']} + +FIELDS_SENDER = """input buy = message { + item: text + count: u32 +} +output swapped = message { + count: u32 + item: text +} +output plain = message { + item: text + count: u32 +} +output noted = message { + item: text + count: u32 + note: text +} +output over_http = message { + item: text + count: u32 +} +output noted_http = message { + item: text + count: u32 + note: text +} +on buy { + send swapped { count: buy.count, item: buy.item } + send plain { item: buy.item, count: buy.count } + send noted { item: buy.item, count: buy.count, note: "kept" } + send over_http { item: buy.item, count: buy.count } + send noted_http { item: buy.item, count: buy.count, note: "kept" } +} +""" + + +def fields_receiver(narrow_notes): + """Inputs for FIELDS_SENDER's outputs; narrow ones lack note unless narrow_notes.""" + note = ' note: text\n' if narrow_notes else '' + kept = lambda name: f' if {name}.note == "kept" {{ {name}_kept = {name}_kept + 1 }}\n' if narrow_notes else '' + return (f"""state swapped_n = 0 +state swapped_total = 0 +state wider_n = 0 +state wider_blank = 0 +state narrow_n = 0 +state narrow_kept = 0 +state wider_http_n = 0 +state wider_http_blank = 0 +state narrow_http_n = 0 +state narrow_http_kept = 0 +input swapped_in = message {{ + item: text + count: u32 +}} +input wider = message {{ + note: text + item: text + count: u32 +}} +input narrow = message {{ + item: text + count: u32 +{note}}} +input wider_http = message {{ + item: text + count: u32 + note: text +}} +input narrow_http = message {{ + item: text + count: u32 +{note}}} +on swapped_in {{ + swapped_n = swapped_n + 1 + swapped_total = swapped_total + swapped_in.count +}} +on wider {{ + wider_n = wider_n + 1 + if wider.note == "" {{ wider_blank = wider_blank + 1 }} +}} +on narrow {{ + narrow_n = narrow_n + 1 +{kept('narrow')}}} +on wider_http {{ + wider_http_n = wider_http_n + 1 + if wider_http.note == "" {{ wider_http_blank = wider_http_blank + 1 }} +}} +on narrow_http {{ + narrow_http_n = narrow_http_n + 1 +{kept('narrow_http')}}} +view counts = "{{swapped_n}} {{swapped_total}} {{wider_n}} {{wider_blank}} {{narrow_n}} {{narrow_kept}} {{wider_http_n}} {{wider_http_blank}} {{narrow_http_n}} {{narrow_http_kept}}" +""") + + +def server_fields(aup, pure, work): + """Deliveries take their fields by name (docs/design/014-fields-by-name.md). + A sender's five outputs, three linked and two over HTTP, to one receiver: + fields in another order are taken; an input with a field more takes + messages without it (empty); an input lacking a field the output sends + refuses it, over the link (REFUSED, fields lacked) and over HTTP + (json_field), so nothing is dropped, and once the receiver starts again + with the field, every waiting message comes, the field kept.""" + import select + import socket + import time + modules = {} + for name, text in (('sender', FIELDS_SENDER), ('receiver-1', fields_receiver(False)), + ('receiver-2', fields_receiver(True))): + source = work / f'fields-{name}.aup' + source.write_text(text) + compile_module(aup, source, source.with_suffix('.pmod')) + modules[name] = source.with_suffix('.pmod') + + def free_port(kind=socket.SOCK_STREAM): + with socket.socket(socket.AF_INET, kind) as probe: + probe.bind(('127.0.0.1', 0)) + return probe.getsockname()[1] + + def start(args, port): + process = subprocess.Popen([str(a) for a in args] + ['--port', str(port)], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + ready, _, _ = select.select([process.stdout], [], [], 10) + line = process.stdout.readline() if ready else b'' + assert line, f'host did not start: {process.stderr.read(2000) if process.poll() is not None else ""}' + return process, json.loads(line) + + def stop(process): + process.terminate() + _, error = process.communicate(timeout=30) + assert process.returncode == 0, error[-1000:] + return error.decode() + + def counts(port): + return [int(n) for n in http(port, 'GET', '/views/counts')[3].split()] + + keys = {} + for name in ('sender', 'receiver'): + process, info = start([pure / 'build/pure-serve', '--module', modules['receiver-1'], '--store', + work / 'fields-key', '--key', work / f'fields-{name}.key'], 0) + keys[name] = info['public_key'] + stop(process) + port_a, port_b, link_port = free_port(), free_port(), free_port(socket.SOCK_DGRAM) + links = {'swapped': 'swapped_in', 'plain': 'wider', 'noted': 'narrow'} + sending = [pure / 'build/pure-serve', '--module', modules['sender'], '--store', work / 'fields-a', '--key', + work / 'fields-sender.key', '--name', 'up', + '--send', f'over_http=http://127.0.0.1:{port_b}/inputs/wider_http', + '--send', f'noted_http=http://127.0.0.1:{port_b}/inputs/narrow_http', + '--seal', f'over_http={keys["receiver"]}', '--seal', f'noted_http={keys["receiver"]}'] + for output, input_name in links.items(): + sending += ['--send', f'{output}=pure://127.0.0.1:{link_port}/{input_name}', '--seal', f'{output}={keys["receiver"]}'] + + def receiving(module): + args = [pure / 'build/pure-serve', '--module', modules[module], '--store', work / 'fields-b', '--key', + work / 'fields-receiver.key', '--link-port', link_port] + for output in (*links, 'over_http', 'noted_http'): + args += ['--sender', f'up.{output}={keys["sender"]}'] + return args + + running, said = {}, '' + try: + running['b'] = start(receiving('receiver-1'), port_b)[0] + running['a'] = start(sending, port_a)[0] + for i in range(3): + assert post(port_a, '/inputs/buy', {'item': f'i{i}', 'count': i + 1})[0] == 200 + deadline = time.time() + 20 + while counts(port_b)[:4] + counts(port_b)[6:8] != [3, 6, 3, 3, 3, 3] and time.time() < deadline: + time.sleep(0.05) + got = counts(port_b) + assert got == [3, 6, 3, 3, 0, 0, 3, 3, 0, 0], got # the narrow inputs took none: nothing is dropped + time.sleep(1.5) # the refused link tried with its fields named: the sender has said why + stop(running.pop('b')) + running['b'] = start(receiving('receiver-2'), port_b)[0] + deadline = time.time() + 30 + while counts(port_b)[4:6] + counts(port_b)[8:] != [3, 3, 3, 3] and time.time() < deadline: + time.sleep(0.1) + got = counts(port_b) + assert got == [3, 6, 3, 3, 3, 3, 3, 3, 3, 3], got + finally: + for name, process in running.items(): + text = stop(process) + said += text if name == 'a' else '' + assert 'its input lacks fields this output sends' in said, said[-2000:] + return {'links': 3, 'wires': 2, 'waited_then_taken': 6} + def server_links_independent(aup, pure, work): """A receiver written from docs/design/012-link-protocol.md alone, in Python with the cryptography package and no code of link/: a Pure node @@ -3146,6 +3332,7 @@ def main(): report['server_memory'] = server_memory(aup, pure, work) report['server_windows'] = server_windows(aup, pure, work) report['server_upgrade'] = server_upgrade(aup, pure, work) + report['server_fields'] = server_fields(aup, pure, work) report['server_links_independent'] = server_links_independent(aup, pure, work) report['server_links_moved'] = server_links_moved(aup, pure, work) report['server_webhook'] = server_webhook(aup, pure, work) |
