WarpSpanExporter

class WarpSpanExporter(replica: ReplicaId, store: DurableStore, maxSpans: Int = DEFAULT_MAX_SPANS, bufferPolicy: BufferPolicy = BufferPolicy.DROP_OLDEST, causalClock: WarpCausalClock? = null)

A CRDT-backed span exporter.

Spans are stored in an ORSet> keyed by SpanRecord.spanId: adding the same span twice is a set-union and therefore idempotent — a retry can never double-count. Reconnecting peers reconcile by sharing their ORSet deltas through the kuilt anti-entropy layer rather than replaying a queue, so only missing spans move over the wire.

Key inversion

export returns ExportResult.Success the moment the span is durably written to the DurableStore — not when it is delivered to any backend. Delivery is asynchronous and eventually consistent; the CRDT merge guarantees that any replica which receives the span will incorporate it correctly, even if the span arrives out of order or more than once.

Buffer cap

When the in-memory CRDT exceeds maxSpans, the oldest span (by SpanRecord.startEpochNanos) is evicted according to the bufferPolicy before the new span is inserted. Every eviction is logged with enough detail to correlate against a backend's orphan-span index.

Parameters

replica

The ReplicaId for this device/process. Must be unique and stable across restarts (a UUID is recommended).

store

The DurableStore to persist CRDT state. Use InMemoryDurableStore in tests; wire a platform WAL (JVM file, IndexedDB, etc.) in production.

maxSpans

Maximum number of spans buffered in memory before eviction. Defaults to DEFAULT_MAX_SPANS.

bufferPolicy

What to do when maxSpans is exceeded. Defaults to BufferPolicy.DROP_OLDEST.

Samples

val replica = ReplicaId("device-uuid-abc123")
val store = InMemoryDurableStore()
val exporter = WarpSpanExporter(replica = replica, store = store)

// Recover persisted state from a previous session.
exporter.recover()

// Span ids are raw bytes (OTLP wire format): 16 bytes for trace id, 8 for span id.
val span = SpanRecord(
    traceId = ByteString(ByteArray(16) { it.toByte() }),
    spanId = ByteString(ByteArray(8) { it.toByte() }),
    parentSpanId = null,
    name = "checkout",
    kind = SpanKind.CLIENT,
    startEpochNanos = 1_000_000_000L,
    endEpochNanos = 2_000_000_000L,
)

// Idempotent: exporting the same span twice is a no-op (ORSet union).
exporter.export(span)
val secondResult = exporter.export(span)
check(secondResult == ExportResult.Success)
check(exporter.snapshot().elements.size == 1) { "duplicate was stored" }

Constructors

Link copied to clipboard
constructor(replica: ReplicaId, store: DurableStore, maxSpans: Int = DEFAULT_MAX_SPANS, bufferPolicy: BufferPolicy = BufferPolicy.DROP_OLDEST, causalClock: WarpCausalClock? = null)

Functions

Link copied to clipboard
suspend fun clear(): ExportResult

Drop every span this exporter holds and persist the emptied set (#2208).

Link copied to clipboard
suspend fun export(span: SpanRecord): ExportResult

Export one span: insert it into the CRDT and durably flush to store.

Link copied to clipboard
suspend fun merge(remote: ORSet<SpanRecord>): ExportResult

Merge an ORSet received from another replica (via anti-entropy / gossip) into this exporter's state, then flush the merged result to store.

Link copied to clipboard
suspend fun recover()

Recover persisted span state from store. Call once at startup before any calls to export.

Link copied to clipboard

Read a snapshot of the current in-memory ORSet for gossip / anti-entropy.