diff options
| author | eunhhu <[email protected]> | 2026-10-19 09:58:47 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-19 09:58:47 +0100 |
| commit | b8e6a2e5b8fc64ec47adfbe583b8a27200cc5a2e (patch) | |
| tree | d3a2a5041e42e4ca2fa4a7ec8e5cd10147c9030b | |
| parent | 31ef28609c6135058ffa0645fed9edd75c1086c7 (diff) | |
| download | pure-b8e6a2e5b8fc64ec47adfbe583b8a27200cc5a2e.tar.gz pure-b8e6a2e5b8fc64ec47adfbe583b8a27200cc5a2e.zip | |
The modules an app ran are freed (research 115's next step): every app's module a Loaded with holds (each snapshot of it, each request read with it); one replaced is retired and freed on the apps thread once no app runs it, every connection thread has begun a pass since (woken for it), and nothing holds it; who keeps a module to ask later whether it is the same asks its serial (core: PureModule.serial, a reader's module_serial, pure_snapshot_module, pure_snapshot_release says it freed; serve: routes, groups, links, bindings, queries, the published snapshot), as an address may come back; a link binds under its receiver's lock, a link naming an input under binding_lock; 030's memory.py: 4,000 changes 0 bytes a change (was 19,284), 4,100 freed; stress.py: readers, view workers, push streams and a body across changes, 586 of 600 loads at a freed module's address, every answer right, ASan 1,000 changes clean; a push group that looked while its app resumed missed the new snapshot (published before the app took requests) and waited for its next save: woken again once it takes them (server_view_workers had failed once under load)
| -rw-r--r-- | core/module.c | 13 | ||||
| -rw-r--r-- | core/module.h | 9 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/2026-10-19-memory.jsonl | 2 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/2026-10-19-stress.jsonl | 2 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/items-body.aup | 43 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/items-same.aup | 46 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/items-third.aup | 43 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/memory.py | 21 | ||||
| -rw-r--r-- | research/experiments/030-app-lifecycle/stress.py | 170 | ||||
| -rw-r--r-- | serve/serve.c | 211 |
10 files changed, 500 insertions, 60 deletions
diff --git a/core/module.c b/core/module.c index f970daf..4b20e25 100644 --- a/core/module.c +++ b/core/module.c @@ -2284,6 +2284,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule } out->runner = out->calls || out->nesting > PURE_MODULE_NESTING ? 2 : out->batches ? 1 : 0; out->jit = pure_jit_make(out); /* none: the executor runs it all */ + static uint64_t loads; + out->serial = __atomic_add_fetch(&loads, 1, __ATOMIC_RELAXED); return PURE_OK; refused: if (error_size) snprintf(error, error_size, "%s (%u)", why, bad); @@ -5171,8 +5173,10 @@ const PureList *pure_snapshot_list(const PureSnapshot *snapshot, uint32_t state) return state >= PURE_LIST_HEARD ? snapshot->runtime[state - PURE_LIST_HEARD] : snapshot->lists[state]; } -void pure_snapshot_release(PureSnapshot *snapshot) { - if (!snapshot || __atomic_sub_fetch(&snapshot->refs, 1, __ATOMIC_ACQ_REL)) return; +const PureModule *pure_snapshot_module(const PureSnapshot *snapshot) { return snapshot->module; } + +bool pure_snapshot_release(PureSnapshot *snapshot) { + if (!snapshot || __atomic_sub_fetch(&snapshot->refs, 1, __ATOMIC_ACQ_REL)) return false; Register *all = snapshot->all; if (all) { /* out of the links: what the one before may read goes to it (still linked, so still there) */ lock_register(all); @@ -5193,6 +5197,7 @@ void pure_snapshot_release(PureSnapshot *snapshot) { pure_retired_free(&all->returned); /* the app is gone: no list takes it back */ free(all); } + return true; } static void let_own_go(PureApp *app); @@ -6058,13 +6063,15 @@ static void let_own_go(PureApp *app) { } bool pure_app_read(PureApp *reader, const PureSnapshot *snapshot) { - if (reader->module != snapshot->module) drop_passes(reader), let_own_go(reader); /* laid out for its module */ + if (reader->module_serial != snapshot->module->serial) /* laid out for its module (by serial: freed, another may */ + drop_passes(reader), let_own_go(reader); /* come to its address) */ if (!reader->states || reader->laid != snapshot->states) { /* its arrays for as many states (027's 4f-2) */ void *was = reader->states; if (!lay_app(reader, snapshot->states)) return false; free(was); } reader->module = snapshot->module; + reader->module_serial = snapshot->module->serial; reader->reading = true; reader->changes = snapshot->changes; /* what the row cache is fresh for */ memcpy(reader->values, snapshot->values, snapshot->states * sizeof(*reader->values)); diff --git a/core/module.h b/core/module.h index 43d9ccb..aff6240 100644 --- a/core/module.h +++ b/core/module.h @@ -453,6 +453,9 @@ typedef struct { uint16_t *signature_record; bool calls; /* it has functions: its blocks run in the executor that calls (module.c) */ uint32_t nesting; /* its loops' deepest nesting (set at load) */ + /* Which load this is, of the process's: never 0, never the same twice. Who keeps a module's address to ask later + * whether it is the same module asks this instead, since another may come to that address once one is freed. */ + uint64_t serial; } PureModule; /* Whether a state of m is transient (PURE_STATE_TRANSIENT). */ @@ -533,6 +536,7 @@ typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena; enum { PURE_LIST_HEARD = 0x10000, PURE_LIST_OUTBOX, PURE_LIST_RUNTIME = 2 }; typedef struct { const PureModule *module; + uint64_t module_serial; /* a reader's: its module's serial when it read (the module may since be freed) */ PureData data; /* The arrays of each state (027's 4f-2), sized to the module in one block (states) for the states it was laid out * for (laid). */ @@ -792,7 +796,10 @@ void pure_app_checkpointed(PureApp *, PureCheckpoint *, PureResult); PureSnapshot *pure_app_snapshot(PureApp *); void pure_snapshot_hold(PureSnapshot *); /* On any thread; the last reference frees it. */ -void pure_snapshot_release(PureSnapshot *); +/* Let a reference go: true when it was the last, the snapshot freed. */ +bool pure_snapshot_release(PureSnapshot *); +/* The module the snapshot's app ran when it was taken. */ +const PureModule *pure_snapshot_module(const PureSnapshot *); /* Show a snapshot's states in reader, a zeroed app or a reader before: its * views read them while the snapshot is held, with the reader's own arena and * row cache (one thread at a time). It applies no events. pure_app_stop frees diff --git a/research/experiments/030-app-lifecycle/2026-10-19-memory.jsonl b/research/experiments/030-app-lifecycle/2026-10-19-memory.jsonl new file mode 100644 index 0000000..af9b7e6 --- /dev/null +++ b/research/experiments/030-app-lifecycle/2026-10-19-memory.jsonl @@ -0,0 +1,2 @@ +{"way": "module", "changes": 4000, "private_dirty_kib": [3024, 3024], "pss_kib": [3953, 3953], "per_change_bytes": 0, "modules_freed": 4100, "modules_retired": 0} +{"way": "budget", "changes": 4000, "private_dirty_kib": [2960, 2960], "pss_kib": [3889, 3889], "per_change_bytes": 0, "modules_freed": 0, "modules_retired": 0} diff --git a/research/experiments/030-app-lifecycle/2026-10-19-stress.jsonl b/research/experiments/030-app-lifecycle/2026-10-19-stress.jsonl new file mode 100644 index 0000000..712e7cc --- /dev/null +++ b/research/experiments/030-app-lifecycle/2026-10-19-stress.jsonl @@ -0,0 +1,2 @@ +{"changes": 600, "answers": 85535, "not_found": 20151, "changing": 733, "pushed": 188, "writes": 255, "writes_refused": 462, "wrong": [], "host_exit": 0, "modules_freed": 600, "modules_retired": 0, "modules_at_freed": 586, "asan": [], "build": "release (-O2)"} +{"changes": 1000, "answers": 113441, "not_found": 33706, "changing": 3123, "pushed": 355, "writes": 313, "writes_refused": 879, "wrong": [], "host_exit": 0, "modules_freed": 1000, "modules_retired": 0, "modules_at_freed": 0, "asan": [], "build": "ASan (-O1 -fsanitize=address)"} diff --git a/research/experiments/030-app-lifecycle/items-body.aup b/research/experiments/030-app-lifecycle/items-body.aup new file mode 100644 index 0000000..dcb5477 --- /dev/null +++ b/research/experiments/030-app-lifecycle/items-body.aup @@ -0,0 +1,43 @@ +record Pick { + n = 0 +} +record Item { + n = 0 +} +state items: list of Item +state more: list of Item +state hits = 0 +input seed = message { n: number } +input grow = message { step: number } +input add = message { step: number } +on seed { + items.append(Item { n: seed.n }) +} +on grow { + for i in items { + more.append(Item { n: i.n + grow.step }) + } + for m in more { + items.append(Item { n: m.n }) + } + for m in more { + more.remove(m) + } +} +on add { + hits = hits + add.step +} +view count = count(i in items where i.n > 0) +view total = hits +view summed { + let t = 0 + for i in items { t = t + i.n } + return t +} +view picked { + let ps: list of Pick + for i in items { + if i.n > 9 { ps.append(Pick { n: i.n }) } + } + return ps.count +} diff --git a/research/experiments/030-app-lifecycle/items-same.aup b/research/experiments/030-app-lifecycle/items-same.aup new file mode 100644 index 0000000..d1bb5f5 --- /dev/null +++ b/research/experiments/030-app-lifecycle/items-same.aup @@ -0,0 +1,46 @@ +record Pair { + n = 0 + m = 0 +} +record Item { + n = 0 +} +state items: list of Item +state more: list of Item +state hits = 0 +input seed = message { n: number } +input grow = message { step: number } +input add = message { step: number } +on seed { + items.append(Item { n: seed.n }) +} +on grow { + for i in items { + more.append(Item { n: i.n + grow.step }) + } + for m in more { + items.append(Item { n: m.n }) + } + for m in more { + more.remove(m) + } +} +on add { + hits = hits + add.step +} +view count = count(i in items where i.n > 1) + 2000 +view tally = hits + 500000 +view summed { + let t = 0 + for i in items { t = t + i.n } + return t +} +view picked { + let ps: list of Pair + for i in items { + if i.n > 9 { ps.append(Pair { n: i.n, m: i.n * 2 }) } + } + let t = 0 + for p in ps { t = t + p.m } + return t + 100000 +} diff --git a/research/experiments/030-app-lifecycle/items-third.aup b/research/experiments/030-app-lifecycle/items-third.aup new file mode 100644 index 0000000..dcb5477 --- /dev/null +++ b/research/experiments/030-app-lifecycle/items-third.aup @@ -0,0 +1,43 @@ +record Pick { + n = 0 +} +record Item { + n = 0 +} +state items: list of Item +state more: list of Item +state hits = 0 +input seed = message { n: number } +input grow = message { step: number } +input add = message { step: number } +on seed { + items.append(Item { n: seed.n }) +} +on grow { + for i in items { + more.append(Item { n: i.n + grow.step }) + } + for m in more { + items.append(Item { n: m.n }) + } + for m in more { + more.remove(m) + } +} +on add { + hits = hits + add.step +} +view count = count(i in items where i.n > 0) +view total = hits +view summed { + let t = 0 + for i in items { t = t + i.n } + return t +} +view picked { + let ps: list of Pick + for i in items { + if i.n > 9 { ps.append(Pick { n: i.n }) } + } + return ps.count +} diff --git a/research/experiments/030-app-lifecycle/memory.py b/research/experiments/030-app-lifecycle/memory.py index 3c15d4a..49ed40c 100644 --- a/research/experiments/030-app-lifecycle/memory.py +++ b/research/experiments/030-app-lifecycle/memory.py @@ -6,9 +6,12 @@ Private_Dirty and Pss) is read after a warm-up of changes, then after CHANGES changes more: in place to another module (its line naming in turn items.pmod and items-next.pmod: each reading loads a module, and keeps the one it replaced), and with its module as it was (its queue budget changed -in turn: no module loaded). The difference a change. +in turn: no module loaded). The difference a change. Each reading waits +SETTLE seconds first: a module replaced is freed once no request or snapshot +reads it and each connection thread has passed by (they wake each second); +the host's own count of the modules it freed and still holds comes with it. -memory.py PURE_SERVE CHANGES a JSON line per way +memory.py PURE_SERVE CHANGES [SETTLE] a JSON line per way """ import json import select @@ -17,6 +20,7 @@ import signal import subprocess import sys import tempfile +import time from pathlib import Path from window import AUP, HERE, grow @@ -32,6 +36,7 @@ def own(pid): def main(): serve, changes = Path(sys.argv[1]).resolve(), int(sys.argv[2]) + settle = float(sys.argv[3]) if len(sys.argv) > 3 else 2 work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm')) try: modules = {} @@ -62,15 +67,23 @@ def main(): for n in range(1, 101): # warm: the allocator's arenas settle change(n) + time.sleep(settle) dirty, pss = own(host.pid) for n in range(101, 101 + changes): change(n) + time.sleep(settle) dirty_after, pss_after = own(host.pid) host.terminate() - host.communicate(timeout=60) + _, errors = host.communicate(timeout=60) + stats = {} + for line in errors.decode().splitlines(): + if '"modules_freed"' in line: + stats = json.loads(line) print(json.dumps({'way': way, 'changes': changes, 'private_dirty_kib': [dirty, dirty_after], 'pss_kib': [pss, pss_after], - 'per_change_bytes': round((dirty_after - dirty) * 1024 / changes)}), flush=True) + 'per_change_bytes': round((dirty_after - dirty) * 1024 / changes), + 'modules_freed': stats.get('modules_freed'), 'modules_retired': stats.get('modules_retired')}), + flush=True) finally: shutil.rmtree(work) diff --git a/research/experiments/030-app-lifecycle/stress.py b/research/experiments/030-app-lifecycle/stress.py new file mode 100644 index 0000000..d713f38 --- /dev/null +++ b/research/experiments/030-app-lifecycle/stress.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Modules replaced and freed while they are read (research 115's next step: the modules an app ran are let go). A +host of apps (--threads 3, --view-workers 1) holds an app of items-body.aup; its line names in turn items-body.pmod, +items-third.pmod (the same app) and items-same.pmod (a view renamed, total and tally; count's fold the same instructions at the same place, counting +n > 1 and adding 2000), so that a module freed and the next loaded take the same address, and a fold remembered for +one (a reader's pass, kept by its module and place) gives the other a value neither has; and picked, a view with a body +whose list is of a record of one field in one module and of two in the other at the same place (a reader laid out for +one and reading the other gives neither's value: 11 and 100,222); total and tally at the same place, tally 500,000 more +(a request resolved with one module and shown with the other at the same address gives the other's). Meanwhile, until CHANGES changes are done: +- readers on connections of their own GET /x/views/count (in both), /x/views/summed and picked (views with a body: the + view workers), and total and tally (one in each module): each answer 200 with the value the items give, 404 for a view + the module lacks, or 503 while it changes; +- subscribers stream count and total (Accept: text/event-stream), again each time a stream ends: every count pushed + one of the two, every total below 500,000 (a push group asked whether its app runs the module it was made in by + address would take items-same, come where its module was freed, for that module and push tally); +- a writer sends POST /x/inputs/add with its body in two parts, a change between them: 200, or 503 refused as stopped. +Then the host is stopped, and its stats say how many modules it freed and how many it loaded at an address one freed +had (the case comparing by address would get wrong). Run with an ASan build: a module read after it +was freed stops the host there. + +stress.py PURE_SERVE CHANGES one JSON line""" +import json +import select +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import threading +import time +from pathlib import Path + +from window import AUP, HERE, grow + + +def answer(sock): + head = b'' + while b'\r\n\r\n' not in head: + part = sock.recv(4096) + if not part: + raise ConnectionError('closed') + head += part + top, rest = head.split(b'\r\n\r\n', 1) + status = int(top.split()[1]) + length = next(int(line.split(b':')[1]) for line in top.split(b'\r\n') if line.lower().startswith(b'content-length')) + while len(rest) < length: + rest += sock.recv(4096) + return status, rest[:length] + + +def main(): + serve, changes = Path(sys.argv[1]).resolve(), int(sys.argv[2]) + work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm')) + try: + modules = {} + for name in ('items-body', 'items-third', 'items-same'): + modules[name] = work / f'{name}.pmod' + subprocess.run([str(AUP), 'build', str(HERE / f'{name}.aup'), '-o', str(modules[name])], check=True) + conf = work / 'apps.conf' + cycle = ('items-body', 'items-third', 'items-same') # items-same can come where items-body was freed + write = lambda n: conf.write_text(f"x {modules[cycle[n % 3]]} {work / 'store'}\n") + write(0) + host = subprocess.Popen([str(serve), '--apps', str(conf), '--port', '0', '--threads', '3', '--view-workers', '1'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + port = json.loads(host.stdout.readline())['port'] + items = grow(port, 1000) + counts = (items, items - 1 + 2000) # count in items-body (every n is 1 or more), in items-same (n > 1, + 2000) + done = threading.Event() + seen = {'answers': 0, 'not_found': 0, 'changing': 0, 'pushed': 0, 'writes': 0, 'writes_refused': 0, 'wrong': []} + lock = threading.Lock() + + def tally(key, wrong=None): + with lock: + seen[key] += 1 + if wrong is not None and len(seen['wrong']) < 10: + seen['wrong'].append(wrong) + + def reader(view): + sock = socket.create_connection(('127.0.0.1', port)) + while not done.is_set(): + sock.sendall(f'GET /x/views/{view} HTTP/1.1\r\nHost: x\r\n\r\n'.encode()) + status, body = answer(sock) + if status == 503: + tally('changing') + elif status == 404: + tally('not_found', None if view in ('total', 'tally') else (view, status, body[:80])) + elif status == 200: + value = json.loads(body) + good = (value in counts if view == 'count' else value >= items if view == 'summed' else + value in (11, 100222) if view == 'picked' else value < 500000 if view == 'total' else + value >= 500000) + tally('answers', None if good else (view, value)) + else: + tally('answers', (view, status, body[:80])) + + def subscriber(view, good): # streams again when its stream ends (its view gone from the module that runs) + while not done.is_set(): + sock = socket.create_connection(('127.0.0.1', port)) + sock.sendall(f'GET /x/views/{view} HTTP/1.1\r\nHost: x\r\nAccept: text/event-stream\r\n\r\n'.encode()) + sock.settimeout(0.5) + buffer = b'' + while not done.is_set(): + try: + part = sock.recv(4096) + except socket.timeout: + continue + if not part: + break + buffer += part + while b'\n\n' in buffer: + event, buffer = buffer.split(b'\n\n', 1) + for line in event.split(b'\n'): + if line.startswith(b'data:'): + value = json.loads(line[5:]) + tally('pushed', None if good(value) else ('pushed', view, value)) + sock.close() + + def writer(): + while not done.is_set(): + sock = socket.create_connection(('127.0.0.1', port)) + body = b'{"step": 1}' + sock.sendall(b'POST /x/inputs/add HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n' + b'Content-Length: %d\r\n\r\n' % len(body) + body[:4]) + time.sleep(0.003) # a change may come between the parts + sock.sendall(body[4:]) + status, said = answer(sock) + if status == 200: + tally('writes') + elif status == 503: + tally('writes_refused') + else: + tally('writes', ('write', status, said[:80])) + sock.close() + + threads = [threading.Thread(target=reader, args=(v,)) for v in ('count', 'picked', 'summed', 'total', 'tally', 'picked')] + threads += [threading.Thread(target=subscriber, args=('count', lambda v: v in counts)), + threading.Thread(target=subscriber, args=('total', lambda v: isinstance(v, (int, float)) and v < 500000)), + threading.Thread(target=writer)] + for t in threads: + t.start() + for n in range(1, changes + 1): + write(n) + host.send_signal(signal.SIGHUP) + ready, _, _ = select.select([host.stdout], [], [], 60) + line = host.stdout.readline() if ready else b'' + if not line: + done.set() + raise SystemExit(f'the host stopped at change {n}: {host.stderr.read().decode()[-3000:]}') + said = json.loads(line) + assert said['apps'][0]['did'] == 'changed' and said['apps'][0]['running'], said + time.sleep(0.002) + time.sleep(1) + done.set() + for t in threads: + t.join(timeout=10) + host.terminate() + _, errors = host.communicate(timeout=60) + stats = next((json.loads(l) for l in errors.decode().splitlines() if '"modules_freed"' in l), {}) + print(json.dumps({'changes': changes, **{k: v for k, v in seen.items()}, 'host_exit': host.returncode, + 'modules_freed': stats.get('modules_freed'), 'modules_retired': stats.get('modules_retired'), + 'modules_at_freed': stats.get('modules_at_freed'), + 'asan': [l for l in errors.decode().splitlines() if 'AddressSanitizer' in l][:3]})) + sys.exit(0 if not seen['wrong'] and host.returncode in (0, -15) else 1) + finally: + shutil.rmtree(work) + + +if __name__ == '__main__': + main() diff --git a/serve/serve.c b/serve/serve.c index 1f3a326..7dd81e4 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -84,6 +84,7 @@ typedef struct { const char *given; App *app; const PureModule *module; + uint64_t serial; /* the module's: compared, not the address (one freed, another may come to it) */ } Route; /* An app's tokens (docs/design/017's 3b): what each grants, by its token's @@ -242,7 +243,7 @@ typedef struct { * itself), and the app's changes and module the last one shows. */ Published *published; uint64_t published_changes, publications; - const PureModule *published_module; + uint64_t published_serial; /* the module's of the last snapshot published */ struct PureSurfaceHost *surface; /* told of each snapshot published */ /* The owner's: outputs wired to HTTP receivers, and how far each was trimmed. */ PureWire *wires[OUTLETS]; @@ -443,8 +444,32 @@ static App *apps; static _Atomic unsigned app_count; static unsigned app_slots = 1; static unsigned places(void) { return atomic_load_explicit(&app_count, memory_order_acquire); } -/* The modules apps ran before the one they run, kept as long as the host runs. */ -static struct Retired { PureModule *module; struct Retired *next; } *retired_modules; +/* A module an app runs or ran (every app's module is one), and what still reads it: its snapshots and the requests + * read with it (holds). One an app ran before is retired, then freed (free_retired) once nothing holds it, no app runs + * it, and every connection thread has begun a pass since (a request read with it took its hold in resolve, within the + * pass it was read in). Who keeps a module to ask later whether it is the same one asks its serial. */ +typedef struct Loaded { + PureModule module; /* first: a module's address is its Loaded's */ + _Atomic uint32_t holds; + uint64_t *passes; /* retired: each connection thread's passes then */ + struct Loaded *next; +} Loaded; +static Loaded *retired_modules; +static uint64_t modules_freed, modules_at_freed; /* (for its stats: freed, and loaded where one freed was) */ +static uintptr_t freed_at[64]; /* the last ones freed: addresses, compared, never read */ +static pthread_mutex_t binding_lock = PTHREAD_MUTEX_INITIALIZER; /* a link naming an input reads an app's module */ +static void hold_module(const PureModule *m) { + if (m) atomic_fetch_add_explicit(&((Loaded *)(uintptr_t)m)->holds, 1, memory_order_relaxed); +} +static void let_go_module(const PureModule *m) { + if (m) atomic_fetch_sub_explicit(&((Loaded *)(uintptr_t)m)->holds, 1, memory_order_release); +} +/* A snapshot let go: when it was the last reference, its module's hold too (taken when it was made). */ +static void let_go_snapshot(PureSnapshot *snapshot) { + if (!snapshot) return; + const PureModule *m = pure_snapshot_module(snapshot); + if (pure_snapshot_release(snapshot)) let_go_module(m); +} static bool named_apps; /* --apps: a route's first segment names its app */ static Host *all_hosts; /* the connection threads, for moving a connection to its app's home */ static unsigned host_count; @@ -458,6 +483,7 @@ struct Host { PureApp *readers; /* by app: shows its owner's snapshots here */ PureSnapshot **snapshots; /* by app: the snapshot a round reads views from, once */ uint64_t round, *read_in, *handed_in; /* by app: the round it was read in, its requests handed in */ + _Atomic uint64_t passes; /* its loop's passes begun: a retired module waits for each thread's next */ unsigned *touched; /* the apps read in this round */ Applied *views; /* a round's views shown here, answered together */ size_t views_capacity; @@ -563,6 +589,8 @@ static void close_connection(Connection *c) { if (c->phase == CLOSED) return; Host *host = c->host; finish_output(c); + let_go_module(c->module); + c->module = NULL; close(c->fd); /* also leaves the epoll set */ c->phase = CLOSED; host->table[c->index] = NULL; @@ -882,7 +910,10 @@ static const char *resolve(Connection *c, char *query, Route *route, int *status return "stopped"; } route->app = app; + let_go_module(c->module); /* (a request before it on this connection: answered, so let go already) */ + hold_module(m); /* within this pass: no retired module is freed before this thread's next */ route->module = c->module = m; + route->serial = m->serial; if (route->kind == ROUTE_NONE || (route->kind == ROUTE_QUERY && !app->owned)) return "route"; /* a query reads states */ if (route->kind == ROUTE_QUERY) { /* the view is the query module's, found when shown */ route->from = 0; @@ -1295,6 +1326,8 @@ static void drive(Connection *c) { if (c->out_at < c->out_count) continue; /* Answered: the next request may already be here. */ finish_output(c); + let_go_module(c->module); + c->module = NULL; c->deadline = c->host->now + c->host->timeout_ms; if (!c->keep_alive) { close_connection(c); return; } c->phase = READING_HEAD; @@ -1768,16 +1801,17 @@ static void surface_changed(PureSurfaceHost *s) { * saved, so a read made after a write's answer sees the write. A failed save * returns the app to the last save's states, which the snapshot still shows. */ static void publish(Executor *ex) { - if (!ex->published || (ex->publications && ex->published_changes == ex->app.changes && ex->published_module == ex->module)) + if (!ex->published || (ex->publications && ex->published_changes == ex->app.changes && ex->published_serial == ex->module->serial)) return; PureSnapshot *made = pure_app_snapshot(&ex->app), *old; + if (made) hold_module(ex->module); /* let go with the snapshot's last reference (let_go_snapshot) */ pthread_mutex_lock(&ex->published->lock); old = ex->published->snapshot; ex->published->snapshot = made; pthread_mutex_unlock(&ex->published->lock); - pure_snapshot_release(old); + let_go_snapshot(old); ex->published_changes = ex->app.changes; - ex->published_module = ex->module; + ex->published_serial = ex->module->serial; ++ex->publications; if (ex->surface) surface_changed(ex->surface); if (atomic_load_explicit(&ex->published->subscribers, memory_order_relaxed)) wake_pushers(); @@ -1803,6 +1837,7 @@ typedef struct Local { Owner *from, *to; uint32_t output, input; /* the sender's output, the receiver's input */ const PureModule *sender, *module; /* the sender's module; the receiver's, which input is in */ + uint64_t serial; /* the receiver's module's: asked, not its address */ char input_name[PURE_MODULE_NAME + 1], why[160]; /* the input by name; why it holds its sends, as last said */ char source[PURE_SOURCE + 1]; unsigned char fields[PURE_FIELDS_NAMED]; /* the sender's, when the input takes them by name */ @@ -1873,6 +1908,7 @@ static bool local_bind(Local *l, const PureModule *r, char *error, size_t size) l->input = (uint32_t)input; l->fields_size = fields_size; l->module = r; + l->serial = r->serial; return true; } @@ -1889,11 +1925,12 @@ static void local_hand(Executor *ex, Local *l, uint64_t now) { pthread_mutex_unlock(&l->lock); if (waits) return; Owner *to = l->to; - pthread_mutex_lock(&to->lock); + char error[160] = "its receiver does not run"; + pthread_mutex_lock(&to->lock); /* bound while it runs the module (a module it ran before may be freed) */ const PureModule *runs = to->stopping ? NULL : to->module; + bool bound = runs && (runs->serial == l->serial || local_bind(l, runs, error, sizeof(error))); pthread_mutex_unlock(&to->lock); - char error[160] = "its receiver does not run"; - if (!runs || (runs != l->module && !local_bind(l, runs, error, sizeof(error)))) { + if (!bound) { if (strcmp(error, l->why)) { /* said once a cause */ snprintf(l->why, sizeof(l->why), "%s", error); fprintf(stderr, "{\"local\":\"%s\",\"input\":\"%s\",\"error\":\"%s; its sends wait\"}\n", l->source, @@ -1931,10 +1968,11 @@ static void local_hand(Executor *ex, Local *l, uint64_t now) { l->count = 0; l->retry = 0; pthread_mutex_unlock(&l->lock); - Job job = {.route = {.kind = ROUTE_INPUT, .target = l->input, .module = l->module}, .link = link, + Job job = {.route = {.kind = ROUTE_INPUT, .target = l->input, .module = l->module, .serial = l->serial}, .link = link, .effect = {.input = PURE_INPUT_WRITE, .key = "", .expected = -1, .value = {ingress, size}}}; pthread_mutex_lock(&to->lock); - bool queued = !to->stopping && to->module == l->module && grow(&to->jobs, &to->capacity, to->count + 1, sizeof(*to->jobs)); + bool queued = !to->stopping && to->module && to->module->serial == l->serial && + grow(&to->jobs, &to->capacity, to->count + 1, sizeof(*to->jobs)); if (queued) { to->jobs[to->count++] = job; ++to->arrivals; @@ -2295,7 +2333,7 @@ static PureSnapshot *read_newest(PureApp *reader, App *app) { if (snapshot) pure_snapshot_hold(snapshot); pthread_mutex_unlock(&published->lock); if (snapshot && !pure_app_read(reader, snapshot)) { /* no room for its states: none shown */ - pure_snapshot_release(snapshot); + let_go_snapshot(snapshot); snapshot = NULL; } reader->work_budget = atomic_load_explicit(&app->work, memory_order_relaxed); @@ -2370,7 +2408,7 @@ static void *run_view_worker(void *unused) { PureApp *reader = &readers[app - apps]; PureSnapshot *snapshot = read_newest(reader, app); Applied a = {.host = job->host, .ticket = job->ticket, .input = job->input, .route = ROUTE_VIEW, .group = job->group}; - if (!snapshot || reader->module != job->route.module) { /* its app stopped, or runs another module now */ + if (!snapshot || reader->module_serial != job->route.serial) { /* its app stopped, or runs another module now */ a.status = PURE_CLOSED; a.refused = "stopped"; } else { @@ -2378,7 +2416,7 @@ static void *run_view_worker(void *unused) { a.changes = reader->changes; if (job->group && a.status != PURE_OK && a.fault == PURE_FAULT_NONE) a.fault = pure_app_view_fault(reader); } - if (snapshot) pure_snapshot_release(snapshot); /* an answer holds what it lends */ + if (snapshot) let_go_snapshot(snapshot); /* an answer holds what it lends */ if (!job->group) atomic_fetch_sub_explicit(&app->viewing, 1, memory_order_relaxed); give_back(job->host, &a, 1); free(job); @@ -2445,7 +2483,9 @@ typedef struct Group { /* A view worker evaluates it (a view with a body): for the module asked * of; again: a newer snapshot came meanwhile. It stays until it is back. */ bool evaluating, again; - const PureModule *asked; + uint64_t asked; /* the module's serial */ + uint8_t view_type; /* its view's, and whether it takes arguments: asked again of the module the app runs next */ + bool arguments; Subscriber *first; struct Group *next; } Group; @@ -2663,19 +2703,19 @@ static void push_group(Host *host, Group *g) { bool quiet = !atomic_load(&app->live); PureSnapshot *snapshot = read_published(host, app); if (quiet && !snapshot) { close_group(host, g); return; } - if (quiet) { pure_snapshot_release(snapshot); return; } + if (quiet) { let_go_snapshot(snapshot); return; } if (!snapshot) return; /* none yet: the next publication wakes this */ PureApp *reader = &host->readers[app - apps]; - if (reader->module != g->route.module) { /* the app changed module: the view of that name, if it is the same kind */ - const PureModule *was = g->route.module, *now = reader->module; + if (reader->module_serial != g->route.serial) { /* the app changed module: the view of that name, if it is the same kind */ + const PureModule *now = reader->module; int view = pure_module_view(now, g->name); - if (view < 0 || now->view_type[view] != was->view_type[g->route.target] || - (pure_module_arguments(now, (uint32_t)view) < 0) != (pure_module_arguments(was, g->route.target) < 0)) { - pure_snapshot_release(snapshot); + if (view < 0 || now->view_type[view] != g->view_type || (pure_module_arguments(now, (uint32_t)view) >= 0) != g->arguments) { + let_go_snapshot(snapshot); close_group(host, g); return; } g->route.module = now; + g->route.serial = reader->module_serial; g->route.target = (uint32_t)view; g->computed = false; } @@ -2684,7 +2724,7 @@ static void push_group(Host *host, Group *g) { if (g->evaluating) g->again = true; else { g->evaluating = true; - g->asked = g->route.module; + g->asked = g->route.serial; hand_view(host, &g->route, (PureTicket){0}, PURE_INPUT_READ, PURE_OBJECT_LIMIT, g); } } else { @@ -2695,7 +2735,7 @@ static void push_group(Host *host, Group *g) { let_go_applied(&a); } } - pure_snapshot_release(snapshot); + let_go_snapshot(snapshot); group_send(host, g); } @@ -2710,7 +2750,7 @@ static void group_evaluated(Host *host, Applied *a) { free_group(g); return; } - if (a->refused || g->asked != g->route.module) g->again = true; + if (a->refused || g->asked != g->route.serial) g->again = true; else group_value(g, a, a->changes); let_go_applied(a); if (!group_send(host, g)) return; @@ -2737,7 +2777,7 @@ static void subscribe(Connection *c, const Route *route, const char *raw, size_t if (c->in_used) { refuse(c, 400, "Bad Request", "request"); return; } /* no request follows a stream */ if (host->subscribers >= subscribers_most) { reject(c, 503, "Service Unavailable", "busy"); return; } Group *g = host->groups; - while (g && !(g->app == route->app && g->route.module == route->module && g->route.target == route->target && + while (g && !(g->app == route->app && g->route.serial == route->serial && g->route.target == route->target && g->route.from == route->from && g->route.count == route->count && strlen(g->text) == raw_size && !memcmp(g->text, raw, raw_size))) g = g->next; @@ -2749,6 +2789,8 @@ static void subscribe(Connection *c, const Route *route, const char *raw, size_t g->text[raw_size] = 0; g->route.given = g->text; snprintf(g->name, sizeof(g->name), "%s", route->module->view_names[route->target]); + g->view_type = route->module->view_type[route->target]; + g->arguments = pure_module_arguments(route->module, route->target) >= 0; g->next = host->groups; host->groups = g; } @@ -2783,7 +2825,7 @@ static void subscribe(Connection *c, const Route *route, const char *raw, size_t * holds up, the owner's writes. */ enum { QUERIES = 64 }; typedef struct { PureModule module; unsigned refs; } Held; -static struct { const App *app; char name[65]; Held *held; const PureModule *checked; } queries[QUERIES]; +static struct { const App *app; char name[65]; Held *held; uint64_t checked; } queries[QUERIES]; /* checked: a serial */ static pthread_mutex_t queries_lock = PTHREAD_MUTEX_INITIALIZER; static void let_go_query(Held *held) { @@ -2838,7 +2880,7 @@ static PureResult register_query(const Job *job) { else { old = queries[slot].held; queries[slot].app = app; - queries[slot].checked = job->route.module; + queries[slot].checked = job->route.serial; snprintf(queries[slot].name, sizeof(queries[slot].name), "%s", job->effect.key); queries[slot].held = held; } @@ -2851,7 +2893,7 @@ static PureResult register_query(const Job *job) { static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a) { Held *held = NULL; - const PureModule *checked = NULL; + uint64_t checked = 0; pthread_mutex_lock(&queries_lock); for (int i = 0; i < QUERIES && !held; ++i) if (queries[i].held && queries[i].app == job->route.app && !strcmp(queries[i].name, job->effect.key)) { @@ -2870,7 +2912,7 @@ static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a) let_go_query(held); return PURE_ALLOCATION; } - if (reader.module != checked && !same_states(&held->module, reader.module)) { /* its app's states are laid out anew */ + if (reader.module_serial != checked && !same_states(&held->module, reader.module)) { /* its states laid out anew */ pure_app_stop(&reader); let_go_query(held); return PURE_CONFLICT; @@ -2935,7 +2977,7 @@ static void serve_bursts(Host *host) { for (size_t k = i; k < count; ++k) { const Job *mine = &host->jobs[k]; if (mine->route.app != app || shown_here(host, mine)) continue; - if (owner->stopping || mine->route.module != owner->module) { /* its app stopped, or runs another module now */ + if (owner->stopping || !owner->module || mine->route.serial != owner->module->serial) { /* stopped, or another module now */ host->views[answers++] = (Applied){.host = host, .ticket = mine->effect.ticket, .input = mine->effect.input, .route = mine->route.kind, .status = PURE_CLOSED, .refused = "stopped"}; continue; @@ -2966,7 +3008,7 @@ static void serve_bursts(Host *host) { bool registering = job->route.kind == ROUTE_QUERY && job->effect.input == PURE_INPUT_WRITE; *applied = (Applied){.host = host, .ticket = job->effect.ticket, .input = job->effect.input, .route = registering ? ROUTE_QUERY : ROUTE_VIEW}; - if (job->route.kind == ROUTE_VIEW && host->readers[a].module != job->route.module) { /* another module's view now */ + if (job->route.kind == ROUTE_VIEW && host->readers[a].module_serial != job->route.serial) { /* another module's view now */ applied->status = PURE_CLOSED; applied->refused = "stopped"; continue; @@ -2989,7 +3031,7 @@ static void serve_bursts(Host *host) { : run_query(job, host->snapshots[a], applied); ++shown; } - for (size_t i = 0; i < touched; ++i) pure_snapshot_release(host->snapshots[host->touched[i]]); /* answers lend their own */ + for (size_t i = 0; i < touched; ++i) let_go_snapshot(host->snapshots[host->touched[i]]); /* answers lend their own */ if (!answers) return; for (size_t i = 0; i < answers; ++i) answer_applied(host, &host->views[i]); host->shown += shown; @@ -3201,7 +3243,7 @@ static PureSnapshot *surface_read(PureSurfaceHost *s, PureApp *reader) { if (snapshot) pure_snapshot_hold(snapshot); pthread_mutex_unlock(&published->lock); if (snapshot && !pure_app_read(reader, snapshot)) { /* no room for its states: none shown */ - pure_snapshot_release(snapshot); + let_go_snapshot(snapshot); snapshot = NULL; } return snapshot; @@ -3209,7 +3251,7 @@ static PureSnapshot *surface_read(PureSurfaceHost *s, PureApp *reader) { static void surface_release(PureSurfaceHost *s, PureSnapshot *snapshot) { (void)s; - pure_snapshot_release(snapshot); + let_go_snapshot(snapshot); } /* An event queued at the owner as a request is, waited for until its save. */ @@ -3309,9 +3351,10 @@ static bool replies_borrow(const PureModule *m, uint32_t input) { /* ---- Main ------------------------------------------------------------- */ /* A module from its file for an app; false, with why, when it does not load - * or cannot be served. The module the app had is kept (see retired_modules). */ + * or cannot be served. The module the app had is retired (see Loaded). */ static bool load_module(App *app, const char *path) { - PureModule *m = calloc(1, sizeof(*m)); + Loaded *loaded = calloc(1, sizeof(*loaded)); + PureModule *m = loaded ? &loaded->module : NULL; char error[160]; if (!m) { snprintf(app->why, sizeof(app->why), "Module: out of memory"); @@ -3319,9 +3362,12 @@ static bool load_module(App *app, const char *path) { } if (pure_module_read(path, m, error, sizeof(error)) != PURE_OK) { snprintf(app->why, sizeof(app->why), "Module: %s", error); - free(m); + free(loaded); return false; } + bool at_freed = false; + for (unsigned i = 0; i < 64; ++i) at_freed = at_freed || freed_at[i] == (uintptr_t)loaded; + modules_at_freed += at_freed; bool served = true; for (uint32_t i = 0; served && i < m->inputs; ++i) { if (m->input[i].kind == PURE_KIND_MESSAGE && !replies_borrow(m, i)) { @@ -3329,24 +3375,60 @@ static bool load_module(App *app, const char *path) { served = false; } } - struct Retired *kept = served && app->module ? malloc(sizeof(*kept)) : NULL; - if (served && app->module && !kept) { + uint64_t *passes = served && app->module ? calloc(host_count ? host_count : 1, sizeof(*passes)) : NULL; + if (served && app->module && !passes) { snprintf(app->why, sizeof(app->why), "Module: out of memory"); served = false; } if (!served) { pure_module_release(m); - free(m); + free(loaded); return false; } - if (kept) { /* requests and snapshots may still point to it */ - *kept = (struct Retired){app->module, retired_modules}; - retired_modules = kept; + if (passes) { /* requests and snapshots may still read it: freed once none does (free_retired) */ + Loaded *was = (Loaded *)(uintptr_t)app->module; + for (unsigned h = 0; h < host_count; ++h) passes[h] = atomic_load_explicit(&all_hosts[h].passes, memory_order_acquire); + was->passes = passes; + was->next = retired_modules; + retired_modules = was; + uint64_t one = 1; + for (unsigned h = 0; h < host_count; ++h) /* each begins a pass now, not when its wait ends: freed sooner */ + if (write(all_hosts[h].wake, &one, sizeof(one)) != sizeof(one)) { /* full: it wakes anyway */ } } app->module = m; return true; } +/* The retired modules nothing reads any more, freed, on the thread that changes apps (run_apps): no app runs one (its + * line, its owner, its executor, what a request reads), every connection thread has begun a pass since it was retired + * (a request resolved with it holds it by then), then (in that order) no snapshot or request holds it. A link naming + * an input reads an app's module under binding_lock: taken, none is reading one. */ +static void free_retired(void) { + for (Loaded **at = &retired_modules; *at;) { + Loaded *l = *at; + const PureModule *m = &l->module; + bool kept = false; + for (unsigned a = 0, count = places(); !kept && a < count; ++a) + kept = apps[a].module == m || apps[a].owner.executor.module == m || apps[a].owner.module == m || + atomic_load_explicit(&apps[a].live, memory_order_acquire) == m; + for (unsigned h = 0; !kept && h < host_count; ++h) + kept = atomic_load_explicit(&all_hosts[h].passes, memory_order_acquire) <= l->passes[h]; + kept = kept || atomic_load_explicit(&l->holds, memory_order_acquire) > 0; + if (kept) { + at = &l->next; + continue; + } + *at = l->next; + pthread_mutex_lock(&binding_lock); + pthread_mutex_unlock(&binding_lock); + pure_module_release(&l->module); + free(l->passes); + freed_at[modules_freed % 64] = (uintptr_t)l; + free(l); + ++modules_freed; + } +} + /* A connection on this thread for fd (accepted, or moved here); NULL, fd * closed, when it is full (the peer sees the close) or out of memory. */ static Connection *adopt(Host *host, int fd) { @@ -3360,6 +3442,7 @@ static Connection *adopt(Host *host, int fd) { c->index = index; c->fd = fd; c->phase = READING_HEAD; + c->module = NULL; c->readable = c->writable = c->keep_alive = true; c->hung_up = c->settled = false; c->deadline = host->now + host->timeout_ms; @@ -3429,6 +3512,8 @@ static bool moved_home(Connection *c, size_t head_size) { if (write(home->wake, &one, sizeof(one)) != sizeof(one)) abort(); Host *host = c->host; /* here it is gone, its descriptor open there */ epoll_ctl(host->epoll, EPOLL_CTL_DEL, c->fd, NULL); + let_go_module(c->module); /* (none yet: a connection moves at its first request) */ + c->module = NULL; c->phase = CLOSED; host->table[c->index] = NULL; c->next = host->closed; @@ -3488,7 +3573,7 @@ static void stop_executor(Executor *ex) { PureSnapshot *last = ex->published->snapshot; ex->published->snapshot = NULL; pthread_mutex_unlock(&ex->published->lock); - pure_snapshot_release(last); + let_go_snapshot(last); if (atomic_load_explicit(&ex->published->subscribers, memory_order_relaxed)) wake_pushers(); /* their streams end */ } pure_app_stop(&ex->app); @@ -3564,6 +3649,7 @@ static void *run_shard(void *context) { host->now = now_ms(); uint64_t next_sweep = host->now + 1000; while (!atomic_load(&stopping)) { + atomic_fetch_add_explicit(&host->passes, 1, memory_order_acq_rel); /* (a request resolved before is held) */ /* A thread that runs the app ticks its timers on time. */ long due = apps[0].owned ? -1 : run_timers(&host->local); if (host->push_waiting) { /* a pass waits for --push-every-ms */ @@ -3683,7 +3769,8 @@ static void stop_shard(Host *host) { * thread's alone. A delivery whose app runs another module now, or none, is * not taken: the receiver keeps it and finds the input again once told * (pure_link_listener_rebind, when the app starts). */ -static struct Binding { unsigned app; const PureModule *module; uint32_t input; } *bindings; +static struct Binding { unsigned app; const PureModule *module; uint64_t serial; uint32_t input; } *bindings; /* serial: the module's */ +static int bind_input(unsigned a, const char *name, uint32_t shape, const unsigned char *fields, size_t size); static size_t binding_count, binding_capacity; static int link_input(void *context, const char *name, uint32_t shape, const unsigned char *fields, size_t size) { (void)context; @@ -3696,6 +3783,15 @@ static int link_input(void *context, const char *name, uint32_t shape, const uns if (a == count) return -PURE_LINK_NO_INPUT; name = slash + 1; } + pthread_mutex_lock(&binding_lock); /* the module read stays while it is read (free_retired takes this lock) */ + int found = bind_input(a, name, shape, fields, size); + pthread_mutex_unlock(&binding_lock); + return found; +} + +/* link_input's binding of app a's input name in the module it runs, under binding_lock. A binding is of a module (its + * shape was checked against that one's): one of a module the app no longer runs stays, refusing what comes to it. */ +static int bind_input(unsigned a, const char *name, uint32_t shape, const unsigned char *fields, size_t size) { const PureModule *m = atomic_load_explicit(&apps[a].live, memory_order_acquire); int index = m ? pure_module_input(m, name) : -1; if (index < 0 || m->input[index].kind != PURE_KIND_MESSAGE) return -PURE_LINK_NO_INPUT; @@ -3703,10 +3799,10 @@ static int link_input(void *context, const char *name, uint32_t shape, const uns if (pure_module_shape(m, (uint32_t)index) != shape && !pure_module_takes(m, (uint32_t)index, fields, size)) return -PURE_LINK_FIELDS_LACKED; for (size_t b = 0; b < binding_count; ++b) - if (bindings[b].app == a && bindings[b].module == m && bindings[b].input == (uint32_t)index) return (int)b; + if (bindings[b].app == a && bindings[b].serial == m->serial && bindings[b].input == (uint32_t)index) return (int)b; if (binding_count > INT32_MAX - 1 || !grow(&bindings, &binding_capacity, binding_count + 1, sizeof(*bindings))) return -PURE_LINK_NO_INPUT; - bindings[binding_count] = (struct Binding){a, m, (uint32_t)index}; + bindings[binding_count] = (struct Binding){a, m, m->serial, (uint32_t)index}; return (int)binding_count++; } @@ -3734,10 +3830,10 @@ static bool link_deliver(void *context, const PureLinkDelivery *d) { memcpy(ingress + 4 + DELIVERY, d->records, d->size); if (d->fields_size) memcpy(ingress + 4 + DELIVERY + d->size, d->fields, d->fields_size); /* laid out with those */ *link = (LinkJob){.listener = link_listener, .token = d->token, .ingress = ingress}; - Job job = {.route = {.kind = ROUTE_INPUT, .target = bound->input, .module = bound->module}, .link = link, + Job job = {.route = {.kind = ROUTE_INPUT, .target = bound->input, .module = bound->module, .serial = bound->serial}, .link = link, .effect = {.input = PURE_INPUT_WRITE, .key = "", .expected = -1, .value = {ingress, size}}}; pthread_mutex_lock(&owner->lock); - bool queued = !owner->stopping && owner->module == bound->module && + bool queued = !owner->stopping && owner->module && owner->module->serial == bound->serial && grow(&owner->jobs, &owner->capacity, owner->count + 1, sizeof(*owner->jobs)); if (queued) { owner->jobs[owner->count++] = job; @@ -4397,6 +4493,9 @@ static bool resume_app(App *app) { pthread_mutex_unlock(&turns.lock); atomic_store(&app->gone, false); atomic_store_explicit(&app->live, app->module, memory_order_release); + /* The snapshot was published before it took requests: a push group that looked meanwhile found it quiet and + * waits for the next word. This is it (else a stream waits for the app's next save). */ + if (ex->published && atomic_load_explicit(&ex->published->subscribers, memory_order_relaxed)) wake_pushers(); app->running = true; if (link_listener) { char prefix[APP_NAME + 2]; @@ -4645,7 +4744,12 @@ static void *run_apps(void *context) { sigset_t hangup; sigemptyset(&hangup); sigaddset(&hangup, SIGHUP); - for (int signal_number; !sigwait(&hangup, &signal_number) && !atomic_load(&stopping);) read_again(path); + for (;;) { /* retired modules waiting: looked at again every 100 ms, until all are freed */ + free_retired(); + int got = retired_modules ? sigtimedwait(&hangup, NULL, &(struct timespec){0, 100 * 1000000}) : sigwaitinfo(&hangup, NULL); + if (atomic_load(&stopping)) break; + if (got == SIGHUP) read_again(path); + } return NULL; } @@ -5002,8 +5106,11 @@ int main(int argc, char **argv) { heard.active, heard.keyed, heard.secrets, heard.epochs, heard.refused, state_bytes, memory_refused, windows_full, unauthorized, forbidden, subscribers, subscribed, pushes, pushed, replaced); if (named_apps) { /* what each app did */ - fprintf(stderr, ",\"workers\":%u,\"moved_home\":%" PRIu64 ",\"handed\":%" PRIu64 ",\"apps\":[", - worker_count, moved, handed); + size_t still = 0; + for (const Loaded *l = retired_modules; l; l = l->next) ++still; + fprintf(stderr, ",\"workers\":%u,\"moved_home\":%" PRIu64 ",\"handed\":%" PRIu64 ",\"modules_freed\":%" PRIu64 + ",\"modules_retired\":%zu,\"modules_at_freed\":%" PRIu64 ",\"apps\":[", worker_count, moved, handed, modules_freed, + still, modules_at_freed); for (unsigned a = 0, count = places(); a < count; ++a) { const App *app = &apps[a]; fprintf(stderr, "%s{\"name\":", a ? "," : ""); |
