feed

package
v1.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package feed serves the ordered event log to pull consumers: cursor pages over expanded envelopes, an SSE-friendly tail, and named compare-and-swap cursors so a replicated consuming service reads as one logical consumer. Design: docs/design/event-delivery.md.

Index

Constants

View Source
const DefaultDeadLetterRetention = 30 * 24 * time.Hour

DefaultDeadLetterRetention bounds how long a dead delivery is kept.

It is far longer than the event retention (7 days by default) because a dead letter is the record of a delivery that never succeeded: it must outlive the events it references long enough for an operator to notice and redrive. It is a bound, not a policy against redriving — 30 days is well past any attempt window.

View Source
const DefaultParkedRetention = 30 * 24 * time.Hour

DefaultParkedRetention bounds how long a parked envelope is kept before the pruner deletes it.

Pruning a parked envelope is deliberate data loss: the event was committed but never delivered, and after the prune it never will be. The default is therefore generous — 30 days, matching the dead-letter retention and far past the window in which the parked gauge should have been alerted on and the envelopes redriven. It is a bound on unbounded growth, not a policy against redriving.

Variables

View Source
var ErrCursorConflict = errors.New("feed: cursor position changed since read")

ErrCursorConflict is returned when a compare-and-swap commit loses the race — another consumer replica advanced the cursor first.

View Source
var ErrGone = errors.New("feed: cursor older than retention; re-baseline required")

ErrGone is returned when a cursor points before the retention floor: events were pruned, so the consumer must re-baseline instead of silently missing history.

Functions

This section is empty.

Types

type CursorStore

type CursorStore interface {
	Get(ctx context.Context, tenant valueobjects.TenantID, consumer string) (int64, error)
	// Commit advances the cursor with compare-and-swap: it fails with
	// ErrCursorConflict unless the stored position equals expected.
	Commit(ctx context.Context, tenant valueobjects.TenantID, consumer string, position, expected int64, now time.Time) error
}

CursorStore persists named consumer cursors.

type Event

type Event struct {
	Seq      int64           `json:"seq"`
	Envelope events.Envelope `json:"envelope"`
}

Event is one feed entry.

type Interactor

type Interactor struct {
	// contains filtered or unexported fields
}

Interactor implements the feed usecases.

func NewInteractor

func NewInteractor(store Store, cursors CursorStore, acl *fieldacl.Resolver) *Interactor

NewInteractor wires the feed usecases. The resolver applies the field ACL to value payloads; pass nil to serve the feed unfiltered, which is only correct when every caller of this interactor is already privileged.

func (*Interactor) CommitCursor

func (i *Interactor) CommitCursor(ctx context.Context, consumer string, position, expected int64) error

CommitCursor advances a named cursor via compare-and-swap. Returns ErrCursorConflict when another replica advanced it first.

func (*Interactor) Cursor

func (i *Interactor) Cursor(ctx context.Context, consumer string) (int64, error)

Cursor returns a named consumer's committed position (0 = start).

func (*Interactor) List

func (i *Interactor) List(ctx context.Context, in ListInput) (*ListOutput, error)

List pages the event log. Returns ErrGone when After points before the retention floor.

type ListInput

type ListInput struct {
	After int64
	Types []string
	Limit int
}

ListInput is one feed page request.

type ListOutput

type ListOutput struct {
	Items []Event `json:"items"`
	// NextCursor feeds the following request's After; equals After when
	// the page is empty.
	NextCursor int64 `json:"next_cursor"`
}

ListOutput is one feed page.

type Pruner

type Pruner struct {
	// contains filtered or unexported fields
}

Pruner deletes events past retention on an interval.

func NewPruner

func NewPruner(store Store, retention time.Duration, onError func(error)) *Pruner

NewPruner builds a retention pruner (interval defaults to hourly).

func (*Pruner) Run

func (p *Pruner) Run(ctx context.Context)

Run prunes until ctx ends.

func (*Pruner) WithDeadLetterRetention added in v1.3.0

func (p *Pruner) WithDeadLetterRetention(d time.Duration) *Pruner

WithDeadLetterRetention overrides how long a dead delivery is kept before it stops pinning its envelope. Non-positive values are ignored.

func (*Pruner) WithParkedRetention added in v1.5.0

func (p *Pruner) WithParkedRetention(d time.Duration) *Pruner

WithParkedRetention overrides how long a parked envelope is kept before it is deleted. Deleting a parked envelope is deliberate data loss — keep this well past the alerting-and-redrive window. Non-positive values are ignored.

type Store

type Store interface {
	// List returns events with feed_seq > after for the tenant, in feed
	// order, optionally filtered by event type.
	List(ctx context.Context, tenant valueobjects.TenantID, after int64, types []string, limit int) ([]Event, error)

	// Floor returns the smallest retained feed_seq for the tenant (0 when
	// no events are retained).
	Floor(ctx context.Context, tenant valueobjects.TenantID) (int64, error)

	// Prune deletes expanded envelopes recorded before the cutoff whose
	// deliveries have all settled. Returns rows removed.
	Prune(ctx context.Context, cutoff time.Time) (int, error)

	// PruneDeadLetters deletes DEAD delivery rows older than the cutoff, so
	// their envelopes become prunable. Returns rows removed.
	//
	// The envelope prune deliberately keeps anything a dead delivery still
	// references — the evidence an operator needs in order to redrive it —
	// but nothing else ever deleted a dead row, so a single decommissioned
	// endpoint pinned its envelopes for ever and FLEXITYPE_EVENT_RETENTION
	// stopped bounding the outbox or the feed at all. The bound has to exist
	// somewhere; it exists here, well past the attempt window.
	PruneDeadLetters(ctx context.Context, cutoff time.Time) (int, error)

	// PruneParked deletes envelopes that were parked before the cutoff.
	// Returns rows removed.
	//
	// A parked envelope has no feed_seq, so the envelope prune never
	// reached it and one poisonous event type could grow the outbox for
	// ever. The bound has to exist somewhere; it exists here, under its own
	// long retention. Deleting a parked envelope is DELIBERATE data loss —
	// the event was never delivered and never will be — so the retention is
	// far past the window in which an operator should have noticed the
	// parked gauge and redriven.
	PruneParked(ctx context.Context, cutoff time.Time) (int, error)
}

Store reads the expanded event log.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL