aboutsummaryrefslogtreecommitdiffstats
path: root/tests
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 /tests
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.
Diffstat (limited to 'tests')
-rw-r--r--tests/check.py68
1 files changed, 68 insertions, 0 deletions
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))