| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver that took no records of a stream (a source the key may not
send as, no input of that name, an input of another shape) said nothing:
its sender heard them acknowledged, never stored, and sent them all
again every second, for ever, and its operator saw nothing. Now the
receiver answers REFUSED (0x0c: the names, and why), and the sender
sends no records for a second, then a packet of them to learn whether
they are taken now, the wait twice as long each time (64 s at most). A
refusal ends when that packet is acknowledged with no REFUSED beside, or
a record is stored. link.c says it once on stderr, as a wire says a
receiver does not take its sends; serve's input says which of the two
the app lacks.
test-link: each of the three is refused seven times in 90 s with nine
packets, and a stream refused until the app has its input goes on then,
all 200 records stored. check.py links a node to an input its receiver
lacks: said once, two packets in 2.5 s.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
nothing past
A sender offers the public half of a pair made for the next epoch
(OFFER) until the receiver accepts with a half of its own (ACCEPT); their
X25519 and the epoch's secret make the next one's, and the private
halves go. The header's low four bits say a packet's epoch; epoch 0 is
the nodes' static secret, as the keys were. The first epoch is offered in
the run's first packet (it begins with the first answer when the
receiver is not busy), then one an hour or GiB. One who later has both
nodes' keys opens only epoch 0, and no record went there after an epoch
began: test-link tries every packet sent under the static keys, with
epochs a second long, loss, reordering, restarts and replays.
The receiver writes the epoch it accepted in its memo (a place is 256
bytes now), so it goes on without an X25519 when it starts again; one
that forgot is not heard from, and its senders, after 2 s and four
probes unanswered, offer again in epoch 0 with no records until an epoch
begins. Keys of four epochs are tried (accepted, now, before, zero), as
two may share the four bits.
An accept costs an X25519 on each side, 220 µs on this Pi: one of a
sender with records costs a token (256 a second, 8 at once) and the
next packet offers again when there is none; one of a sender with
nothing else to send waits for none, one a second for it. The X25519 is
made after the turn's answers, never between a packet and its answer.
serve's report and M2's generator count keys, X25519s and epochs.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
link/link-protocol.h is the protocol with no socket, thread or clock of
its own: packets and time in, packets out. link/link.h carries it over
UDP as before. A sender starts small (8 flights, 16 records, a 4 KiB
ring) so one thread can hold many. The transport's CPU a record is the
same within 2% (five rounds each way, 61 B to 16 KiB).
test-link runs both sides through a network of its own: loss, packets
held back and doubled, failed saves, an idle sweep, and both sides
starting again, every record applied once as sent.
|
| |
|
|
|
|
| |
A sender takes its messages' shape, and a listener asks its host which
input a stream goes to. The core gives the shape (pure_module_shape);
link/ now builds without the core's headers.
|
| |
|
|
|
| |
libpure-link.a holds them; pure-serve and the M5 harness link it instead
of naming its source files.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A record over a piece (1,000 bytes) went in pieces even when it fit the
packet whole: a record of 1,013 bytes went as two RECORD frames, and the
receiver gathered it the slow way (an allocation, pieces, then onto the
line). A record none of whose pieces went yet now goes whole in a RECORDS
frame whenever it fits what is left of the packet; its pieces are marked
sent together. A record too large for a packet, or one resent after
some of its pieces were lost, still goes in pieces, and a receiver takes
a record whole or in pieces alike.
The transport alone takes 2.85 us a record of 1,013 bytes, not 3.18
(quinn: 3.90). transport.c now checks every record delivered: in order,
once each, its bytes as sent. None was wrong in a network namespace with
1 ms of delay, 3 % loss and 10 % reordering, with records of 61, 1,013
and 16,013 bytes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A queued record had an allocation of its own for its bytes and its
pieces' states, freed when the receiver stored it. Records come and go in
order, so their bytes now go one after another into a ring, never cut by
its end, and go back as the ring's head moves past the ones stored; a
ring too small grows to twice its size, the records moved into it in
order. A record of one piece keeps its piece's state in itself.
The transport alone drains 200,000 records of 61 bytes at 5.3 to 6.5
million a second; per record the link takes 0.137 us, the listener 0.14:
0.28 in all, against quinn's 0.26 a message over one stream.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver copied each record into an allocation of its own, copied them
all again into a delivery, and freed each once stored: about 110 ns a
record of 61 bytes. Once the last number heard is known, whole records
that come in order now go onto the stream's line as a delivery carries
them (u32 size, bytes). The line is handed over as it is, and let go by
moving its head; its front is given back when there is no room at its
end. Records that come early or in pieces, or before the last heard is
known, still wait in held, and move onto the line once it reaches them.
The transport alone: the listener took 0.20 us a record of 61 bytes, now
0.15; with the link, 0.31 us a record in all, against quinn's 0.26.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
changes
A listener's socket now asks for GRO: the kernel hands over the datagrams
of one sender that came one after another as one buffer, each the
segment's size but the last, and on the loopback a burst sent with GSO
comes whole instead of cut into datagrams. The listener takes up to eight
such buffers at once. It gives its AES-GCM context a key only when the
packet's run has another than the one it holds, for opening and for
sealing answers.
The transport alone drained 200,000 records of 61 bytes at 4.2 million a
second (0.454 us a record, both sides); now at 4.9 million (0.394 us).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
A RECORD frame carries a number, a size, an offset and a length before
each record's bytes, 19 bytes: a packet held 13.7 records of 61 bytes. A
RECORDS frame (0x06) carries the first record's number and a count, then
whole records whose numbers follow on, each its size as a varint and its
bytes: 17.2 a packet. A piece of a record larger than a piece still goes
in a RECORD frame; a receiver takes both.
The transport alone (research/experiments/019-idle-links/transport.c)
drains 200,000 records of 61 bytes in 11,600 packets instead of 14,600;
per record the link takes 0.217 us and the listener 0.23, 0.447 us in all,
against quinn's 0.43 a message over one stream.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every queued record wrote the link's eventfd, a system call each, and woke
the thread to send a packet of a few records. A record queued now wakes
the thread only when it sleeps and nobody woke it yet; one queued while it
sends marks the link, and the thread looks again before it sleeps. With
records queued one at a time as fast as a thread can, 200,000 small ones
went at 400k a second, and now at 730k; queuing one costs 0.43 us, not
1.0.
research/experiments/019-idle-links/transport.c measures the transport
alone: a link and a listener in one process, no app, no store. A backlog
of 200,000 records of 61 bytes drains at 3 to 4 million a second, 0.24 us
a record on each side.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A backlog made the link slow in its square: the sender looked at every
queued record's pieces twice a turn to know whether any waited to be
stored, and the receiver moved all its held records down each time it let
the first one go.
- The sender counts the pieces received and not stored, and keeps where
the unsent ones begin; every change of a piece's state goes through one
place that keeps both.
- The receiver keeps its held records from a head index. One in order goes
last, and one out of order finds its place by halving. The record's bytes
and its pieces' marks are one allocation.
- Packets go in bursts with UDP GSO, 54 at a time: 0.45 us a packet on this
Pi, against 2.4 us for a send each. A full packet is padded to a whole
datagram (PADDING frames, as QUIC does). Datagrams are taken 64 at a time
(recvmmsg).
Twenty thousand saved sends of 61 bytes drained at 370k a second; now at
990k. Per record, the sender's link thread took 1.1 us and now takes 0.5;
the receiver's took 2.4 and now takes 0.5.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
A listener lets a quiet sender's secret go with its active state, so a
node started again made one X25519 for every sender that came back (169
us on this Pi, most of a wake's CPU): asymmetric work in proportion to the
links, which design 011 counts as a reason to design again. The secret is
now remembered in a file beside the key (KEY.links, 0600, begun again for
another node's key): a place of 64 bytes a sender in the order allowed,
mapped, so a sender at rest takes no memory for it. A place is the first
28 bytes of the sender's key, a check (FNV-1a of the key and the secret)
and the secret, written secret first and key last; a place whose check
fails is made again. The UDP link checks see a receiver started again
make no X25519.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Each run a listener knew held two OpenSSL contexts. With 16 KiB pages,
what those allocations left behind when idle senders went kept 42 MB of
free heap resident after 10,000 senders had come and gone (in use: 0.6
MB). A run now keeps its keys (16 bytes each way and the nonce bases),
and the listener seals and opens with one context, giving it each
packet's key: about 130 ns a packet on this Pi. Two runs a sender, not
four; a sender's address as an IPv6 one (an IPv4 one fits). What idle
senders held goes back to the system once 1,024 have gone, or 10 s after
any went (malloc_trim). HKDF is fetched once, not at every derivation.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A sender allowed is now a peer of 48 bytes (its key, its key's id, its
sources), found through an index by key id; ids are 32 bits, so keys that
share one are each tried. What a sending peer needs besides (the secret
its key shares with the node's, its runs' keys and addresses, its
streams) is made at its first packet and let go once it has been quiet
for the idle period (--link-idle-ms, 30 s at first) with nothing of it
waiting: the next packet makes it again, one X25519, no handshake. The
listener hands over and answers only the streams and runs that changed,
not every one at each turn. --senders FILE allows senders a line each.
The UDP link checks take their sender from such a file among a thousand
others, let it go after 400 ms of quiet, and see an order after a longer
pause arrive all the same.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pure-serve --send OUTPUT=pure://host:port/INPUT sends an output's saved
sends to another node's --link-port with no handshake: the first packet
carries records. Keys come from the two nodes' keys (X25519, HKDF-SHA256,
AES-128-GCM), with a random run in each packet's header so no nonce
repeats across restarts; nothing is stored for them. What lasts is the
apps' own: the sender's outbox and the receiver's heard numbers
(pure_app_heard, new). The sender detects losses as RFC 9002 says, with
NewReno, and trims a send once the receiver says it stored it; the
receiver hands the owner whole records in order, which it applies from
the one after the source's last heard number, stopping at a gap, and
answers how far it stored with every acknowledgment. Stored bytes are
the bytes on the wire: an output and an input of one shape need no JSON.
server_links_udp kills the receiver, the sender, then both (-9) while
four clients buy: every saved order is applied once, with no handshake
(five runs, 10k-11k orders each). server_links_udp_lossy does it again in
a namespace of its own with the link's packets delayed, lost (3 %) and
reordered (10 %) by netem (three runs, 11k-12k orders).
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
GET /views/name?argument=value gives a view that takes arguments its
arguments: the query as it came is kept on the request's work slot (it
outlives the connection, which may go before the view is shown), and when
the view is shown, on the connection thread from a snapshot or by the
owner, its pairs (values %XX decoded) are laid out as a payload of the
arguments' shape and given before the view is evaluated. from and count
still page a table. An argument missing, unknown, given twice, not of its
type or badly escaped answers 400 invalid. A registered query takes its
view's arguments the same way (GET /queries/name?k=4). An arguments input
is not an input to post to.
server_arguments opens 200 accounts, then asks 83 times against a model:
balances by key (404 no_value for a key not there), counts above an
argument, texts with &, =, % and non-ASCII shown back, a table filtered by
two arguments whose column folds with one, paged, counts by an owner's
name, a registered query with an argument, and six refusals. The format's
document (008) describes arguments.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A rows field's JSON value is an array of objects naming the record's
fields (numbers, strings; a field left out takes its default). The
connection thread encodes them as the list's stored form, which may be
longer than the JSON, grows the request's input by that much
(pure_runtime_grow: charged to the payload budget as admission charges an
input, 503 payload_budget when it does not fit) and places them after the
body, so the owner still applies a message it need not parse. A message
whose only variable field is rows takes a body that is not JSON as those
rows in their stored form: a bulk insert without JSON. An API's answer
(--reply) gives rows the same way. A delivered batch holds no rows.
server_rows loads 30,000 stock rows in one JSON request (some 250,000
rows a second, against 6,500 with a request a row) and 20,000 more as
bytes, refuses a load with a key already there whole (409 exists), then
checks 400 random orders against a model (294 applied, 106 refused with
none of their rows applied), texts with escapes and Korean, a walk that
removes, and a restart that keeps every state and no rows.
The format's document (008) describes walks, rows fields and lists of
rows, and OUTER_ROW.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
PUT /queries/NAME takes a module compiled with aup query: the node loads
and verifies it, checks that its states are the app's (names, types,
records) and that it has a view NAME, and keeps it (64 at most; 409 for
another app's module, 400 for bytes that are no module, 404 without the
view). GET /queries/NAME?from=&count= shows that view from the last
save's snapshot with a reader of the query's module, as a declared view
is shown, so a query runs beside writes and sees one save whole. A query
registered again answers anew.
server_queries registers a per-branch total (a correlated fold), a join
of history rows with their accounts and tellers, each rich account's
rank and a filtered count, checks them against a model of the same
transfers, asks while transfers go on, pages, and checks the refusals.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pure-serve --surface LIBRARY [-- ARGUMENTS] maps a library and runs its
pure_surface_run in a thread of its own (serve/surface.h). The surface
reads the app as of the last save where it is, a published snapshot in a
reader app, as a connection thread reads views; an eventfd tells it of
each new snapshot; and its events (a key pressed, text typed) are
requests without a host in the owner's queue, applied and saved with the
round, their results handed back to the surface's waiter. So one node
serves programs over HTTP and people on its display with one owner of
its states, and nothing between the states and the screen. pure-serve
exports its pure_* functions for the surface to call, one copy of the
app model in the process; the table carries sizeof(PureApp) and
sizeof(PureModule) so a surface built for another layout refuses.
The check builds a probe surface that types, presses and reads: its
events show in HTTP views, an HTTP input shows in it, wrong inputs are
refused, it stops with the server, and a module without states cannot
have one. Without a surface the request path measured the same (small
GETs 140.9k vs 139.9k, app totals 126.3k vs 126.3k over eight rounds);
the exported symbols cost 11 KB of the file and 24 KiB idle.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A sealed request cost three X25519 operations (about 0.5 ms), so sealed
sends drained at half the plain rate. A wire now keeps one HPKE context,
encapsulated at its first seal, and seals each request as the context's
next number (a failed seal or a restart starts a new session); the
header carries the session's encapsulated key and the number, so a
receiver opens a session once per connection thread (16 remembered, the
least used replaced) and each seal by its number, also after it restarts,
unasked. A sender with a key of its own seals in HPKE auth mode and names
that key in the header; a receiver started with --sender NAME=KEY takes
deliveries only from the senders it names, each sealed with its key
(403 sender otherwise). The body is now the ciphertext and its tag.
Sealed sends, sender authenticated, drain at 461-464k a second against
462-470k plain and 207-234k sealed a request at a time (Raspberry Pi 5).
The check adds: a wire's requests share one session numbered from 0; a
changed number or sender key does not open; a receiver restarted naming
its sender opens the same session again; another node claiming that
sender's name with its own key is refused and applies nothing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Linking OpenSSL cost every pure-serve 2.3 MiB of resident memory, 0.66
MiB of it the process's own (relocated data), though most servers never
reach an https receiver or seal a send. The code that uses OpenSSL (TLS
to https receivers, sealing) is now built as pure-crypto.so beside
pure-serve and exports one table; pure-serve maps it the first time a
wire to an https URL opens or a key is read or a seal wired
(serve/crypto.c), and a missing library is reported at startup. What
needs no OpenSSL, a key's hex and the suite this CPU seals with, stays in
pure-serve.
An idle server: RSS 4,496 -> 2,320 kB, PSS 1,639 -> 881 kB, private
dirty 1,296 -> 864 kB. A static OpenSSL was measured too and is worse
(RSS 3.8 MiB, but 2.3 MiB private: all of its tables relocated). The
checks, https webhooks and sealed sends included, pass unchanged.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
After answering, a connection went back to recv for its next request and
got EAGAIN nearly every time: a failed syscall a request. A recv that
returns less than it asked for has emptied the socket, and the
edge-triggered epoll reports the next bytes when they come (epoll(7); as
nginx does), so the connection now waits for that; a peer that hung up
is read to its end as before.
Small keep-alive GETs, 64 connections, five rounds in rotation
(Raspberry Pi 5): 131.9k/s before this step, 139.9k after. With the
three steps before it (a burst's keys in one look, the clock once a
round, the head without printf): 116.1k -> 139.9k/s, p99 672 -> 512 us,
above the 127.0k of 2026-09-26's build that the app model had since
slowed. App reads: the total 111.5k -> 125.4k/s, a 20-row page 86.2k ->
94.3k; 1 MiB reads and durable posts unchanged.
|
| |
|
|
|
|
|
|
|
| |
Three snprintf calls a response (the head, its ETag and a delivery's
batch size) spent a sixth of a small read's user time in printf's
format scanning. The head is now written part by part, the same bytes
as before, clipped at the end of its buffer where snprintf's would-be
length could have sent bytes past it.
|
| |
|
|
|
|
|
|
|
|
| |
A connection's deadline was counted from a fresh clock_gettime on every
read and write, three times a request, and every event took the wall
clock again: 5.7% of a small read's user time. A connection thread now
reads the monotonic clock once after each epoll_wait and deadlines count
from that (they are 10 s by default and swept every second), and a
round's events share the time it began.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A message for a document scanned the burst's pending keys with strcmp to
see whether its key was there, and the host scanned the burst's earlier
requests the same way before each one: both quadratic in the burst (64
concurrent reads, about 31 compares each, twice). The app now indexes its
pending documents by key (open addressing over an FNV-1a hash, 1 KiB),
answers pure_app_pending in one look, and the host asks it; whether the
current event's document is pending is a flag instead of a scan by
pointer. Bursts split where they did, except that on an owner, which
applies a round's reads first, an edit of a key read in the same round
now splits the burst at the host rather than the app saving on its own.
A unit test checks the index at 200 keys.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
--seal OUTPUT=KEY seals each request's body whole to the receiving node's
public key with HPKE (RFC 9180, base mode: X25519, HKDF-SHA256, AES-128-GCM
where the CPU has AES, else ChaCha20-Poly1305), bound to the request's
source, first number and count; X-Pure-Seal names the suite. A receiver's
--key is a raw 32-byte file, made 0600 when missing, held in OpenSSL's
secure heap; it opens the body where it lies (400 seal when it does not
open) and zeroes the plain text when the request's ticket goes.
--sealed-only refuses unsealed deliveries (403 unsealed).
The check puts a recording relay between two apps: every order applies
once, no body shows an item or a field name, a flipped bit is refused, a
replayed request applies nothing twice, and a seal to another key does not
open. Its hosts choose their own ports (--port 0). Sealed sends drain at
233-235k a second against 469-470k plain (Raspberry Pi 5): three X25519
operations a request, 164 us each. The plain request path measured the
same as before.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A batch went out as a write for its head, then a bracket or comma and the
send for each of its sends: two writes a send. Now the head and the whole
body are built in one buffer and written once. A receiver stopped while
20,000 sends piled up drained them at 91k sends a second before and at
470k after (three rounds each, Raspberry Pi 5). Writing the head apart
from the body measured the same (468k), so the gain is the body's; one
buffer is kept as the simpler of the two, and a lone send now leaves in
one segment instead of two.
|
|
|
The app model, the tree store and the HTTP host lived in
prototypes/connections among the earlier document runtime, the store was
known only by its format's name, and Vega compiled six Pure sources by path.
Now, as the README's principle has it:
core/ runtime, flow and the app model (module*.c), with what the core
asks of storage (state.h, store.h)
store/ the tree store (PURETRE3, PUREREC1)
serve/ pure-serve: HTTP host, outbound delivery, JSON in place
cli/ pure-run, pure-edit-module
A top-level make builds libpure.a (app model and store, for hosts to link)
and the hosts; make check runs the store's and the app model's tests. The
earlier runtime still builds from prototypes/connections against core/ and
store/ (check, check-tree and check-core pass). Design 009 records the rule:
parts by role behind libraries; git repositories split by coupling, so the
store stays here while its contract moves with the core, and Vega stays apart.
|