Expand description
The records of the log of riff-server.
§Design
Each change that must not be lost is a Record in one log
(01M3T410XDD9W4EC0Y68FAA7XN). A record
has a position (1, 2, 3, and so on), the time of its write, its
cause, and one Change. The change names say what happened, in the
past tense. These Rust types are the schema of the log.
The fields next to the change are the Envelope: one type that
Record and the tolerant reader Line::parse share. So a new
field of the envelope gets to the writer, the reader and each start.
The cause is in the envelope: the caller (By) and the kind of
the command (01M3X4Z60G1FXQTDC5XDJ05BAX). So the log alone shows who made each
change. A record from before this rule has no cause: it reads, and
its cause is not known.
The envelope also has the call ID of the command: the field call
(01M48VFFY5CK9MRXJESV2NHY5F). It came after release 1.0.0. A record
of a command with no call ID, and each record of 1.0.0, has none. A
build of 1.0.0 skips the field. riff-server uses it to give a
repeated call the reply of the first try, also after a start.
§One layout for each type
Each type of the log, the wire and the checkpoint has one struct
(01M49W17GGV1K5FZEKJRPV0GNA). A tolerant reader holds the struct
itself, as Line::parse holds the Envelope with
#[serde(flatten)]: it never keeps a second list of the fields.
Each conversion between two such types names each field of its
source in a destructure with no .. (01M49W17M2JVNYSHDQJWHZ8A7X):
a field that the target does not need is named with _. So a new
field fails the build at each place that must carry it, and no
review must find it:
use riff_core::record::Envelope;
// A conversion that does not name the field `call` does not build.
fn written(envelope: Envelope) -> (u64, u64) {
let Envelope { position, written_at_ms, by: _, command: _ } = envelope;
(position, written_at_ms)
}Each type has a round-trip test: each variant with each optional
field set, written and read back (01M49W18ETF4KZJN848M91VY35). The
records go through the real path of the store in
riff-server/tests/calls.rs.
A record names a session by its URI with no lead mark and no claims: the who and the place at the time of the change (01M3T411QW1SQV12RJVATEJ8YD). So a replay knows where each session was.
§The rules for a change of a record (01M3T4111PFM0C6KPREWFS9EQQ)
- A new field has a default, and the default means “as before”. An old build skips a field that it does not know.
- Do not change the type or the meaning of a field. Do not use the name of a removed field again.
- A new kind of change gets a new name. A build that does not know a
kind skips the record (
Line::Unknown). - A kind is never renamed, and the name of a removed kind is never
used again (01M3XM2C3MND6YB24SGZ565353). The file
kinds.jsonin the directory of each release undercrates/riff-server/tests/fixtures/lists the new kinds of the release (01M43GSRSDJMGAH8SR1GD4Z3XF). The file of 1.0.0 also lists each field of the envelope. A test fails when a name of a list is gone from the code. - The enum
Changeand the listChange::KINDScome from one macro. So a variant cannot be missing from the list. The order of the list is not a part of the format. - A field with a set of named values that can grow has the value
other: the class inBy, theScopeof a pause, and the reason of a start (StartReason). A build reads a value that it does not know asother: a text, and each other form of JSON.applystores nothing for such a value. Such a record counts as a skipped record (Record::other): the build writes no checkpoint past it (01M3XM2C18TT8VSKGD77YPZG53). Thestateof apause_sethas two values and noother. - A
postedrecord has two such parts too (01M3XSF90E9JYYTC13D9THY4WE): the kind of the message (Kind), and each selector of itsto(Selector). A selector that the build does not know is a selectorother: an object with a field or a value that the build does not know, and each other form of JSON. It keeps its JSON as it came, and it matches no session. The message stays in its thread, and a reader shows it as a message. - A session URI keeps each query part that the build does not know,
as it came (
SessionUri::other, 01M3XYYSY536AEJVERBPTQFQYX). The session is the same session, and the part gives no mark: no lead and no claim. Such a record counts as a skipped record too. commandis text. A reader takes a kind of command that it does not know as text.- Only the read of JSON takes a value of a later build. A call with such a kind, such a selector or such a URI of its caller is refused.
§Each type with named values in a record
The list has each enum that a record holds, and each struct that refused a field that it does not know. A struct that is not in the list skips such a field. A new type of these two sorts in a record needs a line here.
| Type | Where | A value that the build does not know |
|---|---|---|
Change | change | The record is skipped (Line::Unknown). |
By | by | other |
Scope | pause_set | other |
StartReason | session_started | other |
Kind | the message of posted | other |
Selector | the to of the message of posted | other: a new field, a new value of a field, and each other form of JSON |
RiffState | pause_set | The line does not read. The set never grows: a pause is set or ended. A new sort of pause is a new Scope. |
§Each text with a grammar in a record
The list has each text that the read of a record checks or takes apart, and each number. A text that is not in the list is free text: the read takes each value. A new text with a grammar in a record needs a line here.
| Text | Where | A value that the build does not know |
|---|---|---|
The query of a session URI (SessionUri) | session of a change, from of a message | The URI keeps the part, and the record counts as a skipped record: a part with a new name, lead with a value other than true, a second session, and a claim with a character that is not allowed. |
| The other parts of a session URI | the same | The line does not read. They never grow: riff://USER@HOST, then /OWNER/REPO or /-, then #WORKTREE, and each part holds only ASCII letters, digits, -, _, . and ~. A new fact of a session is a new query part. |
| The session ID of a session URI | the session part of the query | The line does not read when the ID is empty or has a character that is not allowed. The grammar never grows: the ID is a name with only ASCII letters, digits, -, _, . and ~, and a new fact of a session is a new query part. |
A thread name (ThreadName) | thread | The line does not read. The grammar never grows: a text that is not empty and has no white space. Each new sort of thread name fits it, as dm: did. A name that starts with dm: reads with each character, also white space. The read does not take a name apart. |
The caller in by | {"session":"USER/ID"} | other (By) |
A session in woken (Who) | posted | The read checks no character. A field that the build does not know is skipped. |
| The signature and the payload of a message | sig, payload | The read keeps the text, and checks nothing. A reader checks them: a text that does not check gives a message that is not verified. |
| An item, an email, a user, the ID of a riff, a body, a command, the reason of a hold, the title of a wave | item, email, user, riff_id, body, command, reason, title | Free text. |
| An item of a plan, a need, a done need | items, needs, done of plan_set | Free text: handle of plan checks the form issue-N, and the read does not. |
| A time, a position, a seq, a count, the number of a wave | written_at_ms, at_ms, due_ms, position, seq, after_secs, per_host, number | The line does not read. A number is a whole number that is not negative, and its type never changes (the rule of a field). per_host is at most 65535. |
| A mark | worker, must_clear, admin | The line does not read. A mark is true or false, and its type never changes. |
The header of a chunk has the number of the format. A header of a later format stops the load (01M3T411F3K6FD28R3Q3ZE4VCN): this is the one sign that a build must not read the log.
§Example
use riff_core::name::Who;
use riff_core::record::{By, Change, Claimed, Envelope, Line, Record};
let record = Record {
envelope: Envelope {
position: 1234,
written_at_ms: 1_790_000_000_000,
by: Some(By::Session(Who::new("ann", Some("s1"))?)),
command: Some("claim".into()),
call: None,
},
change: Change::Claimed(Claimed {
session: "riff://ann@heron/acme/app?session=s1".parse()?,
thread: "acme/app".parse()?,
item: "issue-7".into(),
}),
};
let line = serde_json::to_string(&record).unwrap();
assert_eq!(
line,
r#"{"position":1234,"written_at_ms":1790000000000,"by":{"session":"ann/s1"},"command":"claim","change":{"claimed":{"session":"riff://ann@heron/acme/app?session=s1","thread":"acme/app","item":"issue-7"}}}"#
);
assert_eq!(Line::parse(&line)?, Line::Record(Box::new(record)));
// A record with no cause reads. Its cause is not known.
let old = r#"{"position":7,"written_at_ms":1,"change":{"member_invited":{"email":"ann@acme.io"}}}"#;
let Line::Record(old) = Line::parse(old)? else { panic!("a known kind") };
assert_eq!((old.envelope.by, old.envelope.command), (None, None));Structs§
- Admin
Set - Claimed
- A claim of one work item in one thread.
- A person, by the verified email in lower case. A record of the people names a person by the email: the state finds the USER.
- Envelope
- The fields of a record next to its change. See the design of the module.
- Forgotten
- A session that the state forgets.
- Item
Freed - The end of the hold of one item in one repository thread.
- Item
Held - The hold of one item in one repository thread, with its reason (01M43GSGB9ZFHSG0Q83Y50FEGW). The envelope of the record names who held it, and when. A hold is not a claim.
- Member
- A session and a thread.
- Notes
Set - The notes of the lead of one user in one repository thread (01M59258K3TYR09HC4AWHWZPEX). The text is the whole text: it replaces the old text. The envelope names who wrote it, and when.
- Owner
Asked - A request for the owner role.
- Owner
Set - The owner of the riff.
- Pause
Set - A pause that a command set or ended. The envelope of the record names who did it, and when.
- Person
Joined - The first sign-in of a person: the verified email holds the USER (R209).
- Plan
- A plan with no thread: the current wave, its items with their
needs, and the done needs. A need that is not in
doneis open. - Plan
Ended - The end of the plan of one repository thread.
- Plan
Item - One item of a plan, with the items that it needs.
- PlanSet
- The plan of one repository thread (01M4A4YTNSJR0R1T9JNXPBSKHC): the full plan, so the record replaces the plan of the thread. The design is in the book: “Design: the plan on the server”.
- Posted
- A message in a thread.
- Record
- One line of the log: its envelope and its change.
- Released
- A claim that is free now.
- Riff
Made - The ID of a riff (01M3JNVBPMZ1K9WX7Q7DP6Y0DH).
- Session
Started - A start of a session.
- Setting
Changed - Signins
Ended - Wave
- A wave of a plan: its number, and its title on the forge.
Enums§
- By
- Who caused a record: the caller of its command, with its class (01M3X4Z60G1FXQTDC5XDJ05BAX). It holds only the user and the session ID. The session in a change is a full URI, with the place.
- Change
- What happened.
- Line
- One line of the log, as this build reads it.
- Scope
- What a pause stops: the whole riff, or one repository (01M3XAHZG26ECNARX35JD73YXJ).
Constants§
- MARKS
- The marks of a post for an audit, longest first: the start of a body that names the step of the flow (01M3ZWRC3XBFN8FJDGE8XWZ5EA).