Skip to main content

Architecture

dfe-loader is a per-message hot path with a per-table batching tail. A message arrives as bytes, gets routed and promoted into a typed row, accumulates in a per-table buffer, and flushes to ClickHouse as a batch. Kafka offsets commit once per flush cycle, never past a row not placed yet -- at-least-once.

Everything on the hot path is shaped by one rule: do the cheap thing per message, defer the expensive thing to flush. Parse once (SIMD, zero-copy), promote only schema columns, keep the full payload as a reference-counted Arc<[u8]>, and splice it as _json only at serialise time.

Layers​

A higher layer depends on lower layers, never the reverse. The only external dependency for the insert path is the patched clickhouse::Client.

The clickhouse_ext boundary​

The hyperi-port fork keeps insertion typed (T: Row, compile-time schema) -- that is the right upstream default. dfe-loader inserts runtime-shaped Map<String,Value> rows whose columns come from system.columns at runtime. That dynamic layer is HyperI-specific and lives in this repo, on top of the upstream Client:

Every insert goes over HTTP insert_formatted_with: row-wise encode() bytes behind a FORMAT RowBinaryWithNamesAndTypes header, or JSONEachRow. The encoder also has a per-column Serialize mode for the native/TCP with_columns_tcp sink, which is not wired: it waits on clickhouse-rs#15, and clickhouse.protocol: native is refused at startup. clickhouse_ext uses only the fork's stable public surface (Client, insert_formatted_with, row/rowbinary primitives), so it is unaffected by the fork's routine cascade re-pushes. See clickhouse/CLICKHOUSE-EXT.md.

Insert dispatch​

The RowBinary path ships row-wise FORMAT RowBinaryWithNamesAndTypes through insert_formatted_with, whose header lets the server refuse bytes encoded for a column type the table no longer has. Both formats go over HTTP: clickhouse.protocol: native is refused at startup, and the native/TCP sink (insert_native_with_columns, with_columns_tcp) waits on clickhouse-rs#15.

Capture modes​

capture_mode decides what gets stored alongside the promoted columns. Resolution is three-level, highest wins: DDL @capture_mode comment > per-table config > global default.

Mode_json_rawUse
full (default)full payload (JSON type)the record's own raw field, else NULLpath queries + text search
json_onlyfull payload (JSON type)NULLpath queries without the _raw overhead
raw_onlyNULLfull Kafka payload (String)CH CPU saving, no JSON-type overhead
extracted_onlyNULLNULLpromoted columns only

All four still promote schema columns; the difference is whether (and where) the full payload is kept. Under full a JSON record is stored once, in _json: _raw holds only raw data the source sent with it, such as a syslog line the receiver captured.

Resilience​

  • Batch salvage -- on a data error, binary-split the batch to isolate the bad row(s), DLQ only those, keep the good ones.
  • Sink-down gate -- a flush cycle in which every insert failed opens the circuit gate on the KEDA scaling signal, and the next successful insert closes it. A failed insert never sends its batch to the DLQ.
  • Schema-cache recovery -- on SchemaMismatch (e.g. ALTER ... ADD COLUMN), invalidate and re-fetch, then retry.
  • One commit per flush cycle -- offsets commit once per cycle, and on each partition stop below the lowest offset not placed yet. A failed batch or a dead letter the DLQ refused is held and retried with jittered backoff, and holds its partition's commit until it lands. A dead letter no DLQ backend can ever hold is dropped and counted instead (pipeline_dead_letters_dropped_total{reason}). A held batch whose table ClickHouse has since reported absent is retried against the default table.
  • Partition leases -- rows of a partition a rebalance has moved away are discarded before any write, since the partition's next owner reads them again from the committed offset (transport_revoke_discarded_total{stage="buffer"}). See OPERATIONS.md.

Source of truth​

DataSource
Versiongit describe --tags / VERSION
TasksGitHub Issues / TODO.md
Historygit log
Static contextCLAUDE.md