docs.fermihdi.io — ingestion.md

/ › hd › 2.0.5 › md

Download ingestion.mdView raw

Ingestion

Records enter an HD instance through the Message Bus Ingester: one engine role that reads a source, maps each record onto the dataset schema, groups records into batches and streams them to a Storage Cluster Controller over the FermiHDI HD Wire Protocol (FHDWP). Everything downstream — the fan-out to every Storage Node, the hash chain, the filter node — starts from what the ingester sends. This page is what an operator and a data owner need to know about it: where it reads from, what it accepts, and how it decides where one batch ends and the next begins.

Sources

One ingester reads one source, chosen by ingression_type:

Source Setting Reads
Kafka kafka.brokers, kafka.topic, kafka.group_id a topic, as a consumer group member
RabbitMQ rabbitmq.url, rabbitmq.queue a queue
MQTT mqtt.address, mqtt.topic a topic
Local directory file.directory, file.sort_by every file in the directory, one at a time, deleting each once its records are acknowledged
S3 or a compatible object store s3.bucket, s3.prefix, s3.region, s3.endpoint every object under the prefix, one at a time

Kafka is what the installer wires. The others are configured through the ingester's own settings, which reach it as environment variables, a JSON file or command-line flags in that order of precedence; the full table is in the configuration reference of the ingester.

A message from a broker is one record, in JSON or in the RowBinary form (input_format), or a CSV row. A file or object is many records, and its extension says how they are encoded.

File and object formats

Every format decodes to one JSON record per row and then takes the same path a broker message takes, so the schema mapping, address parsing, batch rotation and time compression exist once. The extension is the format:

Extension Format Notes
.json one JSON object, or an array of them
.jsonl, .ndjson one JSON object per line
.csv a header row, then one record per row cells that read as numbers become numbers
.parquet, .pq Apache Parquet a list column is an array (what an array field takes), a dictionary-encoded column is its value; a timestamp or date column is converted to the unit the schema declares for the field (time.unit, or the batch field's time_index_interval_unit), so a PostgreSQL export in microseconds lands in a dataset keyed on seconds as seconds
.orc Apache ORC
.avro Avro object container file the writer's schema travels in the file; a union is its branch, so a nullable long is 5 or null
.pb, .protobuf Protocol Buffers needs a descriptor set (formats.protobuf_descriptor_file, written by protoc --descriptor_set_out --include_imports) and the message type (formats.protobuf_message_type). Messages are length-delimited unless formats.protobuf_delimited is false. 64-bit integers stay numbers
.xml XML each child of the root is a record, or every element named formats.xml_record_element wherever it sits; attributes become @name, repeated children become arrays, text that reads as a number or boolean becomes one

Files are taken in sort_by order — by name, or by modification time — and a file with any other extension is left alone. A file is removed only after the storage cluster has acknowledged its records.

Mapping records onto the schema

The dataset schema names every field, its type and its byte position in the fixed-width record. A JSON key that matches a field name is written there; keys the schema does not name are ignored, and fields no key supplies are zero. Four things deserve a word:

How batches are formed

A batch is the unit the storage cluster stores, hashes, indexes and serves. It carries a 64-byte id, and the ingester decides the id of every record it sends. When the id changes between one record and the next the open batch is closed with an end-of-batch marker and the next one opened — and a closed batch is never reopened. There are three ways the id is chosen, and the dataset schema picks among them.

A time field, rounded to the time index. The schema's batch_id_field names a time field and sets round_batch_id_field; every record's time is rounded down to time_index_interval from the Unix epoch. With an interval of 60 seconds every batch is the top of a minute, whichever second of the minute the first record arrived in, and the batch rotates each time the minute changes. This is how a time-series dataset is stored, and it is what lets a query name its batches from a time window alone. The time field and the interval are in the same unit. The schema's flag is enough on its own; the ingester's own round_batch_id_field setting only forces rounding on.

Any field's value, as it is. Without round_batch_id_field the field's value is the batch id verbatim: a string field is the id text itself, up to 64 bytes; an integer field is its decimal text; a uint64 field is written as eight big-endian bytes. Tenant, customer, region and device ids all work this way. Two things follow. Records have to arrive grouped by that key, or the ingester closes and reopens the same batch over and over. And the query side has to be able to name the ids — a time range enumerates itself, a key set does not, so a client passes each key it wants through the SDK's string or number batch-id generator.

The ingester's clock. With no batch_id_field, or for a record that lacks the field, the id is the ingest time rounded to the interval, as a uint64. The ingester's batch_interval_seconds overrides the interval. This is what a Kafka ingester does by default.

Record size and the data-plane frame

A record travels whole inside one FHDWP frame. The standard 1200-byte frame carries 1133 bytes of records, and a jumbo frame (FERMIHDI_JUMBO=1 on the SCC, every Storage Node and the ingester) carries 8433. A record wider than the payload travels alone in a frame of its own, so a wide dataset works in both modes; jumbo frames only pack more records into each frame. The widest record a dataset may declare is 8433 bytes, and the ingester refuses to start with a wider one.

Closing the last batch

A batch closes when the first record of a later batch arrives. When records stop, the ingester closes the open batch after a timeout instead (batch_close_timeout_mode):

Mode Closes the open batch after
dynamic (default) the longest time any one file or object has spanned so far, measured on its batch field, and never less than one batch interval; batch_close_timeout_sec until a file has been read
manual batch_close_timeout_sec (default 300) with no record
off never; only a later batch closes it

A closed batch is never reopened. Records that arrive for one, from a file delivered out of time order or the second half of a batch split across files that arrived further apart than the timeout, are refused by the storage cluster, and the ingester logs them as an error.

Files that span several batches

A file's rows are rarely in time order, and a closed batch is never reopened, so whenever the batch id is a rounded time field a file or S3 source automatically orders each file's records by it before handing them over — no setting to turn on. A Parquet file holding five minutes of records yields five batches, one per minute, in order, however its rows were written. The ordering applies to numeric keys under time rounding; a file keyed by an unrounded string or integer arrives in file order and should be written grouped by that key — the ingester does not reorder it for you.

A custom ingester

The ingester is one program built on the SDK's ingest client, and a customer can build another: FermiHDIIngestClient registers with the DMS as an HD_CLIENT node (a registration token, a certificate from the DMS-CA, the SCC targets in return), opens the FHDWP data plane on the interface it is given, and sends its batches straight to the Storage Cluster Controllers — the same path the ingester uses, without any of its sources. It draws on the HD_CLIENT element of the licence, or the HD_ANY pool. The ingest client is available from C++ and Python; the SDK overview has the modes and the bindings that support each.

Verifying a source before it feeds a deployment

--dry-run (or FERMIHDI_DRY_RUN=1) runs the same published ingester image with the same source and format handling described above, but registers with no DMS, connects to no SCC and needs no licence. It reads, decodes and maps records exactly as a real ingest would — the same field-name matching, the same batching, the same per-field warnings — and reports a summary instead of sending anything anywhere:

docker run --rm \
  -v /path/to/corpus:/data:ro -v /path/to/schema.json:/schema.json:ro \
  cr.fermihdi.io/hd/ingester:<tag> \
  --dry-run --ingression-type file --file-dir /data --schema /schema.json

Every source and format flag documented above works the same way under --dry-run. Two flags are specific to it: --dry-run-idle-exit-sec (default 5; stop after this many seconds with no new record, including when none ever arrived, so a misconfigured source fails fast rather than waiting forever — 0 runs until Ctrl-C, for watching a live stream as long as wanted) and --dry-run-max-records (default unlimited; stop after this many records, for a quick check against a large corpus).

The run ends with a summary — records read, batches formed, and the same mapping-warning counters What to watch describes below (enum defaults, array truncation, clamped time deltas, unmapped chess or uuid values) — and exits non-zero if nothing was read at all. A clean run, with records read and every warning counter at zero, is the strongest evidence this pipeline has that a source and a dataset schema are actually compatible, without touching a real deployment.

What to watch

The ingester registers with the DMS like every node and serves /healthz and /metrics on its health port (8081 by default). The figures that matter are records in, batches closed, and the acknowledgement lag from the storage cluster; a file source that stops consuming files has usually stopped receiving acknowledgements, and the SCC's log says why. The engine sizing — two cores at the floor, four allocated, 300 × 2 MB hugepages — is in the Administrator's Guide.

Download ingestion.mdView raw