diff options
| author | eunhhu <[email protected]> | 2026-09-30 05:33:32 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-30 05:33:32 +0100 |
| commit | 9aa123ec5e8827a6101fa7c6db139397b39e04f2 (patch) | |
| tree | 4e50377e9a9f29f39c3a1e8ef37ce629dfbe8e17 /core/test-module.c | |
| parent | baaf955959d4b2187e93e35b64cd3f5b2330a59a (diff) | |
| download | pure-9aa123ec5e8827a6101fa7c6db139397b39e04f2.tar.gz pure-9aa123ec5e8827a6101fa7c6db139397b39e04f2.zip | |
Recast waiting sends by field name when an output's fields change
Review 004's F3 made a module refuse to start while sends waited on an
output whose fields it had changed: their bytes would have meant
something else. Safe, but it deadlocks a rolling upgrade. Growing a
board shared by two nodes: one upgraded to a card with a new field
refuses the other's old sends (another shape), and the other, its old
sends waiting, cannot start the new module at all ("Cannot start the
module on the store", saying nothing more).
Waiting sends now move to the new fields by name, as a list's items do:
a value goes over when the new type can hold it (u32 and number are
numbers, bytes and text are bytes; a number that is not a u32, or bytes
that are not UTF-8 for a text, cannot), the rest start at 0 or empty
(pure_send_recast, with the payload layout load uses, pure_fields_lay_out).
A start recasts them and a replace does into a new outbox.
Their meaning must survive a crash between the recast and the save that
names the new fields in the links. So the outbox's image now names the
fields its sends were laid out with ("PLO1": the module's outputs after
the save it holds), and is written again before any send is logged with
other fields; a start reads those names over the links'. Checkpoints of
the outbox name them too.
A module without an output that sends wait on is still refused, now
saying so in PureApp.why, which pure-serve prints ("sends wait on the
output 'ordered', which this module does not have"); a failed start
keeps it.
test-module send_recast (widened, narrowed, UTF-8, a payload not laid
out, there and back) and output_meaning (the swapped fields recast on a
replace, read right after a restart that never saved, back again, sends
logged after the image, the refusal and its reason). check.py
server_upgrade: five sends wait on the old output, the sender starts
with the new one, the old receiver refuses the new shape, the new one
takes all eight.
Diffstat (limited to 'core/test-module.c')
| -rw-r--r-- | core/test-module.c | 107 |
1 files changed, 92 insertions, 15 deletions
diff --git a/core/test-module.c b/core/test-module.c index 70cc02d..b318aee 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -1,5 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include "module.h" +#include "module-save.h" #include "module-store.h" #include "module-text.h" #include "store.h" @@ -4307,40 +4308,115 @@ static void memory_budget(void) { remove_store(path); } -/* An output whose fields are named otherwise (here two numbers swapped) is - * another output: its waiting sends' bytes would mean something else. A - * module that has it so is refused while sends wait, replacing or starting - * over the store; once they are trimmed it replaces (review 004, F3). */ +/* A send laid out for some fields, made anew for others by name: values + * the new types can hold go over (a u32 widened, a number narrowed when it + * is one; text to bytes, and bytes that are UTF-8 to text), the rest start + * at 0 or empty. There and back again, nothing changes. */ +static void send_recast(void) { + PureField ordered[3] = {{"item", PURE_FIELD_TEXT, 0, 0}, {"count", PURE_FIELD_U32, 0, 0}, + {"total", PURE_FIELD_NUMBER, 0, 0}}; + PureField kept[5] = {{"total", PURE_FIELD_NUMBER, 0, 0}, {"item", PURE_FIELD_TEXT, 0, 0}, + {"count", PURE_FIELD_NUMBER, 0, 0}, {"memo", PURE_FIELD_BYTES, 0, 0}, + {"extra", PURE_FIELD_U32, 0, 0}}; + CHECK(pure_fields_lay_out(ordered, 3) == 12 && ordered[0].offset == PURE_FIELD_REST); + CHECK(pure_fields_lay_out(kept, 5) == 24 && kept[1].offset == 8 && kept[3].offset == PURE_FIELD_REST); + unsigned char payload[15] = {2, 0, 0, 0}; /* count 2, total 7.5, then the item */ + double total = 7.5; + memcpy(payload + 4, &total, 8); + memcpy(payload + 12, "pen", 3); + unsigned char *made, *back; + size_t size, back_size; + CHECK(pure_send_recast(ordered, 3, kept, 5, (PureSpan){payload, 15}, &made, &size) == PURE_OK && size == 27); + double count, first; + memcpy(&first, made, 8); + memcpy(&count, made + 12, 8); + CHECK(first == 7.5 && made[8] == 3 && count == 2 && !memcmp(made + 20, "\0\0\0\0", 4) && !memcmp(made + 24, "pen", 3)); + CHECK(pure_send_recast(kept, 5, ordered, 3, (PureSpan){made, size}, &back, &back_size) == PURE_OK && + back_size == 15 && !memcmp(back, payload, 15)); + free(back); + /* A number that is not a u32, and bytes that are not UTF-8, start empty. */ + PureField narrow[2] = {{"count", PURE_FIELD_U32, 0, 0}, {"memo", PURE_FIELD_TEXT, 0, 0}}; + pure_fields_lay_out(narrow, 2); + count = 2.5; + memcpy(made + 12, &count, 8); + memcpy(made + 24, "p\xffn", 3); /* the memo (the rest) is those three bytes now, the item none */ + made[8] = 0; + CHECK(pure_send_recast(kept, 5, narrow, 2, (PureSpan){made, 24 + 3}, &back, &back_size) == PURE_OK && + back_size == 4 && !memcmp(back, "\0\0\0\0", 4)); + free(back); + PureField bytes_memo[1] = {{"memo", PURE_FIELD_BYTES, 0, 0}}; + pure_fields_lay_out(bytes_memo, 1); + CHECK(pure_send_recast(kept, 5, bytes_memo, 1, (PureSpan){made, 24 + 3}, &back, &back_size) == PURE_OK && + back_size == 3 && !memcmp(back, "p\xffn", 3)); + free(back); + /* A payload not laid out by its fields is the store's error. */ + CHECK(pure_send_recast(kept, 5, ordered, 3, (PureSpan){made, 10}, &back, &back_size) == PURE_STORE_ERROR && !back); + free(made); +} + +/* Two numbers of a waiting send, as laid out (pay's fields in the app's module). */ +static bool pay_reads(const PureApp *app, size_t k, double first, double second) { + Outbox o = outbox_of(app); + double a, b; + if (k >= o.count || o.items[k].size != 16) return false; + memcpy(&a, o.items[k].payload, 8); + memcpy(&b, o.items[k].payload + 8, 8); + return a == first && b == second; +} + +/* An output whose fields are named otherwise (here two numbers swapped) + * would mean something else of the bytes its sends wait with: replacing or + * starting the module recasts them by field name, as a list's items move. + * The outbox's image names the fields it holds its sends in, so a start + * after a replace whose save never came (the links still naming the old + * fields) reads them right. A module without the output is refused while + * sends wait on it, and says so. */ static void output_meaning(void) { static const S nums[] = {{"n", 0}}; static const F was_fields[] = {{"debit", PURE_FIELD_NUMBER}, {"credit", PURE_FIELD_NUMBER}}; static const F now_fields[] = {{"credit", PURE_FIELD_NUMBER}, {"debit", PURE_FIELD_NUMBER}}; static const I was_inputs[] = {KEY("tap", "KeyT"), {"pay", NULL, PURE_KIND_OUTPUT, false, was_fields, 2}}; static const I now_inputs[] = {KEY("tap", "KeyT"), {"pay", NULL, PURE_KIND_OUTPUT, false, now_fields, 2}}; + static const I none_inputs[] = {KEY("tap", "KeyT")}; static const O was_code[] = {{C, 0, 10}, {C, 0, 20}, {SEND, 1, 0}}; /* pay { debit: 10, credit: 20 } */ static const O now_code[] = {{C, 0, 20}, {C, 0, 10}, {SEND, 1, 0}}; /* the same, fields in the new order */ - static const H handlers[] = {{1, 0, 3}}; + static const O none_code[] = {{C, 0, 1}, {ST, 0, 0}}; + static const H handlers[] = {{1, 0, 3}}, none_handlers[] = {{1, 0, 2}}; Spec was = {nums, 1, was_inputs, 2, NULL, 0, handlers, 1, was_code, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; Spec now = {nums, 1, now_inputs, 2, NULL, 0, handlers, 1, now_code, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; - CHECK(load(was, &module_a) == PURE_OK && load(now, &module_b) == PURE_OK); + Spec none = {nums, 1, none_inputs, 1, NULL, 0, none_handlers, 1, none_code, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, + NULL, 0, NULL, 0}; + PureModule without; + CHECK(load(was, &module_a) == PURE_OK && load(now, &module_b) == PURE_OK && load(none, &without) == PURE_OK); CHECK(pure_module_shape(&module_a, 1) != pure_module_shape(&module_b, 1)); char *path = new_store(); PureApp app; CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); - CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && outbox_of(&app).count == 1); - CHECK(pure_app_replace(&app, &module_b) == PURE_CONFLICT && app.module == &module_a); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pay_reads(&app, 0, 10, 20)); + CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.module == &module_b && pay_reads(&app, 0, 20, 10)); + pure_app_stop(&app); /* no save since: the links name a's fields, the outbox's image b's */ + CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && pay_reads(&app, 0, 20, 10)); pure_app_stop(&app); - CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_CONFLICT); - CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && outbox_of(&app).count == 1); - pure_app_trim(&app, 1, 1); - CHECK(pure_app_save(&app) == PURE_OK && pure_app_replace(&app, &module_b) == PURE_OK); - CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && outbox_of(&app).items[0].seq == 2); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pay_reads(&app, 0, 10, 20)); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pay_reads(&app, 1, 10, 20)); + CHECK(outbox_of(&app).items[1].seq == 2); pure_app_stop(&app); - CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && outbox_of(&app).count == 1); + CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && pay_reads(&app, 0, 20, 10) && + pay_reads(&app, 1, 20, 10) && outbox_of(&app).items[1].seq == 2); + /* Sent with b's fields, logged after the image named them. */ + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pay_reads(&app, 2, 20, 10)); pure_app_stop(&app); - CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_CONFLICT); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pay_reads(&app, 2, 10, 20)); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &without, pure_tree_data(path)) == PURE_CONFLICT && strstr(app.why, "'pay'")); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && !app.why[0]); + CHECK(pure_app_replace(&app, &without) == PURE_CONFLICT && app.module == &module_a && strstr(app.why, "'pay'")); + pure_app_trim(&app, 1, 3); + CHECK(pure_app_save(&app) == PURE_OK && pure_app_replace(&app, &without) == PURE_OK && !app.why[0]); + pure_app_stop(&app); + pure_module_release(&without); remove_store(path); } @@ -4457,6 +4533,7 @@ int main(void) { keyed_mutations(); snapshots(); links(); + send_recast(); output_meaning(); memory_budget(); fold_mutations(); |
