From 9aa123ec5e8827a6101fa7c6db139397b39e04f2 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Wed, 30 Sep 2026 05:33:32 +0100 Subject: 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. --- core/module-save.c | 191 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 176 insertions(+), 15 deletions(-) (limited to 'core/module-save.c') diff --git a/core/module-save.c b/core/module-save.c index e737276..414d6b7 100644 --- a/core/module-save.c +++ b/core/module-save.c @@ -30,6 +30,13 @@ * its image and empties it. */ static const unsigned char values_magic[4] = {'P', 'V', 'S', '1'}, image_magic[4] = {'P', 'L', 'I', '1'}, list_magic[4] = {'P', 'L', 'S', '2'}, list_magic_1[4] = {'P', 'L', 'S', '1'}; +/* The outbox's image names the fields its sends were laid out with: "PLO1" + * in the place of "PLI1", and after the save it holds u8 outputs, each {u8 + * name length, name, u8 field count, each field's {u8 type, u8 name length, + * name}} (the module's outputs when it was written). The sends logged after + * it are laid out so too: a module whose outputs differ writes it again + * first. So what they mean moves with them, whatever the links hold. */ +static const unsigned char sends_magic[4] = {'P', 'L', 'O', '1'}; /* Logged changes a list keeps before its image is written again: at least * JOURNAL_MIN, else as many bytes as its items; and whatever the lists, the * log is kept within as many bytes as all their items (and LOG_KEPT) by @@ -149,7 +156,8 @@ PURE_COLD PureResult pure_save_open_text(PureApp *app, const PureModule *m, uint /* ---- Lists: images and the log */ -enum { IMAGE_HEADER = 5 + PURE_RECORD_FIELDS * (2 + PURE_MODULE_NAME) + 8 }; +enum { OUTPUTS_NAMED = 1 + PURE_MODULE_INPUTS * (2 + PURE_MODULE_NAME) + PURE_MODULE_FIELDS * (2 + PURE_MODULE_NAME), + IMAGE_HEADER = 5 + PURE_RECORD_FIELDS * (2 + PURE_MODULE_NAME) + 8 + OUTPUTS_NAMED }; static size_t header_of(unsigned count, const uint8_t *text, const char (*names)[PURE_MODULE_NAME], uint64_t seq, unsigned char *out) { @@ -181,6 +189,71 @@ static size_t runtime_header(uint32_t place, uint64_t seq, unsigned char *out) { return header_of(r->fields, r->kinds, r->names, seq, out); } +/* The outbox's image header: m's outputs named after the save it holds. */ +static size_t outbox_header(const PureModule *m, uint64_t seq, unsigned char *out) { + size_t at = runtime_header(PURE_LIST_OUTBOX, seq, out); + memcpy(out, sends_magic, 4); + unsigned char *count = out + at++; + *count = 0; + for (uint32_t i = 0; i < m->inputs; ++i) { + if (m->input[i].kind != PURE_KIND_OUTPUT) continue; + ++*count; + size_t length = strlen(m->input_names[i]); + out[at++] = (unsigned char)length; + memcpy(out + at, m->input_names[i], length); + at += length; + out[at++] = (unsigned char)m->input[i].field_count; + for (uint32_t f = 0; f < m->input[i].field_count; ++f) { + const PureField *field = &m->field[m->input[i].field_start + f]; + size_t named = strlen(field->name); + out[at++] = field->type; + out[at++] = (unsigned char)named; + memcpy(out + at, field->name, named); + at += named; + } + } + return at; +} + +/* What an output's sends were laid out with, as a store names it: its + * fields (laid out), or their types alone (named false: links before PLK4). */ +typedef struct { char name[PURE_MODULE_NAME]; PureField fields[PURE_MODULE_FIELDS]; uint32_t count; bool named; } SentShape; + +/* The outputs an outbox image names (at, size bytes): how many, or -1 when + * they are not well formed; shapes (when not NULL) takes them. */ +static int read_outputs(const unsigned char *at, size_t size, SentShape *shapes, size_t *used) { + size_t k = 0; + if (size < 1) return -1; + unsigned outputs = at[k++]; + for (unsigned t = 0; t < outputs; ++t) { + size_t length = k < size ? at[k] : 0; + if (!length || length >= PURE_MODULE_NAME || size - k < 2 + length) return -1; + unsigned fields = at[k + 1 + length]; + if (fields > PURE_MODULE_FIELDS) return -1; + if (shapes) { + memcpy(shapes[t].name, at + k + 1, length); + shapes[t].name[length] = 0; + shapes[t].count = fields; + shapes[t].named = true; + } + k += 2 + length; + for (unsigned f = 0; f < fields; ++f) { + size_t named = size - k >= 2 ? at[k + 1] : 0; + if (size - k < 2 || !named || named >= PURE_MODULE_NAME || size - k < 2 + named || !at[k] || + at[k] >= PURE_FIELD_ROWS) + return -1; + if (shapes) { + shapes[t].fields[f] = (PureField){.type = at[k]}; + memcpy(shapes[t].fields[f].name, at + k + 2, named); + } + k += 2 + named; + } + if (shapes) pure_fields_lay_out(shapes[t].fields, fields); + } + *used = k; + return (int)outputs; +} + /* A stored list by its place: a list state, or one of the runtime's. */ static PureList *stored_list(const PureApp *app, uint32_t i) { if (i == PURE_LIST_HEARD) return app->heard; @@ -204,6 +277,8 @@ struct PureStartLog { size_t count, capacity; uint64_t tree, last; /* the tree's last save; the last entry that counts */ bool stopped, damaged; + SentShape *sent; /* the outputs the outbox's image names (PLO1), sent_count of them */ + int sent_count; /* -1: its image names none */ }; static struct Named *named_list(struct PureStartLog *index, const char *name, size_t length, bool add) { @@ -259,6 +334,7 @@ void pure_save_close_start(PureApp *app) { if (!index) return; for (size_t i = 0; i < index->count; ++i) free(index->lists[i].changes); free(index->lists); + free(index->sent); free(index); app->start_log = NULL; } @@ -270,6 +346,7 @@ PURE_COLD PureResult pure_save_open_log(PureApp *app) { if (result == PURE_OK) result = open_state(app, "", "seq", &app->stored_seq); struct PureStartLog *index = result == PURE_OK ? calloc(1, sizeof(*index)) : NULL; if (result == PURE_OK && !index) result = PURE_ALLOCATION; + if (index) index->sent_count = -1; if (result == PURE_OK && pure_draft_revision(app->stored_seq)) { unsigned char *bytes; size_t size; @@ -455,14 +532,31 @@ PURE_COLD PureResult pure_save_open_runtime(PureApp *app, uint32_t place, PureLi *out = NULL; if (!directory_of(app)) return PURE_STORE_ERROR; unsigned char *bytes = NULL, header[IMAGE_HEADER]; - size_t size = 0, length = runtime_header(place, 0, header) - 8; + size_t size = 0, length = runtime_header(place, 0, header) - 8, at = length + 8; PureResult result = pure_image_read(directory_of(app), name, &bytes, &size); bool image = result == PURE_OK; - uint64_t holds = image && size >= length + 8 && !memcmp(bytes, header, length) ? get_le(bytes + length, 8) : 0; - if (image && (size < length + 8 || memcmp(bytes, header, length))) result = PURE_STORE_ERROR; + bool named = image && place == PURE_LIST_OUTBOX && size >= 4 && !memcmp(bytes, sends_magic, 4); + bool formed = image && size >= at && (named || !memcmp(bytes, image_magic, 4)) && + !memcmp(bytes + 4, header + 4, length - 4); + uint64_t holds = formed ? get_le(bytes + length, 8) : 0; + if (formed && named) { /* the outputs its sends were laid out with */ + size_t used = 0; + int outputs = read_outputs(bytes + at, size - at, NULL, &used); + SentShape *shapes = outputs > 0 ? calloc((size_t)outputs, sizeof(*shapes)) : NULL; + if (outputs < 0 || (outputs > 0 && !shapes)) formed = false; + else if (app->start_log) { + read_outputs(bytes + at, size - at, shapes, &used); + free(app->start_log->sent); + app->start_log->sent = shapes, app->start_log->sent_count = outputs; + shapes = NULL; + } + free(shapes); + at += used; + } + if (image && !formed) result = PURE_STORE_ERROR; else if (image || result == PURE_NOT_FOUND) { result = (*out = pure_runtime_list_new(place)) ? PURE_OK : PURE_ALLOCATION; - if (result == PURE_OK && image) result = pure_list_read(*out, bytes + length + 8, size - length - 8); + if (result == PURE_OK && image) result = pure_list_read(*out, bytes + at, size - at); } free(bytes); /* Then the changes logged after the save it holds (an image is written @@ -487,11 +581,12 @@ PURE_COLD PureResult pure_save_open_runtime(PureApp *app, uint32_t place, PureLi return PURE_OK; } -PureResult pure_save_runtime_image(PureApp *app, uint32_t place) { +PureResult pure_save_runtime_image(PureApp *app, const PureModule *m, uint32_t place, const PureList *list) { unsigned char header[IMAGE_HEADER]; - size_t header_size = runtime_header(place, app->seq, header); + size_t header_size = place == PURE_LIST_OUTBOX ? outbox_header(m, app->seq, header) : + runtime_header(place, app->seq, header); return write_image(directory_of(app), runtime_lists[place - PURE_LIST_HEARD].name, header, header_size, - stored_list(app, place)); + list ? list : stored_list(app, place)); } /* ---- Checkpoints */ @@ -536,7 +631,8 @@ PureCheckpoint *pure_app_checkpoint(PureApp *app) { c->state = (uint32_t)pick; snprintf(c->name, sizeof(c->name), "%s", stored_name(app, (uint32_t)pick)); c->seq = app->seq; - c->header_size = pick >= PURE_LIST_HEARD ? runtime_header((uint32_t)pick, c->seq, c->header) : + c->header_size = pick == PURE_LIST_OUTBOX ? outbox_header(m, c->seq, c->header) : + pick >= PURE_LIST_HEARD ? runtime_header((uint32_t)pick, c->seq, c->header) : image_header(m, m->state_ref[pick], c->seq, c->header); c->logged = app->logged[pick]; c->since = app->logged_since[pick]; @@ -722,6 +818,24 @@ PURE_COLD static PureResult sent_before(PureApp *app, const char *output, uint64 * for later); the sources and sends links kept before the runtime's lists * go to them (heard_moved, sent_moved: they need images). Links kept * before PLK4 have their fields' types alone. */ +/* The shape of the output of that name among shapes, or NULL. */ +static const SentShape *sent_shape(const SentShape *shapes, unsigned count, const char *name) { + for (unsigned t = 0; t < count; ++t) + if (!strcmp(shapes[t].name, name)) return &shapes[t]; + return NULL; +} + +/* Whether sends laid out as was are laid out as m's output i: the same + * fields, names and types in order (types alone when was names none). */ +static bool same_shape(const SentShape *was, const PureModule *m, uint32_t i) { + if (was->count != m->input[i].field_count) return false; + for (uint32_t f = 0; f < was->count; ++f) { + const PureField *field = &m->field[m->input[i].field_start + f]; + if (field->type != was->fields[f].type || (was->named && strcmp(field->name, was->fields[f].name))) return false; + } + return true; +} + PURE_COLD PureResult pure_save_open_links(PureApp *app, const PureModule *m, bool *heard_moved, bool *sent_moved) { *heard_moved = *sent_moved = false; PureResult result = app->stored_links ? PURE_OK : open_state(app, "", "links", &app->stored_links); @@ -738,6 +852,8 @@ PURE_COLD PureResult pure_save_open_links(PureApp *app, const PureModule *m, boo known = first || second || named || (size >= 5 && !memcmp(bytes, links_magic_3, 4)); unsigned outputs = known ? bytes[4] : 0; result = known ? PURE_OK : PURE_STORE_ERROR; + SentShape *linked = calloc(outputs ? outputs : 1, sizeof(*linked)); /* what each was sent with, as the links say */ + if (!linked) result = PURE_ALLOCATION; for (unsigned t = 0; t < outputs && result == PURE_OK; ++t) { char *name = names[t]; size_t length = at < size ? bytes[at] : 0; @@ -749,14 +865,24 @@ PURE_COLD PureResult pure_save_open_links(PureApp *app, const PureModule *m, boo int i = pure_module_input(m, name); output[t] = i >= 0 && m->input[i].kind == PURE_KIND_OUTPUT ? i : -1; fits[t] = output[t] >= 0 && m->input[i].field_count == fields; + memcpy(linked[t].name, name, length + 1); + linked[t].count = fields; + linked[t].named = named; + if (fields > PURE_MODULE_FIELDS) result = PURE_STORE_ERROR; for (unsigned f = 0; f < fields && result == PURE_OK; ++f) { /* its type, then (PLK4) its name */ size_t named_length = named && size - at >= 2 ? bytes[at + 1] : 0; - if (size - at < (named ? 2 + named_length : 1)) { result = PURE_STORE_ERROR; break; } + if (size - at < (named ? 2 + named_length : 1) || named_length >= PURE_MODULE_NAME) { + result = PURE_STORE_ERROR; + break; + } const PureField *field = fits[t] ? &m->field[m->input[i].field_start + f] : NULL; fits[t] = field && field->type == bytes[at] && (!named || (strlen(field->name) == named_length && !memcmp(field->name, bytes + at + 2, named_length))); + linked[t].fields[f] = (PureField){.type = bytes[at]}; + if (named) memcpy(linked[t].fields[f].name, bytes + at + 2, named_length); at += named ? 2 + named_length : 1; } + if (result == PURE_OK) pure_fields_lay_out(linked[t].fields, fields); if (result != PURE_OK || size - at < 8) { result = PURE_STORE_ERROR; break; } uint64_t last = get_le(bytes + at, 8); at += 8; @@ -795,13 +921,48 @@ PURE_COLD PureResult pure_save_open_links(PureApp *app, const PureModule *m, boo app->links_changed = true; } if (result == PURE_OK && !first && !second && at != size) result = PURE_STORE_ERROR; - /* Every send left goes out on an output of this module with its fields. */ + /* Every send left goes out on an output of this module, laid out with + * its fields: one laid out otherwise (as the outbox's image names them, + * else as the links do) is recast by name. */ + bool imaged = app->start_log && app->start_log->sent_count >= 0; + const SentShape *shapes = imaged ? app->start_log->sent : linked; + unsigned shape_count = imaged ? (unsigned)app->start_log->sent_count : outputs; for (size_t k = 0, count = pure_list_count(app->outbox); result == PURE_OK && k < count; ++k) { - PureCell name = pure_list_get(app->outbox, k, 0); - unsigned t = 0; - while (t < outputs && (strlen(names[t]) != name.size || memcmp(names[t], name.text, name.size))) ++t; - if (t == outputs || !fits[t]) result = PURE_CONFLICT; + PureCell cell = pure_list_get(app->outbox, k, 0); + char name[PURE_MODULE_NAME] = ""; + if (cell.size < PURE_MODULE_NAME) memcpy(name, cell.text, cell.size); + int i = pure_module_input(m, name); + const SentShape *was = sent_shape(shapes, shape_count, name); + if (i < 0 || m->input[i].kind != PURE_KIND_OUTPUT) { + snprintf(app->why, sizeof(app->why), "sends wait on the output '%s', which this module does not have", name); + result = PURE_CONFLICT; + } else if (!was) result = PURE_STORE_ERROR; /* sent on an output its store does not name */ + else if (!same_shape(was, m, (uint32_t)i)) { + PureCell payload = pure_list_get(app->outbox, k, 2); + unsigned char *made = NULL; + size_t made_size = 0; + if (!was->named) { + snprintf(app->why, sizeof(app->why), + "sends wait on '%s', laid out with fields its store names by type alone: start the old module once", + name); + result = PURE_CONFLICT; + } else if ((result = pure_send_recast(was->fields, was->count, &m->field[m->input[i].field_start], + m->input[i].field_count, (PureSpan){payload.text, payload.size}, + &made, &made_size)) == PURE_OK) { + result = pure_list_set(app->outbox, k, 2, (PureCell){.text = made, .size = made_size}, NULL); + *sent_moved = true; + } + free(made); + } + } + /* Sends from now on are laid out with this module's fields: an image + * naming others is written again, naming these. */ + for (uint32_t i = 0; result == PURE_OK && i < m->inputs && !*sent_moved; ++i) { + if (m->input[i].kind != PURE_KIND_OUTPUT) continue; + const SentShape *was = sent_shape(shapes, shape_count, m->input_names[i]); + *sent_moved = !was || !same_shape(was, m, i); } + free(linked); free(bytes); app->sent_saved = pure_list_count(app->outbox); memcpy(app->saved_sent, app->last_sent, sizeof(app->saved_sent)); -- cgit v1.2.3