From a05b9428c9d12ce0ea76f6e4539aa911f42b6eef Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 1 Oct 2026 02:36:06 +0100 Subject: Run many apps on one host, by turns on workers (017) pure-serve --apps FILE hosts an app a line: NAME MODULE STORE and its budgets (memory=, work=, queue=). Each app's routes start with its name and reach its own module, states, snapshot and queries; a view is shown on the connection thread from its own app's snapshot, read once a round. No app has a thread of its own. Its owner is what used to be a thread's loop, now a turn: its due timers, what its wires delivered, and the requests waiting for it applied and saved once, its answers handed back. Workers (--workers N; one for a host of one app, else one a CPU at most) take apps from a queue in turn; a worker sleeps until the soonest timer. A turn takes every waiting request when no other app waits, else 256; it gathers the clients its last save answered only when alone. An app's queue budget refuses what would wait past it (503 busy) on the connection thread, before its owner sees it. An app whose module does not load, whose store does not open, or whose store is another app's stops alone: the start line says why, and its routes answer 503 stopped. The stop line counts each app's turns, events and refusals. A host of one app (--module) runs as before, its owner on one worker named pure-owner: every check passes unchanged. server_apps runs six apps of which two stop, floods one past its queue while another answers, refuses a walk past one app's work budget that another runs, holds an app's store from another process (that app answers 503 store_busy and applies nothing, the others save), and restarts. --- tests/check.py | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) (limited to 'tests/check.py') diff --git a/tests/check.py b/tests/check.py index 47ddf23..fdb4f56 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3726,6 +3726,122 @@ def server_work(aup, pure, work): return first +APPS_SOURCE = """record Item { + n = 0 +} +state n = 0 +state items: list of Item +input add = message { step: number } +input fill = message { k: number } +input walk = message { step: number } +on add { + n = n + add.step +} +on fill { + items.append(Item { n: fill.k }) +} +on walk { + for i in items { + i.n = i.n + walk.step + } +} +view total = n +view count = count(i in items where i.n > 0) +""" + + +def server_apps(aup, pure, work): + """One host, many apps (docs/design/017): each app's routes start with its + name and reach its own states (two apps of one module share nothing); an + app whose module does not load, or whose store is another app's, stops + alone (503 stopped) while the others run; past an app's queue budget its + requests are refused (503 busy) and another app answers meanwhile; past + its work budget an event is refused (422 work) where the same event runs + in an app without one; an app whose store another process holds cannot + save (503 store_busy, nothing applied) while the others save; and after a + restart each app has its states back.""" + import fcntl + import socket + source = work / 'apps.aup' + source.write_text(APPS_SOURCE) + compile_module(aup, source, work / 'apps.pmod') + (work / 'broken.pmod').write_bytes(b'PUREMOD6 not a module') + manifest = work / 'apps.conf' + manifest.write_text(f"""# name module store budgets +a {work / 'apps.pmod'} {work / 'store-a'} +b {work / 'apps.pmod'} {work / 'store-b'} +busy {work / 'apps.pmod'} {work / 'store-busy'} queue=2 +tight {work / 'apps.pmod'} {work / 'store-tight'} work=55 +broken {work / 'broken.pmod'} {work / 'store-broken'} +twin {work / 'apps.pmod'} {work / 'store-a'} +""") + serve = pure / 'build/pure-serve' + + def get(port, path): + status, why, _, body = http(port, 'GET', path) + return status, why, body + + def post(port, path): + status, why, _, _ = http(port, 'POST', path) + return status, why + + host = HttpHost([serve, '--apps', manifest, '--workers', 2]) + try: + running = {app['name']: app for app in host.info['apps']} + assert [name for name, app in running.items() if app['running']] == ['a', 'b', 'busy', 'tight'], running + assert running['broken']['why'].startswith('Module:') and running['twin']['why'] == "The store is a's", running + port = host.port + assert post(port, '/a/inputs/add?step=2') == (200, 'none') + assert get(port, '/a/views/total')[::2] == (200, b'2') and get(port, '/b/views/total')[::2] == (200, b'0') + assert get(port, '/broken/views/total')[:2] == (503, 'stopped') and post(port, '/twin/inputs/add?step=1')[0] == 503 + assert get(port, '/nobody/views/total')[:2] == (404, 'no_app') and get(port, '/views/total')[:2] == (404, 'no_app') + # Work: ten items to walk; the tight app refuses the walk and changes nothing, a's runs it. + for app in ('a', 'tight'): + for k in range(10): + assert post(port, f'/{app}/inputs/fill?k={k}') == (200, 'none') + assert post(port, '/tight/inputs/walk?step=1') == (422, 'work') and get(port, '/tight/views/count')[::2] == (200, b'9') + assert post(port, '/a/inputs/walk?step=1') == (200, 'none') and get(port, '/a/views/count')[::2] == (200, b'10') + # Queue: requests at once, from as many connections; b answers meanwhile. + flood = [] + for _ in range(120): + connection = socket.create_connection(('127.0.0.1', port)) + flood.append(connection) + for connection in flood: + connection.sendall(b'POST /busy/inputs/add?step=1 HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n') + assert post(port, '/b/inputs/add?step=5') == (200, 'none') + answers = [] + for connection in flood: + head = b'' + while b'\r\n\r\n' not in head: + head += connection.recv(4096) + answers.append(head.split(b' ', 2)[1]) + connection.close() + ok, busy = answers.count(b'200'), answers.count(b'503') + assert ok + busy == len(flood) and busy > 0 and ok > 0, (ok, busy) + assert get(port, '/busy/views/total')[::2] == (200, str(ok).encode()) # the refused changed nothing + # The store of b held by another process: b cannot save, a can. + with open(work / 'store-b' / '.writer.lock', 'r+b') as held: + fcntl.lockf(held, fcntl.LOCK_EX | fcntl.LOCK_NB) + assert post(port, '/b/inputs/add?step=1') == (503, 'store_busy') + assert post(port, '/a/inputs/add?step=1') == (200, 'none') + assert get(port, '/b/views/total')[::2] == (200, b'5') + fcntl.lockf(held, fcntl.LOCK_UN) + assert post(port, '/b/inputs/add?step=1') == (200, 'none') + finally: + stats = host.stop()[-1] + by_name = {app['name']: app for app in stats['apps']} + assert by_name['busy']['busy'] == busy and by_name['a']['events'] > 0 and stats['workers'] == 2, stats['apps'] + # Restarted: each app's states are back. + host = HttpHost([serve, '--apps', manifest]) + try: + assert get(host.port, '/a/views/total')[::2] == (200, b'3') and get(host.port, '/b/views/total')[::2] == (200, b'6') + assert get(host.port, '/a/views/count')[::2] == (200, b'10') and get(host.port, '/busy/views/total')[::2] == \ + (200, str(ok).encode()) + finally: + host.stop() + return {'apps': len(running), 'stopped': 2, 'flooded': len(flood), 'busy': busy} + + def main(): global EDITOR parser = argparse.ArgumentParser() @@ -3775,6 +3891,7 @@ def main(): 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) + report['server_apps'] = server_apps(aup, pure, work) print(json.dumps(report)) -- cgit v1.2.3