Documentation
¶
Overview ¶
Package eventorder puts out-of-order events back in event-time order.
Events from sensors, message brokers and mobile clients arrive late and shuffled. A Buffer holds them until a watermark says no earlier event is expected, then releases them sorted by event time. The watermark is the largest event time seen so far minus a bounded delay, the same model stream processors use, without a stream processor.
Choosing a type ¶
- Buffer orders one stream. Add returns what is ready, AdvanceTo moves the watermark from a timer, Flush empties it at shutdown.
- Keyed orders many streams (devices, users, partitions) independently, so one slow key never holds back the others.
- Pipe wraps a channel and, with WithIdleTimeout, releases the tail when the source goes quiet.
- Safe and SafeKeyed add a mutex for several goroutines.
- Reorder wraps an iter.Seq of events, and Merge joins several ordered sequences into one.
- Tracker and KeyedTracker say which broker offset is safe to commit while events are still held, see Committing offsets.
- Dedup drops repeated events from an at-least-once source, see Duplicates.
- Stream wires Keyed, KeyedTracker and Dedup for a broker consumer in the right order, see Consumer glue.
- Aligned turns the watermarks of several sources into one safe watermark, see Several sources.
Buffer and Keyed are not safe for concurrent use; serialise calls with a mutex or a single goroutine, or use Safe, SafeKeyed or Pipe.
Choosing a delay ¶
WithMaxDelay is how far out of order an event may arrive and still be placed correctly. Measure the largest gap between an event's time and the newest time seen when it arrives, and set the delay a little above it. A larger delay tolerates more disorder and holds events longer; a smaller one releases sooner and turns more events into late ones. WithMaxBuffered bounds memory when the delay is generous: the oldest events are released early and the watermark moves up to them.
When the disorder changes over time, WithDelayStrategy with PercentileDelay follows it: the delay becomes a percentile of the lags seen in the last window events, and WithMaxDelay stays as the floor. A rare very late event is then late instead of stretching the delay for everyone.
Late events ¶
An event whose time is at or before the watermark when it arrives is late. The default policy drops it and counts it in Stats.Late; Emit returns it at once with Late set. OnLate sees every late event whatever the policy, for a dead-letter queue or a metric.
Committing offsets ¶
A broker commit means everything before an offset is done. Events held in a Buffer are not done, so committing the newest offset seen loses them if the process crashes. Call Tracker.Track when an event enters the buffer and Tracker.Done when it has been processed or dropped as late (OnLate), and commit what Tracker.Commit returns: the lowest offset not yet done. After a restart the consumer re-reads from there, so nothing is lost; events released before the crash but after the commit are read again, so processing should tolerate a repeat. With one Keyed buffer and one KeyedTracker per partition, a rebalance revoke is Keyed.Flush(partition), Done for each flushed event, then KeyedTracker.Revoke(partition) for the offset to commit.
Duplicates ¶
At-least-once delivery repeats events, often out of order. Dedup.Seen(t, id) reports whether id was seen within a window of event time, and bounds its memory with the window and an optional cap on ids. Call it before Buffer.Add with a window at least as long as the delay plus the longest retry. It lives in memory only: after a restart the earlier ids are gone.
Consumer glue ¶
Using Dedup, Keyed and KeyedTracker together has an order that is easy to get wrong: track the offset first, drop duplicates, finish late events, mark released events done only after processing. Stream does that. Per message call Ingest, process the Records it returns, call Done for each, and commit Commit(key). Revoke(key, fn) handles a lost partition and Flush(fn) a clean shutdown. Watermark(key) gives the value to feed to an Aligned when the partitions are merged into one ordered stream.
Several sources ¶
Ordering a merged stream by the fastest source's clock makes every event of a slower source late. Aligned keeps one watermark per source and releases by their minimum: report each source's watermark with Observe (for example its Buffer.Watermark) and pass the combined value to the downstream Buffer.AdvanceTo. Register every source up front so the others wait for it; a source that Observe meets first is not waited for. A silent source holds everything back, so mark it Idle or set WithSourceIdleTimeout; Lag names the source that is holding the watermark. The combined watermark never moves back, so a source that rejoins behind it only has late events of its own.
Compared with a stream processor ¶
Flink and similar systems offer watermarks, allowed lateness and state backends for whole pipelines. This package is the ordering step alone, in memory, in one process. It does not persist what it holds: a crash loses the buffered events, so keep the broker offset and replay after a restart.
Index ¶
- func Merge[T any](sources ...iter.Seq[Event[T]]) iter.Seq[Event[T]]
- func PercentileDelay(q float64, window int) func() DelayStrategy
- func Pipe[T any](ctx context.Context, in <-chan Event[T], opts ...Option) <-chan Event[T]
- func Reorder[T any](seq iter.Seq[Event[T]], opts ...Option) iter.Seq[Event[T]]
- type Aligned
- func (a *Aligned[S]) Idle(src S) (combined time.Time, advanced, ok bool)
- func (a *Aligned[S]) Lag() []SourceStatus[S]
- func (a *Aligned[S]) Len() int
- func (a *Aligned[S]) Observe(src S, wm time.Time) (combined time.Time, advanced, ok bool)
- func (a *Aligned[S]) Register(src S)
- func (a *Aligned[S]) Remove(src S) (combined time.Time, advanced, ok bool)
- func (a *Aligned[S]) Watermark() (time.Time, bool)
- type AlignedOption
- type Buffer
- func (b *Buffer[T]) Add(t time.Time, v T) []Event[T]
- func (b *Buffer[T]) AddTo(dst []Event[T], t time.Time, v T) []Event[T]
- func (b *Buffer[T]) AdvanceTo(t time.Time) []Event[T]
- func (b *Buffer[T]) Flush() []Event[T]
- func (b *Buffer[T]) Len() int
- func (b *Buffer[T]) OnLate(fn func(Event[T]))
- func (b *Buffer[T]) Stats() Stats
- func (b *Buffer[T]) Watermark() time.Time
- type Dedup
- type DelayStrategy
- type Event
- type Keyed
- func (k *Keyed[K, T]) Add(key K, t time.Time, v T) []Event[T]
- func (k *Keyed[K, T]) AdvanceAll(t time.Time, fn func(key K, events []Event[T]))
- func (k *Keyed[K, T]) AdvanceTo(key K, t time.Time) []Event[T]
- func (k *Keyed[K, T]) Flush(key K) []Event[T]
- func (k *Keyed[K, T]) FlushAll(fn func(key K, events []Event[T]))
- func (k *Keyed[K, T]) Len() int
- func (k *Keyed[K, T]) OnLate(fn func(key K, e Event[T]))
- func (k *Keyed[K, T]) Stats(key K) (s Stats, ok bool)
- type KeyedTracker
- type LatePolicy
- type Option
- type Record
- type Safe
- type SafeKeyed
- func (s *SafeKeyed[K, T]) Add(key K, t time.Time, v T) []Event[T]
- func (s *SafeKeyed[K, T]) AdvanceAll(t time.Time, fn func(key K, events []Event[T]))
- func (s *SafeKeyed[K, T]) AdvanceTo(key K, t time.Time) []Event[T]
- func (s *SafeKeyed[K, T]) Flush(key K) []Event[T]
- func (s *SafeKeyed[K, T]) FlushAll(fn func(key K, events []Event[T]))
- func (s *SafeKeyed[K, T]) Len() int
- func (s *SafeKeyed[K, T]) OnLate(fn func(key K, e Event[T]))
- func (s *SafeKeyed[K, T]) Stats(key K) (Stats, bool)
- type SourceStatus
- type Stats
- type Stream
- func (s *Stream[K, T]) AdvanceAll(t time.Time, fn func(key K, records []Record[T]))
- func (s *Stream[K, T]) Commit(key K) (offset int64, ok bool)
- func (s *Stream[K, T]) Done(key K, offset int64) bool
- func (s *Stream[K, T]) Flush(fn func(key K, r Record[T]))
- func (s *Stream[K, T]) Ingest(key K, offset int64, t time.Time, id string, v T) []Record[T]
- func (s *Stream[K, T]) OnLate(fn func(key K, r Record[T]))
- func (s *Stream[K, T]) Revoke(key K, fn func(Record[T])) (offset int64, ok bool)
- func (s *Stream[K, T]) Skip(key K, offset int64) bool
- func (s *Stream[K, T]) Stats() StreamStats
- func (s *Stream[K, T]) Watermark(key K) (time.Time, bool)
- type StreamOption
- type StreamStats
- type Tracker
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Merge ¶
Merge combines sources that are each already in event-time order into one sequence in event-time order, for example several partitions or devices after Reorder. Events with equal times come out in source order, then in the order within the source. The sources are read one event at a time, so memory is one event per source. Stopping the loop early stops reading every source.
A source that is not itself in order is not fixed: its events are passed along in the order they come, which can leave the output out of order. Run such a source through Reorder first.
Example ¶
package main
import (
"fmt"
"slices"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
at := func(s int, v string) eventorder.Event[string] {
return eventorder.Event[string]{Time: time.Unix(int64(s), 0), Value: v}
}
north := slices.Values([]eventorder.Event[string]{at(1, "n1"), at(4, "n4")})
south := slices.Values([]eventorder.Event[string]{at(2, "s2"), at(3, "s3")})
for e := range eventorder.Merge(north, south) {
fmt.Println(e.Value)
}
}
Output: n1 s2 s3 n4
func PercentileDelay ¶
func PercentileDelay(q float64, window int) func() DelayStrategy
PercentileDelay returns a strategy factory whose delay is the q-th quantile (0 to 1, for example 0.99) of the lags of the last window events. It adapts to a source whose disorder changes over time while ignoring rare outliers: at q = 0.99 about one event in a hundred may arrive later than the delay and be treated as late. Before any event is seen the delay is zero. Observe does no allocation once the window is filled; the window is clamped to at least 1 and q to [0, 1].
Example ¶
package main
import (
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
b := eventorder.New[string](eventorder.WithDelayStrategy(eventorder.PercentileDelay(0.99, 1000)))
base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for i, s := range []string{"a", "b", "c"} {
for _, e := range b.Add(base.Add(time.Duration(i)*time.Second), s) {
fmt.Println(e.Value)
}
}
// With no disorder seen the delay is zero, so each event is held only
// until the next newer one arrives.
Output:
func Pipe ¶
Pipe reads events in arrival order from in and sends them in event-time order on the returned channel. When in is closed, what is still held is flushed in order and the output closes. When ctx is cancelled the output closes at once and held events are discarded. With WithIdleTimeout, a source that stays quiet for that long has everything it holds released.
The caller must drain the returned channel or cancel ctx; the goroutine stops when either happens.
Example ¶
package main
import (
"context"
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
in := make(chan eventorder.Event[string])
out := eventorder.Pipe(context.Background(), in, eventorder.WithMaxDelay(5*time.Second))
go func() {
defer close(in)
for _, r := range []struct {
sec int
val string
}{{3, "c"}, {1, "a"}, {2, "b"}} {
in <- eventorder.Event[string]{Time: t0.Add(time.Duration(r.sec) * time.Second), Value: r.val}
}
}()
for ev := range out {
fmt.Println(ev.Value)
}
}
Output: a b c
Types ¶
type Aligned ¶
type Aligned[S comparable] struct { // contains filtered or unexported fields }
Aligned combines the watermarks of several sources into one. Events from partitions, devices or upstream services arrive on separate streams, each with its own progress. Releasing events by the fastest stream's clock makes the slower streams' events late, so the safe watermark is the minimum over the sources: nothing earlier is expected from any of them.
Report each source's watermark with Observe, for example the value of that source's Buffer.Watermark, and feed the result to the Buffer that orders the merged stream with AdvanceTo. A source that falls silent would hold the minimum forever, so it can be marked Idle by hand, or automatically with WithSourceIdleTimeout, and is left out until it reports again. Lag shows which source is holding the watermark back.
The combined watermark never moves backwards. A source that rejoins behind it, or is added behind it, is not waited for: its events older than the combined watermark are late in the downstream Buffer, as for any late event. A source that has been registered but has not reported yet blocks the watermark until it reports, goes idle or is removed.
Aligned is safe for concurrent use.
func NewAligned ¶
func NewAligned[S comparable](opts ...AlignedOption) *Aligned[S]
NewAligned returns an Aligned with no sources.
func (*Aligned[S]) Idle ¶
Idle leaves src out of the minimum until it next reports with Observe. It registers src if it is unknown. Returns as Observe does: leaving out a slow source can advance the combined watermark.
func (*Aligned[S]) Lag ¶
func (a *Aligned[S]) Lag() []SourceStatus[S]
Lag reports every source, the ones holding the watermark back first, then by watermark from lowest to highest.
func (*Aligned[S]) Observe ¶
Observe records watermark wm for src and returns the combined watermark. advanced is true when the combined watermark moved up because of this call; then pass the returned value to Buffer.AdvanceTo. ok is false while no combined watermark exists yet. A watermark lower than the source's previous one is ignored, and a report from an idle source makes it active again.
func (*Aligned[S]) Register ¶
func (a *Aligned[S]) Register(src S)
Register declares a source before it reports, so the combined watermark waits for it. Observe registers a source on first use, which is too late to make the others wait. Registering a known source does nothing.
type AlignedOption ¶
type AlignedOption func(*alignedOptions)
AlignedOption configures an Aligned.
func WithAlignedClock ¶
func WithAlignedClock(now func() time.Time) AlignedOption
WithAlignedClock replaces time.Now as the wall clock used for the idle timeout, for tests.
func WithSourceIdleTimeout ¶
func WithSourceIdleTimeout(d time.Duration) AlignedOption
WithSourceIdleTimeout leaves a source out of the minimum once it has not reported for d of wall-clock time, so a stalled source does not hold the watermark. It counts from the source's last Observe, or from Register for a source that never reported. The default, zero, never times out.
type Buffer ¶
type Buffer[T any] struct { // contains filtered or unexported fields }
Buffer reorders events by event time.
Example ¶
package main
import (
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
b := eventorder.New[string](eventorder.WithMaxDelay(5 * time.Second))
// Readings arrive shuffled; event times are 3s, 1s, 2s, then a much later one.
for _, r := range []struct {
sec int
val string
}{{3, "c"}, {1, "a"}, {2, "b"}, {20, "z"}} {
for _, ev := range b.Add(t0.Add(time.Duration(r.sec)*time.Second), r.val) {
fmt.Println(ev.Value)
}
}
}
Output: a b c
func (*Buffer[T]) Add ¶
Add inserts an event and returns the events that are now safe to process, in event-time order. The slice is empty when nothing can be released yet. Under the Emit late policy a late event is returned first with Late set.
func (*Buffer[T]) AddTo ¶
AddTo is Add appending to dst, for a hot path that reuses one slice:
out = b.AddTo(out[:0], t, v)
Once the slice has grown to the largest burst, Add costs no allocation.
func (*Buffer[T]) AdvanceTo ¶
AdvanceTo moves the watermark up to t without an event, for example from a heartbeat or a timer when a source goes quiet. It never moves backwards. It returns the events released by the move.
func (*Buffer[T]) Flush ¶
Flush releases everything still held, in order, for shutdown or end of input. The watermark moves to the last released event.
func (*Buffer[T]) OnLate ¶
OnLate sets a function called for every late event, whatever the policy, so dropped events can go to a dead-letter queue or a metric instead of vanishing. It runs inside Add; keep it fast. A nil fn clears it.
Example ¶
package main
import (
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
b := eventorder.New[string]()
b.OnLate(func(e eventorder.Event[string]) { fmt.Println("dead letter:", e.Value) })
b.Add(t0.Add(10*time.Second), "now")
b.Add(t0, "stale")
}
Output: dead letter: stale
type Dedup ¶
type Dedup[K comparable] struct { // contains filtered or unexported fields }
Dedup drops repeated events from an at-least-once source. Brokers and retrying producers deliver the same event twice, often out of order, so a duplicate can arrive well after the original. Dedup remembers event ids for a window of event time and answers whether an id was already seen.
The window is measured against the newest event time seen, the same clock a Buffer uses: an id is forgotten once it is more than window behind. Set the window at least as large as the Buffer's delay plus how late a retry can arrive. A duplicate that arrives after its id was forgotten is not caught, and the set lives in memory only, so after a restart earlier ids are gone: keep the processing itself idempotent where a duplicate costs money.
Dedup is safe for concurrent use.
Example ¶
A retrying producer sends some events twice. Dedup drops the second copy before it reaches the Buffer.
package main
import (
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
d := eventorder.NewDedup[string](time.Minute, 0)
b := eventorder.New[string](eventorder.WithMaxDelay(5 * time.Second))
for _, m := range []struct {
id string
sec int
}{{"a", 1}, {"b", 2}, {"a", 1}, {"c", 3}} {
ts := t0.Add(time.Duration(m.sec) * time.Second)
if d.Seen(ts, m.id) {
continue
}
b.Add(ts, m.id)
}
for _, ev := range b.Flush() {
fmt.Println(ev.Value)
}
fmt.Println("duplicates dropped:", d.Duplicates())
}
Output: a b c duplicates dropped: 1
func NewDedup ¶
func NewDedup[K comparable](window time.Duration, maxIDs int) *Dedup[K]
NewDedup returns a Dedup that remembers ids for window of event time. When maxIDs is above zero and more ids are held, the oldest are forgotten first, which bounds memory at the price of missing older duplicates.
func (*Dedup[K]) Duplicates ¶
Duplicates is the number of duplicates found so far.
type DelayStrategy ¶
DelayStrategy chooses the delay of a Buffer from the disorder it has seen, instead of a fixed WithMaxDelay. Observe is called for every event added, late ones included, with how far behind the newest event time it arrived (zero for an event that is the newest so far). Delay is read after each Observe and is the watermark delay to use from now on. A strategy belongs to one Buffer and is never called from two goroutines at once.
type Event ¶
type Event[T any] struct { Time time.Time Value T // Late is true when the event arrived after the watermark had passed its // time. It is only ever set on events released under LatePolicy Emit. Late bool }
Event is a value with the time it happened (not the time it arrived).
type Keyed ¶
type Keyed[K comparable, T any] struct { // contains filtered or unexported fields }
Keyed keeps one Buffer per key, so events of different devices, users or partitions are reordered independently and one slow key does not hold back the others. Not safe for concurrent use.
Example ¶
package main
import (
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
k := eventorder.NewKeyed[string, int](eventorder.WithMaxDelay(time.Second))
k.Add("sensor-1", t0.Add(2*time.Second), 20)
k.Add("sensor-1", t0.Add(1*time.Second), 10)
k.Add("sensor-2", t0, 99) // its own clock: not held back by sensor-1
k.FlushAll(func(key string, evs []eventorder.Event[int]) {
if key == "sensor-1" {
for _, e := range evs {
fmt.Println(key, e.Value)
}
}
})
}
Output: sensor-1 10 sensor-1 20
func NewKeyed ¶
func NewKeyed[K comparable, T any](opts ...Option) *Keyed[K, T]
NewKeyed returns an empty Keyed; every key's Buffer is made with opts.
func (*Keyed[K, T]) Add ¶
Add inserts an event for key and returns that key's newly released events.
func (*Keyed[K, T]) AdvanceAll ¶
AdvanceAll moves every key watermark up to t, see Buffer.AdvanceTo, calling fn once per key that released events. Keys left empty are forgotten, so a periodic call with the current time keeps the number of tracked keys bounded by the keys that were active recently. Key order is not defined.
func (*Keyed[K, T]) AdvanceTo ¶
AdvanceTo moves one key's watermark, see Buffer.AdvanceTo. An unknown key releases nothing.
func (*Keyed[K, T]) Flush ¶
Flush releases and forgets one key. Call it when a key is finished, or the map of buffers grows with every key ever seen.
func (*Keyed[K, T]) FlushAll ¶
FlushAll releases every key, calling fn once per key with its events in order, and forgets them all. Key order is not defined.
type KeyedTracker ¶
type KeyedTracker[K comparable] struct { // contains filtered or unexported fields }
KeyedTracker keeps one Tracker per key, usually per partition. Pair it with Keyed: Keyed.Flush(partition) on a revoke releases that partition's held events, process them and call Done, then Revoke returns the offset to commit before the partition goes to another consumer. Safe for concurrent use.
func NewKeyedTracker ¶
func NewKeyedTracker[K comparable]() *KeyedTracker[K]
NewKeyedTracker returns an empty KeyedTracker.
func (*KeyedTracker[K]) Commit ¶
func (k *KeyedTracker[K]) Commit(key K) (offset int64, ok bool)
Commit is Tracker.Commit for one key; ok is false for an unknown key.
func (*KeyedTracker[K]) Done ¶
func (k *KeyedTracker[K]) Done(key K, offset int64) bool
Done is Tracker.Done for one key; false for an unknown key.
func (*KeyedTracker[K]) Len ¶
func (k *KeyedTracker[K]) Len() int
Len is the number of keys currently tracked.
func (*KeyedTracker[K]) Revoke ¶
func (k *KeyedTracker[K]) Revoke(key K) (offset int64, ok bool)
Revoke forgets a key and returns the offset to commit for it, as Commit would have. Events still pending are not done, so the offset stops at the first of them and the next owner of the partition will read them again.
func (*KeyedTracker[K]) Track ¶
func (k *KeyedTracker[K]) Track(key K, offset int64) bool
Track is Tracker.Track for one key.
type LatePolicy ¶
type LatePolicy int
LatePolicy says what happens to an event that arrives after the watermark has passed its event time.
const ( // Drop discards late events and counts them in Stats.Late. Drop LatePolicy = iota // Emit returns late events immediately, flagged Late, ahead of the // in-order events released by the same call. Emit )
type Option ¶
type Option func(*options)
Option configures a Buffer.
func WithDelayStrategy ¶
func WithDelayStrategy(newStrategy func() DelayStrategy) Option
WithDelayStrategy lets a DelayStrategy set the delay. newStrategy is called once per Buffer, so Keyed gives every key its own state. WithMaxDelay, when also given, is the floor: the delay used is the larger of the two. A nil newStrategy, or one that returns nil, leaves the fixed delay in place.
func WithIdleTimeout ¶
WithIdleTimeout makes Pipe release everything it holds when no event has arrived for d of wall-clock time, so a source that goes quiet does not strand its last events. Zero, the default, waits for input or Flush. Buffer and Keyed ignore it: they have no clock, drive them with AdvanceTo.
func WithLatePolicy ¶
func WithLatePolicy(p LatePolicy) Option
WithLatePolicy chooses what happens to late events. The default is Drop.
func WithMaxBuffered ¶
WithMaxBuffered caps how many events are held. When the cap is exceeded the oldest events are released early and the watermark moves up to them, so memory stays bounded at the price of more late events. Zero means no cap.
func WithMaxDelay ¶
WithMaxDelay sets how far out of order events may arrive. Larger values tolerate more disorder and hold events longer. The default is zero: only events with the same or later time than everything seen are in order.
type Record ¶
Record is an event as Stream hands it back: the event time, the broker offset it came from and the value.
type Safe ¶
type Safe[T any] struct { // contains filtered or unexported fields }
Safe is a Buffer that can be used from several goroutines. Calls are serialised by a mutex; the OnLate function runs while it is held, so it must not call back into the same Safe.
type SafeKeyed ¶
type SafeKeyed[K comparable, T any] struct { // contains filtered or unexported fields }
SafeKeyed is a Keyed that can be used from several goroutines.
func NewSafeKeyed ¶
func NewSafeKeyed[K comparable, T any](opts ...Option) *SafeKeyed[K, T]
NewSafeKeyed returns an empty SafeKeyed, configured like NewKeyed.
func (*SafeKeyed[K, T]) AdvanceAll ¶
AdvanceAll is Keyed.AdvanceAll under the lock; fn runs while it is held.
func (*SafeKeyed[K, T]) FlushAll ¶
FlushAll is Keyed.FlushAll under the lock; fn runs while it is held.
type SourceStatus ¶
type SourceStatus[S comparable] struct { Source S // Watermark is the source's latest reported watermark; zero when Reported // is false. Watermark time.Time Reported bool // Behind is how far the source's watermark is behind the newest watermark // among the active sources. Behind time.Duration // Idle is true when the source is left out of the minimum, by hand or by // timeout. Idle bool // Holding is true when the source is what keeps the combined watermark // from advancing: active and either not yet reported or alone at the // minimum behind some other active source. Holding bool }
SourceStatus is one source's state in the report from Lag.
type Stats ¶
type Stats struct {
Buffered int // events currently held
Released uint64 // events released in order
Late uint64 // events that arrived late (dropped or emitted)
Peak int // most events held at once, to size WithMaxBuffered
Watermark time.Time // zero until the first event or AdvanceTo
}
Stats is a snapshot of a Buffer's counters.
type Stream ¶
type Stream[K comparable, T any] struct { // contains filtered or unexported fields }
Stream wires the pieces a broker consumer needs into one place, in the order that is correct: duplicates are dropped first, the rest are held per key in a Keyed buffer, and a KeyedTracker follows which offsets are finished so that Commit never passes an event that is still held.
The flow for each message is Ingest, process the Records it returns, Done for each of them, and commit what Commit returns. A message that is a duplicate, late (older than the key's watermark) or has an offset the key already saw is finished by Stream itself; it never comes back as a Record.
Key is usually the partition. On a rebalance call Revoke for each lost partition, and on shutdown call Flush. Stream is safe for concurrent use, so worker goroutines may call Done and Commit.
func NewStream ¶
func NewStream[K comparable, T any](opts ...StreamOption) *Stream[K, T]
NewStream returns a Stream. The late hook, if any, is set with OnLate.
func (*Stream[K, T]) AdvanceAll ¶
AdvanceAll releases what is due at time t in every key, see Keyed.AdvanceAll, calling fn once per key with its records. Call it periodically so quiet keys drain and are forgotten.
func (*Stream[K, T]) Commit ¶
Commit is the offset to commit for key: everything before it is processed. ok is false for a key with no messages yet.
func (*Stream[K, T]) Done ¶
Done marks the message at offset as processed. It reports false for an offset that is not pending.
func (*Stream[K, T]) Flush ¶
Flush is for a clean shutdown. It releases everything still held, passes each record to fn and marks it done; Commit then returns the final offsets.
func (*Stream[K, T]) Ingest ¶
Ingest takes one message: key and offset say where it came from, t is its event time, id its unique id for deduplication (empty to skip) and v the value. It returns the records that became ready, in event-time order for the key. Call Done for each once processed.
func (*Stream[K, T]) OnLate ¶
OnLate sets a function called for every late message, for a dead-letter queue or a metric. The offset is already finished when it runs. A nil fn clears it. The function must not call back into the Stream.
func (*Stream[K, T]) Revoke ¶
Revoke is for a partition about to be given up. It releases what key still holds, passes each record to fn (in order) and marks it done, forgets the key and returns the offset to commit. The key starts clean if it comes back.
type StreamOption ¶
type StreamOption func(*streamConfig)
StreamOption configures a Stream.
func WithBuffer ¶
func WithBuffer(opts ...Option) StreamOption
WithBuffer sets the options of every key's Buffer: WithMaxDelay, WithMaxBuffered, WithDelayStrategy. WithLatePolicy is ignored, because Stream always drops late events and reports them to WithOnLate.
type StreamStats ¶
type StreamStats struct {
// Duplicates are messages dropped because their id was seen in the window.
Duplicates uint64
// Late are messages dropped because they arrived at or before the
// watermark of their key.
Late uint64
// Skipped are messages whose offset was not above the last one seen for
// their key, for example a replay after a rebalance that was not Revoked.
Skipped uint64
// Keys is the number of keys currently held.
Keys int
}
StreamStats are the counters of a Stream.
type Tracker ¶
type Tracker struct {
// contains filtered or unexported fields
}
Tracker answers one question for a consumer that holds events in a Buffer before processing them: which broker offset is safe to commit?
A broker commit means "everything before this offset is done". Events held for reordering are not done, so committing the highest offset seen would lose them if the process crashes. Call Track(offset) when an event enters the buffer, Done(offset) when it has been processed (or dropped as late), and commit what Commit returns. Offsets may finish in any order; the commit position only moves past a gap when the gap is done.
A Tracker is for one partition, whose offsets are tracked in increasing order. It is safe for concurrent use, so workers may call Done.
Example ¶
A consumer that holds events for reordering must not commit the offset of events still in the buffer. The Tracker gives the offset that is safe.
package main
import (
"fmt"
"time"
eventorder "github.com/A-RYAN-1/event-order"
)
func main() {
t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
b := eventorder.New[int64](eventorder.WithMaxDelay(2 * time.Second))
tr := eventorder.NewTracker()
// Offsets 0, 1, 2 arrive; their event times are 3s, 1s, 2s.
for offset, sec := range []int{3, 1, 2} {
tr.Track(int64(offset))
for _, ev := range b.Add(t0.Add(time.Duration(sec)*time.Second), int64(offset)) {
tr.Done(ev.Value) // processed
}
}
next, _ := tr.Commit()
fmt.Println("safe to commit:", next, "pending:", tr.Pending())
for _, ev := range b.Flush() { // shutdown: process the rest
tr.Done(ev.Value)
}
next, _ = tr.Commit()
fmt.Println("after flush:", next)
}
Output: safe to commit: 0 pending: 3 after flush: 3
func (*Tracker) Commit ¶
Commit returns the offset to commit: the lowest tracked offset that is not done, or one past the last tracked offset when everything is done. ok is false until the first Track.
func (*Tracker) Done ¶
Done records that the event at offset has been processed or discarded. It reports false for an offset that is not tracked or is already done.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
event-order
command
Command event-order sorts a stream of timestamped lines into event-time order using a bounded delay, without reading the whole input into memory.
|
Command event-order sorts a stream of timestamped lines into event-time order using a bounded delay, without reading the whole input into memory. |
|
examples
|
|
|
sensors
command
Command sensors feeds shuffled temperature readings into a Buffer and prints them in the order they happened.
|
Command sensors feeds shuffled temperature readings into a Buffer and prints them in the order they happened. |