57 lines
2 KiB
Go
57 lines
2 KiB
Go
package ingest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
)
|
|
|
|
// Sink materializes a stream into the reporting database.
|
|
//
|
|
// Idempotency lives HERE and not in the reader, because the effect and the high-water mark
|
|
// (run_attempts.last_seq) have to move in ONE transaction: an implementation that applies an event
|
|
// and then records it has a crash window that duplicates work.
|
|
//
|
|
// This is a reporting database, not an event store (research/23 §3): the events are not kept.
|
|
type Sink interface {
|
|
// Begin binds the stream to an attempt — the engine's run id is the idempotency namespace.
|
|
Begin(ctx context.Context, h Hello) error
|
|
// Apply materializes one event AND the cursor it moves, in one transaction. It MUST ignore an
|
|
// event whose Seq is not greater than the stored high-water mark, and it MUST ignore an unknown
|
|
// Type. The reader applies the same rule before calling, so the two agree even when a re-read
|
|
// races a live writer; the sink's copy is the one that holds under a crash.
|
|
Apply(ctx context.Context, ev Envelope, c Cursor) error
|
|
}
|
|
|
|
// Ingest reads a PIPE to its end, feeding a sink. That is the DEV path (D39.106 §3): production
|
|
// tails the book's journal with Tail, and there is no pipe at all.
|
|
//
|
|
// It stops at the first error and returns it: a gap or a malformed line is not recoverable by
|
|
// reading further — the caller reconciles from `tmctl status --json`, which is the whole reason
|
|
// that channel is part of the ratified seam. The cursor it hands the sink is empty, because a pipe
|
|
// has no position to resume from — which is exactly why the ratified transport is a file.
|
|
func Ingest(ctx context.Context, r io.Reader, sink Sink) error {
|
|
d := NewDecoder(r)
|
|
h, err := d.Hello()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := sink.Begin(ctx, h); err != nil {
|
|
return err
|
|
}
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ev, err := d.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := sink.Apply(ctx, ev, Cursor{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|