aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* Say what bool and i64 mean at every boundary, before building them (016)eunhhu2026-09-301-0/+74
|
* Say what is left: the memory steps are done, finding items in large lists is noteunhhu2026-09-301-1/+2
|
* Say what 109 inferred and what it did not measureeunhhu2026-09-301-1/+3
|
* Say that prefetching items did not pay, and why (109)eunhhu2026-09-3018-0/+258
| | | | | | | | | Fetching a found item's cells, a leaf's keys and each level's keys ahead made finding and reading items 20% faster on 100,000 items in a micro benchmark, but TPC-C's clean pairs moved 1% at most and finding alone got 48% slower, so it is not in. The owner's finding samples were the descent, not the fields: a write right after a read copies the leaf whole anyway, and what costs is walking structures larger than the caches at random.
* Say what finding items should gain, before measuring it (109)eunhhu2026-09-301-0/+34
|
* Say what the screen showed of a value, and what Vega now keeps from acting ↵eunhhu2026-09-302-3/+34
| | | | (015, 108)
* Say what a value means at each boundary, and what checking it found (015, 108)eunhhu2026-09-302-0/+115
|
* Check that a value comes out the same on every path (108)eunhhu2026-09-301-1/+281
| | | | | | | | | | Values at the edges of what JSON, rows, a query and a plain body can say go in to one node, over a link to a second that takes them by name in another order and wider, and over HTTP to a third; each must come out the same from views, a query, an answer, a restart, a restart after a checkpoint and a replaced module: a number's bits (zero without its sign), a text's bytes. What must be refused is refused on every way in and leaves nothing behind.
* Decode a query's %XX for an input's fields, as for a view's argumentseunhhu2026-09-301-23/+26
| | | | | | | A view's arguments were decoded and an input's fixed fields were not, so a client that sends an exponent's + as %2B, as Python's urlencode and browsers do, had ?n=1e%2B21 refused. show_given's comment is back above show_given.
* Say what pinning a value across every boundary checks, before writing the ↵eunhhu2026-09-301-0/+37
| | | | check (108)
* Say TPC-C's RAM numbers since snapshot nodes are reusedeunhhu2026-09-303-6/+8
|
* Say what snapshots cost the owner, and what reusing their nodes gave (107)eunhhu2026-09-3025-2/+400
| | | | | | | | | Most of it was the allocator: leaves past glibc's thread cache, freed by whichever thread let the last snapshot go. jemalloc was 11-14% faster at more memory; reusing the nodes in Pure gave a median +9.4% over six pairs, 4-8% less CPU a transaction in all six, and 1.97x the state bytes resident where it was 2.18x. TPC-C in RAM is now 4.0x a tuned SQLite at W=1 and 3.5x at W=4; finding items is the owner's largest part now.
* Find pure-serve's owner by its thread, and sort its samples as 106 doeseunhhu2026-09-301-45/+74
|
* Let a list make its next nodes of those no snapshot reads any moreeunhhu2026-09-304-47/+150
| | | | | | | | | | | | | | Each node a list copies once a snapshot shares it went, once the last snapshot that could read it was released, back to the system: a leaf of TPC-C's (1.3 KB) past glibc's thread cache, often freed by a connection thread into the owner's arena, while the owner allocated the next. That was 70% of what snapshots cost the owner. Now what no snapshot reads comes back to the app under the register's lock, and each snapshot the owner makes hands it to the lists of its size as spares, as many as each made lately; retired entries carry what the pass and the reuse ask of a node, so neither reads it again. TPC-C in RAM, W=1, 64 terminals: 10-14% more a second at 7% less CPU a transaction, resident 1.97x the state bytes where it was 2.18x (docs/research/107).
* Compare TPC-C on the SD card with SQLite committing in groups, not each ↵eunhhu2026-09-303-5/+11
| | | | | | | | transaction The 90x of 092 was against SQLite's defaults committing each transaction; tuned for the card and grouped as Pure groups, SQLite is 4-11x behind, the bytes it writes a transaction being the difference.
* Say what TPC-C costs when SQLite commits in groups too, and with four ↵eunhhu2026-09-3020-2/+547
| | | | | | | | | | | | | warehouses (106) SQLite tuned first, by the criteria committed before (and, beyond them, checkpoints each 40 MB of WAL, in its favour). On the SD card Pure stays 4.2-10.8x ahead at equal terminals, SQLite writing 8-18x the bytes a transaction; in RAM 3.2-3.5x. All tables equal at W=1 and W=4, and every run consistent. In RAM the one owner is the limit (0.94 of a core at W=4, no gain from 16 to 64 terminals), a third of its busy time copying nodes the snapshots share; checkpoints and memory are within their criteria.
* Read TPC-C's table dumps from Pure a page at a timeeunhhu2026-09-301-4/+10
| | | | | | | With four warehouses the order lines answered at once (about 140 MB of JSON) outgrew pure-serve's 64 MiB budget, and the check stopped with 503; a larger budget would have left the answer's memory in the resident size measured after.
* Checkpoint TPC-C's SQLite each 40 MB of WAL, as tuning on both devices choseeunhhu2026-09-301-6/+11
| | | | | | Its default, each 1,000 pages, is 1 MB of WAL with the 1 KiB pages the SD card favours: grouped commits there went from 203 to 694 a second, and in RAM a thread's p99 from 1.2 to 0.2 ms (docs/research/106).
* Say what the TPC-C comparison decides, before running it (106)eunhhu2026-09-301-0/+44
|
* Let TPC-C's SQLite commit in groups as pure-serve does, and say what each ↵eunhhu2026-09-302-50/+276
| | | | | | | | | | | | | | | run costs The terminals hand their writing transactions to one writer, which runs those waiting in one transaction, each in a savepoint, and commits them with one sync; reads stay on the terminals' connections, as pure-serve shows views from the last save. An undone New-Order is rolled back by its caller now (it rolled back the whole group before, and counted as failed after the split). Each run reports its CPU a transaction, the owner thread's share of a core, its memory, and the bytes a transaction had written, by the process and to the device. SQLite runs with its database mapped, a 64 MiB cache and savepoint journals in memory unless told otherwise.
* Name the owner's thread and the checkpoint's, so their CPU is seen aparteunhhu2026-09-301-0/+2
|
* Say where the shared board now lives, and what is left (105)eunhhu2026-09-301-1/+9
| | | | | | | | | The board is Vega's examples/shared-board.aup, and Vega's shared_board check runs its two nodes: changes on either agree, and a partition healed agrees on the later change. Reading a named field list once a delivery would save part of 310 ns a record during upgrades alone: not worth an API. What review 004 asked that is left is 103's third item, broader performance comparisons under conditions stated first.
* Say that sends and applies now spread their fields (105)eunhhu2026-09-301-4/+8
| | | | | | | | | aup spreads an item or a message into a send or a record, and sets an item whole (c69682e): the board sends { ..at } and applies cards[k] = Card { ..remote }, and a new field is written in two places, the record and the output. v6 on both nodes, cut apart and joined again, agrees on the later change; its module is byte for byte the one with every field written.
* Say what laying a record out anew costs (014)eunhhu2026-09-302-2/+2
| | | | | | | A board record of five fields (38 B) sent with v3's output fields took 310 ns to lay out for v4's input, reading the field list each time. Only streams of another shape pay it, and only while nodes are upgraded one after the other: less than a delivery's own work.
* Say how growing a board two nodes share went (105)eunhhu2026-09-302-1/+120
| | | | | | | | | | | | | | | | | | 103's next step: grow a real app, adding and reordering fields, cutting the connection, starting again, and write down what gets in the way. Two nodes share a board (from Vega's examples/board.aup), each sending its changes to the other, the later change winning. Upgrading them one after the other deadlocked: fixed by recasting waiting sends by name (9aa123e) and taking fields by name (014, 5ce7655), and under it a link that stopped for good when a refused stream's try was lost (baaf955). Now the older node's changes reach the newer one during the upgrade, the newer one's new field waits for the older, and the boards agree a second after. Fields in another order agree at once; a partition healed agrees on the later change; both nodes started again go on. Two rough edges eased on the way (the node's name, message like in aup); sends and applies are still written field by field. CHECKPOINT's row for links says so.
* Say a node's name when pure-serve startseunhhu2026-09-301-2/+7
| | | | | | | | | A node's sends are named NAME.OUTPUT, and a receiver's --sender must say that name. Without --name it is the module file's name without .pmod, which nothing said: two board nodes built from board2.aup sent as board2.changed and were refused by receivers expecting a.changed. The first line out now carries the name beside the port and the key, and the usage says where it comes from.
* Take a delivery's fields by name, dropping noneeunhhu2026-09-3011-47/+629
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A message from another node was taken only in its input's exact shape (the link's STREAM shape, fields and order alike; over HTTP, every field). So a board whose nodes are upgraded one after the other stopped both ways until both were, and an output and its input whose fields differ only in order were refused for good (docs/design/014). An input now takes a message when it has every field the sender sends, by name, of the same type or one that holds its values (a u32 in a number, text in bytes); a field only the input has is 0 or empty. One that sends a field the input lacks is refused: taking it would drop the value, and with its number heard the value would never come again (two boards apart for good). So add fields to receivers first and take them from senders first, and nothing stops or is lost. The link carries a sender's fields in a new frame, FIELDS (0x0d: names, shape, u8 count, each {type, name length, name}): after a REFUSED for another shape the sender puts it in its packets (after OFFER, before the records, so a receiver that knows no FIELDS reads what it read before) until the stream is taken, the first time at once. A receiver seeing another shape waits for the packet's FIELDS before refusing, and asks its host with them (PureLinkHost.input takes them); REFUSED says 4 when the input lacks a field. Deliveries carry the fields and the host lays each record out anew (pure_module_takes, pure_module_recast, pure_module_fields, over pure_send_recast's rule). A stream whose sender starts again in another shape lets go what it held; the sender sends it again laid out anew. A refusal already queued for a stream now says the last reason judged, not the first. Over HTTP a delivery (X-Pure-Source) may lack fields; an unknown member is still refused. test-link named: a stream of another shape naming its fields is taken, 200 stored in 47 ms with every delivery naming them; an input that does not take them refuses (4) and the sender backs off. check.py server_fields: five outputs, three linked and two over HTTP, fields in another order taken, an input with a field more taking messages without it, one lacking a field refusing until it starts again with it, then taking all. server_upgrade's new receiver declares its fields in another order. 012 names the frame and the rules.
* Balance the snapshot register's count, and free the runtime lists in ordereunhhu2026-09-303-7/+17
| | | | | | | | | | | | | | | | | | | | | Under AddressSanitizer with leaks counted, the module checks lost 672 KB in 10,185 allocations. Two causes, both from the snapshot rework (d1e8f38): - A snapshot's register counted its snapshots with ++ under its spin, but a snapshot's last release took one off atomically outside it. A release on a reader's thread racing the owner's next snapshot could lose either change: the register then outlived every snapshot (the leak seen), or went while one still used it. The count is atomic now. - pure_app_stop handed what its lists let go to the newest snapshot (or freed it) and only then freed the outbox and the numbers heard, whose shared nodes and texts went to that hand-over's list again, for good. They are freed with the other lists now. make check builds the module checks a second time under AddressSanitizer and UndefinedBehaviorSanitizer (build/test-module-sanitized, 25 s) and fails on a leak. Before d1e8f38 the same build stops at a heap-use-after-free in retire; now it is clean.
* Recast waiting sends by field name when an output's fields changeeunhhu2026-09-307-66/+616
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Review 004's F3 made a module refuse to start while sends waited on an output whose fields it had changed: their bytes would have meant something else. Safe, but it deadlocks a rolling upgrade. Growing a board shared by two nodes: one upgraded to a card with a new field refuses the other's old sends (another shape), and the other, its old sends waiting, cannot start the new module at all ("Cannot start the module on the store", saying nothing more). Waiting sends now move to the new fields by name, as a list's items do: a value goes over when the new type can hold it (u32 and number are numbers, bytes and text are bytes; a number that is not a u32, or bytes that are not UTF-8 for a text, cannot), the rest start at 0 or empty (pure_send_recast, with the payload layout load uses, pure_fields_lay_out). A start recasts them and a replace does into a new outbox. Their meaning must survive a crash between the recast and the save that names the new fields in the links. So the outbox's image now names the fields its sends were laid out with ("PLO1": the module's outputs after the save it holds), and is written again before any send is logged with other fields; a start reads those names over the links'. Checkpoints of the outbox name them too. A module without an output that sends wait on is still refused, now saying so in PureApp.why, which pure-serve prints ("sends wait on the output 'ordered', which this module does not have"); a failed start keeps it. test-module send_recast (widened, narrowed, UTF-8, a payload not laid out, there and back) and output_meaning (the swapped fields recast on a replace, read right after a restart that never saved, back again, sends logged after the image, the refusal and its reason). check.py server_upgrade: five sends wait on the old output, the sender starts with the new one, the old receiver refuses the new shape, the new one takes all eight.
* Try a refused stream again when its try is goneeunhhu2026-09-302-0/+69
| | | | | | | | | | | | | | | | | | A sender whose stream the receiver refuses sends one packet of records a wait, and the refusal is over when that packet is acknowledged with no REFUSED. When the try itself went to no one (the receiver started again, as a node upgraded one after the other does, or the path lost it) nothing acknowledged it: the sender took its epoch for lost, began another, and then sent nothing more, still waiting on the try, with no record in flight to probe for. Two nodes of a shared board stayed apart for good after the second one's upgrade. A try declared lost, or dropped with the epoch's flights, now lets the next packet try again. test-link's refused_restarted case: the receiver refuses a stream, is killed while the next try is due and comes back 8 s later with the input; before, nothing was stored in 120 s, now all 200 are by 18.2 s. Streams refused for good send as few packets as before (9 in 90 s).
* Say what the windows and checkpoints cost (013 steps 2 and 3, 104)eunhhu2026-09-305-15/+79
| | | | | | | | | | | | | | | | | | 013's second step is in (d9e0b68): each transport holds a window of its output's sends. 104 measures it with the receivers down: once the window is full a node grows by the outbox alone, 219 and 218 B a send over the link and HTTP (399 and 534 B before). The third step is a measurement. A checkpoint holds one list's snapshot while it writes the image, so what it adds is what the app changed meanwhile: nothing visible on tmpfs or for a list that fits the card's fast writes; on the SD card, a 22.4 MB list written for about 8 s while 1,400 items a second were set left +3.0 and +4.4 MiB. The highest resident memory was twice the state bytes, inside 013's bound (the state and one snapshot) and its stop criterion (3x). 021's run.py takes --items, --stores (a directory on the device to measure) and --label, and reports the peak (VmHWM).
* Hold a window of each output's sends in its transport, not the outboxeunhhu2026-09-3010-60/+196
| | | | | | | | | | | | | | | | | | | | | | | A link's ring and a wire's queue took every saved send of their output and kept it until delivered: with the receiver down they held the outbox a second time, 178 B and 313 B more a send (104). Each outlet now holds a window, --window bytes (4 MiB) and 65,536 sends past what its receiver has, at least one; the outbox keeps the rest. The owner remembers each outlet's place in the outbox and looks on from there after a save and after its transports deliver. Places move back by the sends that left the outbox since (PureApp.sends_gone), so a trim before a place skips nothing, and sends queued before are not looked at again however many wait. pure_app_sent reads a send by its place (the newest-first visit it replaces is gone); pure_link_held and pure_wire_held say what a transport holds. With the receiver down a node now grows by the outbox alone once the window is full: 219 and 218 B a send over the link and HTTP (399 and 534 B before). check.py server_windows keeps the receivers of a sender's two outputs, one linked and one over HTTP, down through 300 buys with a 2,000-byte window, then starts them: each applies all 300 once. Without the refill after deliveries the link stops at 117.
* Say where a node's memory goes, and the budget it now keeps (104, 013)eunhhu2026-09-3010-1/+473
| | | | | | | | | | | | | | | | | | 104 measures a node's memory under a receiver that is down and under a snapshot held for long (021's run.py: resident memory against what was posted, 30 s a case). A held snapshot grew the node without end (6.4 to 678 MiB, fixed in d1e8f38); a slow receiver grows it by what was sent, 221 B a send, which is state until delivered, so the budget bounds state and refuses past it. 013 is that budget's design: what it counts (state bytes, deterministic), what an event over it does, why an answer takes its send's room, how links and HTTP wait, what it does not count and those bounds, and the checks. With --memory 4000000 the same load fills it in about 4 s and the node stays at 9.6-9.8 MiB (45.9 MiB and growing without). 012 records the hole the budget found in the link (the full flag), and 103 that its clock fix fell short (385754c). CHECKPOINT gains the row.
* Wait on the monotonic clock everywhere a deadline is waited foreunhhu2026-09-306-30/+146
| | | | | | | | | | | | | | | | | | | | | | | Review 004's timer fix (103) counted the deadlines on the monotonic clock, but the owner still slept until a wall-clock time: with the clock set back an hour a node's timers would stop for that hour. So would a wire's pause between tries, and a lone commit after a group would wait an hour for the others to join it. The test's LD_PRELOAD moves only what the process reads, so there each wait ended at once instead, and the timers went on ticking while the owner spun a core. core/wait.h makes conditions that wait on the monotonic clock (pthread_condattr_setclock; macOS keeps the wall clock) and deadlines on it. The owner's waits, the wire's pauses and the group commit's gathering use them. clock-back.so can now move the clock forward too (CLOCK_SHIFT_S), and check.py server_clocks moves it an hour each way under a node with a timer and a wire to a port no one listens on, and under a document server whose three threads commit together, then one alone. Before: back, 159 % CPU and 50,886 tries in 1.2 s; forward, 1 tick, 1 try, and a lone commit that never ended. After: 59 ticks, 0 % CPU, 4 tries and a lone commit of 2.6 ms, both ways.
* Count only the states the memory budget countseunhhu2026-09-302-5/+12
| | | | | | | | | | An event under a budget counted its states twice, before and after, walking every state. For the smallest event (a number added, no store) that cost 21 ns in an app of one state and 125 ns in one of 32 (+17 %, +38 %). The app now keeps a bit for each list (not a message's rows) and text when it opens its module, and walks those alone: +10 ns and +29 ns (+8 %, +9 %), and nothing without a budget. A served write costs tens of microseconds, so its throughput does not see it.
* Bound what an app holds (--memory): refuse what would grow past iteunhhu2026-09-307-59/+512
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | pure-serve --memory BYTES gives the app a budget of state bytes: its lists' items as they are stored, its texts, the outbox and the numbers heard (docs/design/013-memory-budget.md). The count is deterministic, the same on any device. An event after which the states hold more than the budget, and more than before it, fails with the fault memory and changes nothing; HTTP answers 503 memory. One that frees or keeps them goes on, so an app over its budget can always shrink. Sends waiting for their receivers are state: a full outbox refuses the next send until some are stored, which is the backpressure a slow receiver puts on clients. A delivery refused for memory is not heard: the link keeps it (c9c0edf) and HTTP senders try again after 503. An answer (--reply) now takes its send out of the outbox in its own event (pure_app_answer), so it has the room its send held. Apart, an app whose outbox filled the budget could never apply an answer that keeps anything, and the send leaves only after its answer: both would stay for good. An answer that still does not fit waits, with the wire's after it, and is tried again every 100 ms: pure_wire_answer leaves it in place until pure_wire_answered. Checks: test-module memory_budget (refused and undone, freeing over the budget, a full outbox, a delivery not heard, answers that fit or wait, across a restart); check.py server_memory (a sender refused 503 memory with its receiver down and taking buys once it is up; a receiver restarted below what it keeps, refusing 7 times in 2 s, then taking all) and server_answers_memory (answers wait while receipts fill the budget, then apply once each; the API called once per order). The stats report state_bytes and memory_refused.
* Let a receiver whose app is full keep what came, and say soeunhhu2026-09-307-34/+86
| | | | | | | | | | | | | | | | | | | | A link's delivery the app had no room for was handed over again at the next turn: a receiver over its memory budget would refuse the same records as fast as the owner could apply them. And its sender, seeing records received but not stored for a second, sent the whole window again every second. Now a host says a delivery found it full (PureLinkDone.full). The receiver keeps the records and hands them over again after a pause, 100 ms doubling to 500 ms, and meanwhile its STORED frames carry flag 2: the app is full and what came is kept. A sender hearing it starts its one-second wait for the save again, and sends none of it again. The spec (012) says both sides; the pause is a local choice that must stay under the sender's wait. pure_link_done takes the PureLinkDone itself. test-link's new case keeps the app full for 5 s: 12 hand-overs meanwhile, taken within 0.5 s of having room, and no record sent again (21,640 without the flag).
* Let a held snapshot keep the states of its time, not all that came aftereunhhu2026-09-304-37/+174
| | | | | | | | | | | | | | | | | | | | | | | | An app's snapshots formed a chain, each holding the next, and what the app's lists let go while a snapshot was the newest went with it. So one snapshot held for long held every later one and every node and text let go since: a surface holding the snapshot of 20,000 items while their notes were set again grew the node from 6.5 MB to 696 MB in 30 s, 3.4 KB an update, without end. A snapshot reads only what was made before it. Snapshots are now numbered (their epoch, given to the lists' copies and so to each node and text made after), and the ones held are linked under a register they share. What the app lets go while a snapshot is its newest goes with it, as before; when that one goes, what the newest older one still held may read (made before its epoch) goes to it, and the rest back to the system at once. The links move under a spin the register keeps; what is freed is freed outside it. The same held snapshot now costs its states once: 6.6 MB to 10.6 MB, flat after the notes diverge. test-module's snapshot_held holds a snapshot through 40,000 events and snapshots: none of the second 20,000's allocations stays (0 B; with the chain's keeping, it fails). The kept-snapshots tests read eight held in random order, and the module tests pass under AddressSanitizer too.
* Say how review 004's seven defects went (103)eunhhu2026-09-291-0/+61
| | | | | | | | | | | | Three broke what data means (a walk's range missed the indices an insert moves; an uncertain save of numbers alone let the next save through; an output whose fields swapped names kept its waiting sends), four were errors (a stateless timer never ticked, Vega's terminal lost a key cut across reads, an input named edit forced documents, timers waited on the wall clock). Each is fixed with a check that fails on the code before and passes now, in Pure's and Vega's make check. The review's other advice (a whole app's memory budget, one real app grown, comparisons with their conditions set first) is what comes next.
* Say what adopting the link took, and how it fares against ngtcp2 (102)eunhhu2026-09-295-18/+411
| | | | | | | | | | | | | | | | | | 102: the spec (012) and a receiver written from it alone; the four holes writing it found (REFUSED, the record's size, a peer's run places, the memo's places beside) and the credit M6 showed fixed at 1,024 records; M6 with ngtcp2 as a third, two rounds. Under reordering Pure carries the most (45 Mbit/s; ngtcp2 29-47, quinn 1-2), under random loss the three are the same, on a clean bottleneck Pure carries 3-5 % less (its framing, some 112 B a packet to QUIC's 41), and where loss and reordering meet ngtcp2 carries 1.3-3.6 times as much: undoing a loss it took for reordering, it restores the window a real loss had cut too (its log: 22 of 34 undos past the window before the last cut; none without reordering). Shares beside TCP stay within 25 %. 011 and the status table say so; 012 also says that a receiver answers within 25 ms (a sender's PTO counts on it), that a shape and a record's bytes are the app's, and why a packet of the past does no harm.
* Size a receiver's credit by what it holds, not 1,024 recordseunhhu2026-09-292-9/+22
| | | | | | | | | | | | | | | | | | | | A receiver gave every stream a credit of 1,024 records past its last stored, however small the records: a link carried at most 1,024 records a round trip. On a path of 100 ms that is 10,000 records a second of 61 B, a ninth of what 50 Mbit/s carries (the path simulator: 9,953 a second, 90,814 at 10 ms); and records of 1 KiB, a queue's worth of them in flight, went a record or two a packet as each STORED let a few more through (M6: 107,968 packets where 20 ms took 84,106). A receiver bounds what it holds of a stream (16 MiB) apart, so its credit is now what fits there at the size its last delivery's records had: 1 to 65,536 (what a sender takes), 1,024 before any delivery. It takes records that far past its last stored, as its memory lets it. Simulator at 100 ms: 56,296 records a second of 61 B. M6 at 100 ms: 45.4 Mbit/s of 1 KiB records in 83,566 packets (44.9 in 107,968). The window, not the credit, now fills the bottleneck's queue: losses there where the credit hid them (61 B at 10 ms: 87,130 a second, 1,371 lost). 012 says so.
* Lead pure-serve's usage with pure:// between nodes, and count refusalseunhhu2026-09-292-8/+12
| | | | | | | | | The usage showed --send OUTPUT=http[s]:// first; links are now the way between nodes (012), so it shows pure:// for a Pure node and HTTP for anything else. The statistics at exit count the streams a node's links were refused (link_refused) and the ones its listener refused (listener_refused), so a wiring mistake is in the numbers too, not only the line on stderr; server_links_refused checks both.
* Serve documents when a module keeps a collection, not by its inputs' nameseunhhu2026-09-292-2/+35
| | | | | | | | | | pure-serve took a module with an input named show or edit for a document app and refused it unless both were keyed messages: an app with a plain message named edit compiled and then would not start (review 004, an earlier P2). Documents are what a module keeps in a keyed collection; now the host serves them when the module declares one, and inputs named show and edit are otherwise messages as any other. check.py's server_plain_edit starts such an app and posts to edit.
* Tick a stateless app's timers, and wait for them on the monotonic clockeunhhu2026-09-294-13/+74
| | | | | | | | | | | | | | | | | | | | run_timers ticked nothing in an app without states, taking a timer to be able to change nothing else; but a tick may send, and the outbox is the app's state (the host gives such an app an owner for it). A timer that only sends never ticked (review 004, F4). Now an app ticks when it has states or outputs; one with neither runs on every connection thread and has nothing a tick could do. When a timer is due was reckoned on the wall clock: set back, a timer waited for the time it had reached again (review 004, an earlier P2). The next tick is now reckoned on the monotonic clock; events still see the wall clock (now). check.py's server_timers runs the review's heartbeat (a timer, an output, no state) and sees it tick, then again with the wall clock set back an hour 0.2 s in (build/clock-back.so, an LD_PRELOAD of clock_gettime): 29 ticks in 0.6 s, where the server before this stops at 9.
* Refuse a module whose output names its fields otherwise while sends waiteunhhu2026-09-293-21/+86
| | | | | | | | | | | | | | | | Replacing a module, or starting one over a store, kept an output's waiting sends when the module had the output with as many fields of the same types. Their names were not compared, and the stored links kept the types alone: an output whose two numbers swapped places read debit 10, credit 20 as credit 10, debit 20, replaced and started again (review 004, F3). A send's bytes mean its fields' names and types in order; now both are compared, in memory and in the store, whose links (PLK4) keep each field's name. Links kept before (PLK3) have the types alone, as before; the next save writes PLK4. test-module's output_meaning: the swapped module is refused, replacing and starting, while a send waits; trimmed, it replaces and its sends go on; and the store then refuses the module before.
* Hold an app uncertain whatever its save changed, not only a listeunhhu2026-09-292-6/+42
| | | | | | | | | | | | A save whose tree commit may or may not be stored leaves the app apart from its store: no save may follow until it starts again from what the store has. The app was marked so only when the save also logged a list's change, so a save of numbers alone that came back uncertain let the next event (a list's alone) save on the state the memory had rolled back to: after a restart, count 1 and a row made from count 0 (review 004, F2). Now any uncertain tree commit marks it; a failed one still takes back the logged entry. test-module's uncertain_numbers does the review's case: the list's event is refused, and the store is the commit's.
* Keep a walk's range only when its body moves no state its bound readseunhhu2026-09-294-4/+188
| | | | | | | | | | | | | | | | | | A walk whose where bounds a keyed list's key finds the bound once, when the walk begins, if its body writes nothing the bound reads. The body's writes counted only states stored and lists changed, not what those changes move with them: an insert or remove moves the indices into the list, a text's change the cursors in it. A walk bounded by an index into a list its body inserts into saw the bound it began with: of five rows it took one (review 004, F1). Those states now count as written, and a bound that moves an index (whose list is whichever the index's is) is kept only when the body changes no list. build/module-probe runs a module twice from the same keys, as it loads and with every range let go, and says which state first differs. check.py's module_ranges runs the review's case (all rows twice: done 10, at 10) and a walk whose bound its body cannot move, still ranged. The probe finds the difference in the runtime before this.
* Let ngtcp2's handshake through a path that reorderseunhhu2026-09-291-1/+8
| | | | | | | | OpenSSL 3.5's ClientHello offers ML-KEM with X25519 and takes two Initials; when the path brought the second first, ngtcp2's server was asked for a Retry and the harness gave up. The client now offers X25519 alone, as Pure's links agree, and the server lets go of a connection it may not keep, so the client's next Initial begins another.
* Put ngtcp2 beside quinn in M6, as it is in M5eunhhu2026-09-292-66/+210
| | | | | | | | | drain.c takes a rate and seconds as quinn's client does (paced messages stamped with the time they were written, and no more written past the seconds), and its service writes quinn's line for each stream: the messages read in each 100 ms and how long they took. 020's run.py runs it as a third, on port 40005 behind the same bottleneck, and --who picks whom to run.
* Remember the epoch of each of a peer's runs that send, not oneeunhhu2026-09-294-55/+158
| | | | | | | | | | | | | | | | | A receiver's memo kept one run's epoch a peer. A node whose outputs send to this one sends in a run each, so after the receiver started again (or let the peer rest) all runs but the last accepted were told LOST and began their epochs again: records under the static keys for a round trip, and an X25519 each. Now a run whose peer's place holds another run still sending keeps its epoch beside, in one of two of 1,024 places its peer and run find (160 KiB at the file's head: the memo is PLM3, and a PLM2 one is begun again). A run's place beside goes once its peer's place has it, so an epoch's secrets stay only until the next one's are. test-link's three outputs of one node, each in epoch 1, go on after the receiver starts again: no LOST, no X25519 (before: two LOSTs, an accept). 012 says so.