export

suspend fun export(record: LogRecord): ExportResult

Export one log record — the degenerate one-element case of export.

See that overload for the full contract; nothing about durability differs. A one-record call still returns after its own durable write.


suspend fun export(records: List<LogRecord>): ExportResult

Export a batch of log records: append them to the Rga and durably flush to store as one write turn.

Returns ExportResult.Success after the durable write, exactly as the single-record overload does — the durability contract is unchanged. This is not OpenTelemetry's BatchLogRecordProcessor, which trades a flush window for amortisation; nothing here is held back in the hope more arrives. A batch is whatever the caller already has in hand, and it is written before this returns. What is amortised is the fixed cost of a turn — one CRDT append pass, one CBOR encode of the active segment, one segment write — across however many records the caller supplied, instead of paying it once per record (#2194).

Records are admitted in order, and dedup and the buffer cap stay per-record decisions: a LogRecord.recordId already exported (including earlier in this same batch, and including across restarts after recover) is skipped, and a record refused by the cap under BufferPolicy.DROP_NEWEST is not inserted. Neither counts towards ExporterHealth.accepted, which means records durably taken.

A batch may span more than one turn

segmentOps bounds how many bytes one export rewrites, so a batch that would overfill the active segment is split: each turn takes as many records as still fit, writes, rolls, and the next turn continues. (The bound is approximate by exactly one op: a windowPass runs after the records are admitted and can piece one RgaOp.Compact into the active segment outside the turn's budget, so a turn can end at segmentOps + 1 before the roll seals it. That overshoot exists on the per-record path today, and is bounded at one op either way.)

Splitting means the batch is not atomic: an earlier turn's records are durable even if a later one fails.

Every turn is attempted, even after one fails. A failing turn has already admitted its records to the in-memory log and to activeSegment before its write was refused, so those records are not lost — the next successful active-segment write carries them to disk (the same property retirableSegments relies on). Abandoning the remainder of a batch would therefore lose records that looping the single-record overload keeps: a quota-bound store that refuses the ~123 KB segment write while accepting small ones would drop everything after the first failed turn, permanently, on every batch, for as long as the condition lasts. So the loop runs to the end and the first ExportResult.Failure is returned once it does.

A turn also never admits more than maxRecords records, so the eviction it computes is always a prefix of what the buffer already held.

An empty records is ExportResult.Success and touches neither the log nor the store.

Never throws. Every failure — a throwing store, and also a failure inside the in-memory CRDT insert, the eviction, or the CBOR encode — is returned as ExportResult.Failure and reflected on health. The store is the failure this method was originally written for, but it is not the only one reachable, and a caller on the logging path cannot handle a thrown exception: it would surface inside an application's own logging call (#1860).

Samples

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

// Hand the exporter everything you already hold, in one call. One CRDT append pass,
// one CBOR encode of the active segment, one segment write — for the whole run,
// instead of once per record.
val pending: List<LogRecord> = drainedFromSomeQueue()
when (val result = exporter.export(pending)) {
    ExportResult.Success -> Unit // every record in the run is now durable
    is ExportResult.Failure -> {
        // The store refused. Earlier records in the run may already be durable — a run
        // too large for one segment is split across turns — so this is "stop", not
        // "none of it landed".
        println("export failed: ${result.cause}")
    }
}