Add storm-iceberg module: an Apache Iceberg sink bolt - #8950
Open
GGraziadei wants to merge 8 commits into
Open
Conversation
New external module writing Trident batches to Apache Iceberg tables directly from a topology, with exactly-once semantics. Each batch is committed in a single Iceberg transaction that atomically appends the data files and records the transaction id in the table property storm.trident.<topologyName>.<partitionIndex>.last-committed-txid, so replayed batches are detected and skipped even across worker crashes and commits whose outcome was unknown to the writer. Includes: - IcebergOptions: catalog properties passed verbatim to Iceberg's CatalogUtil.buildIcebergCatalog, so any catalog works with its standard keys; file format, target file size and table auto-creation. - RecordMapper with a field-name based default doing the standard primitive conversions; required columns without a value fail loudly. - Partitioned tables through Iceberg's fanout writer, one open data file per partition. - Per-batch table refresh, so schema and partition spec evolution is picked up without restarting the workers. - Metrics-v2 instrumentation: records written, data files and bytes committed, commit latency, commit failures, skipped replays. - A shutdown hook releasing the catalog client, since Trident's State has no lifecycle callback. - Documentation and two runnable example topologies, unpartitioned and partitioned.
…dent # Conflicts: # storm-dist/binary/final-package/src/main/assembly/binary.xml
…ingested per commit.
The Iceberg version was pinned in the module's own properties, where a project-wide dependency bump would not see it. It now lives in the root pom alongside hadoop.version, with the artifacts managed in dependencyManagement; storm-iceberg declares them without versions. This does not widen the dependency's reach. dependencyManagement fixes versions without adding dependencies, so modules that do not declare Iceberg still resolve none of it: storm-client and storm-server both show zero Iceberg artifacts. The direction also prevents it, since storm-iceberg depends on storm-client (provided) and nothing in core depends on storm-iceberg. Neither binary distribution bundles it either: like every other external/* connector, storm-iceberg ships only its README in both the full and the lite assembly, so Iceberg reaches only topologies that ask for it. The catalog implementations (iceberg-aws, -gcp, -hive, -nessie, ...) are deliberately absent from the managed set: they are supplied by the topology rather than pulled in transitively.
Contributor
|
@GGraziadei just checking in. This is still a WIP, correct? Cheers |
Member
Author
|
Hi Rui, yes, I'm still working on this. The current commit works in principle, but I'm exploring an approach to reduce both the commit count and the ACK latency. Once that's sorted out, I'll move the PR to Ready . |
IcebergBolt writes and commits in the same task, so a sink at parallelism N committing every T seconds produces N/T snapshots per second, each a metadata rewrite plus a compare-and-swap on the catalog. Past a certain parallelism the only way to keep that load down is to commit less often, which is to say to accept more latency. IcebergWriterBolt and IcebergCommitterBolt break that coupling. Writers seal batches on the same thresholds IcebergBolt uses, but instead of committing they emit one descriptor tuple carrying the batch's data files, anchored to every tuple of the batch, and then ack those tuples. Anchoring preserves the guarantee: acking an input after emitting an anchored child does not close the spout tuple's ack tree, so the source advances only once the descriptor is acked by the committer. A single committer accumulates descriptors and appends them in one Iceberg commit, so snapshot rate is set by the group-commit cadence rather than by writer parallelism. Measured on a 6-way sink at a 10 s cadence: 115 snapshots with IcebergBolt against 20 with the split sink, for the same 360k rows, the same 115 data files and the same visibility latency (p50 5,454 ms against 5,506 ms). Supporting changes: - DataFileCodec is extracted from CommitWal, since the descriptor tuples and the WAL now share one data-file serialisation. - The commit WAL is keyed by component and task index rather than task id, so a writer and a committer in the same topology cannot collide and a recovered task finds its own entries. - IcebergOptions gains group-commit thresholds (interval and max data files) and a per-component tick interval, so writer and committer can be paced independently. - Seal metrics and pending-commit gauges, so a stalled committer is visible. - Pending state stays visible for the duration of a commit, and write errors fail fast rather than being deferred to the seal. - A split-sink example topology, with both shapes documented.
GGraziadei
marked this pull request as ready for review
August 8, 2026 15:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the purpose of the change
Adds
storm-iceberg, a sink that writes tuples from a Storm topology directlyinto an Apache Iceberg table, with no Kafka Connect or Spark job in between.
The guarantee is atomic commits with at-least-once delivery. A batch becomes
visible in one Iceberg append or not at all, so readers never see a partial
batch; tuples are acked only after the commit containing them has landed, so a
crash costs orphan data files and a replay rather than lost rows. Duplicates are
possible and are not removed — the sink is append-only and writes no equality
deletes — so they stay visible until something downstream dedupes.
Exactly-once is deliberately not claimed. It would require a deterministic
identity of the input, which comes from the source rather than the sink, and a
general-purpose module cannot assume every user has a replayable,
deterministically addressed source.
Two sink shapes
IcebergBoltwrites and commits in the same task. Simple, but a sink atparallelism N committing every T seconds produces N/T snapshots per second,
each a metadata rewrite plus a compare-and-swap on the catalog.
IcebergWriterBolt+IcebergCommitterBoltbreak that coupling. Writers sealbatches on the usual thresholds and emit one descriptor tuple carrying the
batch's data files, anchored to every tuple of the batch, then ack those
tuples; anchoring keeps the spout's ack tree open, so the source advances only
once the committer has committed. A single committer groups descriptors into
one Iceberg commit, so snapshot rate follows the group-commit cadence instead
of writer parallelism.
Measured on a 6-way sink at a 10 s cadence, same 360k rows: 115 snapshots
with
IcebergBolt, 20 with the split sink, the same 115 data files and thesame visibility latency (p50 5,454 ms vs 5,506 ms).
Commit recovery
Data files are made durable first, then a write-ahead log entry naming them is
written under the table's own metadata location with a freshly minted commit id,
then the files are appended in one operation that stamps that id on the snapshot
summary, then the entry is deleted and the tuples are acked. On startup a task
asks the table whether each pending commit id is present in a snapshot,
replaying only those that are absent — the table answers the question, so no
identity from the source is needed. A failed commit is settled in flight rather
than left to the next startup: if it landed, the tuples are acked; if it did
not, the WAL entry is dropped before the tuples are failed, so the replay writes
those rows exactly once.
Scope and packaging
merge-on-read are out of scope.
remove_orphan_files,rewrite_data_files,expire_snapshots) is out of scope but documented as still necessary.external/*connector it ships only its README, in both the full and the lite assembly.
iceberg-aws,-gcp,-hive,-nessie, ...) aresupplied by the topology, not pulled in transitively.
dependencyManagement, so aproject-wide dependency bump sees it.
What this is, and is not
An earlier revision of this PR exposed a Trident
State. It is now a bolt,following feedback on the dev list.
Benchmarking against a Kafka + Spark Structured Streaming path (same Iceberg
release, same REST catalog) found the two equivalent at matched commit
cadence — latency within 4 %, file count within 1 %, snapshot count identical
once the split sink is used. Claims for this module resting on latency or
throughput would not be supportable, and none are made here.
The one capability the indirect path does not have is closing a commit on
accumulated bytes rather than elapsed time. Under a 12:1 burst profile that
produced files whose median equalled their maximum (1,644 KB, CV 0.33) against
42–792 KB for the time-triggered job (CV 0.78, 18.8× spread), at 3.1× fewer
files — costing latency (p50 41 s vs 5.3 s). Where predictable table layout
under variable load matters more than freshness, that is the reason to use this
sink. Otherwise the indirect path remains the right choice.
How was the change tested
62 tests, all green, plus checkstyle and PMD, on JDK 25
(
mvn verify -pl external/storm-iceberg,examples/storm-iceberg-examples):IcebergCommitterTestIcebergOptionsTestIcebergCommitterBoltTestIcebergBoltTestFieldNameRecordMapperTestIcebergWriterBoltTestIcebergWriterTestCommitWalTestDataFileCodecTestIcebergMetricsTestIcebergSplitSinkTestThe tests run against a real Iceberg table (
HadoopCatalogover a JUnit@TempDir), not mocks of Iceberg. What they cover:IcebergGenericsand asserted — for both sink shapes, including the writerand committer wired together.
appeared is replayed on startup; one that is already visible is dropped
rather than appended twice.
successful and its tuples acked; one that did not land clears its WAL entry
before failing, so the source's replay writes those rows exactly once. Both
are exercised with a real table and only the append made flaky.
writer's input tuples, so a failed group commit fails the whole batch back to
the source.
a threshold commits and acks the whole batch, a tick tuple flushes a partial
batch, and a failed commit fails the buffered tuples instead of acking them.
buffered-byte accounting.
iceberg-commit-failuresincrement corresponds to replayed tuples.Beyond the unit tests, both sink shapes were run on a 2-supervisor Storm cluster
against Kafka and an Iceberg REST catalog, at up to 1.17M records per run, with
row counts, duplicate counts and file/snapshot profiles verified from the
Iceberg metadata after each run.
Three operational constraints found that way are documented in
docs/storm-iceberg.md, since each fails silently:topology.message.timeout.secs, or everytuple is replayed before its commit lands and then committed anyway.
are all retained until it is sealed.
file, so a task owning k partitions writes k files of about B/k.
Not covered, and worth knowing before merge:
catalogs, and S3 / object-store
FileIO, are untested here.optimistic retries, which were observed retrying successfully under load but
not deliberately stressed.
warehouse; on object storage the commit path is dominated by request latency
and the snapshot-count results in particular may differ.