Reference / The wire protocol, version 1
The wire protocol, version 1
The bytes a TinyStore server and its clients exchange over any byte stream: a child's stdin and stdout, a Unix socket, a Windows named pipe, TCP or TLS. Read it to write a client in a language TinyStore has no SDK for, or to see what an SDK does on the wire; the Bun and Python SDKs are built to this page.
Frames, the profile, the handshake, the errors and every engine's methods, kv,
jobs, blobs, sql, records and metrics, are built in server/wire, and why the
server behaves as it does is
its design.
Every example on this page is a vector of
testdata/wire/vectors.json,
which the server and every SDK are tested against: a value in a typed
notation, the bytes it is, and the bytes a decoder refuses, each named after
the rule it breaks. Every message is also a vector of
messages.json, its bytes and its
fields by the names the tables below give them, with the methods' numbers and
the errors' codes, so that an SDK checks each field of each message in one
loop; the Go types write it, through a schema of each message's keys that
TestMessageVectors holds.
Frames
Everything is little-endian. A frame is a twelve-byte header and a body:
0 4 5 6 8 12
│ body length │ kind │ flags │ method │ stream │ body …
u32 u8 u8 u16 u32- The length is the body's. A frame is 12 + body length bytes; a body past the agreed maximum is refused before a byte of it is read.
- What is reserved is zero. An unknown kind, a flag this kind does not
define, a method outside a
REQUESTor a stream where the kind names none is a protocol error, and the connection ends.
| kind | stream | method | body | flags | |
|---|---|---|---|---|---|
| 1 | HELLO | 0 | 0 | a message | — |
| 2 | WELCOME | 0 | 0 | a message | — |
| 3 | REQUEST | the new stream | the operation | a message | END |
| 4 | RESPONSE | its stream | 0 | a message, or an error | END, ERROR |
| 5 | DATA | its stream | 0 | bytes, or a message, as the method says | END, ERROR |
| 6 | CANCEL | its stream | 0 | empty | — |
| 7 | CREDIT | its stream, or 0 for the connection | 0 | u32: bytes granted | — |
| 8 | PING | 0 | 0 | 8 bytes | — |
| 9 | PONG | 0 | 0 | the PING's 8 bytes | — |
| 10 | GOAWAY | 0 | 0 | a message | — |
END is 1: the sender's last frame on its stream. ERROR is 2, only beside END: the body is an error.
A method's high byte is its engine and its low byte the operation, so a new engine takes a new high byte and the frame stays as it is:
| high byte | engine |
|---|---|
0x01 | kv |
0x02 | jobs |
0x03 | blobs |
0x04 | sql |
0x05 | records |
0x06 | metrics |
A HELLO from Bun:
16 00 00 00 01 00 00 00 00 00 00 00 a body of 22 bytes, HELLO
82 a map of two
01 01 protocol: 1
02 b1 74 69 6e 79 73 74 6f 72 65 2d 62 75 6e 2f 30 2e 31 client: "tinystore-bun/0.1"A connection
- The client sends
HELLO, and the server answersWELCOME, orGOAWAYand closes. Nothing comes before them, and the server waits five seconds forHELLO. The connection speaks the older of the client's newest protocol and the server's, whichWELCOMEnames: a client newer than its server speaks the server's protocol, or closes when it no longer does, and only a client older than the oldest protocol the server speaks is refused. - The client opens streams with
REQUEST, as many at once asWELCOMEallows. - Either side may send
PING; the other answersPONGwith the same eight bytes. - The server ends the connection with
GOAWAY. The client opens no stream after it; aREQUESTthat crossed it on the way is answeredunavailablewithout being run, so it may be sent again elsewhere; the streams already running finish, and the server closes. Stream numbers are reused, so no number could say which requests the server saw: each answer says it.
HELLO:
| key | field | type | |
|---|---|---|---|
| 1 | protocol | uint | the newest the client speaks, 1 |
| 2 | client | str | a name and version, for logs |
| 3 | token | str | required on TCP |
| 4 | max body | uint | the largest body the client takes; the server's when absent |
| 5 | stream credit | uint | the DATA the server may send on a stream before the client grants more; 2 MiB when absent |
| 6 | challenge | bin | 16 random bytes, when the client found the server through SERVE |
A body must fit the credit it is sent under, so the max body both sides agree is the smallest of the server's, the client's and the client's stream credit: a client granting 64 KiB a stream takes bodies of 64 KiB at most.
WELCOME:
| key | field | type | |
|---|---|---|---|
| 1 | protocol | uint | the one this connection speaks |
| 2 | server | str | its version |
| 3 | instance | bin | 16 random bytes a start |
| 4 | capability | str | admin, data or read |
| 5 | max body | uint | the largest body either side sends |
| 6 | in flight | uint | the streams a client may have open at once |
| 7 | connection credit | uint | the bytes of REQUEST and DATA bodies a client may send before credit comes back; at least the max body |
| 8 | stream credit | uint | the bytes of DATA a client may send on a stream before credit comes back |
| 9 | engines | array of str | what this server serves |
| 10 | now | int | the store's clock, unix milliseconds |
| 11 | proof | bin | on a local connection whose HELLO carried a challenge: the HMAC-SHA256 of the challenge, keyed with the 32 bytes of SERVE's secret |
| 12 | durability | str | full or os, when the store was opened with one: how far its files' commits go before they return |
A client that found the server through SERVE sends no REQUEST before the
proof checks, compared in constant time: only the store directory's owner can
read SERVE, so a process that took the endpoint of a server gone cannot
answer, and a client whose proof fails reads SERVE again, as a stale one.
HELLO carries nothing a stranger could use, since a local connection takes
no token. A challenge of another length is a HELLO the server cannot take;
a remote connection's challenge gets no proof, its server being the one its
TLS certificate names. The vectors hold a proof, proofs, and a HELLO with
a challenge.
A server that does not take the connection at all, past its connections or
closing, still reads the HELLO and answers GOAWAY where the WELCOME
belongs, with limit or unavailable.
GOAWAY:
| key | field | type | |
|---|---|---|---|
| 1 | code | str | as an error's |
| 2 | message | str |
Finding a local server
A store directory has at most one server, since its LOCK lets one process
open it. That server publishes how to reach it in <dir>/server/, which only
the directory's owner may enter: mode 0700 and, on Windows, which lets anyone
traverse a directory to a file whose path they know, a protected DACL naming
its owner alone, which every file made in it inherits. SERVE there is a
hint; LOCK is the truth.
{"protocol": 1, "server": "0.4.0", "pid": 4212, "instance": "q1q0l3yZ3JvYw8g7oM2sYA",
"secret": "Zm9vYmFyYmF6cXV4Zm9vYmFyYmF6cXV4Zm9vYmFyYmE",
"endpoints": ["unix:///srv/app/data/server/tinystore.sock"], "sidecar": true}| field | |
|---|---|
| protocol | the newest protocol the server speaks |
| server | its version |
| pid | its process, for a person reading the file; no client trusts it, since a pid is reused |
| instance | 16 random bytes a start, base64url without padding, which WELCOME repeats |
| secret | 32 random bytes a start, base64url without padding, the key of the proof |
| endpoints | where it listens: unix:// and a socket's path, or pipe: and a Windows named pipe's name |
| sidecar | true when the server is a sidecar, which tinystore serve --local starts for its clients. A client of a newer release may replace a sidecar. The field is absent for a Go program's own server and for tinystore serve <dir>, which a person runs |
- Written whole, under the lock. Only the store holding
LOCKwritesserver/. A server removes aSERVEleft behind before it listens, since the endpoint that file names may be another process's by now, writes its own once it listens, as a temporary file renamed into place, and removes it when it leaves, before it lets go ofLOCK. - Find or start. A client reads
SERVE, connects to an endpoint and checks the proof. Any failure, no file, a refused connection or a proof that does not check, starts `tinystore serve --dir--local --log /server/serve.log`. That child takes `LOCK`, which means the old server is gone, and publishes itself; or it finds `LOCK` held and exits with code 3, and the client reads `SERVE` again until the winner answers, five seconds at most, starting one again when none does. - The proof. A client that found its server through
SERVEsends aHELLOwith 16 random bytes as its challenge, and sends noREQUESTuntilWELCOME's proof equals the HMAC-SHA256 of the challenge keyed with the secret, compared in constant time. An endpoint's name is anyone's to take once its server is gone, a Windows pipe's above all, and only the directory's owner can read the secret. A client whose proof fails readsSERVEagain, as a stale one. - A reader delays a change and never fails it. On Windows a client
holding
SERVEopen refuses its rename or removal, and so may a scanner; the server tries again eight times, from 1 to 64 ms apart.
How a client knows its server depends on the transport:
| transport | how the client knows its server |
|---|---|
| stdio | it started the server itself, as a private child |
a Unix socket or a named pipe found through SERVE | the proof |
| TCP with TLS | the certificate, checked before its token leaves |
| TCP | not at all: the token travels in the clear, on a network its operator trusts |
Where a local server listens:
- A Unix socket,
<dir>/server/tinystore.sock, when that absolute path fits asockaddr_un, 104 bytes on macOS and the BSDs and 108 on Linux; past it, the same name in a directory of the owner's own,$XDG_RUNTIME_DIR/tinystore-<hash>, or in the temporary directory without one.<hash>is the first eight bytes of the SHA-256 of the store's absolute path, in hex: eight, so that a socket under macOS's temporary directory still fits. - A named pipe on Windows,
pipe:tinystore-<hash>, created as the first instance of its name with a DACL naming its owner alone, which refuses remote clients. No local endpoint takes a token: its permission is the file system's, and a local connection isadmin.
A remote server's tokens file gives each token one capability: admin
may do anything, data reads and writes, and read reads only. A read
connection may call only the methods the schema marks read, every other
one being permission; its SQL runs on a reader and is a query, a
statement that writes, attaches a file or sets a pragma being permission
too; and its sql.open applies no migration. Its opens may still name a
bucket, a queue or files the store did not have, as any first open does,
and its reads of an idle key renew it, as any read does.
Streams
A client names a stream with any number from 1 not in use. A number is in use
from its REQUEST to the server's final frame on it, and may be named again
after. A REQUEST on a number in use, or past the streams in flight, ends the
connection.
| shape | the client sends | the server sends |
|---|---|---|
| a call | REQUEST·END | RESPONSE·END |
| an upload | REQUEST, DATA…, DATA·END | RESPONSE·END |
| a download | REQUEST·END | RESPONSE, DATA…, DATA·END |
| both ways | REQUEST, DATA…, DATA·END | RESPONSE, DATA…, DATA·END |
- The server ends every stream once:
RESPONSE·END orDATA·END, either with ERROR, a cancelled stream too. It takes the number out of use before that frame leaves, so a client may name it again the moment it arrives. - A download's
RESPONSEis its header, a message of the method's,{}when it has nothing to say; its items follow asDATA, and its lastDATA·END is its trailer, a message saying where the next page begins. DATAcarries bytes or items. A blob's bytes are raw; rows, jobs and series are a message each. The method says which. ADATAcarries at least one byte, but the one that ends its stream.- A transfer shares the connection. A sender splits bytes into frames of 64 KiB at most, so that no stream holds the connection longer than one such frame takes to send.
CANCELasks the server to stop. It answers with the final frame it would have sent, or with the errorcancelled; a client may drop what arrives on a stream it cancelled. A client ends an upload early withCANCEL: ERROR is the server's alone.- A frame for a stream no longer in use is dropped: a
DATAthe client sent before the final frame reached it, aCANCELor aCREDITthat crossed it. The droppedDATAstill gives its connection credit back. - A body that is not its method's message fails its stream with
invalid, naming the rule it broke; a frame that is not a frame ends the connection.
Credit
A sender never sends more than it was granted, and a receiver grants only room it has, so a connection holds no more than its grants and the server's reader never waits.
- Client to server. Every
REQUESTandDATAbody counts against the connection's credit, and aDATAbody against its stream's too. The server gives credit back withCREDITas it lets go of bodies: on stream 0 for the connection, on a stream's number for that stream. - Server to client.
DATAbodies count against the stream creditHELLOstated, and the client grants more withCREDITon the stream as it consumes them. ARESPONSEbody is not counted: it is one message within the agreed body, and every collection travels asDATAinstead, so a client holds at most its streams in flight times the agreed body in answers. Nor is theDATA·END·ERROR that fails a download, so that a stream always has room for its end. A client grants nothing for the connection: itsCREDITnames a stream. - A grant waits for half the window. A receiver grants what it let go of once half its window has gone, and not a frame for every frame it takes.
- A body past its credit ends the connection with
GOAWAYandprotocol, the connection's last frame.
64 KiB of credit on stream 5:
04 00 00 00 07 00 00 00 05 00 00 00 a body of 4 bytes, CREDIT, stream 5
00 00 01 00 65,536Messages: the MessagePack profile
A message is one MessagePack value: a map from small unsigned integer keys to values, under these rules.
-
Types. Nil, bool, integers, float 64, str, bin, array and map. A float 32, an extension type, the timestamp among them, or any other type byte is invalid.
-
Integers. Each field says signed or unsigned and its range. A decoder takes any encoding of a value that fits,
ce 00 00 01 2cfor 300 as well ascd 01 2c, and refuses one that does not. -
A float keeps its value; bits that must survive are bin. A float field is a float 64:
cb 80 00 00 00 00 00 00 00is -0. The server keeps its bits, socb 7f f8 00 00 00 00 00 01, a NaN whose payload is 1, comes back as it went; an SDK keeps the value, NaN, the infinities and -0 included, as far as its language does, and JavaScript promises no NaN's payload. A float whose bits are the data therefore travels as bin: a kv float value, a series' samples, an aggregate's values. A float field also takes an integer that a float 64 holds exactly, since a JavaScript encoder writes 3.0 as 3. -
Text is str, bytes are bin. A str is valid UTF-8, and neither stands for the other.
-
Keys. A message's keys are unsigned integers; a field that is a map of names, an error's
whator a series' labels, says so and has str keys. A key twice is invalid. A key the reader does not know is read past, its value still well formed and within bounds, and then it depends on the reader. A server refuses a request carrying one,unimplementednaming the field, since a field it skipped could change what the client asked, and the answer would be to another question. A client reading an answer, and a server readingHELLO, skip it. So a client leaves out a field at its zero value, and an older server takes everything a newer client sends that it would have sent anyway; and a server adds an answer field only where a client that skips it still reads the answer right, or else answers it only to a client that asked for it in a field of its request. An absent key is the field's default, and nil stands only where a field allows it. -
Bounds come before allocation. A length or a count past the bytes left in the body is invalid, since every element takes at least a byte; nesting deeper than 8 is invalid; a body is at most the agreed maximum.
-
Time is an integer: unix milliseconds, unless a field says nanoseconds. Records keep nanoseconds, past what a JavaScript number holds, so an SDK reads such a field as a
BigInt:1,790,000,000,123,456,789 ns → cf 18 d7 5b 84 2b 4e cd 15A duration is an integer of milliseconds.
-
Columns of numbers are bin: a series' times and values, little-endian int64 and float64 one after another.
-
Encoders are canonical, decoders are not. An encoder writes the shortest form of every integer, length and count, keys in ascending order, every float in eight bytes, so that a vector compares byte for byte; a decoder takes any valid form in any order.
-
An SDK's codec is its own: a general library writes plain maps and reads 64-bit integers without rounding when told, but takes a float 32, an extension, a key twice or a value nested nine deep, which this profile refuses. The SDKs' codecs refuse every
refusedvector.
Errors
An error is the final frame of its stream, with ERROR set:
| key | field | type | |
|---|---|---|---|
| 1 | code | str | below |
| 2 | message | str | what failed, as the engine's error says it |
| 3 | what | a map of names | the item it names: a bucket and key, a queue and key, a series' labels, a table and constraint |
| code | means | sent again |
|---|---|---|
invalid | the request cannot be done as asked | no |
limit | a bound: memory, size or count; what names the limit, what the call wanted of it and the bound, where the engine knows them | later, or smaller |
closed | the store or the handle closed | after opening again |
in_use | a name is taken, or a lock's key stayed held past a hold's wait | later |
conflict | a condition or a version no longer holds | after reading again |
corrupt | stored bytes no longer read | no |
too_old, too_new | a time outside its engine's window | no |
suspended | a series in quarantine | after its repair |
outcome_unknown | a commit whose result is unknown | after reading what it wrote |
committed_locally | a synchronous commit its home made and no copy it waits for held in time: it stands, and losing that home's disk would lose it | no |
lost | a lease ran out or was revoked at its maxHold: another holder may have run beside it | no |
permission | the connection's capability does not allow it, as a read connection opening an encrypted bucket | no |
unimplemented | a method, or a field of a request, this server does not have: what names the field, the message the server's version | no |
cancelled | the client cancelled it | — |
unavailable | the server is closing | on another connection |
internal | a fault of the server's | — |
The first nine are the root's sentinels, so that an SDK's error classes mean
what errors.Is means in Go. A frame or a HELLO the server cannot take is a
GOAWAY with protocol or unauthenticated, and the connection ends. A
name's value in what is str, or bin where its bytes are not UTF-8, as a kv
key of bytes may be.
A conflict on stream 7:
2c 00 00 00 04 03 00 00 07 00 00 00 a body of 44 bytes, RESPONSE, END and ERROR, stream 7
83 a map of three
01 a8 63 6f 6e 66 6c 69 63 74 code: "conflict"
02 ad 73 74 61 74 65 20 63 68 61 6e 67 65 64 message: "state changed"
03 81 a6 62 75 63 6b 65 74 a8 73 65 73 73 69 6f 6e 73 what: {"bucket": "sessions"}Versions
A server and its clients upgrade apart: a remote server deployed once while the applications' SDKs move on, or a sidecar still serving a directory after its application's SDK was updated. So each side promises what it does with an older or a newer one:
- A connection speaks the older protocol of its two sides.
HELLOsays the newest the client speaks andWELCOMEthe one the connection speaks; a server refuses only a client older than the oldest protocol it still speaks. A protocol moves only for what a field cannot carry: frames, the handshake, credit. - Within a protocol a message grows by fields, and a request is understood
whole or refused. A field the server does not know is
unimplemented, naming the field and the server's version, so a newer client using something new learns which server to upgrade rather than reading an answer to another question. A client leaves a field out at its zero value, so one using nothing new talks to any server of its protocol. - An answer grows only by what a client may skip. A field whose meaning a client must know is answered only to a client that asked for it with a field of its request, which an older server refuses.
- A method is added, never repurposed. An older server answers a newer
method
unimplemented, naming its version. Fromv0.1.0on, a message never changes meaning and a field's number is never reused.
A client that finds a sidecar of an older release than its own replaces it:
- The client sends
server.stop. The sidecar answers, lets its running streams finish, and gives backSERVEandLOCK. - The client starts its own binary, as it does when no sidecar runs, and prints one line about it on stderr.
- Every other client of the old sidecar gets
GOAWAYand connects to the new sidecar when it connects again.
A client doesn't replace a server that SERVE doesn't call a sidecar, such as
a Go program's own server or one that a person runs. It also keeps a sidecar
that refuses to stop. In both cases, it prints a warning once on stderr.
Methods
Each engine's messages are fixed on this page with its slice, before its
code: numbers, keys, types, shapes and vectors. A call answers with one
message; whatever returns a collection is a download, its items DATA under
credit and its last DATA saying where the next page begins. What each will
carry:
| engine | operations | shape |
|---|---|---|
| the server | stop | a call |
| kv | open, get, has, set, delete, take, touch, add, max, clear, batch, view, allow, configure | calls |
| scan | a download, an entry a message | |
| watch | a download that does not end: a config's kept fields, again after each change | |
| run | both ways: the answer a key keeps, or the key's run handed over and its answer back | |
| usage, refund | calls on a quota, which allow uses | |
| jobs | open, enqueue of many, update, cancel, get, claim, settle of many, step, keep | calls |
| scan | a download, an entry a message | |
| watch | a download that ends with its job: its entry, again after each change | |
| work | both ways: jobs out, their outcomes back | |
| blobs | open, stat, delete, copy, move, usage, clear | calls |
| scan | a download, an object a message | |
| put | an upload | |
| get, with an offset and a length | a download; a whole read whose bytes do not match its hash ends with ERROR and corrupt instead of END | |
| sql | open, with its migration files; exec; batch | calls |
| query | a download: the columns, then rows a message at a time | |
| records | append of many, drop | calls |
| read, follow | a download, records a message at a time | |
| lines | an upload of another program's output | |
| metrics | ingest, drop | calls |
| read, aggregate | a download, a series a message, or several for a series longer than a body holds | |
| search | open, set, setMany, delete, get, search, status, dropIndex, list | calls |
The values they carry:
- the server's own call carries nothing,
{}each way; - a kv value is nil, an integer or bin, as its row keeps it, a bin's first byte the kind that wrote it (1 text, 2 bytes, 3 JSON, 4 a float's eight bytes), and a version is an opaque bin compared only for equality;
- a job's value is its JSON as str;
- an SQL value is one of SQLite's five: nil, an integer, a float, str or bin;
- a record's attribute keeps its JSON spelling as str;
- a series' samples, and an aggregate's buckets and values, are bin columns.
- a search's document is its JSON as str, and a vector bin, four bytes a little-endian float a dimension.
the server
The server's own methods take the range below the engines', 0x00xx.
| method | request | answer | |
|---|---|---|---|
0x0001 | stop | {} | {}, then the server stops as its closing does: the streams running finish, every connection is told GOAWAY, and a sidecar gives back SERVE and its directory |
0x0002 | clock | a clock: a time to set it to, a while to move it forward by, or neither to read it | a clock: the time it reads once moved |
0x0003 | backup | {1: files} | a download: {}, then the zip of the whole store in DATA, its bytes as they come, the last with END |
A stop is an admin connection's alone, permission to a data connection, and
a server whose program said nothing of stopping refuses it with permission
too: a program serving its own store decides when that store's server stops.
tinystore serve stops at one, as it does at Ctrl+C, which is how
tinystore stop ends a sidecar and how an SDK newer than the sidecar it found
replaces it with its own binary, as Versions says.
Only an admin connection can ask for a backup. The server first opens each
engine whose file is in the directory. It copies each database that no client
has opened, without opening it. Then it sends what the backup package
writes: a copy of every engine's file, and a manifest of their sizes and
checksums, which a restore checks. files, an array of str, names files of
the application's by their paths inside the store's directory, which the zip
holds beside the engines' with the engine host; a path outside the
directory, or an engine's own, is invalid.
A clock is {1: at, 2: advance}. at is a time in unix milliseconds, and
advance is a duration in milliseconds. Only a private server started with
tinystore serve --stdio --clock <time> has a clock that moves, and the store
runs on it instead of the system's clock. An SDK starts such a server when you
open a store with private and clock. Only an admin connection can move the
clock, and only forward. The server answers invalid to a time before the
clock's, a negative duration, both fields at once, or a clock call to a server
that runs on the system's clock.
kv
kv.open answers a handle on a bucket of values, on counters, on a config, on
a limiter, on once's answers or on a quota, and every other call carries it. A handle holds kv.Raw values, so the server reads
what any bucket wrote.
| method | request | answer | |
|---|---|---|---|
0x0101 | open | a bucket | a handle |
0x0102 | get | a call | an entry: found, value, version, expires; on counters the value, 0 when absent |
0x0103 | has | a call | an entry: found |
0x0104 | set | a call with its value | an entry: found, version, expires; with if absent, found is false when a live key was there, and the entry is that key's |
0x0105 | delete | a call | {} |
0x0106 | take | a call | an entry: found, value |
0x0107 | touch | a call with ttl or expire at | an entry: found |
0x0108 | add | a call with n | an entry: the counter's value |
0x0109 | max | a call with n | an entry: the counter's value |
0x010a | clear | a call naming a branch | {} |
0x010b | batch | calls, in one transaction | results; a call that fails fails them all, and what names it as call |
0x010c | view | calls, get and has, from one snapshot | results |
0x010d | scan | a call naming a branch, after and limit | a download: {}, an entry a DATA with its key, a page |
0x010e | allow | a call on a limiter or a quota: a key, and n requests or uses, 1 when absent | an allowance |
0x010f | configure | a config's fields to keep and paths to forget | {} |
0x0110 | watch | a call on a config | a download that does not end: {}, then the kept fields a DATA, now and after each change |
0x0111 | run | a call on once's answers, whose REQUEST leaves the client's side open | the answer kept, found, which ends the stream; or not found, the run handed over: the client's last DATA is the entry to keep, and the server's, {}, follows once it is kept |
0x0112 | usage | a call on a quota: a key | an allowance, nothing used: ok says one more use would pass |
0x0113 | refund | a call on a quota: a key, and n uses, 1 when absent | {} |
A bucket:
| key | field | type | |
|---|---|---|---|
| 1 | name | str | [a-z0-9][a-z0-9_-]{0,63} |
| 2 | counters | bool | counters rather than values |
| 3 | default ttl | uint | milliseconds |
| 4 | sliding | uint | milliseconds; values alone |
| 5 | lose at most | uint | milliseconds; counters alone |
| 6 | config | bool | a config rather than values |
| 7 | rate | uint | a limiter's requests every per; a bucket with a rate is a limiter |
| 8 | per | uint | milliseconds |
| 9 | burst | uint | the requests a limiter lets through at once; rate when absent |
| 10 | once | bool | the answers kv.run keeps, a day unless default ttl says; get and delete read and forget one |
| 11 | windows | array of windows | a quota's, one to eight: a bucket with windows is a quota, which delete also takes |
| 12 | in | str | a SQL database's name, already opened; omitted for kv.db |
A window is {1: name, 2: limit, 3: per}: a key may use up to limit every per
milliseconds from its first use, the name [a-z][a-z0-9_]{0,31}.
A handle is {1: uint}. A call:
| key | field | type | |
|---|---|---|---|
| 1 | handle | uint | |
| 2 | owners | array of keys | the branch below the bucket's root; absent for the root |
| 3 | key | a key | |
| 4 | value | nil, int or bin | nil when absent |
| 5 | ttl | uint | milliseconds |
| 6 | expire at | int | unix milliseconds |
| 7 | if version | bin | a version that an entry carried. A get or a has with it fails conflict if the key no longer has that version |
| 8 | if absent | bool | A get or a has with it fails conflict if the key holds a value |
| 9 | n | int | what add adds, and max compares |
| 10 | after | a key | the key a scan's page begins after |
| 11 | limit | uint | the keys a page returns: 100 when absent, 1000 at most |
A key is text: a str, a bin, or an integer, which is its decimal spelling, so
that 42 and "42" name one key. The server writes a key as str, or as bin
when it is not UTF-8. An entry:
| key | field | type | |
|---|---|---|---|
| 1 | found | bool | |
| 2 | value | nil, int or bin | |
| 3 | version | bin | |
| 4 | expires | int | unix milliseconds; absent for a key that never expires |
| 5 | key | a key | a scan's item alone |
A limiter answers kv.allow with an allowance, {1: ok, 2: left, 3: retry after}: whether the requests pass, how many more would pass now, and how many
milliseconds until they would, rounded up, when they do not. More requests
than the burst at once are invalid. A quota's allowance adds 4: windows,
each {1: name, 2: used, 3: limit, 4: left, 5: reset at} in the order its
open gave them, reset at in unix milliseconds and absent for a window not
started; its kv.allow counts in every window or in none, and more uses than
a window's limit are invalid.
kv.configure is {1: handle, 2: set, 3: reset}, set an array of str, a path
and its JSON each, and reset the paths to forget; the server keeps both in one
transaction, checking only that a value is JSON within the config's bounds,
since the types are the client's. kv.watch answers {}, then {1: changes, 2: fields} a DATA, fields a path and its JSON each, now and after each
change, whoever made it: a watcher behind is sent the latest fields, never a
state the config did not have. It ends when the client cancels it, when the
client's side of the connection ends, or with the server.
kv.run lets one run of a key go at a time, every client's: another
kv.run of the key waits until that run ends, then is answered what it kept,
or is handed the run when it kept nothing. The client that is handed a run,
{} as its RESPONSE, runs its function and sends an entry as its last
DATA: found with the value to keep, or not found when the function failed,
which keeps nothing. A client that cancels or leaves keeps nothing either,
and the next run of the key is handed over again.
A page, a scan's trailer, is {1: more, 2: after}: more says the limit or
the page's 4 MiB of values ended it before the branch did, and after is the
key the next page begins after. Calls are {1: [call…]}, each call's map
holding its method under key 0, and results {1: [entry…]}.
kv.get of token under the owner 7, on handle 1, and its answer:
0e 00 00 00 03 01 02 01 01 00 00 00 a body of 14 bytes, REQUEST, END, 0x0102, stream 1
83 a map of three
01 01 handle: 1
02 91 a1 37 owners: ["7"]
03 a5 74 6f 6b 65 6e key: "token"
19 00 00 00 04 01 00 00 01 00 00 00 a body of 25 bytes, RESPONSE, END, stream 1
84 a map of four
01 c3 found: true
02 c4 05 68 65 6c 6c 6f value: bin "hello"
03 c4 01 31 version: bin "1"
04 cf 00 00 01 a0 c4 50 6c 00 expires: 1,790,000,000,000jobs
jobs.open answers a handle on a queue of JSON values, or on a schedule,
the queue of one repeating job under its name; every other call carries it.
A job's value is its JSON as str, checked before it is kept; a schedule's is
{}.
| method | request | answer | |
|---|---|---|---|
0x0201 | open | a queue | a handle |
0x0202 | enqueue | a batch: jobs one transaction adds, all or none | {}; what names a refused job as call |
0x0203 | update | a change: a job by its key, a new value, time or repeat | {} |
0x0204 | cancel | a key | an entry: found says there was a job, which a running one's handler is told |
0x0205 | get | a key | an entry |
0x0206 | claim | a lease | a held job, found false when none was due |
0x0207 | settle | outcomes of claimed jobs, written in one group | settled: nil or an error each |
0x0208 | scan | a query | a download: {}, an entry a DATA, a page |
0x0209 | work | workers | both ways: {}, then held jobs out and outcomes back, DATA·END each side |
0x020a | watch | a key | a download: {}, then an entry a DATA, now and after each change, until the job ends |
0x020b | step | an answer's job and name | kept: the answer the job's run kept under the name, found false for none |
0x020c | keep | an answer | {} once the answer is written, the attempt holding the job's lease |
A queue:
| key | field | type | |
|---|---|---|---|
| 1 | name | str | [a-z0-9][a-z0-9_-]{0,63} |
| 2 | lease | uint | milliseconds; 30 seconds when absent |
| 3 | max attempts | uint | 10 when absent |
| 4 | backoff first | uint | milliseconds, the first wait after a failure; a second when absent |
| 5 | backoff most | uint | milliseconds, the longest; an hour when absent |
| 6 | max waiting | uint | ten million when absent |
| 7 | keep failed | uint | milliseconds; seven days when absent |
| 8 | keep done | uint | milliseconds; absent forgets a key when its job is done |
| 9 | schedule | a repeat | a schedule rather than a queue |
| 10 | max running | uint | the jobs that may run at once, across every worker of the store; absent bounds none |
| 11 | in | str | a database's name. The queue lives in that database's file instead of jobs.db, and the database's batches can enqueue on it. A client opens the database first, on any connection. Absent means jobs.db |
| 12 | max running in group | uint | the jobs of one group that may run at once, across every worker of the store; absent bounds none |
| 13 | rate | uint | the jobs that may start in any span of per; absent bounds none |
| 14 | per | uint | milliseconds, the rate's span, at least one |
A repeat is {1: cron, 2: zone}, five cron fields and the zone's name, or
{3: every, 4: spread}, milliseconds, at least a second; spread true runs
each key's job at a phase of its own within the interval, which the job's
repeat text then shows, @every 30s +6178ms. A job, a batch's item:
| key | field | type | |
|---|---|---|---|
| 1 | value | str | its JSON |
| 2 | key | str | 1 to 1024 bytes |
| 3 | at | int | unix milliseconds; a time past runs now |
| 4 | after | uint | milliseconds from now |
| 5 | repeat | a repeat | needs a key |
| 7 | move | bool | sets the time of its key's job either way: a waiting one moves, a running one runs again then, and a key without a job gets one; needs a key |
| 8 | group | str | 1 to 1024 bytes: the group whose running jobs max running in group bounds |
A batch is {1: handle, 2: [job…]}; a change is a job's fields with the
handle under key 6; a key is {1: handle, 2: key}. An entry:
| key | field | type | |
|---|---|---|---|
| 1 | found | bool | |
| 2 | key | str | |
| 3 | value | str | |
| 4 | at | int | unix milliseconds: the time it runs for |
| 5 | attempt | uint | its attempts, a running one included |
| 6 | state | uint | 1 waiting, 2 running, 3 failed, 4 done while keep done keeps its key, 5 cancelled, which only a watch sends |
| 7 | err | str | its last failure |
| 8 | repeat | str | a repeating job's cron text and zone, as jobs.db keeps it |
| 9 | ahead | uint | the jobs that run before a waiting one, up to 10,000; get and watch alone |
| 10 | progress | str | what a running job's worker last reported, as JSON |
| 11 | ran | int | unix milliseconds: when the last run a worker finished began |
| 12 | took | uint | milliseconds: how long that run took |
A job a work loop claimed ahead for a busy worker is waiting, 0 ahead, until
a worker has it. A lease is {1: handle, 2: lease}, milliseconds, the
queue's when absent. A held job is {1: found, 2: job, 3: key, 4: value, 5: at, 6: attempt, 7: cancelled}, job being the number its outcome names: a
claim's lives on its connection until it is settled, and a work stream's on
its stream. An outcome:
| key | field | type | |
|---|---|---|---|
| 1 | job | uint | |
| 2 | how | uint | 1 ack, 2 retry, 3 fail for good, 4 snooze, 5 extend, 6 progress |
| 3 | err | str | why a retry or a failure |
| 4 | at | int | unix milliseconds: when a retry or a snooze runs again |
| 5 | after | uint | milliseconds: the same from now, or how long an extend holds; written when given, 0 too, which runs a retry or a snooze now where one without a time waits its backoff |
| 6 | progress | str | a progress's report, any JSON, which get and watch show until the job is settled |
A progress settles nothing: the job stays in its worker's hands, claimed or on
a work stream, and a report that is not JSON is invalid. The server keeps
the last one in memory, 4 KiB of JSON at most, a larger one dropped and
logged, so the SDKs refuse one past it before it leaves.
Outcomes are {1: [outcome…]} and settled {1: [nil or error…]}; settling a
claimed job a cancel took is conflict. A query is {1: handle, 2: prefix, 3: state, 4: after, 5: limit}, state 1, 2 or 3, and a page {1: more, 2: after}, as jobs.Query and jobs.Page are.
Workers are {1: handle, 2: workers, 3: timeout, 4: until idle, 5: cancels}:
the queue's own Work loop runs for the stream, workers at once, 1 when
absent and at most the streams in flight, each job timeout milliseconds in
the client's hands, a minute when absent, and with until idle only until no
job is due and none runs, as a test wants it. Each job it hands a handler goes
out as a held job; the client settles it by sending its outcome as DATA on
the same stream, and ends its side with DATA·END once it takes no more. An
extend is not one of those outcomes: the loop extends its leases itself, and
ends the stream invalid on one rather than take it as an ack. A progress is,
and leaves the job in the client's hands. When that side ends, or the client
cancels or leaves, the jobs in its hands fail that attempt, as a worker whose
process died fails it, and only after their handlers return does the loop
stop, so that the jobs it claimed ahead and never handed over go back
uncounted. The server's DATA·END, {}, follows.
A cancel that takes a job in the client's hands sends it again as {2: job, 7: true} on a stream that asked with cancels, so that its handler stops; a
stream that did not ask is told nothing. Either way the outcome the client
sends for that job settles nothing, and the stream goes on.
A held job's number is its connection's, the same for a claim's and a work
stream's, so that jobs.step and jobs.keep name either; a work stream's job
is settled on its stream alone, and jobs.settle of one is invalid. An
answer names a step of the held job's run:
| key | field | type | |
|---|---|---|---|
| 1 | job | uint | the held job's number |
| 2 | name | str | the step's, 1 to 256 bytes, within the run |
| 3 | answer | str | jobs.keep's: the step's answer as JSON, at most 1 MiB; jobs.step carries none |
and kept is {1: found, 2: answer}. A step kept by an attempt whose lease
another claim took is conflict, and nothing is kept; a run that ends takes
its steps along, and a repeating job's next run starts without them. A worker
asks jobs.step before it runs a step and sends jobs.keep once it has, so
that the attempt after a retry or a lost worker gets the answer back without
running the step again.
jobs.watch answers {}, then the job under the key as an entry a DATA,
now and each time its state, place, attempt, time, progress or error changes,
and ends after the entry in which the job ended: done, failed or cancelled. A
key that names no job ends it at once. A watcher behind a busy queue is sent
the latest entry, read at most five times a second, never every change. It
follows the job it found, so a later job under the key is another watch's,
and it also ends when the client cancels it, when the client's side of the
connection ends, or with the server.
blobs
blobs.open answers a handle on a bucket; every other call carries it, the
owners a call names being the folder under the bucket's root it works in.
| method | request | answer | |
|---|---|---|---|
0x0301 | open | a bucket | a handle |
0x0302 | stat | a call | an object, found false for a key that holds none |
0x0303 | delete | a call, if match its one option | {} |
0x0304 | copy | a call from key to to, with the destination's options | the object written |
0x0305 | move | the same | the object written |
0x0306 | usage | a call naming a folder | a total |
0x0307 | clear | a call naming a folder | {} |
0x0308 | scan | a call naming a folder, prefix, after and limit | a download: {}, an object a DATA, a page |
0x0309 | put | a call with the object's options | an upload: its bytes as DATA, 64 KiB at most each; the object committed |
0x030a | get | a call, with an offset and a length for a range | a download: the object, then its bytes as DATA; or the object alone, found false, with END |
A bucket is {1: name, 2: default ttl, 3: max size}, milliseconds and bytes.
A call:
| key | field | type | |
|---|---|---|---|
| 1 | handle | uint | |
| 2 | owners | array of str or int | the folder's segments |
| 3 | key | str | a path |
| 4 | to | str | copy's and move's destination |
| 5 | content type | str | absent keeps what a copy's source has |
| 6 | meta | a map of names | absent keeps it; present replaces all of it |
| 7 | ttl | uint | milliseconds |
| 8 | expire at | int | unix milliseconds |
| 9 | size | uint | a put's length: a stream that disagrees leaves nothing |
| 10 | if match | str | as an HTTP If-Match header spells it |
| 11 | if none match | bool | writes only where no live object is |
| 12, 13, 14 | prefix, after, limit | str, str, uint | a scan's |
| 15, 16 | offset, length | uint, uint | a get's range; a length absent reads to the end |
A method refuses an option it does not take, as the engine's call does. An object:
| key | field | type | |
|---|---|---|---|
| 1 | found | bool | absent, and nothing else set, for a key that holds no live object |
| 2 | key | str | its path under the handle's folder |
| 3 | size | uint | |
| 4 | etag | str | quoted, as a header carries it |
| 5 | content type | str | |
| 6 | modified | int | unix milliseconds |
| 7 | expires | int | unix milliseconds; absent for an object that does not expire |
| 8 | meta | a map of names |
A total is {1: objects, 2: bytes} and a page {1: more, 2: after}. A put
that does not commit leaves nothing: a client that cancels it, a stream
shorter or longer than its size, and a connection lost halfway. A get read
whole is checked against the object's hash, and one whose bytes changed ends
with DATA·END·ERROR and corrupt instead of its last bytes; a range is not
checked.
sql
sql.open answers a handle on a database by its name, and every other call
carries it. The first open of a name in the server opens it: an admin
connection's applies the migrations it carries, and a data connection's finds
them applied, or is refused with permission and makes no file. A database
opens once a store, so every later open, and an open of a database the
embedding program passed the server, checks the migrations it carries against
those the file applied: one the file has not applied is permission for a
data connection and in_use for an admin one, since a database applies its
migrations as it opens, and one changed after it was applied is invalid. A
first open without migrations is invalid; a later one may carry none.
| method | request | answer | |
|---|---|---|---|
0x0401 | open | a database | a handle |
0x0402 | exec | a statement | done: the rows it changed and the rowid of the last it inserted |
0x0403 | query | a statement | a download: the columns, a row a DATA, then {} |
0x0404 | batch | statements one transaction runs, all or none, or with read, reads from one snapshot | results; a statement that fails fails them all, and what names it as call |
A database:
| key | field | type | |
|---|---|---|---|
| 1 | name | str | [a-z0-9][a-z0-9_-]{0,63}, its file sql/<name>.db |
| 2 | migrations | array of files | each {1: name, 2: text}: a .sql file's name, without a directory, and its text |
A statement:
| key | field | type | |
|---|---|---|---|
| 1 | handle | uint | absent in a batch, whose handle is its own |
| 2 | sql | str | |
| 3 | args | array of values | positional |
| 4 | named | a map of names | each name without the :, @ or $ the statement spells it with |
| 5 | write | bool | a query's: run on the writer, for a returning clause |
| 6 | rows | bool | a batch statement's: answer the rows it returns rather than what it changed |
An argument is nil, an integer, a float, str, bin, or a bool, which SQLite
keeps as 1 or 0; a NaN is invalid, since SQLite would keep NULL. A value
that comes back is one of SQLite's five, nil, an integer, a float, str or bin,
bin also carrying TEXT that is not UTF-8, and it is what the row keeps: a time
held as text travels as its text, whatever its column declares.
Done is {1: changes, 2: last id}, the columns {1: [name…]} and a row
{1: [value…]}. Statements are {1: handle, 2: [statement…], 3: read, 4: [jobs…], 5: [kv…]} and their results {1: [result…]}, a result being done, or {3: columns, 4: [[value…]…]} for a statement that returned rows.
Each item of jobs has the shape of an enqueue's request, {1: handle, 2: [job…]}, and its queue must be opened in this database. The transaction
writes the jobs after the statements, so a job commits with the rows it is
about, or not at all. A job has no result. When a job fails, what names it as
call, counted after the statements. A job in a view, or on a queue that lives
in another file, is invalid.
Each item of kv is {1: handle, 2: [call…]}, with the calls of
kv.batch: each has its method under key 0 and the fields of a KV call.
Its outer handle identifies the bucket. The calls may set, delete or clear
keys in a bucket opened in this database, and run after the statements and
jobs. A key change has no result. A read, a set-if-absent, a change in a view,
or a bucket in another file is invalid. A failure's what.call counts
key changes after the statements and jobs.
A message past the agreed body is limit: a row a query downloads, or a
batch's results, which travel in one message. A query holds its rows in the
server's memory, 64 MiB of them at most, before the first leaves.
A data connection's statement, each of a batch's included, runs only once the
check server.md describes lets it, and
one it refuses is permission; it runs 30 seconds at most, past which it is
limit. An admin connection's runs as it is.
sql.exec of an insert on handle 1, and its answer:
31 00 00 00 03 01 02 04 03 00 00 00 a body of 49 bytes, REQUEST, END, 0x0402, stream 3
83 a map of three
01 01 handle: 1
02 d9 24 69 6e 73 65 72 74 20 69 6e 74 6f 20 6e 6f 74 65 73 20 28 74 sql: "insert into notes (title) values (?)"
69 74 6c 65 29 20 76 61 6c 75 65 73 20 28 3f 29
03 91 a4 6d 69 6c 6b args: ["milk"]
05 00 00 00 04 01 00 00 03 00 00 00 a body of 5 bytes, RESPONSE, END, stream 3
82 a map of two
01 01 changes: 1
02 07 last id: 7records
Records are one log of the store's, and each call names the streams it is about, so none opens a handle.
| method | request | answer | |
|---|---|---|---|
0x0501 | append | records, one transaction writes all or none | {}; what names a refused record as call, with its stream and name |
0x0502 | read | a query | a download: {}, a record a DATA, then a page |
0x0503 | follow | a cursor | a download: {}, a record a DATA, then the cursor after them |
0x0504 | lines | a stream | an upload: another program's output as DATA, cut anywhere; {} |
0x0505 | damaged | {} | the damage: {1: [damage…]} |
0x0506 | drop | a damage | {}; an admin connection's alone, since it is a repair |
A record:
| key | field | type | |
|---|---|---|---|
| 1 | at | int | unix nanoseconds, past what a JavaScript number holds |
| 2 | stream | str | a namespace the application names |
| 3 | name | str | the event: log for a log line |
| 4 | level | int | slog's: -4 debug, 0 info, 4 warn, 8 error; absent for none |
| 5 | body | str | absent for none, which an empty body is not |
| 6 | trace id | bin | 16 bytes; absent for none |
| 7 | span id | bin | 8 bytes; absent for none |
| 8 | context | array | who produced it: a key, then its value's JSON, and so on |
| 9 | attrs | array | what happened, as context |
["user", "42", "tags", "[\"a\"]"] is two fields, user and tags, whose
values are JSON spelled as they were given, 1.2300, -0 and a big integer
coming back byte for byte; keys keep their order and may repeat. A text,
a stream, a name, a body, a key or a value, whose bytes are not UTF-8, as
another program's output may be, travels as bin and comes back byte for byte.
A query:
| key | field | type | |
|---|---|---|---|
| 1, 2 | from, to | int | unix nanoseconds, to excluded; absent for an open end |
| 3 | streams | array of str | absent or empty for every stream |
| 4 | names | array of str | absent or empty for every name |
| 5 | min level | int | a record without a level does not match |
| 6 | trace id | bin | 16 bytes |
| 7, 8 | attrs, context | array | fields as a record's, each one a record must hold |
| 9 | newest | bool | newest first; oldest first when absent |
| 10 | limit | uint | the records a page holds: 1000 when absent, at most 10000 |
| 11 | budget blocks | uint | the blocks one read may open; each budget narrows the server's |
| 12 | budget bytes | uint | the bytes it may fetch |
| 13 | budget records | uint | the records it may decode |
| 14 | search | str | text a record's body or name holds, its case ignored; 1 KiB at most |
A page, a read's trailer, is {1: more, 2: from, 3: to}: more says the limit
or the budget ended the page before the range did, and from and to are the
range the next page reads, the query's own with one end moved past this page.
A page never splits a timestamp; more records at one time than a page holds
is limit.
A cursor is {1: segment, 2: row, 3: limit, 4: expired}: follow's request
names the place the records it asks for begin at, {} being the oldest
segment kept, and the records it takes at most; its trailer the place the
next follow begins at, and the segments retention or a drop removed before
the cursor reached them. A record reaches follow once its head is sealed, a
segment's worth or the server's seal age, an hour unless it says otherwise,
after it arrived; read sees it at once.
records.lines is {1: stream}, then the program's output in DATA frames:
each line becomes a record of the stream as Go's Lines makes it, a stack
trace's lines joined, a JSON or logfmt line's fields kept, its level found
where its program writes it. The upload's end hands over the record the
writer holds, a line cut short included, and its answer follows; a line
longer than a record holds is dropped and counted in the engine's stats.
A damage is {1: stream, 2: segment, 3: head row, 4: from, 5: to, 6: reason}: a sealed segment, dropped whole, or a head row, the other absent;
the times it held, unix nanoseconds; the invariant its bytes broke. A read or
a follow that meets one ends with corrupt, its what naming it by the same
names, segment or head row, stream, from, to and reason, and a
drop of one that still reads is conflict.
records.append of one click on stream 5:
2a 00 00 00 03 01 01 05 05 00 00 00 a body of 42 bytes, REQUEST, END, 0x0501, stream 5
81 a map of one
01 91 records: an array of one
84 a map of four
01 cf 18 d7 5b 84 2b 4e cd 15 at: 1,790,000,000,123,456,789 ns
02 a3 77 65 62 stream: "web"
03 a5 63 6c 69 63 6b name: "click"
09 92 a7 65 6c 65 6d 65 6e 74 a5 22 62 75 79 22 attrs: ["element", "\"buy\""]metrics
Metrics are one store of series, each named by its labels, so none opens a handle.
| method | request | answer | |
|---|---|---|---|
0x0601 | ingest | series and their samples, stored all or none | {}; what is a refused series' labels |
0x0602 | read | a range | a download: {}, a series a DATA, then {} |
0x0603 | aggregate | a range with a width and an operation | a download: {}, a series' buckets a DATA, then {} |
0x0604 | drop | labels | {1: found, 2: unreadable groups} |
0x0605 | explain | a range; with an operation, the aggregate's | a plan |
0x0606 | latest | a range | a download: {}, a series' newest sample a DATA, then {}; a series without one is left out |
0x0607 | describe | a description | {} |
0x0608 | described | a description of a name alone | the name's description, its unit and help empty when it has none |
A description is what a metric's name means. It belongs to the name, and every series of the name shares it:
| key | field | type | |
|---|---|---|---|
| 1 | name | str | the metric's name, its __name__ |
| 2 | unit | str | such as ms, bytes or %, 32 bytes at most; absent when none |
| 3 | help | str | a line of help, 1024 bytes at most; absent when none |
A describe with neither a unit nor help removes the name's description.
A plan is what a read, or an aggregate when the range names an operation, would spend, found in one snapshot from the series, their block directories and their heads, with no payload fetched and no sample decoded:
| key | field | type | |
|---|---|---|---|
| 1 | series | uint | the series it matches |
| 2 | blocks | uint | the blocks it opens |
| 3 | summarized | uint | the blocks an aggregate answers from their summaries |
| 4 | bytes | uint | the bytes it fetches |
| 5 | decoded | uint | the samples it decodes |
| 6 to 10 | limit series, blocks, bytes, decoded, answered | uint | the limits it runs under |
| 11 | stops | an error | the limit it would end with, what naming it; absent when it fits |
A series:
| key | field | type | |
|---|---|---|---|
| 1 | labels | a map of names | the series' name as __name__ among them, required on ingest; names unique, every text UTF-8 |
| 2 | kind | str | gauge or counter |
| 3 | times | bin | unix milliseconds, a little-endian int64 each |
| 4 | values | bin | a little-endian float64 each, its bits the data: -0 and a NaN's payload come back as they went |
The wire carries a series' name as the store keeps it, its label __name__;
the Go API and every SDK give it apart from the labels, as name, and refuse a
label of the application's beginning with __.
The two columns hold as many values each. A series longer than half a body
holds comes in several DATA, one after another, each with its labels and
kind, its samples going on in time order; a client joins them. Ingest keeps
the value given last for a time repeated.
A range:
| key | field | type | |
|---|---|---|---|
| 1 | matchers | a map of names | the labels a series has, exactly, its name as __name__ |
| 2, 3 | from, to | int | unix milliseconds, to excluded; both required, 2^63−1 the open end |
| 4 | limit series | uint | the series it matches; each limit narrows the server's |
| 5 | limit blocks | uint | the blocks it decodes |
| 6 | limit bytes | uint | the bytes it fetches |
| 7 | limit decoded | uint | the samples it decodes |
| 8 | limit answered | uint | the samples or buckets it answers |
| 9 | width | uint | aggregate's: milliseconds, the buckets starting at from |
| 10 | op | str | aggregate's: count, sum, min, max, avg, first, last, increase or rate, a counter's alone, or delta, a gauge's |
| 11 | where | array of conditions | labels beyond equality, each label once, in the byte order of its name |
| 12, 13 | by, without | array of str | aggregate's: the labels a group keeps, or all but these; an empty by is sent and joins every series of a name |
| 14 | lookback | uint | aggregate's: milliseconds before from where an increase, a rate or a delta looks for its first step; the width when absent |
A condition:
| key | field | type | |
|---|---|---|---|
| 1 | label | str | the label's name |
| 2 | kind | str | one_of its values, none_of them, which a series without the label is too, or prefix, its one value |
| 3 | values | array of str | one at least, a thousand at most |
A range finds its series by a matcher, a one_of or a prefix: a range of
none_of alone is invalid, since it would scan every series. A condition of a
kind the server does not have is unimplemented, what naming it, as is an
operation, what naming it as op.
The server reads a range whole, within its limits, before the first series leaves, so that a slow client holds none of the engine's readers; a read or an aggregate that fails sends no series.
An aggregate's item is a series, keys 1 and 2, and its buckets as columns, as many values each:
| key | field | type | |
|---|---|---|---|
| 3 | from | bin | each bucket's start, unix milliseconds, a little-endian int64 each |
| 4 | to | bin | its end, excluded |
| 5 | count | bin | the samples it counted, an int64 each |
| 6 | resets | bin | the resets among them, an int64 each |
| 7 | values | bin | its value, a float64 computed exactly and rounded once |
| 8 | flags | bin | a byte each: 1 when the value overflowed to an infinity, 2 when retention cut the bucket, which counted only its samples from the cutoff on, or not the step into its first, 4 when its first step started from a sample before the range |
An increase, a rate and a delta count each step between samples in the bucket where it ends, the first bucket's from the last sample up to the lookback before from, so the buckets add up to the range; only a bucket holding samples is answered.
Drop removes one series and everything it holds, whether it still reads or not, the labels naming it exactly; unreadable groups counts the groups removed without the payload rows their directory no longer names.
metrics.ingest of one sample of cpu{host="web-1"} on stream 9:
3b 00 00 00 03 01 01 06 09 00 00 00 a body of 59 bytes, REQUEST, END, 0x0601, stream 9
81 a map of one
01 91 series: an array of one
84 a map of four
01 82 labels: a map of two names
a8 5f 5f 6e 61 6d 65 5f 5f a3 63 70 75 __name__: "cpu"
a4 68 6f 73 74 a5 77 65 62 2d 31 host: "web-1"
02 a5 67 61 75 67 65 kind: "gauge"
03 c4 08 00 6c 50 c4 a0 01 00 00 times: [1,790,000,000,000]
04 c4 08 00 00 00 00 00 00 e0 3f values: [0.5]search
An index opens a handle, as a bucket does; its documents are JSON as str, and
a vector is bin, its floats little-endian, four bytes a dimension.
protocol/search.wire holds every message.
| method | request | answer | |
|---|---|---|---|
0x0b01 | open | a name and the index's settings | a handle; a read connection's opens an index only as it is |
0x0b02 | set | a handle, an id, a document, a vector or none | {} |
0x0b03 | setMany | a handle and documents, all set or none | {}; what names a refused one's place and id |
0x0b04 | delete | a handle and an id | {1: deleted} |
0x0b05 | get | a handle and an id | {1: found, 2: the document} |
0x0b06 | search | a handle, text, a vector, a where, a page | {1: hits, 2: corrected}, a hit {1: id, 2: document, 3: score} |
0x0b07 | status | a handle | {1: ready, rebuilding or failed, 2: done, 3: error} |
0x0b08 | dropIndex | a name | {1: dropped} |
0x0b09 | list | {} | the indexes: name, documents, vectors, opened, status |
A set on an index with vectors gives vector, or noVector: true for a
document only its words find; one that says neither is invalid.
A table's index opens with database, a handle sql.open gave, and
table, with key and vectorColumn where it needs them: its fields and
filters are columns of the table. Its search answers columns once and
each hit's row, a cell a column, as sql.Rows carries rows, and value
null. A set, setMany, delete or get on it is invalid, since the
table owns the rows, and dropIndex names the database beside the name.
search's where
A search's where is a JSON list of conditions, every one of which a
document passes, as the Bun SDK writes its where object:
| condition | passes a document that |
|---|---|
{"eq": [path, value]} | holds the value, or a list holding it |
{"in": [path, [values]]} | holds one of them; an empty list passes none |
{"missing": path} | holds no value there: no field, null or an empty list |
{"range": [path, {"gte": 10, "lt": 100}]} | holds a number within it: gt, gte, lt and lte |
{"or": [conditions]}, {"and": [conditions]} | passes any, or every, of them |
A value is text, a number or a boolean: the text "50" never passes for the
number 50, and an integer past 2⁵³ is written as its digits and matched
exactly. A path the index was not opened with is invalid.