aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-01 02:19:04 +0100
committereunhhu <[email protected]>2026-10-01 02:19:04 +0100
commit2a0f601cbd3ea6af1bb031b29a07d5facb0a4579 (patch)
tree70b134543c91bb3e2b0e3b344e91896bad1d9973
parenta09ef095623c3b3f951edff2eb0c013708159f70 (diff)
downloadpure-2a0f601cbd3ea6af1bb031b29a07d5facb0a4579.tar.gz
pure-2a0f601cbd3ea6af1bb031b29a07d5facb0a4579.zip
Bound what an event or a view may run: a work budget (017)
An event's run was bounded only by its lists: a walk or a fold is its block's length for each item, and a list holds up to 16 million. One event could hold the owner for seconds, one view a connection thread. pure_app_work(app, n) budgets one unit of work: an event, or one evaluation of a view, a cell or a table's filter. It is counted as written, as each part begins: a block's length, and a fold's or walk's block length for every item of its list (or slice), whatever the plan runs (lanes, a group or rank pass, a range). Nothing is counted inside the interpreter's loop. Past the budget an event fails with the new fault work and changes nothing (PURE_LIMIT, so 422 work over HTTP); a view, a cell or a table's rows have no value and pure_app_view_fault says work, which pure-serve answers 422 work, for a whole table when a cell of it ran past. pure-serve --work N sets it (0: none). test-module counts exact amounts on the shop's walk, the tasks' fold view and a filtered table, and lane_folds now checks that a planned fold counts what the same fold run item by item counts (3,600 folds, every view, cell, filter and handler). server_work grows a list over HTTP until the walk, the table and the fold each pass a budget of 400.
-rw-r--r--core/module.c53
-rw-r--r--core/module.h25
-rw-r--r--core/test-module.c78
-rw-r--r--serve/serve.c39
-rw-r--r--tests/check.py68
5 files changed, 239 insertions, 24 deletions
diff --git a/core/module.c b/core/module.c
index 2688395..a5bc3c5 100644
--- a/core/module.c
+++ b/core/module.c
@@ -165,7 +165,7 @@ static void write_i64(unsigned char *p, int64_t value) {
const char *pure_fault_name(PureFault fault) {
static const char *const names[PURE_FAULT_COUNT] = {"none", "divide", "non_finite", "clamp", "limit", "not_found",
- "encoding", "exists", "memory", "overflow"};
+ "encoding", "exists", "memory", "overflow", "work"};
return fault < PURE_FAULT_COUNT ? names[fault] : "none";
}
@@ -1437,10 +1437,17 @@ typedef struct {
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 :
+ return kind == PURE_FAULT_LIMIT || kind == PURE_FAULT_WORK ? PURE_LIMIT : kind == PURE_FAULT_NOT_FOUND ? PURE_NOT_FOUND :
kind == PURE_FAULT_EXISTS ? PURE_CONFLICT : kind == PURE_FAULT_MEMORY ? PURE_BUDGET : PURE_INVALID;
}
+/* Count what the unit running (an event, or a view's evaluation) is about
+ * to run, as written: false past the app's work budget. */
+static bool charge(PureApp *app, uint64_t instructions) {
+ app->work += instructions;
+ return !app->work_budget || app->work <= app->work_budget;
+}
+
static bool reserve(PureArena *arena, size_t more) {
if (more <= arena->capacity - arena->used) return true;
size_t capacity = arena->capacity ? arena->capacity : 256;
@@ -2598,6 +2605,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
Slot high = stack[--sp], low = stack[--sp];
slice_of(list, m->state_key[arg >> 8 & 0xFF] - 1u, low, high, &from, &to);
}
+ if (!charge(app, (uint64_t)inner.length * (to - from))) { kind = PURE_FAULT_WORK; goto failed; } /* as written */
if (!op->shape || !fold_shaped(m, values, op, list, &per, &fold)) {
if (op->range) /* only the items a range of keys allows can count */
range_bounds(m, values, &m->range[op->range - 1], list, m->state_key[arg >> 8 & 0xFF] - 1u, &per, &from, &to);
@@ -2628,7 +2636,13 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
if (arg & PURE_EACH_SLICED) { /* the items with keys from low up to before high */
Slot high = stack[--sp], low = stack[--sp];
slice_of(app->lists[walk.list], m->state_key[walk.list] - 1u, low, high, &walk.at, &walk.end);
- } else if (op->range) /* a where that bounds the key: only the items in bounds */
+ }
+ size_t items = pure_list_count(app->lists[walk.list]); /* as written: what it starts with, the list only shrinking */
+ if (!charge(app, (uint64_t)inner.length * ((walk.end < items ? walk.end : items) - walk.at))) {
+ kind = PURE_FAULT_WORK;
+ goto failed;
+ }
+ if (!(arg & PURE_EACH_SLICED) && op->range) /* a where that bounds the key: only the items in bounds */
range_bounds(m, values, &m->range[op->range - 1], app->lists[walk.list], m->state_key[walk.list] - 1u, &per,
&walk.at, &walk.end);
while (walk.at < walk.end && walk.at < pure_list_count(app->lists[walk.list])) {
@@ -3083,6 +3097,13 @@ static void clear_rows(PureApp *app) {
void pure_app_memory(PureApp *app, size_t bytes) { app->memory = bytes; }
+void pure_app_work(PureApp *app, uint64_t instructions) {
+ app->work_budget = instructions;
+ for (unsigned i = 0; i < app->row_tables; ++i) app->rows[i].changes = UINT64_MAX; /* a filter refused before may pass */
+}
+
+PureFault pure_app_view_fault(const PureApp *app) { return app->view_fault; }
+
size_t pure_app_state_bytes(const PureApp *app) {
size_t bytes = (app->outbox ? pure_list_size(app->outbox) : 0) + (app->heard ? pure_list_size(app->heard) : 0);
for (uint32_t left = app->counted; left; left &= left - 1) {
@@ -3108,9 +3129,12 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
memcpy(pending, app->values, sizeof(app->values));
app->fault = PURE_FAULT_NONE;
app->fault_pc = UINT32_MAX;
+ app->work = 0;
PureResult result = PURE_OK;
for (uint32_t i = 0; i < m->handlers && result == PURE_OK; ++i)
- if (m->handler[i].event == event) result = run(m, pending, m->handler[i].code, ev, NULL);
+ if (m->handler[i].event == event)
+ result = charge(app, m->handler[i].code.length) ? run(m, pending, m->handler[i].code, ev, NULL)
+ : fault(app, PURE_FAULT_WORK, m->handler[i].code.start);
/* An edit needs the revision it was made against. A save rebases an
* edited document and releasing texts may reclaim earlier text versions:
* keep such a reply by copying it first. */
@@ -3420,7 +3444,11 @@ static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) {
if (!argue(app, view, &ev)) return PURE_INVALID;
PureFault fault = app->fault;
uint32_t fault_pc = app->fault_pc;
- PureResult result = run(app->module, values, app->module->view_code[view], &ev, out);
+ PureBlock code = app->module->view_code[view];
+ app->work = 0;
+ app->fault = PURE_FAULT_NONE;
+ PureResult result = charge(app, code.length) ? run(app->module, values, code, &ev, out) : PURE_LIMIT;
+ app->view_fault = result == PURE_OK ? PURE_FAULT_NONE : app->fault != PURE_FAULT_NONE ? app->fault : PURE_FAULT_WORK;
app->fault = fault;
app->fault_pc = fault_pc;
return result;
@@ -3461,9 +3489,18 @@ static struct PureRows *filtered(PureApp *app, uint32_t table) {
}
if (rows->changes == app->changes && (!m->view_arguments[table] || rows->given == app->arguments_given)) return rows;
rows->count = 0;
+ /* A unit of work of its own, counted as written: the filter for every item. */
+ const PureList *list = app->lists[m->view_list[table]];
+ app->work = 0;
+ rows->fault = charge(app, (uint64_t)m->view_code[table].length * pure_list_count(list)) ? PURE_FAULT_NONE : PURE_FAULT_WORK;
+ uint64_t work = app->work;
+ if (rows->fault != PURE_FAULT_NONE) {
+ rows->changes = app->changes;
+ rows->given = app->arguments_given;
+ return rows;
+ }
/* A leaf of the list at a time when the filter has lane forms; a leaf
* where an item would fault goes item by item, leaving that item out. */
- const PureList *list = app->lists[m->view_list[table]];
PureRun run;
if (m->view_plan[table]) {
double store[PURE_MODULE_STACK][PURE_LIST_RUN], scratch[PURE_LIST_RUN];
@@ -3488,6 +3525,7 @@ static struct PureRows *filtered(PureApp *app, uint32_t table) {
} else
for (size_t item = 0, items = pure_list_count(list); item < items; ++item)
if (!keep_evaluated(app, table, rows, item)) return NULL;
+ app->work = work; /* the filter's, whichever items ran alone */
rows->changes = app->changes;
rows->given = app->arguments_given;
return rows;
@@ -3501,6 +3539,7 @@ bool pure_app_row_item(PureApp *app, uint32_t table, size_t row, size_t *item) {
return row < pure_list_count(app->lists[m->view_list[table]]);
}
struct PureRows *rows = filtered(app, table);
+ app->view_fault = rows ? rows->fault : PURE_FAULT_NONE;
if (!rows || row >= rows->count) return false;
*item = rows->items[row];
return true;
@@ -3528,6 +3567,7 @@ bool pure_app_item_row(PureApp *app, uint32_t table, size_t item, size_t *row) {
/* A view outside a table (column false), or a column and a row of its table. */
static PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type, Slot *slot) {
const PureModule *m = app->module;
+ app->view_fault = PURE_FAULT_NONE;
if (view >= m->views) return PURE_INVALID;
uint8_t kind = m->view_type[view] == PURE_VIEW_BOOL ? PURE_VIEW_NUM : m->view_type[view]; /* a bool's value is 0 or 1 */
if (kind != type || (m->view_table[view] != 0) != column) return PURE_INVALID;
@@ -3559,6 +3599,7 @@ size_t pure_app_rows(PureApp *app, uint32_t table) {
const PureModule *m = app->module;
if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return 0;
struct PureRows *rows = filtered(app, table);
+ app->view_fault = rows ? rows->fault : PURE_FAULT_NONE;
return rows ? rows->count : m->view_code[table].length ? 0 : pure_list_count(app->lists[m->view_list[table]]);
}
diff --git a/core/module.h b/core/module.h
index e106fc7..c4919c9 100644
--- a/core/module.h
+++ b/core/module.h
@@ -153,14 +153,15 @@ 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). MEMORY: the event would leave the states holding
* more than the app's budget (pure_app_memory; it reports PURE_BUDGET).
- * OVERFLOW: an i64 past its range. */
+ * OVERFLOW: an i64 past its range. WORK: the event (or a view's evaluation)
+ * would run more than the app's work budget (pure_app_work; PURE_LIMIT). */
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_MEMORY, PURE_FAULT_OVERFLOW, PURE_FAULT_COUNT
+ PURE_FAULT_MEMORY, PURE_FAULT_OVERFLOW, PURE_FAULT_WORK, PURE_FAULT_COUNT
} PureFault;
/* A fault's name as hosts report it: none, divide, non_finite, clamp, limit,
- * not_found, encoding, exists, memory, overflow. */
+ * not_found, encoding, exists, memory, overflow, work. */
const char *pure_fault_name(PureFault);
/* plan, shape and range are the runtime's, set at load: a fold whose block
@@ -377,6 +378,8 @@ typedef struct {
PureFault fault;
uint32_t fault_pc;
size_t memory; /* the budget of what the states hold (0: none): pure_app_memory */
+ uint64_t work_budget, work; /* pure_app_work's budget (0: none), and what the unit running has used */
+ PureFault view_fault; /* why the last view evaluated had no value (pure_app_view_fault) */
char why[128]; /* why the last start or replace was refused, when it says (else empty; a failed start keeps it) */
uint32_t counted; /* the states it counts, a bit each: lists (not a message's rows) and texts */
uint64_t ticks, events;
@@ -384,7 +387,7 @@ typedef struct {
* sets it, so a script can replay any time; views never see it. */
double now;
uint64_t changes; /* counts every change of states, never back: what a filter's rows are fresh for */
- struct PureRows { uint32_t table; uint64_t changes, given; size_t *items, count, capacity; } *rows; /* a filtered table's items */
+ struct PureRows { uint32_t table; uint64_t changes, given; size_t *items, count, capacity; PureFault fault; } *rows; /* a filtered table's items */
unsigned row_tables;
/* Current message or text event, kept until pure_app_finish. */
void *document;
@@ -544,6 +547,20 @@ 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 *);
+/* An app's work budget (docs/design/017-apps-on-one-host.md): what one
+ * event, or one evaluation of a view (a value, a cell, a table's filter),
+ * may run, in instructions counted as written, whatever the plan: a block's
+ * length each time it runs, and a fold's or a walk's block length for each
+ * item of its list (of its slice, when it takes one) it goes over, all
+ * counted as the fold or walk begins. Past it the event fails with the fault
+ * PURE_FAULT_WORK (PURE_LIMIT) and changes nothing, and a view has no value.
+ * 0: no budget, at first. */
+void pure_app_work(PureApp *, uint64_t instructions);
+/* Why the last view evaluated on this app (or a table's rows asked for)
+ * had no value: PURE_FAULT_WORK past the work budget, another fault, or
+ * PURE_FAULT_NONE when it had one. */
+PureFault pure_app_view_fault(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 4fe5182..f627dea 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -2692,13 +2692,14 @@ static void lane_folds(void) {
double x = 0, y = 0;
bool ok_a = pure_app_view(&a, v, &x), ok_b = pure_app_view(&b, v, &y);
faults += !ok_a;
+ CHECK(a.work == b.work); /* counted as written, however it ran */
if (!same_number(ok_a, x, ok_b, y)) {
fprintf(stderr, "lane round %u view %u: %d %g / %d %g\n", round, v, ok_a, x, ok_b, y);
abort();
}
}
size_t rows = pure_app_rows(&a, VIEWS);
- CHECK(rows == pure_app_rows(&b, VIEWS));
+ CHECK(rows == pure_app_rows(&b, VIEWS) && a.work == b.work);
for (size_t row = 0; row < rows; ++row) {
size_t item_a, item_b;
CHECK(pure_app_row_item(&a, VIEWS, row, &item_a) && pure_app_row_item(&b, VIEWS, row, &item_b) && item_a == item_b);
@@ -2706,6 +2707,7 @@ static void lane_folds(void) {
double x = 0, y = 0;
bool ok_a = pure_app_cell(&a, VIEWS + 1 + c, row, &x), ok_b = pure_app_cell(&b, VIEWS + 1 + c, row, &y);
++cells;
+ CHECK(a.work == b.work);
if (!same_number(ok_a, x, ok_b, y)) {
fprintf(stderr, "lane round %u column %u row %zu: %d %g / %d %g\n", round, c, row, ok_a, x, ok_b, y);
abort();
@@ -2714,7 +2716,8 @@ static void lane_folds(void) {
}
passes += pure_app_passes(&a);
PureResult went_a = pure_app_input(&a, 0, PURE_EDGE_DOWN), went_b = pure_app_input(&b, 0, PURE_EDGE_DOWN);
- if (went_a != went_b || a.fault != b.fault || a.fault_pc != b.fault_pc || memcmp(a.values, b.values, 4 * sizeof(double))) {
+ if (went_a != went_b || a.fault != b.fault || a.fault_pc != b.fault_pc || a.work != b.work ||
+ memcmp(a.values, b.values, 4 * sizeof(double))) {
fprintf(stderr, "lane round %u handler: %d %d %u / %d %d %u\n", round, went_a, a.fault, a.fault_pc, went_b, b.fault,
b.fault_pc);
abort();
@@ -4334,6 +4337,76 @@ static void links(void) {
* 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. */
+/* ---- Work budgets (docs/design/017) ------------------------------------
+ * An event, or one evaluation of a view, runs at most the app's work
+ * budget, counted as written as it begins: a block's length, and a fold's or
+ * a walk's block length for every item of its list. Past it an event fails
+ * with work and changes nothing; a view, a cell or a table's rows have no
+ * value, and pure_app_view_fault says why. (lane_folds checks that a planned
+ * fold counts what the same fold run item by item counts.) */
+static void work_budget(void) {
+ CHECK(!strcmp(pure_fault_name(PURE_FAULT_WORK), "work"));
+ /* The shop: restock runs 3 instructions; clean 23, and its walk's 22 for each item stocked. */
+ CHECK(load(walk_spec(walk_code, LEN(walk_code), walk_inputs, 3, walk_handlers, 3, walk_lists, NULL, 0), &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ unsigned char payload[8];
+ for (uint32_t item = 0; item < 5; ++item) {
+ put32(put32(payload, item), 2);
+ CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_OK && app.work == 3);
+ }
+ pure_app_work(&app, 23 + 22 * 5 - 1);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_LIMIT && app.fault == PURE_FAULT_WORK);
+ for (size_t i = 0; i < 5; ++i) CHECK(pure_list_get(app.lists[W_STOCK], i, 1).num == 2); /* nothing changed */
+ pure_app_work(&app, 23 + 22 * 5);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.work == 23 + 22 * 5);
+ for (size_t i = 0; i < 5; ++i) CHECK(pure_list_get(app.lists[W_STOCK], i, 1).num == 3);
+ pure_app_work(&app, 2);
+ put32(put32(payload, 9), 1);
+ CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_LIMIT && app.fault == PURE_FAULT_WORK);
+ CHECK(pure_list_count(app.lists[W_STOCK]) == 5);
+ pure_app_work(&app, 0); /* none */
+ CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_OK && pure_list_count(app.lists[W_STOCK]) == 6);
+ pure_app_stop(&app);
+ pure_module_release(&module_a);
+
+ /* Views: open is 4 instructions and its fold's 3 for each task. */
+ CHECK(load(fold_spec(fold_code, LEN(fold_code), fold_views, LEN(fold_views), fold_handlers, 2), &module_a) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ for (const char *word = "abc"; *word; ++word)
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)word, 1}) == PURE_OK && app.work == 7);
+ double value;
+ CHECK(number_view(&app, 0) == 3 && app.work == 4 + 3 * 3 && pure_app_view_fault(&app) == PURE_FAULT_NONE);
+ pure_app_work(&app, 4 + 3 * 3 - 1);
+ CHECK(!pure_app_view(&app, 0, &value) && pure_app_view_fault(&app) == PURE_FAULT_WORK);
+ CHECK(app.fault == PURE_FAULT_NONE); /* a view leaves the app's own fault alone */
+ pure_app_work(&app, 4 + 3 * 3);
+ CHECK(number_view(&app, 0) == 3 && pure_app_view_fault(&app) == PURE_FAULT_NONE);
+ pure_app_work(&app, 0);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && pure_app_point(&app, 1, 0) == PURE_OK &&
+ pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && pure_app_point(&app, 1, 1) == PURE_OK &&
+ pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); /* all done */
+ CHECK(!pure_app_view(&app, 4, &value) && pure_app_view_fault(&app) == PURE_FAULT_NOT_FOUND); /* fewest open: none */
+ pure_app_stop(&app);
+ pure_module_release(&module_a);
+
+ /* A table's filter is a unit of its own: 3 instructions for each task; a cell's column is 2. */
+ CHECK(load(filter_spec(filter_code, LEN(filter_code), filter_views, 3), &module_a) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ for (const char *word = "abcd"; *word; ++word)
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)word, 1}) == PURE_OK);
+ pure_app_work(&app, 3 * 4 - 1);
+ size_t at;
+ CHECK(pure_app_rows(&app, 0) == 0 && pure_app_view_fault(&app) == PURE_FAULT_WORK && !pure_app_row_item(&app, 0, 0, &at));
+ CHECK(!pure_app_cell(&app, 2, 0, &value) && pure_app_view_fault(&app) == PURE_FAULT_WORK);
+ pure_app_work(&app, 3 * 4); /* the rows refused before are asked again */
+ CHECK(pure_app_rows(&app, 0) == 4 && app.work == 3 * 4 && pure_app_view_fault(&app) == PURE_FAULT_NONE);
+ CHECK(pure_app_cell(&app, 2, 3, &value) && value == 3 && app.work == 2);
+ pure_app_stop(&app);
+ pure_module_release(&module_a);
+ puts("{\"work_budget\":\"events-views-cells-rows-rolled-back\"}");
+}
+
static void memory_budget(void) {
enum { ADD, DROP, SEND_KEY, OUT, TAKE, ANSWER };
static const S nums[] = {{"n", 0}};
@@ -5166,6 +5239,7 @@ int main(void) {
bools();
integers();
memory_budget();
+ work_budget();
fold_mutations();
pointing();
list_mutations();
diff --git a/serve/serve.c b/serve/serve.c
index 6d9d5d6..b283874 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -83,6 +83,8 @@ static PureSealKey *seal_key;
static bool sealed_only;
static struct Sender { char name[PURE_SOURCE + 1]; unsigned char key[PURE_SEAL_KEY]; } senders[PURE_MODULE_INPUTS];
static unsigned sender_count;
+static size_t memory_budget; /* --memory: the app's budget of what its states hold (0: none) */
+static uint64_t work_budget; /* --work: what an event or a view's evaluation may run (0: none) */
/* A delivery's X-Pure-Seal: the suite, the sender's session (its
* encapsulated key), the seal's number in it and, when the sender sealed
@@ -1333,6 +1335,7 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
} else {
*type = "application/json";
size_t rows = pure_app_rows(app, view), from = route->from < rows ? route->from : rows;
+ if (pure_app_view_fault(app) == PURE_FAULT_WORK) status = PURE_LIMIT, rows = from = 0; /* its filter's */
size_t end = from + (route->count < rows - from ? route->count : rows - from);
/* Each column's key, ,"name": (the first row's first without the comma), made once. */
struct { uint32_t view; size_t length; char key[PURE_MODULE_NAME + 4]; } columns[PURE_MODULE_VIEWS];
@@ -1355,26 +1358,33 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
int64_t integer;
if (!pure_app_cell_i64(app, columns[k].view, row, &integer)) put_bytes(&o, "null", 4);
else put_integer(&o, integer);
- continue;
- }
- if (kind == PURE_VIEW_NUM || kind == PURE_VIEW_BOOL) {
+ } else if (kind == PURE_VIEW_NUM || kind == PURE_VIEW_BOOL) {
if (!pure_app_cell(app, columns[k].view, row, &value)) put_bytes(&o, "null", 4);
else if (kind == PURE_VIEW_BOOL) put_bytes(&o, value ? "true" : "false", value ? 4 : 5);
else put_number(&o, value);
- continue;
+ } else {
+ size_t mark = o.size;
+ put_bytes(&o, "\"", 1);
+ if (pure_app_cell_text(app, columns[k].view, row, put_json_span, &o) == PURE_OK) put_bytes(&o, "\"", 1);
+ else if (!o.failed) {
+ o.size = mark;
+ put_bytes(&o, "null", 4);
+ }
}
- size_t mark = o.size;
- put_bytes(&o, "\"", 1);
- if (pure_app_cell_text(app, columns[k].view, row, put_json_span, &o) == PURE_OK) put_bytes(&o, "\"", 1);
- else if (!o.failed) {
- o.size = mark;
- put_bytes(&o, "null", 4);
+ if (pure_app_view_fault(app) == PURE_FAULT_WORK) { /* a cell past the work budget fails the table */
+ status = PURE_LIMIT;
+ end = row;
+ break;
}
}
put_bytes(&o, "}", 1);
}
put_bytes(&o, "]}", 2);
}
+ if (status != PURE_OK && pure_app_view_fault(app) == PURE_FAULT_WORK) { /* past the work budget: 422 work */
+ answer->fault = PURE_FAULT_WORK;
+ status = PURE_LIMIT;
+ }
if (status == PURE_OK && !o.bytes && !(o.bytes = malloc(1))) o.failed = true; /* an empty value has a buffer too */
if (status == PURE_OK && o.failed) status = PURE_ALLOCATION;
if (status == PURE_OK && o.size > available) status = PURE_BUDGET;
@@ -1925,6 +1935,7 @@ static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a)
}
PureApp reader = {0};
pure_app_read(&reader, snapshot);
+ pure_app_work(&reader, work_budget);
reader.module = &held->module; /* the app's states, the query's views */
Route route = job->route;
route.target = (uint32_t)pure_module_view(&held->module, job->effect.key);
@@ -2262,7 +2273,6 @@ 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;
@@ -2275,6 +2285,7 @@ static bool start_executor(Executor *ex, const PureModule *module, uint32_t show
}
pure_app_background_checkpoints(&ex->app, true);
pure_app_memory(&ex->app, memory_budget);
+ pure_app_work(&ex->app, work_budget);
return true;
}
@@ -2294,6 +2305,7 @@ static bool start_shard(Host *host, const PureModule *module, bool documents, ui
host->module = module;
host->documents = documents;
host->wake = -1;
+ pure_app_work(&host->reader, work_budget); /* the views it shows from snapshots */
if (!host->owner && !start_executor(&host->local, module, show, edit, store)) return false;
host->work = calloc(host->capacity, sizeof(*host->work));
host->table = calloc(host->capacity, sizeof(*host->table));
@@ -2683,6 +2695,7 @@ int main(int argc, char **argv) {
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], "--work") && number(argv[i + 1], UINT64_MAX, &value) && value) work_budget = value;
else if (!strcmp(argv[i], "--window") && number(argv[i + 1], SIZE_MAX, &value) && value) window_bytes = (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;
@@ -2874,7 +2887,7 @@ 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] [--memory BYTES] "
+ "[--sender NAME=KEY ...] [--link-port PORT] [--senders FILE] [--link-idle-ms 30000] [--memory BYTES] [--work N] "
"[--window 4194304] [--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"
@@ -2884,6 +2897,8 @@ usage:
" .pmod unless given; the first line out says it)\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"
+ " --work N: what an event, or one evaluation of a view, may run, in instructions counted as written: past\n"
+ " it the event is refused and the view has no value, 422 work (docs/design/017-apps-on-one-host.md)\n"
" --window BYTES: what each --send holds of its sends beyond what its receiver has (and 65,536 sends);\n"
" the outbox keeps the rest\n", stderr);
return 2;
diff --git a/tests/check.py b/tests/check.py
index b982256..47ddf23 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -3659,6 +3659,73 @@ def server_meaning(aup, pure, work):
return {'values': len(expected), 'refused': refused, 'exits': len(exits), 'checkpoints': checkpoints}
+WORK_SOURCE = """record Item {
+ n = 0
+}
+state items: list of Item
+input add = message { n: number }
+input touch = message { step: number }
+on add {
+ items.append(Item { n: add.n })
+}
+on touch {
+ for i in items {
+ i.n = i.n + touch.step
+ }
+}
+view total = sum(i.n for i in items)
+view big = each i in items where i.n > 0 {
+ n = i.n
+}
+"""
+
+
+def server_work(aup, pure, work):
+ """A work budget (docs/design/017) over HTTP: as the list grows, the
+ walking event, the folding view and the filtered table each come to run
+ past the budget, from then on answer 422 work, and a refused event
+ changes nothing; the cheap event that adds an item still runs."""
+ source = work / 'work.aup'
+ source.write_text(WORK_SOURCE)
+ compile_module(aup, source, work / 'work.pmod')
+ host = HttpHost([pure / 'build/pure-serve', '--module', work / 'work.pmod', '--store', work / 'work-store', '--work', 400])
+ first = {}
+ try:
+ def total():
+ status, _, _, body = http(host.port, 'GET', '/views/total')
+ return status, body
+
+ for n in range(1, 200):
+ status, why, _, _ = http(host.port, 'POST', '/inputs/add?n=1')
+ assert status == 200, (n, status, why) # adding stays cheap
+ before = total()
+ status, why, _, _ = http(host.port, 'POST', '/inputs/touch?step=1')
+ assert status in (200, 422) and (status == 200 or why == 'work'), (n, status, why)
+ if status == 422:
+ first.setdefault('touch', n)
+ assert total() == before, n # refused: nothing changed
+ else:
+ assert 'touch' not in first, n # once past the budget, always past it
+ status, why, _, body = http(host.port, 'GET', '/views/total')
+ assert status == 200 or (status, why) == (422, 'work'), (n, status, why)
+ if status == 422:
+ first.setdefault('total', n)
+ else:
+ assert 'total' not in first, n
+ status, why, _, body = http(host.port, 'GET', '/views/big')
+ assert status == 200 or (status, why) == (422, 'work'), (n, status, why)
+ if status == 422:
+ first.setdefault('big', n)
+ else:
+ assert 'big' not in first and json.loads(body)['rows'] == n, (n, body[:80])
+ if len(first) == 3:
+ break
+ assert len(first) == 3 and first['touch'] < first['big'], first # a walk's block is longer than a filter's
+ finally:
+ host.stop()
+ return first
+
+
def main():
global EDITOR
parser = argparse.ArgumentParser()
@@ -3707,6 +3774,7 @@ def main():
report['server_answers'] = server_answers(aup, pure, work)
report['server_answers_memory'] = server_answers_memory(aup, pure, work)
report['server_meaning'] = server_meaning(aup, pure, work)
+ report['server_work'] = server_work(aup, pure, work)
print(json.dumps(report))