sightings: a dimension row is written once per window, not once per batch

The ingest writers issue one INSERT round-trip per table, and the receivers
call them SYNCHRONOUSLY on the connection handler — so round-trips per batch
are intake latency, not just database load. datastorelogs took five per batch,
datastoretraces six. Four and five of those wrote the same dimension rows over
and over.

Measured on the live plane, from the 14-minute window this writer last held
production traffic:

	event.log_attribute     21,475 rows written →    52 distinct     413x
	event.log_key            3,902 rows written →     5 distinct     780x
	event.log_resource_key  17,572 rows written →    17 distinct   1,034x
	event.log_resource       1,302 rows written →    65 distinct      20x

44,251 dimension rows carrying 139 rows of information, in front of the socket
that drains the fleet's logs. In that window this path sustained ~390 event.log
rows/min against a ~5,500 rows/min baseline.

pkg/sightings remembers what has already been written and drops the repeats.
Steady state falls from five round-trips per batch to ONE — the fact table
alone — proven end-to-end in TestSteadyStateCostsOneRoundTrip against a conn
that counts prepared INSERTs.

Three invariants, each with a test, because each is a way to lose data rather
than merely re-send it:

  - Only DIMENSION rows are suppressed. event.log and event.span are facts. And
    event.trace is a PARTIAL contribution folded by an AggregatingMergeTree —
    suppressing a repeat there would quietly subtract spans from a trace's own
    span count, so it has no Set at all.
  - A row is recorded only after its write SUCCEEDS. Remembering a failed
    INSERT would drop that sighting permanently; sightings.Write enforces the
    ordering so no call site can get it wrong.
  - One Set per TABLE, never per row type. log_key and log_resource_key are
    both keyRow; a shared Set would let a key seen as a tag suppress the same
    name as a resource key.

Keys carry the 1800s resource bucket, so a sighting refreshes once per window
instead of never: log_attribute.unix_milli and log_resource.seen_at_ts_bucket_start
stay accurate to one bucket. operationRow is keyed WITHOUT its time, which is
the latest span start and therefore differs every batch — (service, name) is
the identity that repeats.

Bounded by two map generations rather than an LRU: forgetting early costs a
redundant write, never a lost row, so the cheap structure is the correct one.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-01 19:53:38 -07:00
co-authored by Hanzo Dev
parent cb8ee83161
commit f37c8d89da
5 changed files with 601 additions and 32 deletions
+172
View File
@@ -0,0 +1,172 @@
// Copyright (C) 2025-2026, Hanzo AI Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
package datastorelogs
import (
"context"
"regexp"
"strings"
"testing"
"time"
"github.com/hanzo-ds/go/lib/column"
"github.com/hanzo-ds/go/lib/driver"
)
// countingConn records every INSERT the writer opens, so a test can assert on
// ROUND TRIPS rather than on rows. The receiver calls the writer synchronously
// on its connection handler, so round-trips per batch are intake latency.
type countingConn struct {
prepared []string
fail bool
}
var tableRe = regexp.MustCompile(`INTO\s+(\S+)`)
func (c *countingConn) PrepareBatch(_ context.Context, query string, _ ...driver.PrepareBatchOption) (driver.Batch, error) {
name := query
if m := tableRe.FindStringSubmatch(query); m != nil {
name = m[1]
}
c.prepared = append(c.prepared, name)
return &countingBatch{fail: c.fail}, nil
}
func (c *countingConn) tables() string { return strings.Join(c.prepared, " ") }
func (c *countingConn) Contributors() []string { return nil }
func (c *countingConn) ServerVersion() (*driver.ServerVersion, error) { return nil, nil }
func (c *countingConn) Select(context.Context, any, string, ...any) error { return nil }
func (c *countingConn) Query(context.Context, string, ...any) (driver.Rows, error) {
return nil, nil
}
func (c *countingConn) QueryRow(context.Context, string, ...any) driver.Row { return nil }
func (c *countingConn) Exec(context.Context, string, ...any) error { return nil }
func (c *countingConn) AsyncInsert(context.Context, string, bool, ...any) error {
return nil
}
func (c *countingConn) Ping(context.Context) error { return nil }
func (c *countingConn) Stats() driver.Stats { return driver.Stats{} }
func (c *countingConn) Close() error { return nil }
type countingBatch struct {
n int
fail bool
}
func (b *countingBatch) Append(...any) error { b.n++; return nil }
func (b *countingBatch) Send() error { return nil }
func (b *countingBatch) Abort() error { return nil }
func (b *countingBatch) AppendStruct(any) error { return nil }
func (b *countingBatch) Column(int) driver.BatchColumn { return nil }
func (b *countingBatch) Flush() error { return nil }
func (b *countingBatch) IsSent() bool { return false }
func (b *countingBatch) Rows() int { return b.n }
func (b *countingBatch) Columns() []column.Interface { return nil }
func (b *countingBatch) Close() error { return nil }
func testWriter(c *countingConn) *Writer {
return NewWriter(c, WithNow(func() time.Time { return testNow }))
}
// The FIRST batch pays for every table. Every batch after it, with the same
// services and attribute keys, costs ONE round-trip — the fact table alone.
// That is the whole point: four of the five INSERTs carried ~0.3% new
// information and sat in the intake path.
func TestSteadyStateCostsOneRoundTrip(t *testing.T) {
c := &countingConn{}
w := testWriter(c)
ctx := context.Background()
if err := w.WriteLogs(ctx, testBatch()); err != nil {
t.Fatalf("first WriteLogs: %v", err)
}
first := len(c.prepared)
if first != 5 {
t.Fatalf("first batch opened %d inserts (%s); want 5 (log + 4 dimensions)", first, c.tables())
}
c.prepared = nil
for i := 0; i < 10; i++ {
if err := w.WriteLogs(ctx, testBatch()); err != nil {
t.Fatalf("repeat WriteLogs: %v", err)
}
}
if got := len(c.prepared); got != 10 {
t.Fatalf("10 repeat batches opened %d inserts (%s); want 10 (the fact table only)", got, c.tables())
}
for _, tbl := range c.prepared {
if !strings.HasSuffix(tbl, "."+"log") && !strings.Contains(tbl, ".log") {
t.Fatalf("repeat batch wrote %q; only the fact table should repeat", tbl)
}
if strings.Contains(tbl, "log_") {
t.Fatalf("repeat batch re-wrote dimension table %q", tbl)
}
}
}
// The fact table is NEVER suppressed: every log line must be written, however
// many times an identical line repeats.
func TestFactTableIsNeverSuppressed(t *testing.T) {
c := &countingConn{}
w := testWriter(c)
for i := 0; i < 4; i++ {
if err := w.WriteLogs(context.Background(), testBatch()); err != nil {
t.Fatal(err)
}
}
logWrites := 0
for _, tbl := range c.prepared {
if strings.HasSuffix(tbl, ".log") {
logWrites++
}
}
if logWrites != 4 {
t.Fatalf("event.log written %d times for 4 batches; want 4", logWrites)
}
}
// log_key and log_resource_key share the keyRow type. They must NOT share a
// sighting Set, or a key seen as a tag would suppress the same name as a
// resource key and the read plane would lose it.
func TestKeyTablesDoNotSuppressEachOther(t *testing.T) {
c := &countingConn{}
w := testWriter(c)
if err := w.WriteLogs(context.Background(), testBatch()); err != nil {
t.Fatal(err)
}
var sawKey, sawResourceKey bool
for _, tbl := range c.prepared {
switch {
case strings.HasSuffix(tbl, ".log_resource_key"):
sawResourceKey = true
case strings.HasSuffix(tbl, ".log_key"):
sawKey = true
}
}
if !sawKey || !sawResourceKey {
t.Fatalf("log_key=%v log_resource_key=%v; both must be written (%s)", sawKey, sawResourceKey, c.tables())
}
}
// A new attribute key appearing later must still reach the dimension table —
// suppression is per sighting, not a one-shot latch.
func TestNovelDimensionStillWritesAfterWarmup(t *testing.T) {
c := &countingConn{}
w := testWriter(c)
ctx := context.Background()
if err := w.WriteLogs(ctx, testBatch()); err != nil {
t.Fatal(err)
}
c.prepared = nil
b := testBatch()
b.Records[0].Attributes["brand.new.key"] = "v"
if err := w.WriteLogs(ctx, b); err != nil {
t.Fatal(err)
}
if !strings.Contains(c.tables(), "log_attribute") {
t.Fatalf("a new attribute key did not reach log_attribute (%s)", c.tables())
}
}
+66 -16
View File
@@ -48,6 +48,7 @@ import (
"github.com/hanzo-ds/go"
"github.com/hanzo-ds/go/lib/driver"
"github.com/hanzoai/o11y/pkg/sightings"
"github.com/hanzoai/o11y/pkg/telemetrylogs"
"github.com/hanzoai/o11y/pkg/zaplogreceiver"
)
@@ -152,6 +153,24 @@ type Writer struct {
db string
org string
now func() time.Time
// One sighting Set per DIMENSION table. Not one per row TYPE: log_key and
// log_resource_key are both keyRow, and a shared Set would let a key seen
// in one table suppress the same key in the other.
//
// event.log itself has no Set — a log line is a fact, never a repeat.
seenAttrs *sightings.Set[bucketed[attrRow]]
seenKeys *sightings.Set[bucketed[keyRow]]
seenResourceKeys *sightings.Set[bucketed[keyRow]]
seenResources *sightings.Set[resourceRow] // already carries its bucket
}
// bucketed qualifies a sighting by the time window it was seen in, so a
// dimension row is rewritten once per bucket instead of once per batch (never
// rewriting it would freeze the tables' recency columns).
type bucketed[T comparable] struct {
bucket int64
row T
}
// Option configures a Writer.
@@ -174,10 +193,14 @@ func WithNow(now func() time.Time) Option { return func(w *Writer) { w.now = now
// record written here is immediately searchable.
func NewWriter(conn datastore.Conn, opts ...Option) *Writer {
w := &Writer{
conn: conn,
db: telemetrylogs.DBName,
org: "hanzo",
now: func() time.Time { return time.Now().UTC() },
conn: conn,
db: telemetrylogs.DBName,
org: "hanzo",
now: func() time.Time { return time.Now().UTC() },
seenAttrs: sightings.New[bucketed[attrRow]](0),
seenKeys: sightings.New[bucketed[keyRow]](0),
seenResourceKeys: sightings.New[bucketed[keyRow]](0),
seenResources: sightings.New[resourceRow](0),
}
for _, o := range opts {
o(w)
@@ -203,33 +226,60 @@ func (w *Writer) WriteLogs(ctx context.Context, batch *zaplogreceiver.LogBatch)
}); err != nil {
return fmt.Errorf("datastorelogs: write log: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(attributeSQLTmpl, w.db, telemetrylogs.LogAttributeTableName), r.attrs,
func(b driver.Batch, a attrRow) error {
return b.Append(a.org, a.tagKey, a.tagType, a.tagDataType, a.stringValue, a.numberValue, nowMilli)
// The four DIMENSION tables below are sightings, ~99.7% of which repeat
// batch after batch. Each suppressed set is one INSERT round-trip not taken
// on the receiver's connection handler, which calls this writer
// synchronously — so this is intake latency, not just database load.
bucket := bucketOf(now)
if err := sightings.Write(w.seenAttrs, r.attrs,
func(a attrRow) bucketed[attrRow] { return bucketed[attrRow]{bucket, a} },
func(novel []attrRow) error {
return send(ctx, w.conn, fmt.Sprintf(attributeSQLTmpl, w.db, telemetrylogs.LogAttributeTableName), novel,
func(b driver.Batch, a attrRow) error {
return b.Append(a.org, a.tagKey, a.tagType, a.tagDataType, a.stringValue, a.numberValue, nowMilli)
})
}); err != nil {
return fmt.Errorf("datastorelogs: write log_attribute: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(keySQLTmpl, w.db, telemetrylogs.LogKeyTableName), r.keys,
func(b driver.Batch, k keyRow) error {
return b.Append(k.org, k.name, k.datatype)
if err := sightings.Write(w.seenKeys, r.keys,
func(k keyRow) bucketed[keyRow] { return bucketed[keyRow]{bucket, k} },
func(novel []keyRow) error {
return send(ctx, w.conn, fmt.Sprintf(keySQLTmpl, w.db, telemetrylogs.LogKeyTableName), novel,
func(b driver.Batch, k keyRow) error {
return b.Append(k.org, k.name, k.datatype)
})
}); err != nil {
return fmt.Errorf("datastorelogs: write log_key: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(resourceKeySQLTmpl, w.db, telemetrylogs.LogResourceKeyTableName), r.resourceKeys,
func(b driver.Batch, k keyRow) error {
return b.Append(k.org, k.name, k.datatype)
if err := sightings.Write(w.seenResourceKeys, r.resourceKeys,
func(k keyRow) bucketed[keyRow] { return bucketed[keyRow]{bucket, k} },
func(novel []keyRow) error {
return send(ctx, w.conn, fmt.Sprintf(resourceKeySQLTmpl, w.db, telemetrylogs.LogResourceKeyTableName), novel,
func(b driver.Batch, k keyRow) error {
return b.Append(k.org, k.name, k.datatype)
})
}); err != nil {
return fmt.Errorf("datastorelogs: write log_resource_key: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(resourceSQLTmpl, w.db, telemetrylogs.LogResourceTableName), r.resources,
func(b driver.Batch, res resourceRow) error {
return b.Append(res.org, res.fingerprint, res.labels, res.bucket)
if err := sightings.Write(w.seenResources, r.resources,
func(res resourceRow) resourceRow { return res }, // bucket is already a field
func(novel []resourceRow) error {
return send(ctx, w.conn, fmt.Sprintf(resourceSQLTmpl, w.db, telemetrylogs.LogResourceTableName), novel,
func(b driver.Batch, res resourceRow) error {
return b.Append(res.org, res.fingerprint, res.labels, res.bucket)
})
}); err != nil {
return fmt.Errorf("datastorelogs: write log_resource: %w", err)
}
return nil
}
// bucketOf is the resource-bucket floor, the one window every sighting in this
// package is qualified by — the same 1800s grid event.log_resource stores.
func bucketOf(t time.Time) int64 {
return t.Unix() / resourceBucketSeconds * resourceBucketSeconds
}
// send is the one batch-INSERT shape: prepare, append every row, send.
// Empty input is a no-op so callers never open a batch they won't fill.
func send[T any](ctx context.Context, conn datastore.Conn, sql string, rows []T, appendRow func(driver.Batch, T) error) error {
+78 -16
View File
@@ -44,6 +44,7 @@ import (
"github.com/hanzo-ds/go"
"github.com/hanzo-ds/go/lib/driver"
"github.com/hanzoai/o11y/pkg/sightings"
"github.com/hanzoai/o11y/pkg/telemetrytraces"
"github.com/hanzoai/o11y/pkg/zapreceiver"
)
@@ -182,6 +183,35 @@ type Writer struct {
db string
org string
now func() time.Time
// One sighting Set per DIMENSION table, suppressing the repeats that make
// up almost all dimension traffic. See pkg/sightings.
//
// event.span and event.trace have NO Set, for different reasons: a span is
// a fact, and a trace row is a PARTIAL contribution folded by an
// AggregatingMergeTree — suppressing a repeat there would silently drop
// spans from the trace's own span count.
seenAttrs *sightings.Set[bucketed[attrRow]]
seenKeys *sightings.Set[bucketed[keyRow]]
seenResources *sightings.Set[resourceRow] // already carries its bucket
seenOperations *sightings.Set[bucketed[operationIdentity]]
}
// bucketed qualifies a sighting by the window it was seen in, so a dimension
// row refreshes once per bucket instead of once per batch.
type bucketed[T comparable] struct {
bucket int64
row T
}
// operationIdentity is an operationRow WITHOUT its time: the row's time is the
// latest span start in the batch, so it differs every batch and the identity
// that should be deduplicated is the (service, name) pair alone. Bucketing it
// keeps event.operation's time column fresh to one window.
type operationIdentity struct {
org string
service string
name string
}
// Option configures a Writer.
@@ -204,10 +234,14 @@ func WithNow(now func() time.Time) Option { return func(w *Writer) { w.now = now
// written here is immediately searchable.
func NewWriter(conn datastore.Conn, opts ...Option) *Writer {
w := &Writer{
conn: conn,
db: telemetrytraces.DBName,
org: "hanzo",
now: func() time.Time { return time.Now().UTC() },
conn: conn,
db: telemetrytraces.DBName,
org: "hanzo",
now: func() time.Time { return time.Now().UTC() },
seenAttrs: sightings.New[bucketed[attrRow]](0),
seenKeys: sightings.New[bucketed[keyRow]](0),
seenResources: sightings.New[resourceRow](0),
seenOperations: sightings.New[bucketed[operationIdentity]](0),
}
for _, o := range opts {
o(w)
@@ -233,27 +267,49 @@ func (w *Writer) WriteSpans(ctx context.Context, batch *zapreceiver.SpanBatch) e
}); err != nil {
return fmt.Errorf("datastoretraces: write span: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(attributeSQLTmpl, w.db, telemetrytraces.SpanAttributeTableName), r.attrs,
func(b driver.Batch, a attrRow) error {
return b.Append(a.org, a.tagKey, a.tagType, a.tagDataType, a.stringValue, a.numberValue, nowMilli)
// The four DIMENSION tables below are sightings that repeat batch after
// batch. Each suppressed set is one INSERT round-trip not taken on the
// receiver's connection handler, which calls this writer synchronously.
bucket := bucketOf(now)
if err := sightings.Write(w.seenAttrs, r.attrs,
func(a attrRow) bucketed[attrRow] { return bucketed[attrRow]{bucket, a} },
func(novel []attrRow) error {
return send(ctx, w.conn, fmt.Sprintf(attributeSQLTmpl, w.db, telemetrytraces.SpanAttributeTableName), novel,
func(b driver.Batch, a attrRow) error {
return b.Append(a.org, a.tagKey, a.tagType, a.tagDataType, a.stringValue, a.numberValue, nowMilli)
})
}); err != nil {
return fmt.Errorf("datastoretraces: write span_attribute: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(keySQLTmpl, w.db, telemetrytraces.SpanKeyTableName), r.keys,
func(b driver.Batch, k keyRow) error {
return b.Append(k.org, k.tagKey, k.tagType, k.dataType, false, nowMilli)
if err := sightings.Write(w.seenKeys, r.keys,
func(k keyRow) bucketed[keyRow] { return bucketed[keyRow]{bucket, k} },
func(novel []keyRow) error {
return send(ctx, w.conn, fmt.Sprintf(keySQLTmpl, w.db, telemetrytraces.SpanKeyTableName), novel,
func(b driver.Batch, k keyRow) error {
return b.Append(k.org, k.tagKey, k.tagType, k.dataType, false, nowMilli)
})
}); err != nil {
return fmt.Errorf("datastoretraces: write span_key: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(resourceSQLTmpl, w.db, telemetrytraces.SpanResourceTableName), r.resources,
func(b driver.Batch, res resourceRow) error {
return b.Append(res.org, res.bucket, res.fingerprint, res.labels)
if err := sightings.Write(w.seenResources, r.resources,
func(res resourceRow) resourceRow { return res }, // bucket is already a field
func(novel []resourceRow) error {
return send(ctx, w.conn, fmt.Sprintf(resourceSQLTmpl, w.db, telemetrytraces.SpanResourceTableName), novel,
func(b driver.Batch, res resourceRow) error {
return b.Append(res.org, res.bucket, res.fingerprint, res.labels)
})
}); err != nil {
return fmt.Errorf("datastoretraces: write span_resource: %w", err)
}
if err := send(ctx, w.conn, fmt.Sprintf(operationSQLTmpl, w.db, telemetrytraces.OperationTableName), r.operations,
func(b driver.Batch, o operationRow) error {
return b.Append(o.org, o.service, o.name, o.time)
if err := sightings.Write(w.seenOperations, r.operations,
func(o operationRow) bucketed[operationIdentity] {
return bucketed[operationIdentity]{bucket, operationIdentity{o.org, o.service, o.name}}
},
func(novel []operationRow) error {
return send(ctx, w.conn, fmt.Sprintf(operationSQLTmpl, w.db, telemetrytraces.OperationTableName), novel,
func(b driver.Batch, o operationRow) error {
return b.Append(o.org, o.service, o.name, o.time)
})
}); err != nil {
return fmt.Errorf("datastoretraces: write operation: %w", err)
}
@@ -266,6 +322,12 @@ func (w *Writer) WriteSpans(ctx context.Context, batch *zapreceiver.SpanBatch) e
return nil
}
// bucketOf is the resource-bucket floor, the one window every sighting in this
// package is qualified by — the same 1800s grid event.span_resource stores.
func bucketOf(t time.Time) int64 {
return t.Unix() / resourceBucketSeconds * resourceBucketSeconds
}
// send is the one batch-INSERT shape: prepare, append every row, send.
// Empty input is a no-op so callers never open a batch they won't fill.
func send[T any](ctx context.Context, conn datastore.Conn, sql string, rows []T, appendRow func(driver.Batch, T) error) error {
+140
View File
@@ -0,0 +1,140 @@
// Copyright (C) 2025-2026, Hanzo AI Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
// Package sightings suppresses the dimension writes an ingest batch would
// otherwise repeat forever.
//
// The event plane stores each signal as one FACT table plus several DIMENSION
// tables: event.log beside log_attribute / log_key / log_resource_key /
// log_resource, event.span beside span_attribute / span_key / span_resource /
// operation. The fact row is new every time. The dimension rows are
// "sightings" — this org uses this attribute key, this resource exists — and
// they are overwhelmingly the SAME rows batch after batch. They are
// ReplacingMergeTree, so re-writing them is harmless; it is merely expensive.
//
// Measured on 24h of live traffic, one 14-minute production window:
//
// event.log_attribute 21,475 rows written → 52 distinct 413x
// event.log_key 3,902 rows written → 5 distinct 780x
// event.log_resource_key 17,572 rows written → 17 distinct 1,034x
// event.log_resource 1,302 rows written → 65 distinct 20x
//
// That redundancy is not free. A writer issues one INSERT round-trip per
// table, and the receiver calls the writer SYNCHRONOUSLY on its connection
// handler — so five round-trips per batch is five times the latency on the
// path that drains the socket, and the sender feels it as backpressure. Four
// of those five carried ~0.3% new information.
//
// A Set remembers what has already been written and drops the repeats, taking
// the steady state from five round-trips per batch to one.
//
// TWO RULES, both load-bearing:
//
// - Only DIMENSION rows may pass through here. A partial contribution to an
// aggregate — event.trace's per-batch span count, folded by an
// AggregatingMergeTree — is not a sighting, and suppressing a repeat would
// silently lose spans from the total.
// - A row is recorded only after its write SUCCEEDS. Remembering a row whose
// INSERT failed would drop it permanently. Write enforces this so no
// caller has to.
//
// Keys carry a time bucket so a sighting refreshes periodically rather than
// never: the tables' recency columns (log_attribute.unix_milli,
// log_resource.seen_at_ts_bucket_start) stay accurate to one bucket.
package sightings
import "sync"
// DefaultSize is the per-Set entry budget. Live cardinality is ~10^2 distinct
// sightings per table; this leaves four orders of magnitude of headroom while
// staying bounded, because the key includes a rotating bucket and would
// otherwise grow without limit.
const DefaultSize = 20000
// Set records the dimension rows already written for ONE table. One Set per
// table, never shared: two tables can share a Go row type (log_key and
// log_resource_key are both keyRow), and a single Set would let a sighting in
// one suppress the same sighting in the other.
//
// Bounded by two generations rather than an LRU: when the live generation
// fills, it becomes the previous generation and a fresh one starts. Lookups
// consult both, so an entry survives at least one full generation and at most
// two are ever held. Cost is a map swap instead of per-entry bookkeeping, and
// the failure mode of forgetting early is a redundant write, not a lost row.
type Set[K comparable] struct {
mu sync.Mutex
max int
cur map[K]struct{}
prev map[K]struct{}
}
// New returns a Set holding at most 2*size keys. size <= 0 takes DefaultSize.
func New[K comparable](size int) *Set[K] {
if size <= 0 {
size = DefaultSize
}
return &Set[K]{max: size, cur: make(map[K]struct{}), prev: make(map[K]struct{})}
}
func (s *Set[K]) seen(k K) bool {
if _, ok := s.cur[k]; ok {
return true
}
if _, ok := s.prev[k]; ok {
// Promote, so a still-live sighting is not forgotten by rotation.
s.cur[k] = struct{}{}
return true
}
return false
}
func (s *Set[K]) record(k K) {
if len(s.cur) >= s.max {
s.prev, s.cur = s.cur, make(map[K]struct{}, s.max/4)
}
s.cur[k] = struct{}{}
}
// Write sends only the rows this Set has not already recorded, then records
// them — and only if the send succeeded. Duplicates within rows are collapsed
// too, so a batch repeating one attribute writes it once.
//
// send is not called at all when nothing is novel: that is the whole point,
// and it is why the steady state costs no round-trip.
func Write[T any, K comparable](s *Set[K], rows []T, key func(T) K, send func([]T) error) error {
if len(rows) == 0 {
return nil
}
novel := make([]T, 0, len(rows))
keys := make([]K, 0, len(rows))
s.mu.Lock()
batch := make(map[K]struct{}, len(rows))
for _, r := range rows {
k := key(r)
if _, dup := batch[k]; dup {
continue
}
batch[k] = struct{}{}
if s.seen(k) {
continue
}
novel = append(novel, r)
keys = append(keys, k)
}
s.mu.Unlock()
if len(novel) == 0 {
return nil
}
if err := send(novel); err != nil {
return err // deliberately NOT recorded: the next batch must retry it
}
s.mu.Lock()
for _, k := range keys {
s.record(k)
}
s.mu.Unlock()
return nil
}
+145
View File
@@ -0,0 +1,145 @@
package sightings
import (
"errors"
"sync"
"testing"
)
type row struct {
bucket int64
name string
}
func id(r row) row { return r }
// The steady state must cost NO send: the second time the same sighting
// arrives, the writer does not touch the database.
func TestRepeatCostsNoSend(t *testing.T) {
s := New[row](0)
sends := 0
rows := []row{{1, "a"}, {1, "b"}}
for i := 0; i < 5; i++ {
if err := Write(s, rows, id, func(got []row) error { sends++; return nil }); err != nil {
t.Fatalf("Write: %v", err)
}
}
if sends != 1 {
t.Fatalf("send called %d times; the repeats must cost nothing", sends)
}
}
func TestOnlyNovelRowsAreSent(t *testing.T) {
s := New[row](0)
var sent [][]row
send := func(got []row) error { sent = append(sent, append([]row(nil), got...)); return nil }
_ = Write(s, []row{{1, "a"}, {1, "b"}}, id, send)
_ = Write(s, []row{{1, "a"}, {1, "c"}}, id, send)
if len(sent) != 2 {
t.Fatalf("sent %d batches, want 2", len(sent))
}
if len(sent[1]) != 1 || sent[1][0].name != "c" {
t.Fatalf("second send = %v, want only the novel row c", sent[1])
}
}
// A repeated row inside ONE batch is written once.
func TestDuplicatesWithinBatchCollapse(t *testing.T) {
s := New[row](0)
var got []row
_ = Write(s, []row{{1, "a"}, {1, "a"}, {1, "a"}}, id, func(r []row) error { got = r; return nil })
if len(got) != 1 {
t.Fatalf("sent %d rows, want 1", len(got))
}
}
// A different bucket is a different sighting: recency columns must refresh.
func TestBucketRotationRefreshes(t *testing.T) {
s := New[row](0)
sends := 0
send := func([]row) error { sends++; return nil }
_ = Write(s, []row{{1, "a"}}, id, send)
_ = Write(s, []row{{1, "a"}}, id, send)
_ = Write(s, []row{{2, "a"}}, id, send) // next bucket
if sends != 2 {
t.Fatalf("sends=%d, want 2 (once per bucket)", sends)
}
}
// THE correctness rule: a failed write is never remembered, or the row is lost
// forever.
func TestFailedWriteIsRetried(t *testing.T) {
s := New[row](0)
attempts := 0
fail := func([]row) error { attempts++; return errors.New("datastore down") }
if err := Write(s, []row{{1, "a"}}, id, fail); err == nil {
t.Fatal("Write must surface the send error")
}
if err := Write(s, []row{{1, "a"}}, id, fail); err == nil {
t.Fatal("Write must surface the send error")
}
if attempts != 2 {
t.Fatalf("attempts=%d; a failed write must be retried, not remembered", attempts)
}
// And once it succeeds, it is remembered.
ok := 0
_ = Write(s, []row{{1, "a"}}, id, func([]row) error { ok++; return nil })
_ = Write(s, []row{{1, "a"}}, id, func([]row) error { ok++; return nil })
if ok != 1 {
t.Fatalf("after success, sends=%d, want 1", ok)
}
}
// Bounded: far more distinct keys than the budget must not grow without limit.
func TestBounded(t *testing.T) {
s := New[row](100)
for i := int64(0); i < 5000; i++ {
_ = Write(s, []row{{i, "a"}}, id, func([]row) error { return nil })
}
s.mu.Lock()
total := len(s.cur) + len(s.prev)
s.mu.Unlock()
if total > 200 {
t.Fatalf("holding %d keys, want <= 200 (2 generations of 100)", total)
}
}
func TestEmptyInputDoesNotSend(t *testing.T) {
s := New[row](0)
called := false
if err := Write(s, nil, id, func([]row) error { called = true; return nil }); err != nil {
t.Fatal(err)
}
if called {
t.Fatal("send called for an empty slice")
}
}
func TestConcurrentWriteIsSafe(t *testing.T) {
s := New[row](0)
var mu sync.Mutex
sent := 0
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
_ = Write(s, []row{{int64(n % 5), "a"}}, id, func(r []row) error {
mu.Lock()
sent += len(r)
mu.Unlock()
return nil
})
}(i)
}
wg.Wait()
mu.Lock()
defer mu.Unlock()
if sent < 5 || sent > 50 {
t.Fatalf("sent %d rows; want between 5 (perfect dedup) and 50 (no dedup)", sent)
}
}