| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
After each save the owner walked the whole outbox to find the sends
not yet queued, so a receiver long down made every save walk the
backlog. pure_app_outbox_newest visits saved sends newest first, and
the owner stops once every wired output reaches the number it queued
last, then queues what it found in order.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The final answer to each send of a wired output (a 2xx, or a 4xx but
408 and 429) becomes a message event of the given input: fields from
the answer's JSON object by name, other members skipped; a field the
answer lacks from the send's field of that name and type, so the
request's own id is there; a u32 http_status field is the status. It is
applied with the send's number from the wire's source, before the send
is trimmed and in the same save, so an answer that comes again after a
restart (the API repeating itself for the Idempotency-Key) applies once.
Sends that wait for answers go one per request; answers are saved with
the next round, or at once when none waits.
A server that writes an answer in pieces holds the second until the
first is acknowledged, and a delayed acknowledgement stalled every
answer 40 ms: the client now acknowledges at once (TCP_QUICKACK) after
each read. Against a payments API that answers 200 or 402, with the
sender killed (-9) midway, paid and declined orders matched the API's
answers one to one.
|
| |
|
|
|
|
|
|
|
| |
The in-place JSON reader of pure-serve's request bodies moves into its
own file with a small interface (pure_json_message, pure_json_fixed,
pure_json_space), so the delivery client can read an API's answers
with it. It gains a lenient mode for answers: a member that names no
field is skipped, value and all. Requests read as before.
|
| |
|
|
|
|
|
|
|
|
| |
A wire to an https:// URL speaks TLS 1.2 or later through OpenSSL: the
receiver's certificate must chain to the system's CAs (or to the file
--send-ca names) and name its host (SNI and SSL_set1_host, or the IP
address). An untrusted receiver gets nothing: the send stays in the
outbox, the wire logs it and tries again every 10 s. pure-serve now
links libssl and libcrypto.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pure-serve --send OUTPUT=http://host:port/path wires an output: after
every save the owner queues the new sends as JSON (fields and groups)
and a thread per output posts them in order with X-Pure-Source,
X-Pure-Sequence and Idempotency-Key. A 2xx, or a Pure receiver's
refusal, delivers; the owner then trims. 408, 429, 5xx and no answer
are sent again after a pause growing to 10 s, other answers every 10 s:
nothing is dropped. A Pure receiver applies a delivered message with
pure_app_deliver (now taking placed fields) and advertises
X-Pure-Batch: 256; the sender then posts up to that many numbers in a
row as one JSON array (X-Pure-Count), applied in order in one burst
with one save.
Delivery kept up with orders once batched (391 deliveries/s one at a
time; a backlog of 20,000 went at 60k/s). With four clients ordering,
killing the receiver and then the sender (-9) left the receiver's count
and total equal to what the sender saved, five times and under ASan.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An output (input kind 5) is a message the app sends; SEND (72) takes
an output's field values in order in a handler, lays them out as a
message's payload and keeps it, numbered per output from 1, with the
event's changes: a failed event sent nothing, and a failed save takes
back every send since the last save with its numbers. A host sees only
saved sends (pure_app_outbox) and trims what its receiver has saved
(pure_app_trim, gone with the next save). pure_app_deliver applies a
message from a named source once: a number heard before succeeds
without applying, a refused message counts as heard, a failed save
forgets what it heard.
Sends, numbers and heard numbers are the app's links state, stored
whole in one runtime document (PLK1) with the states, in the same
commit. Outputs follow their names across module changes; a module
that drops or retypes an output with sends left is refused, and a
dropped output's number is kept for when it returns. Killing the
process at every commit stage leaves an event and its send, or a
delivery and its effect, together or neither. pure-serve answers a
post to an output or a timer with 404 no_input.
|
| |
|
|
|
|
|
|
| |
One connection thread for each CPU the process may run on (its
affinity, or the online CPUs), less the one the owner of an app with
states takes: three CPUs give two connection threads and the owner.
The default stays one thread.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
After a round that saved, the owner waited for 100 us without a new
request (1 ms at most) before the next round. Requests that queued up
during the save made that quiet start at once, so the clients just
answered missed the next round; the clients then settled into two
halves taking turns, each save carrying half of them. It showed with
two connection threads: 34 events a round and 15.8-16.0k posts/s with
64 connections, against about 61 and 22-24k/s with one or three. The
quiet now counts only once a request has come after the answers were
handed back: two threads carry 61.6 events a round, 22.9-23.9k/s.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pure-serve no longer copies a JSON body's strings into the message's
payload. The ingress is the prefix, a table of each field's start and
size, and the body as it came; the connection thread scans the body
once, puts numbers into the prefix and records where each string is.
Only a string with escapes is unescaped, over itself (it never grows).
A group of fields comes as an object inside the body and, for a fixed
field, as its path in the query (?to.zip=7). A group where a field
belongs, a value where a group does, an array or true/false/null is
json_type; an unknown member, inside a group too, json_field.
The string scan and UTF-8 checks now pass over eight bytes at a time
while none needs a look (pure_utf8_step, so every text check gains).
On the Pi, posting {"name":"k","text":"x..."} to a stateless app went
from 652-655 to 3,737-3,953 requests/s with 256 KiB of text (server
CPU 1.52 to 0.25 ms a request, peak RSS 5.2 to 3.2-3.4 MiB) and from
29.1k to 76.3-76.7k/s with 4 KiB; the load generator gains a json
method and benchmarks/http/echo.aup is the app.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The owner publishes a snapshot of the saved states after every save,
before it hands back the answers; connection threads take the newest
one each round and show views from it with a reader app of their own.
A read made after a write's answer sees the write, nothing unsaved or
undone is ever shown, and reads no longer wait while the owner applies
events or syncs. Apps with states always get an owner thread, even
with one connection thread. A round's inputs go to the owner first;
its views are shown and then answered together (answering each one as
it was shown made one thread's 20-row page 8% slower).
On the Pi 5, with 8 connections posting durable scores and 56 reading
the total, reads went from 10-16k/s at p99 13-29 ms to 107-149k/s at
p99 0.6-0.7 ms, and posts stayed in range; axum and drogon, which sync
under their lock, read 1.1-1.3k/s at p99 111-156 ms. A snapshot of a
changed 1,024-item list costs 1.2 us (65,536 items: 87 us) against a
3-4 ms save.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
With durable writes going on, a read waited for the save of the burst it
landed in. A round's requests come from different connections, one
request each at a time, so nothing orders them: the owner now applies a
round's reads first and answers them at once, seeing only saved states,
then its writes in bursts. 8 writers and 56 readers of the scoreboard,
owner mode: reads 7,100-7,700 to 16,100-16,600 a second, their p99 55 to
13 ms, writes 1,000-1,100 to 2,300 a second. A single thread keeps the
order: it waits out each save itself, and reads answered first come back
first and split its next writes over more saves (the owner gathers after
a save instead); there reads are 12,800-13,400 a second as before.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A view that shows a text state as it is (pure_module_text_state) now goes
out as the state's stored extents, as a document read does: the answer
holds the store mapping, charged to the budget, until it is sent and its
ticket released; a state with edits not yet stored is copied as before.
Documents and views share one loan path, and the extent array grows as
needed. A 1 MiB note read by 8 connections: 776-817 answers a second
copied, 1,875-1,901 lent, p50 9.2 to 3.5 ms, peak RSS 10.3 to 5.7 MiB.
The load generator learns GET /views/page.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A real app (a board, a stopwatch) already used a quarter of the 512
instructions, since a fn is inlined where it is called, and every text
it shows is one of 64 constants. The code and the constants now live in
one block sized to the module, which load takes and pure_module_release
gives back; the limits rise to 65,535 instructions and 4,096 constants
of up to 1 MiB, while PureModule shrinks from 28,664 to 11,936 bytes.
The verifier's branch marks are sized to the block instead of 64 KiB on
the stack. pure_module_read reads a module file whole, and every host
uses it instead of a 128 KiB buffer.
|
| |
|
|
|
|
|
|
|
|
| |
pure-serve sets the time an event sees to the wall clock when the event
is applied, and the thread that runs an app with states (the connection
thread alone, or the owner) ticks its timers on time: epoll waits no
longer than the next tick, the owner's condition wait is timed to it, and
the ticks due at once share one save, so a restarted server goes on from
the stored ticks. The statistics count ticks.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A message field is now a u32, a number (binary64), bytes or text, and
variable fields may be several and anywhere: the payload's prefix holds
the fixed fields and each variable field's length but the last one's,
which takes the rest; a module whose one bytes field came last is laid
out as before. On arrival a length past the payload, a text that is not
UTF-8 or a number that is not finite fails the message.
pure-serve fills an input's message from a JSON object body
(Content-Type: application/json) on the connection thread before the
request is ready, so the owner never parses and replies are still
borrowed: the ingress is [u32 payload size][payload][the JSON as it
came]. A missing, unknown, doubled or mistyped field, or text that is not
JSON, answers 400 and the request never runs. Numbers may come in the
query too.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
States have one owner, so a module with them ran on a single thread,
which spent most of its time in the kernel sending and receiving. With
--threads N such a module now gets N connection threads and one owner
thread: connection threads accept, parse and answer; the owner takes the
ready requests of every thread as one round, applies them in bursts with
one save each and hands each thread back its applied requests (eventfd),
which it completes and answers. A request's route and payload stay held
by its thread's runtime core until completion, so an accepted event runs
even when its connection closes first.
After a burst that saved, the owner gathers requests until none has come
for 100 µs (1 ms at most), so the requests its answers bring back share
the next save as they would on one thread: 30 events per save became 50
and the median latency of a durable post fell from 5.4 to 3.5 ms. A page
of 20 rows went from 87,600 to 141,900 answers a second on the Pi.
|
| |
|
|
|
|
|
| |
Each column's key is made once per request and a page's buffer is taken
at once, instead of three small appends per cell. A POST with no payload
(a key press) no longer copies nothing to a missing ingress.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pure-serve answers GET /views/NAME with a view's value (a number or text
as text/plain, a table as JSON rows, a page of them with ?from=&count=,
each cell computed only for the rows asked) and POST /inputs/NAME with an
event: a text input's text is the body, a key input is pressed, a message
takes its u32 fields from the query and its bytes field from the body and
answers its handler's reply. A failed event answers with its fault (404
not_found, 409 exists, 422 the others). No routes are written: the app's
views and inputs are its interface. Documents stay served when the module
has show and edit.
The route of a request is kept per work slot, so an accepted event runs
even if its connection closes before the burst; views and app events join
any burst, whose one save comes before its answers.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A read answered with extents of the store mapping was charged nothing,
so clients that stopped reading could keep any number of old file
versions mapped. The runtime core now takes resources a host sends in
pieces: body.data is NULL, body.size is charged like any other resource,
the outcome has no body to read and cannot be forwarded. pure-serve hands
the mapping reference over as such a resource, so a lent answer holds its
bytes of budget until it is in the socket and its ticket releases the
mapping; past the budget a read gets 503 payload_budget, as a copied one
would. The async core grows 52 bytes (5,776 of its 6,144).
|
| |
|
|
|
|
|
|
|
| |
pure-serve renewed a connection's deadline when a request arrived or an
answer finished, not while an answer was being sent, so a client that
read a large answer steadily but slowly was cut off after ten seconds
(a 1 MiB document over a 50 KB/s link never arrived). Every write that
makes progress now renews it; only a stalled connection expires.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A read of a stored document now answers with the store mapping's extents
through writev, without copying the document. pure_draft_mapping takes a
reference to the mapping a draft opened through a handle reads from; the
answer keeps it until the last byte is sent, so the version read goes out
intact even if the document changes or a compaction replaces the file
meanwhile. Replies the mapping does not hold are copied as before.
pure-serve's output is now a list of extents (the head first). One thread
serves 2,761 1 MiB reads per second (axum: 1,380) and 128k small ones.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Threads that commit at the same time now share one commit: they queue,
the first becomes the leader and publishes every queued request of the
same store with one head, and the others wait for its result. Each
request stays all or nothing on its own: one with a stale draft is left
out, and so is one whose key an earlier request of the group changes.
When several threads have been committing together, a leader waits up to
2 ms for as many to join as groups held lately; a single thread never
waits.
A test pauses a publication and checks that five commits make exactly two
publications. Under eight writers on three pure-serve shards, 1,234
bursts went out in 421 publications. pure_store_group_counts reports the
groups, and pure-serve prints them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A burst now looks at the store once (pure_store_pin) instead of once per
request; another shard's commit ends before its answer is sent, so a
request made after that answer still reads it. With this alone one thread
serves 125k small keep-alive reads per second, as many as axum on three
cores, with a shorter p99.
--threads N runs a shard per thread: its own SO_REUSEPORT listener, epoll
set, runtime domain, app and store handle. Writer sessions of the tree
store now take turns inside a process with a mutex (the file lock belongs
to the process and does not keep threads apart), and revisions settle
concurrent edits of a key. A module with states runs on one thread. Three
shards serve 151k reads per second; they still commit one by one, so
durable writes and 1 MiB reads are better on one thread for now, which
stays the default. Note 075 records the numbers.
|
|
|
pure-serve replaces the module mode of server.c, which goes back to what
it was. It keeps the document contract, the runtime core (a work slot per
connection, the payload budget, core-owned ingress, borrowed PUT replies)
and the group commits, and adds what server.c lacked: HTTP/1.1 persistent
connections with in-order pipelining, an edge-triggered epoll owner
thread, up to 1,024 connections allocated as they arrive and reused, one
writev per answer, and documents opened through a store handle.
On the Pi 5 with one thread it serves 106k small keep-alive reads per
second (axum on three cores: 126k) at the same p99, 1,741 1 MiB reads
(axum: 1,370), and 1,126 durable writes from 64 connections (crash-safe
axum: 372), in 7.3 MiB against axum's 17-20 MiB. Note 075 has the table.
|