Reference

The command, its files and the Go packages, for Inorderly 0.1. The contract on GitHub has the exact ordering rules.

The Command

inorderly [flags]           read NDJSON events, write them in order
inorderly version           print the version as JSON
inorderly demo [flags]      run the self-checking demo and print its report

Flags

Flag Default Meaning
-in FILE standard input An input file. Repeat it to read several files a line at a time in turn: one line from the first, one from the second, and so on. - is standard input.
-out FILE standard output The ordered output.
-window N 0 How far, in event-time units, an event may arrive behind the newest time seen.
-hold N the window How long past its own time an event waits for a parent.
-remember N 0 (always) How long behind the watermark released events are remembered, for duplicates and parents.
-rejects FILE none Late, conflicting and invalid lines, one JSON object each.
-state DIR none Checkpoints. A run killed part way carries on from the last checkpoint when started again with the same flags. Needs -in files and an -out file.
-checkpoint-every N 10000 Input lines between checkpoints.
-stats FILE standard error Where the summary goes.
-strict off Exit with status 3 if any line was late, conflicting or invalid.

Input

One event per line, as JSON. Blank lines are skipped. Lines may end in \n or \r\n, and the last line needs no line ending. A line longer than 16 MiB is invalid. Unknown fields, a missing producer or time, a fraction where an integer belongs, a key repeated in one data object, or anything after the event on the same line make the line invalid.

{"id":"o-1001","producer":"checkout","time":1760000000123,"data":{"total":42.5}}
{"producer":"feed-a","seq":88,"time":1760000000124,"logical":1}
{"id":"pay-77","producer":"payments","time":1760000000120,"parents":["o-1001"]}

Output

Each line is the event’s canonical form with pos (from 1) and, when a parent didn’t come first, missing:

{"pos":1,"id":"o-1001","producer":"checkout","time":1760000000123,"data":{"total":42.5}}
{"pos":2,"id":"pay-77","producer":"payments","time":1760000000120,"parents":["o-1001"]}
{"pos":3,"producer":"feed-a","seq":88,"time":1760000000124,"logical":1}

Here pay-77 has a clock 3 ms behind its parent’s, so it waited for o-1001 and went straight after it.

Rejects

{"reason":"late","input":"a.ndjson","line":4021,"event":{"id":"x","producer":"p","time":12}}
{"reason":"conflict","input":"b.ndjson","line":77,"event":{"id":"o-1001","producer":"checkout","time":1760000000123}}
{"reason":"invalid","input":"-","line":9,"error":"time is missing","raw":"{\"id\":\"y\",\"producer\":\"p\"}"}

raw holds at most the first 1,000 bytes of the line.

Summary

Printed as JSON when the run ends:

Field Meaning
hash The replay hash: sha256: and the SHA-256 of the output. sha256sum on the output file gives the same.
in Lines read, not counting blank ones.
ordered Events written.
duplicates Repeats dropped.
late, conflicts, invalid Lines refused, by reason.
missing_parents Events written with at least one parent missing.
seq_gaps, gaps Jumps in each producer’s seq numbers, and the first 1,000 of them.
seq_backwards Events whose seq was below one already written for their producer.
resumed Whether this run carried on from a checkpoint.
config The window, hold and remember used.

Exit Status

0 when the run finished; 1 for an error such as an unreadable file or a checkpoint that doesn’t match the flags; 2 for bad flags; 3 with -strict when any line was refused.

The State Directory

-state DIR holds checkpoint.json (where each input had got to, how much output was written, and the held events) and released.ndjson (one record per event written, for spotting duplicates and parents after a restart). Every checkpoint first makes the output, the rejects and the record log durable, then replaces checkpoint.json by writing a new file and renaming it over the old one. Started again, inorderly cuts each file back to the checkpoint and carries on; anything written after the checkpoint is written again, the same. A finished run started again prints its summary and does nothing else. Starting with different inputs, output or settings is refused.

The Go Packages

import "github.com/inorderly/inorderly"

o, err := inorderly.New(inorderly.Config{Window: 500, Hold: 500})
status, released, err := o.Push(inorderly.Event{ID: "a", Producer: "p", Time: 1760000000123})
for _, r := range released {
    fmt.Println(string(r.Line)) // pos, event, missing
}
released = o.Flush() // at the end of the stream
fmt.Println(o.Hash())
Name What it does
New(Config) A new Orderer.
(*Orderer).Push(Event) Takes an event: Accepted, Duplicate, Late or Conflict, and the events released by it, in order.
(*Orderer).Flush() Releases everything held. Push can’t follow it.
(*Orderer).Hash() The replay hash so far.
(*Orderer).Stats(), Gaps(), Watermark() Counts, sequence gaps and the watermark.
(*Orderer).Snapshot() The whole state, to save.
(*Orderer).SnapshotHeld() The state without the remembered released events; keep each Released.Record yourself.
Restore(*Snapshot, []Record) An Orderer that carries on where the snapshot was taken.
ParseLine([]byte) One event from one NDJSON line, strictly.
(*Event).Canonical() The event’s canonical bytes.
(*Event).Identity() id, or producer#seq.

An Orderer is not safe for concurrent use; give each stream its own.

import "github.com/inorderly/inorderly/hlc"

clock := hlc.New(nil) // milliseconds since 1970
wall, logical := clock.Now()                      // stamp a local event
wall, logical = clock.Observe(msg.Time, msg.Logical) // stamp what you make from a received event

A producer that stamps with hlc gives every child a timestamp above its parents’, so Inorderly never has to hold a child back.