aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/module.c86
-rw-r--r--core/module.h29
-rw-r--r--core/test-module.c99
-rw-r--r--serve/deliver.c25
-rw-r--r--serve/deliver.h12
-rw-r--r--serve/serve.c72
-rw-r--r--tests/check.py248
7 files changed, 512 insertions, 59 deletions
diff --git a/core/module.c b/core/module.c
index 79d3c5b..cbf0efe 100644
--- a/core/module.c
+++ b/core/module.c
@@ -1179,7 +1179,7 @@ static PureResult fault(PureApp *app, PureFault kind, uint32_t pc) {
app->fault = kind;
app->fault_pc = pc;
return kind == PURE_FAULT_LIMIT ? PURE_LIMIT : kind == PURE_FAULT_NOT_FOUND ? PURE_NOT_FOUND :
- kind == PURE_FAULT_EXISTS ? PURE_CONFLICT : PURE_INVALID;
+ kind == PURE_FAULT_EXISTS ? PURE_CONFLICT : kind == PURE_FAULT_MEMORY ? PURE_BUDGET : PURE_INVALID;
}
static bool reserve(PureArena *arena, size_t more) {
@@ -2645,7 +2645,7 @@ void pure_app_stop(PureApp *app) {
typedef struct {
PureVersion texts[PURE_MODULE_STATES];
size_t lists[PURE_MODULE_STATES], heard, outbox; /* stored lists' journal lengths */
- size_t undo;
+ size_t undo, sent_saved;
uint64_t last_sent[PURE_MODULE_INPUTS];
bool opened;
} Mark;
@@ -2660,6 +2660,7 @@ static void mark_event(PureApp *app, Mark *mark) {
mark->heard = app->heard ? pure_list_changes(app->heard).size : 0;
mark->outbox = app->outbox ? pure_list_changes(app->outbox).size : 0;
mark->undo = app->undo.count;
+ mark->sent_saved = app->sent_saved;
memcpy(mark->last_sent, app->last_sent, sizeof(mark->last_sent));
}
@@ -2671,6 +2672,7 @@ static void undo_event(PureApp *app, const Mark *mark) {
if (app->heard) pure_list_forget(app->heard, mark->heard);
if (app->outbox) pure_list_forget(app->outbox, mark->outbox);
pure_undo_rollback(&app->undo, mark->undo);
+ app->sent_saved = mark->sent_saved;
memcpy(app->last_sent, mark->last_sent, sizeof(app->last_sent));
if (mark->opened) close_burst(app, true); /* nothing happened: release the holds */
}
@@ -2696,6 +2698,24 @@ static PureResult hear(PureApp *app, const char *source, uint64_t seq) {
return pure_list_insert(app->heard, at, item, &app->undo);
}
+/* The output's saved sends through seq leave the outbox: their receiver
+ * stored them, or answered them (in the answer's event). */
+static PureResult let_sends_go(PureApp *app, uint32_t output, uint64_t through) {
+ if (!app->outbox) return PURE_OK;
+ const char *name = app->module->input_names[output];
+ size_t length = strlen(name);
+ for (size_t i = 0; i < app->sent_saved;) {
+ PureCell of = pure_list_get(app->outbox, i, 0);
+ if (of.size != length || memcmp(of.text, name, length)) { ++i; continue; }
+ if ((uint64_t)pure_list_get(app->outbox, i, 1).num > through) break;
+ PureResult removed = pure_list_remove(app->outbox, i, &app->undo);
+ if (removed != PURE_OK) return removed;
+ --app->sent_saved;
+ app->links_changed = true;
+ }
+ return PURE_OK;
+}
+
static PureResult keep_reply(PureApp *app);
/* Every list of a message's rows emptied: an event's rows are its own. */
@@ -2705,14 +2725,29 @@ static void clear_rows(PureApp *app) {
if (m->state_type[i] == PURE_STATE_LIST && m->state_flags[i] & PURE_LIST_ROWS >> 8) pure_list_clear(app->lists[i]);
}
+void pure_app_memory(PureApp *app, size_t bytes) { app->memory = bytes; }
+
+size_t pure_app_state_bytes(const PureApp *app) {
+ const PureModule *m = app->module;
+ size_t bytes = (app->outbox ? pure_list_size(app->outbox) : 0) + (app->heard ? pure_list_size(app->heard) : 0);
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (app->lists[i] && !(m->state_flags[i] & PURE_LIST_ROWS >> 8)) bytes += pure_list_size(app->lists[i]);
+ else if (app->texts[i]) bytes += pure_draft_size(app->texts[i]);
+ }
+ return bytes; /* a message's rows are its own, gone when it ends */
+}
+
/* One event: every handler for it runs in module order against pending
* states, all or nothing. A key, text or message event saves before it
* returns unless saving is deferred; a frame leaves that to the next event
- * (frames come 60 times a second). A message's document joins the burst. */
+ * (frames come 60 times a second). A message's document joins the burst.
+ * With a memory budget, an event that leaves the states over it, and
+ * holding more than before, fails. */
static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
const PureModule *m = app->module;
double pending[PURE_MODULE_STATES], locals[PURE_MODULE_LOCALS] = {0};
ev->locals = locals;
+ size_t held = app->memory ? pure_app_state_bytes(app) : 0;
Mark mark;
mark_event(app, &mark);
memcpy(pending, app->values, sizeof(app->values));
@@ -2728,8 +2763,14 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
if (result == PURE_OK && app->has_reply &&
((app->reply.source == FROM_DOCUMENT && ev->modified) || app->reply.source == FROM_TEXT))
result = keep_reply(app);
- /* A delivery's number is heard with its changes. */
+ /* A delivery's number is heard with its changes, and an answer's sends go with it. */
if (result == PURE_OK && app->delivering.pending) result = hear(app, app->delivering.source, app->delivering.seq);
+ if (result == PURE_OK && app->delivering.pending && app->delivering.release)
+ result = let_sends_go(app, app->delivering.release - 1, app->delivering.seq);
+ if (result == PURE_OK && app->memory) {
+ size_t holds = pure_app_state_bytes(app);
+ if (holds > app->memory && holds > held) result = fault(app, PURE_FAULT_MEMORY, UINT32_MAX);
+ }
if (result != PURE_OK) {
undo_event(app, &mark);
clear_rows(app);
@@ -2950,23 +2991,14 @@ void pure_app_outbox_newest(const PureApp *app, PureSentVisit visit, void *conte
* store with the next save; a failed save brings them back. Its sends are
* in order among the others: the look stops at its first one kept. */
void pure_app_trim(PureApp *app, uint32_t output, uint64_t through) {
- const PureModule *m = app->module;
- if (!app->outbox || output >= m->inputs) return;
- const char *name = m->input_names[output];
- size_t length = strlen(name);
+ if (!app->outbox || output >= app->module->inputs) return;
open_burst(app);
- for (size_t i = 0; i < app->sent_saved;) {
- PureCell of = pure_list_get(app->outbox, i, 0);
- if (of.size != length || memcmp(of.text, name, length)) { ++i; continue; }
- if ((uint64_t)pure_list_get(app->outbox, i, 1).num > through) break;
- if (pure_list_remove(app->outbox, i, &app->undo) != PURE_OK) break;
- --app->sent_saved;
- app->links_changed = true;
- }
+ let_sends_go(app, output, through);
}
-PureResult pure_app_deliver(PureApp *app, uint32_t input, const char *source, uint64_t seq, PureSpan payload,
- const uint32_t *placed) {
+/* A delivery, or an answer to the sends of output (release: its number + 1). */
+static PureResult deliver(PureApp *app, uint32_t input, uint32_t release, const char *source, uint64_t seq,
+ PureSpan payload, const uint32_t *placed) {
const PureModule *m = app->module;
size_t length = source ? strlen(source) : 0;
app->duplicate = false;
@@ -2983,16 +3015,19 @@ PureResult pure_app_deliver(PureApp *app, uint32_t input, const char *source, ui
}
app->delivering.source = source;
app->delivering.seq = seq;
+ app->delivering.release = release;
app->delivering.pending = true;
PureResult result = message(app, input, NULL, -1, payload, placed);
if (!app->delivering.pending) return result;
/* Not applied. Refused by the app (a fault), the message was delivered
- * all the same: its number is heard, saved like a key event's changes. */
+ * all the same: its number is heard, saved like a key event's changes.
+ * Refused for memory it was not: its sender sends it again. */
app->delivering.pending = false;
- if (app->fault == PURE_FAULT_NONE) return result;
+ if (app->fault == PURE_FAULT_NONE || app->fault == PURE_FAULT_MEMORY) return result;
Mark mark;
mark_event(app, &mark);
PureResult heard = hear(app, source, seq);
+ if (heard == PURE_OK && release) heard = let_sends_go(app, release - 1, seq);
if (heard != PURE_OK) {
undo_event(app, &mark);
return heard;
@@ -3001,6 +3036,17 @@ PureResult pure_app_deliver(PureApp *app, uint32_t input, const char *source, ui
return saved == PURE_OK ? result : saved;
}
+PureResult pure_app_deliver(PureApp *app, uint32_t input, const char *source, uint64_t seq, PureSpan payload,
+ const uint32_t *placed) {
+ return deliver(app, input, 0, source, seq, payload, placed);
+}
+
+PureResult pure_app_answer(PureApp *app, uint32_t input, uint32_t output, const char *source, uint64_t seq,
+ PureSpan payload, const uint32_t *placed) {
+ if (output >= app->module->inputs || app->module->input[output].kind != PURE_KIND_OUTPUT) return PURE_INVALID;
+ return deliver(app, input, output + 1, source, seq, payload, placed);
+}
+
PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), void *context) {
if (!app->has_reply) return PURE_OK;
return visit_value(app, &app->arena, app->reply, visit, context);
diff --git a/core/module.h b/core/module.h
index 0b45ba2..c11c682 100644
--- a/core/module.h
+++ b/core/module.h
@@ -109,10 +109,12 @@ typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2 } Pure
typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge;
/* EXISTS: an item with that key is already in the keyed list (the event
- * reports PURE_CONFLICT). */
+ * reports PURE_CONFLICT). MEMORY: the event would leave the states holding
+ * more than the app's budget (pure_app_memory; it reports PURE_BUDGET). */
typedef enum {
PURE_FAULT_NONE, PURE_FAULT_DIVIDE, PURE_FAULT_NON_FINITE, PURE_FAULT_CLAMP,
- PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND, PURE_FAULT_ENCODING, PURE_FAULT_EXISTS
+ PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND, PURE_FAULT_ENCODING, PURE_FAULT_EXISTS,
+ PURE_FAULT_MEMORY
} PureFault;
/* plan, shape and range are the runtime's, set at load: a fold whose block
@@ -310,6 +312,7 @@ typedef struct {
bool document_pending; /* the current event's document is among them */
PureFault fault;
uint32_t fault_pc;
+ size_t memory; /* the budget of what the states hold (0: none): pure_app_memory */
uint64_t ticks, events;
/* The time events see as now, in milliseconds since 1970 (UTC). The host
* sets it, so a script can replay any time; views never see it. */
@@ -346,7 +349,7 @@ typedef struct {
PureDraft *stored_links;
bool links_changed; /* since the last save */
bool duplicate; /* the last delivery was heard before and not applied */
- struct { const char *source; uint64_t seq; bool pending; } delivering;
+ struct { const char *source; uint64_t seq; uint32_t release; bool pending; } delivering;
/* The arguments given for a view (pure_app_arguments): its input + 1 (0:
* none), where their fields are, and a count of the times they were given. */
unsigned argued;
@@ -443,13 +446,31 @@ void pure_app_trim(PureApp *, uint32_t output, uint64_t through);
* before is not applied again: it succeeds with app->duplicate set. The
* number counts as heard with the event's changes, and also when the app
* refuses the message (a fault: it was delivered, and refused); not when a
- * save fails. placed is as for pure_app_message_fields, or NULL. */
+ * save fails, nor when the app has no memory for it (pure_app_memory: it
+ * comes again). placed is as for pure_app_message_fields, or NULL. */
PureResult pure_app_deliver(PureApp *, uint32_t input, const char *source, uint64_t seq, PureSpan payload,
const uint32_t *placed);
+/* An answer to the send numbered seq of output (serve/deliver.h's --reply),
+ * delivered as pure_app_deliver delivers from source; in the same event the
+ * output's saved sends through seq leave the outbox, so the answer has the
+ * room its send held. */
+PureResult pure_app_answer(PureApp *, uint32_t input, uint32_t output, const char *source, uint64_t seq,
+ PureSpan payload, const uint32_t *placed);
/* The last number heard from source (0 when none): a host that gets
* numbered messages out of order hands them over from the one after it. */
uint64_t pure_app_heard(const PureApp *, const char *source);
+/* An app's memory budget (docs/design/013-memory-budget.md): what its
+ * states hold, as they are stored (their lists' items, their texts, the
+ * outbox and the numbers heard), at most bytes (0: no budget, at first). An
+ * event after which they hold more than that, and more than before it,
+ * fails with the fault PURE_FAULT_MEMORY (PURE_BUDGET) and changes nothing;
+ * one that frees or keeps them goes on. A delivery refused so is not heard:
+ * its sender sends it again. */
+void pure_app_memory(PureApp *, size_t bytes);
+/* What the states hold now, as the budget counts it. */
+size_t pure_app_state_bytes(const PureApp *);
+
/* Readers on other threads. Right after a save an app can make a snapshot of
* its saved states, for evaluating views anywhere while it keeps applying
* events: numbers are copied; text states share their stored versions and
diff --git a/core/test-module.c b/core/test-module.c
index 1446e73..70cc02d 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -4209,6 +4209,104 @@ static void links(void) {
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "handler event"));
}
+/* The app's memory budget (docs/design/013-memory-budget.md): an event that
+ * would leave the states over it and holding more is refused and changes
+ * nothing; one that frees them goes on; sends waiting for their receivers
+ * count, so a full outbox refuses more until some are trimmed; a delivery
+ * refused so is not heard, and comes again; an answer has its send's room. */
+static void memory_budget(void) {
+ enum { ADD, DROP, SEND_KEY, OUT, TAKE, ANSWER };
+ static const S nums[] = {{"n", 0}};
+ static const T states[] = {{"rows", 3, 0}};
+ static const RF fields[] = {{"value", 0, 0}};
+ static const R records[] = {{"row", fields, 1}};
+ static const F value[] = {{"value", PURE_FIELD_NUMBER}};
+ static const I inputs[] = {KEY("add", "KeyA"), KEY("drop", "KeyD"), KEY("send", "KeyS"),
+ {"out", NULL, PURE_KIND_OUTPUT, false, value, 1},
+ {"take", NULL, PURE_KIND_MESSAGE, false, value, 1},
+ {"answer", NULL, PURE_KIND_MESSAGE, false, value, 1}};
+ static const O code[] = {
+ {L, 0, 0}, {LA, 1, 0}, /* add: rows.append(row { n }) */
+ {XF, 1, 0}, {LR, 1, 0}, /* drop: rows.remove(rows.first) */
+ {C, 0, 1}, {SEND, 3, 0}, /* send: send out { value: 1 } */
+ {FIELD, 0, 0}, {LA, 1, 0}, /* take: rows.append(row { take.value }) */
+ {FIELD, 0, 0}, {LA, 1, 0}, {FIELD, 0, 0}, {LA, 1, 0}, {FIELD, 0, 0}, {LA, 1, 0}, /* answer: six rows */
+ {FIELD, 0, 0}, {LA, 1, 0}, {FIELD, 0, 0}, {LA, 1, 0}, {FIELD, 0, 0}, {LA, 1, 0},
+ };
+ static const H handlers[] = {{1 + 2 * ADD, 0, 2}, {1 + 2 * DROP, 2, 2}, {1 + 2 * SEND_KEY, 4, 2}, {1 + 2 * TAKE, 6, 2},
+ {1 + 2 * ANSWER, 8, 12}};
+ Spec spec = {nums, 1, inputs, 6, NULL, 0, handlers, 5, code, LEN(code), NULL, 0, states, 1, NULL, 0, NULL, 0,
+ records, 1, NULL, 0};
+ CHECK(load(spec, &module_a) == PURE_OK);
+ char *path = new_store();
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ for (int k = 0; k < 3; ++k) CHECK(pure_app_input(&app, ADD, PURE_EDGE_DOWN) == PURE_OK);
+ size_t three = pure_app_state_bytes(&app);
+ CHECK(three == 3 * 8); /* a row's number as it is stored */
+ /* Room for one row more: the next is refused, and nothing of it stays. */
+ pure_app_memory(&app, three + 8);
+ CHECK(pure_app_input(&app, ADD, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.lists[1]) == 4);
+ CHECK(pure_app_input(&app, ADD, PURE_EDGE_DOWN) == PURE_BUDGET && app.fault == PURE_FAULT_MEMORY &&
+ pure_list_count(app.lists[1]) == 4 && pure_app_state_bytes(&app) == three + 8);
+ /* Over the budget (made smaller), what frees goes on and what grows does not. */
+ pure_app_memory(&app, 8);
+ CHECK(pure_app_input(&app, ADD, PURE_EDGE_DOWN) == PURE_BUDGET);
+ CHECK(pure_app_input(&app, DROP, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.lists[1]) == 3);
+ /* Sends count while they wait: a full outbox refuses the next until some are trimmed. */
+ size_t before = pure_app_state_bytes(&app);
+ pure_app_memory(&app, before);
+ CHECK(pure_app_input(&app, SEND_KEY, PURE_EDGE_DOWN) == PURE_BUDGET && !pure_list_count(app.outbox));
+ pure_app_memory(&app, 0);
+ CHECK(pure_app_input(&app, SEND_KEY, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.outbox) == 1);
+ size_t one = pure_app_state_bytes(&app) - before;
+ pure_app_memory(&app, before + 2 * one);
+ CHECK(pure_app_input(&app, SEND_KEY, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(pure_app_input(&app, SEND_KEY, PURE_EDGE_DOWN) == PURE_BUDGET && pure_list_count(app.outbox) == 2);
+ pure_app_trim(&app, OUT, 1);
+ CHECK(pure_app_save(&app) == PURE_OK && pure_list_count(app.outbox) == 1);
+ CHECK(pure_app_input(&app, SEND_KEY, PURE_EDGE_DOWN) == PURE_OK && outbox_of(&app).items[1].seq == 3);
+ /* A delivery refused for memory is not heard: given again with room, it is applied once. */
+ double taken = 7;
+ pure_app_memory(&app, pure_app_state_bytes(&app));
+ CHECK(pure_app_deliver(&app, TAKE, "far.out", 1, (PureSpan){(const unsigned char *)&taken, 8}, NULL) == PURE_BUDGET &&
+ app.fault == PURE_FAULT_MEMORY && pure_app_heard(&app, "far.out") == 0 && pure_list_count(app.lists[1]) == 3);
+ pure_app_finish(&app);
+ pure_app_memory(&app, 0);
+ CHECK(pure_app_deliver(&app, TAKE, "far.out", 1, (PureSpan){(const unsigned char *)&taken, 8}, NULL) == PURE_OK &&
+ !app.duplicate && pure_app_heard(&app, "far.out") == 1 && pure_list_count(app.lists[1]) == 4);
+ pure_app_finish(&app);
+ CHECK(pure_list_get(app.lists[1], 3, 0).num == 7);
+ /* An answer takes its send out of the outbox in its own event: one that
+ * keeps more than the send held is refused whole at the budget (the send
+ * stays), and goes on with room; one that keeps less goes on at it. */
+ size_t full = pure_app_state_bytes(&app);
+ CHECK(one < 6 * 8 && one > 8);
+ double paid = 9;
+ PureSpan nine = {(const unsigned char *)&paid, 8};
+ pure_app_memory(&app, full);
+ CHECK(pure_app_answer(&app, ANSWER, OUT, "wire.out", 2, nine, NULL) == PURE_BUDGET &&
+ app.fault == PURE_FAULT_MEMORY && pure_list_count(app.outbox) == 2 && pure_list_count(app.lists[1]) == 4 &&
+ pure_app_heard(&app, "wire.out") == 0 && pure_app_state_bytes(&app) == full);
+ pure_app_finish(&app);
+ size_t heard = 4 + strlen("wire.out") + 8; /* its source among the numbers heard */
+ pure_app_memory(&app, full + 6 * 8 + heard - one);
+ CHECK(pure_app_answer(&app, ANSWER, OUT, "wire.out", 2, nine, NULL) == PURE_OK && pure_list_count(app.outbox) == 1 &&
+ outbox_of(&app).items[0].seq == 3 && pure_list_count(app.lists[1]) == 10 &&
+ pure_app_heard(&app, "wire.out") == 2 && pure_app_state_bytes(&app) == full + 6 * 8 + heard - one);
+ pure_app_finish(&app);
+ pure_app_memory(&app, pure_app_state_bytes(&app));
+ CHECK(pure_app_answer(&app, TAKE, OUT, "wire.out", 3, nine, NULL) == PURE_OK && !pure_list_count(app.outbox) &&
+ pure_list_count(app.lists[1]) == 11);
+ pure_app_finish(&app);
+ CHECK(pure_app_answer(&app, TAKE, TAKE, "wire.out", 4, nine, NULL) == PURE_INVALID); /* not an output */
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pure_list_count(app.lists[1]) == 11 &&
+ !pure_list_count(app.outbox) && pure_app_heard(&app, "far.out") == 1 && pure_app_heard(&app, "wire.out") == 3);
+ pure_app_stop(&app);
+ 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
@@ -4360,6 +4458,7 @@ int main(void) {
snapshots();
links();
output_meaning();
+ memory_budget();
fold_mutations();
pointing();
list_mutations();
diff --git a/serve/deliver.c b/serve/deliver.c
index b4d9e57..68c5f14 100644
--- a/serve/deliver.c
+++ b/serve/deliver.c
@@ -47,7 +47,6 @@ struct PureWire {
int answer_input; /* the input answers become events of, or -1 */
Answer *answers; /* made, for the owner to take in order */
size_t answer_head, answer_count, answer_capacity;
- Answer taken; /* the one the owner took last, kept until it takes the next */
uint32_t placed[2 * PURE_MODULE_FIELDS];
/* Sealed sends: to peer's key, authenticated by the node's own key when
* it has one (its public half is from), in one session made at the first
@@ -240,23 +239,26 @@ bool pure_wire_reply(PureWire *w, uint32_t input, char *error, size_t size) {
}
bool pure_wire_answer(PureWire *w, PureWireAnswer *out) {
- free(w->taken.bytes);
- w->taken = (Answer){0};
pthread_mutex_lock(&w->lock);
bool some = w->answer_count > 0;
- if (some) {
- w->taken = w->answers[w->answer_head++];
- --w->answer_count;
- w->delivered = w->taken.seq; /* trimmed after the owner applies it */
- }
+ Answer next = some ? w->answers[w->answer_head] : (Answer){0}; /* its bytes stay put while the array moves */
pthread_mutex_unlock(&w->lock);
if (!some) return false;
const PureInputShape *in = &w->module->input[w->answer_input];
- for (size_t i = 0; i < 2 * (size_t)in->field_count; ++i) w->placed[i] = read_u32(w->taken.bytes + 4 * i);
- *out = (PureWireAnswer){(uint32_t)w->answer_input, w->taken.seq,
- {w->taken.bytes + w->taken.table, w->taken.size - w->taken.table}, w->placed, w->source};
+ for (size_t i = 0; i < 2 * (size_t)in->field_count; ++i) w->placed[i] = read_u32(next.bytes + 4 * i);
+ *out = (PureWireAnswer){(uint32_t)w->answer_input, next.seq, {next.bytes + next.table, next.size - next.table},
+ w->placed, w->source};
return true;
}
+
+void pure_wire_answered(PureWire *w) {
+ pthread_mutex_lock(&w->lock);
+ Answer done = w->answers[w->answer_head++];
+ --w->answer_count;
+ w->delivered = done.seq; /* trimmed after the owner applies it */
+ pthread_mutex_unlock(&w->lock);
+ free(done.bytes);
+}
uint64_t pure_wire_queued(const PureWire *w) { return w->queued; }
uint64_t pure_wire_delivered(PureWire *w) {
@@ -771,7 +773,6 @@ void pure_wire_close(PureWire *w) {
}
for (size_t i = 0; i < w->answer_count; ++i) free(w->answers[w->answer_head + i].bytes);
free(w->answers);
- free(w->taken.bytes);
free(w->queue);
pthread_cond_destroy(&w->changed);
pthread_mutex_destroy(&w->lock);
diff --git a/serve/deliver.h b/serve/deliver.h
index 71cc38e..a7ccac3 100644
--- a/serve/deliver.h
+++ b/serve/deliver.h
@@ -49,11 +49,10 @@ void pure_wire_close(PureWire *);
* http_status is the answer's status. An answer that makes no message is
* logged and the send tried again every 10 s. Answers go one per request. */
bool pure_wire_reply(PureWire *, uint32_t input, char *error, size_t size);
-/* The owner's side: the next answer in order, or false. Taking it counts
- * its send delivered (the owner applies the answer, then trims); what out
- * points to is the wire's until the next call. The answer is applied with
- * pure_app_deliver from the wire's source with the send's number, so an
- * answer that comes again after a restart is applied once. */
+/* The owner's side: the next answer in order, or false; it stays the next,
+ * and what out points to the wire's, until pure_wire_answered. The answer
+ * is applied with pure_app_answer from the wire's source with the send's
+ * number, so an answer that comes again after a restart is applied once. */
typedef struct {
uint32_t input;
uint64_t seq;
@@ -62,6 +61,9 @@ typedef struct {
const char *source;
} PureWireAnswer;
bool pure_wire_answer(PureWire *, PureWireAnswer *out);
+/* The next answer was applied, or refused by the app: it goes, and its send
+ * counts delivered (the owner trims it). */
+void pure_wire_answered(PureWire *);
/* Counts for a host's report. */
void pure_wire_counts(PureWire *, uint64_t *delivered, uint64_t *attempts);
#endif
diff --git a/serve/serve.c b/serve/serve.c
index 95ac37f..ab386ae 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -185,6 +185,8 @@ typedef struct {
bool reads_first; /* the owner's: a round's reads before its writes */
uint64_t next[PURE_MODULE_INPUTS]; /* when each timer ticks next (monotonic clock, ms; 0: not yet begun) */
uint64_t ticks;
+ uint64_t memory_refused; /* events refused for the app's memory budget */
+ bool answers_wait; /* an answer the app had no room for: tried again after ANSWER_WAIT_MS */
/* The owner's: the last save's snapshot (NULL: the owner shows views),
* taken under the lock, and the app's changes it shows. */
bool publishes;
@@ -517,11 +519,12 @@ static void answer(Connection *c, PureOutcome result) {
}
static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp", "limit", "not_found", "encoding",
- "exists"};
+ "exists", "memory"};
/* An app route's answer: a view's value, or what an event replied. A failed
- * event names its fault: 404 not_found, 409 exists, 422 for the others;
- * a view without a value is 404 no_value. */
+ * event names its fault: 404 not_found, 409 exists, 503 memory (the app's
+ * budget: again later), 422 for the others; a view without a value is 404
+ * no_value. */
static void answer_app(Connection *c, PureOutcome result, bool view, PureFault fault, const char *type) {
const char *named = fault != PURE_FAULT_NONE ? fault_names[fault] : NULL;
if (result.status == PURE_OK) {
@@ -532,6 +535,8 @@ static void answer_app(Connection *c, PureOutcome result, bool view, PureFault f
respond(c, 404, "Not Found", view ? "no_value" : named ? named : "not_found", 0, (PureSpan){0});
} else if (result.status == PURE_CONFLICT) {
respond(c, 409, "Conflict", named ? named : "conflict", 0, (PureSpan){0});
+ } else if (fault == PURE_FAULT_MEMORY) {
+ respond(c, 503, "Service Unavailable", named, 0, (PureSpan){0});
} else if (named && (result.status == PURE_LIMIT || result.status == PURE_INVALID)) {
respond(c, 422, "Unprocessable Content", named, 0, (PureSpan){0});
} else if (result.status == PURE_INVALID) {
@@ -1346,6 +1351,10 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
return PURE_OK;
}
+/* The app refused a message it was given (a fault of its own): it was
+ * delivered, and is heard. Refused for memory it was not. */
+static bool app_refused(const PureApp *app) { return app->fault != PURE_FAULT_NONE && app->fault != PURE_FAULT_MEMORY; }
+
/* An input's event: a text, a key pressed (down, then up), or a message
* whose reply (a field of it) is borrowed from the request. */
static PureResult apply_input(PureApp *app, const Route *route, const PureEffect *effect, Applied *out) {
@@ -1360,6 +1369,7 @@ static PureResult apply_input(PureApp *app, const Route *route, const PureEffect
unsigned count = get_u32(delivery + 8);
const unsigned char *at = effect->value.data + 4 + DELIVERY;
out->gap = false;
+ out->fault = PURE_FAULT_NONE;
for (unsigned i = 0; i < count; ++i) {
uint32_t size = get_u32(at);
PureSpan record = {at + 4, size};
@@ -1370,11 +1380,13 @@ static PureResult apply_input(PureApp *app, const Route *route, const PureEffect
break;
}
PureResult status = pure_app_deliver(app, route->target, source, first + i, record, NULL);
- if (status != PURE_OK && app->fault == PURE_FAULT_NONE) break; /* not applied: sent again */
+ if (status != PURE_OK && !app_refused(app)) { /* not applied: sent again */
+ out->fault = app->fault;
+ break;
+ }
heard = first + i;
}
out->heard = pure_app_heard(app, source);
- out->fault = PURE_FAULT_NONE;
return PURE_OK; /* the save says whether they are stored */
}
unsigned batch = header & DELIVERED ? get_u32(delivery + 8) : 1;
@@ -1396,14 +1408,15 @@ static PureResult apply_input(PureApp *app, const Route *route, const PureEffect
source[delivery[12]] = 0;
status = pure_app_deliver(app, route->target, source, get_u64(delivery), payload, table ? placed : NULL);
/* The rest of a batch, in order: an app's refusal was heard and the
- * batch goes on; another failure stops it, to be sent again. */
- for (unsigned i = 1; i < batch && (status == PURE_OK || app->fault != PURE_FAULT_NONE); ++i) {
+ * batch goes on; another failure (the budget's too) stops it, to
+ * be sent again. */
+ for (unsigned i = 1; i < batch && (status == PURE_OK || app_refused(app)); ++i) {
const unsigned char *at = effect->value.data + lead + i * table;
for (size_t k = 0; k < 2 * (size_t)shape->field_count; ++k) placed[k] = get_u32(at + 4 * k);
PureSpan next = {payload.data + i * shape->prefix, payload.size - i * shape->prefix};
status = pure_app_deliver(app, route->target, source, get_u64(delivery) + i, next, placed);
}
- if (batch > 1 && app->fault != PURE_FAULT_NONE) status = PURE_OK;
+ if (batch > 1 && app_refused(app)) status = PURE_OK;
} else
status = table ? pure_app_message_fields(app, route->target, NULL, -1, payload, placed) :
pure_app_message(app, route->target, NULL, -1, payload);
@@ -1428,6 +1441,7 @@ static void apply_effect(Executor *ex, const Job *job, Applied *out) {
if (job->route.kind == ROUTE_INPUT) {
out->status = apply_input(app, &job->route, effect, out);
if (out->status == PURE_OK) ++ex->burst_events;
+ ex->memory_refused += out->fault == PURE_FAULT_MEMORY;
return;
}
bool read = effect->input == PURE_INPUT_READ;
@@ -1549,22 +1563,34 @@ static void after_save(Executor *ex) {
free(q.fresh);
}
-/* The answers the wires made become events, in order; then what the wires
+enum { ANSWER_WAIT_MS = 100 };
+
+/* The answers the wires made become events, in order, each taking its send
+ * out of the outbox; one the app has no room for waits, with the wire's
+ * after it, and is tried again after ANSWER_WAIT_MS. Then what the wires
* delivered leaves the outbox (with the next save). True when an answer was
* applied (the caller saves). */
static bool take_deliveries(Executor *ex) {
bool answered = false;
+ ex->answers_wait = false;
for (unsigned i = 0; i < ex->wire_count; ++i) {
PureWireAnswer answer;
while (pure_wire_answer(ex->wires[i], &answer)) {
ex->app.now = wall_ms();
- PureResult result = pure_app_deliver(&ex->app, answer.input, answer.source, answer.seq, answer.payload,
- answer.placed);
+ PureResult result = pure_app_answer(&ex->app, answer.input, pure_wire_output(ex->wires[i]), answer.source,
+ answer.seq, answer.payload, answer.placed);
+ if (result != PURE_OK && ex->app.fault == PURE_FAULT_MEMORY) {
+ pure_app_finish(&ex->app);
+ ++ex->memory_refused;
+ ex->answers_wait = true;
+ break;
+ }
if (result != PURE_OK && ex->app.fault == PURE_FAULT_NONE) {
ex->held[i] = true; /* not applied and not refused: sent again after a restart */
fprintf(stderr, "{\"wire\":\"%s\",\"seq\":%" PRIu64 ",\"error\":\"its answer was not applied\"}\n",
answer.source, answer.seq);
}
+ pure_wire_answered(ex->wires[i]);
pure_app_finish(&ex->app);
answered = true;
++ex->answers;
@@ -1711,7 +1737,7 @@ static void hand_back(void *context, Applied *applied, size_t count) {
LinkJob *link = applied[i].link;
if (!link) continue; /* records a link delivered: stored now, or not */
pure_link_done(link->listener, (PureLinkDone){link->token, applied[i].heard, applied[i].status == PURE_OK,
- applied[i].gap, false});
+ applied[i].gap, applied[i].fault == PURE_FAULT_MEMORY});
free(link->ingress);
free(link);
}
@@ -1948,7 +1974,8 @@ static void *run_owner(void *context) {
owner->delivered = false;
pthread_mutex_unlock(&owner->lock);
long due = run_timers(&owner->executor);
- bool answered = delivered && take_deliveries(&owner->executor);
+ bool answered = (delivered || owner->executor.answers_wait) && take_deliveries(&owner->executor);
+ if (owner->executor.answers_wait && (due < 0 || due > ANSWER_WAIT_MS)) due = ANSWER_WAIT_MS;
pthread_mutex_lock(&owner->lock);
/* Answers are saved with the next round, or now if none waits. */
if (answered && !owner->count && !owner->stopping) {
@@ -2193,6 +2220,8 @@ static void accept_all(Host *host) {
}
}
+static size_t memory_budget; /* --memory: the app's budget of what its states hold (0: none) */
+
static bool start_executor(Executor *ex, const PureModule *module, uint32_t show, uint32_t edit, const char *store) {
ex->module = module;
ex->show = show;
@@ -2203,6 +2232,7 @@ static bool start_executor(Executor *ex, const PureModule *module, uint32_t show
return false;
}
pure_app_background_checkpoints(&ex->app, true);
+ pure_app_memory(&ex->app, memory_budget);
return true;
}
@@ -2600,6 +2630,7 @@ int main(int argc, char **argv) {
else if (!strcmp(argv[i], "--senders")) senders_path = argv[i + 1];
else if (!strcmp(argv[i], "--link-idle-ms") && number(argv[i + 1], 86400000, &value) && value) link_idle_ms = value;
else if (!strcmp(argv[i], "--budget") && number(argv[i + 1], SIZE_MAX, &value) && value) budget = (size_t)value;
+ else if (!strcmp(argv[i], "--memory") && number(argv[i + 1], SIZE_MAX, &value) && value) memory_budget = (size_t)value;
else if (!strcmp(argv[i], "--connections") && number(argv[i + 1], 1u << 20, &value) && value) capacity = (size_t)value;
else if (!strcmp(argv[i], "--threads") && !strcmp(argv[i + 1], "auto")) threads = 0;
else if (!strcmp(argv[i], "--threads") && number(argv[i + 1], 256, &value) && value) threads = (unsigned)value;
@@ -2721,7 +2752,7 @@ int main(int argc, char **argv) {
pure_link_listener_close(link_listener, &heard); /* after the owner, which tells it what it stored */
size_t active = 0, retained = 0;
uint64_t accepted = 0, requests = 0, bursts = owner.executor.bursts, events = owner.executor.burst_events, expired = 0,
- ticks = owner.executor.ticks, shown = 0, checkpoints = 0;
+ ticks = owner.executor.ticks, shown = 0, checkpoints = 0, memory_refused = owner.executor.memory_refused;
for (unsigned i = 0; i < threads; ++i) {
shown += hosts[i].shown;
active += hosts[i].active;
@@ -2731,6 +2762,7 @@ int main(int argc, char **argv) {
bursts += hosts[i].local.bursts;
events += hosts[i].local.burst_events;
ticks += hosts[i].local.ticks;
+ memory_refused += hosts[i].local.memory_refused;
expired += hosts[i].expired;
stop_shard(&hosts[i]);
}
@@ -2750,6 +2782,7 @@ int main(int argc, char **argv) {
sent.refused += c.refused;
pure_link_close(owner.executor.links[i]); /* what was not stored stays in the outbox */
}
+ size_t state_bytes = owned ? pure_app_state_bytes(&owner.executor.app) : 0;
if (owned) {
checkpoints = owner.executor.checkpoints;
stop_executor(&owner.executor);
@@ -2768,12 +2801,13 @@ int main(int argc, char **argv) {
",\"link_refused\":%" PRIu64 ",\"listener_packets\":%" PRIu64 ",\"listener_received\":%" PRIu64
",\"listener_deliveries\":%" PRIu64 ",\"listener_woken\":%" PRIu64 ",\"listener_remembered\":%" PRIu64
",\"listener_swept\":%" PRIu64 ",\"listener_active\":%" PRIu64 ",\"listener_keyed\":%" PRIu64
- ",\"listener_secrets\":%" PRIu64 ",\"listener_epochs\":%" PRIu64 ",\"listener_refused\":%" PRIu64 "}\n",
+ ",\"listener_secrets\":%" PRIu64 ",\"listener_epochs\":%" PRIu64 ",\"listener_refused\":%" PRIu64
+ ",\"state_bytes\":%zu,\"memory_refused\":%" PRIu64 "}\n",
active, retained, accepted, requests, bursts, events, ticks, expired, threads, owned ? "true" : "false",
owner.executor.publications, shown, delivered, attempts, owner.executor.answers, groups, grouped,
checkpoints, sent.packets, sent.records, sent.resent, sent.lost, sent.acknowledged, sent.stored,
sent.refused, heard.packets, heard.received, heard.deliveries, heard.woken, heard.remembered, heard.swept,
- heard.active, heard.keyed, heard.secrets, heard.epochs, heard.refused);
+ heard.active, heard.keyed, heard.secrets, heard.epochs, heard.refused, state_bytes, memory_refused);
free(hosts);
free(thread);
return 0;
@@ -2781,11 +2815,13 @@ usage:
fputs("pure-serve --module FILE.pmod --store DIRECTORY [--port 8767] [--threads 1|N|auto] [--budget BYTES] "
"[--connections 1024] [--timeout-ms 10000] [--send OUTPUT=URL ...] [--name NAME] "
"[--send-ca FILE] [--reply OUTPUT=INPUT ...] [--seal OUTPUT=KEY ...] [--key FILE] [--sealed-only] "
- "[--sender NAME=KEY ...] [--link-port PORT] [--senders FILE] [--link-idle-ms 30000] "
+ "[--sender NAME=KEY ...] [--link-port PORT] [--senders FILE] [--link-idle-ms 30000] [--memory BYTES] "
"[--surface LIBRARY [-- ARGUMENTS]]\n"
" --send OUTPUT=pure://host:port/INPUT: to a Pure node's --link-port, the way between nodes (UDP, no\n"
" connection, docs/design/012-link-protocol.md; its key by --seal OUTPUT=KEY)\n"
" --send OUTPUT=http[s]://host:port/path: to anything else, over HTTP\n"
- " --senders FILE: more senders the --link-port takes, a line each: NAME KEY\n", stderr);
+ " --senders FILE: more senders the --link-port takes, a line each: NAME KEY\n"
+ " --memory BYTES: what the app's states may hold (lists' items, texts, sends not yet stored): an event\n"
+ " past it is refused, 503 memory (docs/design/013-memory-budget.md)\n", stderr);
return 2;
}
diff --git a/tests/check.py b/tests/check.py
index 3c77dc4..e15a996 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -444,6 +444,20 @@ view leaders = each e in board {
"""
+def post(port, path, body):
+ """A JSON body posted on its own connection: status, X-Pure-Error."""
+ import http.client
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=30)
+ try:
+ data = json.dumps(body).encode()
+ connection.request('POST', path, data, {'Content-Length': str(len(data)), 'Content-Type': 'application/json'})
+ response = connection.getresponse()
+ response.read()
+ return response.status, response.getheader('X-Pure-Error')
+ finally:
+ connection.close()
+
+
def http(port, method, path, body=b''):
"""One request on its own connection: status, X-Pure-Error, content type, body."""
import http.client
@@ -1074,6 +1088,131 @@ def server_links_refused(aup, pure, work):
return {'said': said[0]['error'], 'packets': stats['a']['link_packets']}
+
+MEMORY_RECEIVER = """record Order {
+ item: text
+ count = 0
+}
+state kept: list of Order
+state got = 0
+input order = message {
+ item: text
+ count: u32
+ to: {
+ city: text
+ }
+}
+input clean = message {
+ below: u32
+}
+on order {
+ kept.append(Order { item: order.item, count: order.count })
+ got = got + 1
+}
+on clean {
+ for o in kept where o.count < clean.below {
+ kept.remove(o)
+ }
+}
+view count = got
+view held = kept.count
+"""
+
+
+def server_memory(aup, pure, work):
+ """What an app holds, bounded (--memory, docs/design/013-memory-budget.md).
+ A sender whose link's receiver is down fills its outbox: its next buy is
+ refused, 503 memory, and changes nothing, while its views still read;
+ once the receiver is up and stores what waited, it takes buys again. The
+ receiver, started again with a budget below what it keeps, refuses the
+ link's deliveries (and a client's order, 503 memory) but not the clean
+ that frees them; meanwhile its link hands the records over a few times a
+ second, not at every turn, and once freed it takes them all, once."""
+ import select
+ import socket
+ import time
+ sender_module, receiver_module = work / 'memory-sender.pmod', work / 'memory-receiver.pmod'
+ for module, text in ((sender_module, LINK_SENDER), (receiver_module, MEMORY_RECEIVER)):
+ source = module.with_suffix('.aup')
+ source.write_text(text)
+ compile_module(aup, source, module)
+
+ 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 [json.loads(line) for line in error.decode().splitlines() if line.startswith('{"active"')][-1]
+
+ def number(port, view):
+ return int(http(port, 'GET', f'/views/{view}')[3])
+
+ def until(test, seconds): # test once more at most after the deadline: it may post
+ deadline = time.time() + seconds
+ while not test():
+ if time.time() > deadline:
+ return False
+ time.sleep(0.02)
+ return True
+
+ keys = {}
+ for name in ('sender', 'receiver'):
+ process, info = start([pure / 'build/pure-serve', '--module', receiver_module, '--store', work / 'memory-key',
+ '--key', work / f'memory-{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)
+ receiving = [pure / 'build/pure-serve', '--module', receiver_module, '--store', work / 'memory-b',
+ '--key', work / 'memory-receiver.key', '--link-port', link_port,
+ '--sender', f'memory-sender.ordered={keys["sender"]}']
+ sender = start([pure / 'build/pure-serve', '--module', sender_module, '--store', work / 'memory-a',
+ '--key', work / 'memory-sender.key', '--send', f'ordered=pure://127.0.0.1:{link_port}/order',
+ '--seal', f'ordered={keys["receiver"]}', '--memory', 1000], port_a)[0]
+ receiver, stats, item = None, {}, 'x' * 40
+ try:
+ accepted, refused = 0, None
+ while accepted < 200 and not refused:
+ status = post(port_a, '/inputs/buy', {'item': item, 'count': 1})
+ accepted += status[0] == 200
+ refused = status if status[0] != 200 else None
+ assert refused == (503, 'memory') and 3 <= accepted < 200, (accepted, refused)
+ assert number(port_a, 'sent') == accepted
+ receiver = start(receiving, port_b)[0]
+ assert until(lambda: number(port_b, 'count') == accepted, 20), number(port_b, 'count')
+ assert until(lambda: post(port_a, '/inputs/buy', {'item': item, 'count': 1})[0] == 200, 10)
+ assert until(lambda: number(port_b, 'count') == accepted + 1, 10)
+ stop(receiver)
+ receiver = start(receiving + ['--memory', 300], port_b)[0] # below what it keeps
+ order = {'item': item, 'count': 1, 'to': {'city': 'Seoul'}}
+ assert post(port_b, '/inputs/order', order) == (503, 'memory')
+ for _ in range(3):
+ assert post(port_a, '/inputs/buy', {'item': item, 'count': 1})[0] == 200
+ time.sleep(2.0)
+ assert number(port_b, 'count') == accepted + 1 and number(port_a, 'sent') == accepted + 4
+ assert post(port_b, '/inputs/clean', {'below': 100})[0] == 200
+ assert until(lambda: number(port_b, 'count') == accepted + 4, 5), number(port_b, 'count')
+ assert number(port_b, 'held') == 3
+ finally:
+ for name, process in (('b', receiver), ('a', sender)):
+ if process:
+ stats[name] = stop(process)
+ assert stats['a']['memory_refused'] >= 1 and stats['a']['state_bytes'] <= 1000, stats['a']
+ assert 2 <= stats['b']['memory_refused'] <= 40 and stats['b']['state_bytes'] <= 300, stats['b']
+ return {'sends_held': accepted, 'receiver_refused': stats['b']['memory_refused'],
+ 'link_resent': stats['a']['link_resent']}
+
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
@@ -2312,6 +2451,113 @@ def server_answers(aup, pure, work):
return {'orders': sent, 'paid': paid, 'declined': declined, 'answers_after_restart': stats['answers']}
+
+RECEIPT_SOURCE = """record Receipt {
+ order: text
+ note: text
+ status = 0
+}
+state receipts: list of Receipt
+state sent = 0
+state answered = 0
+input buy = message {
+ order: text
+}
+output charge = message {
+ order: text
+}
+input charged = message {
+ order: text
+ note: text
+ http_status: u32
+}
+input clean = message {
+ below: u32
+}
+on buy {
+ sent = sent + 1
+ send charge { order: buy.order }
+}
+on charged {
+ answered = answered + 1
+ receipts.append(Receipt { order: charged.order, note: charged.note, status: charged.http_status })
+}
+on clean {
+ for r in receipts where r.status < clean.below {
+ receipts.remove(r)
+ }
+}
+view sent_view = sent
+view answered_view = answered
+view receipts_view = receipts.count
+"""
+
+
+def server_answers_memory(aup, pure, work):
+ """Answers (--reply) under a memory budget: an answer keeps a receipt
+ larger than its send, so once receipts fill the budget the next answer
+ waits (it is not lost, and not applied twice) and buys are refused, 503
+ memory. A clean that frees the receipts lets the answers in, each once."""
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+ import threading
+ import time
+ module = work / 'receipt.pmod'
+ module.with_suffix('.aup').write_text(RECEIPT_SOURCE)
+ compile_module(aup, module.with_suffix('.aup'), module)
+ seen, lock = {}, threading.Lock()
+
+ class Api(BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+
+ def do_POST(self):
+ body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
+ with lock:
+ seen[body['order']] = seen.get(body['order'], 0) + 1
+ data = json.dumps({'note': 'n' * 200}).encode()
+ self.send_response(200)
+ self.send_header('Content-Type', 'application/json')
+ self.send_header('Content-Length', str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+
+ def log_message(self, *args):
+ pass
+
+ server = ThreadingHTTPServer(('127.0.0.1', 0), Api)
+ threading.Thread(target=server.serve_forever, daemon=True).start()
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'receipt-store', '--memory', 3000,
+ '--send', f'charge=http://127.0.0.1:{server.server_address[1]}/charge', '--reply', 'charge=charged'])
+
+ def number(view):
+ return int(http(host.port, 'GET', f'/views/{view}')[3])
+
+ try:
+ bought, refusals = 0, 0
+ while refusals < 3 and bought < 200:
+ status = post(host.port, '/inputs/buy', {'order': f'o{bought}'})
+ if status[0] == 200:
+ bought += 1
+ else:
+ assert status == (503, 'memory'), status
+ refusals += 1
+ time.sleep(0.02)
+ time.sleep(0.5)
+ assert 3 <= bought < 200 and number('sent_view') == bought, bought
+ waiting = bought - number('answered_view')
+ assert waiting >= 1, (bought, number('answered_view'))
+ assert post(host.port, '/inputs/clean', {'below': 1000})[0] == 200
+ deadline = time.time() + 10
+ while number('answered_view') < bought and time.time() < deadline:
+ time.sleep(0.02)
+ assert number('answered_view') == bought and number('receipts_view') == waiting, (number('answered_view'), waiting)
+ finally:
+ stats = host.stop()
+ server.shutdown()
+ with lock:
+ assert len(seen) == bought and all(count == 1 for count in seen.values()), seen
+ assert stats[-1]['memory_refused'] >= refusals + 1, stats[-1]
+ return {'bought': bought, 'answers_waited': waiting, 'refused': stats[-1]['memory_refused']}
+
BEAT_SOURCE = """state beats = 0
state last = 0
state stamped = 0
@@ -2605,6 +2851,7 @@ def main():
report['server_links_udp'] = server_links_udp(aup, pure, work)
report['server_links_udp_lossy'] = server_links_udp_lossy(aup, pure, work)
report['server_links_refused'] = server_links_refused(aup, pure, work)
+ report['server_memory'] = server_memory(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)
@@ -2614,6 +2861,7 @@ def main():
report['server_rows'] = server_rows(aup, pure, work)
report['server_arguments'] = server_arguments(aup, pure, work)
report['server_answers'] = server_answers(aup, pure, work)
+ report['server_answers_memory'] = server_answers_memory(aup, pure, work)
print(json.dumps(report))