Repository navigation
chore(iceberg): move Iceberg export to iceberg-go v0.7.0 - #1131
Merged
Merged
Conversation
v0.7.0 drops a manifest left with no surviving entries instead of carrying it forward in every later snapshot, which is the upstream half of #1106 (apache/iceberg-go#1153, #1393), and it parallelises the manifest scan. Measured on the same 300-file table, one file replaced per pass: manifests scan plan pass cost per manifest v0.6.0 129 after 64 passes 18.62 ms ~1.6 ms v0.7.0 66 after 64 passes 7.12 ms ~0.24 ms Arc needed no source changes for the bump. What this commit carries is the behavioural fallout. A dotted Iceberg namespace is now refused rather than published broken. Arc builds ONE namespace component per database, nsPrefix + "_" + sanitizeNamespaceDB(database), and v0.7.0 addresses a namespace whose component contains a dot by a JSON-encoded catalog key rather than the plain dotted string, deliberately refusing the legacy key. A table published that way is unreachable: the v0.6.0 table and its history are orphaned, the warehouse directory becomes __iceberg_namespace_v1__:["arc_x.y"].db which isWarehouseDir does not recognise and Measurements() then walks back in as a user database, and the percent-encoded metadata location leaves no version-hint.text for directory readers, which also declines the fingerprint cache so the measurement re-reconciles every tick forever. Two refusals, at the two scopes the problem has: - iceberg.namespace_prefix containing a dot is refused at config load, because it would affect every database on the node. - a database whose namespace component would contain a dot is refused per measurement, logged and skipped, so the rest of the node keeps exporting. This covers what a config check cannot see: an edge-sync spoke registering after startup. Arc database names are dot-free by their create-time rule; a spoke ID may carry a single dot, since validateSpokeID rejects "/", "\", ":" and ".." but not ".". The permanent fix — emitting an addressable namespace, and migrating the tables already published under a dotted one, which have to ship together — is #1129. manifestCollapseThreshold stays at 12, re-derived: growth is one manifest per append pass instead of two per removal pass, and the per-manifest cost fell to ~0.24 ms, which moves the optimum to sqrt(C(N)/0.12) = 6.5 at 300 live files and 23.5 at 10 000. Twelve is within ~1.25x across that range. Six comments that v0.7.0 made false are rewritten rather than left to rot: the #633 tombstone premise, the empty-manifest refusal, collapseManifests' purpose, the RemoveProperties note, the snapshot-age default (which was wrong for v0.6.0 and is correct for v0.7.0), and the threshold derivation. TestManifestCollapse_EmptyingPassSucceeds and TestManifestCollapse_AllSurvivorsSkippedDoesNotCollapse no longer discriminate: v0.7.0 drops the entry-less manifests that made the merge illegal, so both pass with their guard removed. They are kept as behaviour pins and labelled as such, and the guards stay as belt-and-braces - reachability returns with a multi-spec table or any change to that drop. TestManifestCollapse_BelowThresholdLeavesManifestsAlone had hard-coded v0.6.0's two-per-pass growth and now asserts that the count never drops, which is what a collapse actually looks like. tableDataFiles gains a comment: v0.7.0 can plan several tasks for one data file above read.split.target-size, and the function is correct only because it is a map keyed on path storing the FILE's size. New transitive dependencies, all inert for Arc: OpenTelemetry's API, RoaringBitmap, and geospatial encoders, for deletion vectors, geometry columns and remote scan planning. No telemetry is registered or emitted - the OTel SDK is not in the module graph, the only instrumented path needs a scan-planning mode Arc never selects, and the reporter defaults to a no-op. Verified on a running node with iceberg.namespace_prefix=wh_nondefault, retain_snapshots=3, reconcile_interval=5: the dotted prefix refuses at startup; the warehouse lands at wh_nondefault_hv.db and publishes version-hint.text; the manifest count climbs 2,3,4...12 over twelve append passes and the next pass collapses it to 1; DuckDB reads the table throughout; no Iceberg error or warning. Full suite green under go test -tags=duckdb_arrow -race -count=1 ./... (39 packages, 0 races). Refs #1106, #835. Follow-up: #1129.
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.
Summary
Moves Arc's Iceberg exporter from
iceberg-gov0.6.0 to v0.7.0. No Arc source changes were needed for the bump itself — it is source-compatible. What this PR carries is the behavioural fallout, which is where the risk in a bump like this lives.Why
v0.7.0 drops a manifest left with no surviving entries instead of carrying it into every later snapshot, and parallelises the manifest scan. That is the upstream half of #1106 — and it is already fixed upstream, so no new issue was filed:
apache/iceberg-go#1153and#1393cover it and are closed. Measured on the same 300-file rig, one file replaced per pass:Per accumulated manifest, a reconcile pass costs ~0.24 ms instead of ~1.6 ms. The
.avrocount on disk is unchanged (258 after 64 passes on both), so #835's orphan sweep is still the only thing reclaiming metadata files.Blocker found and mitigated: a dotted namespace becomes unaddressable
Arc builds one namespace component per database,
nsPrefix + "_" + sanitizeNamespaceDB(database). v0.7.0's SQL catalog addresses a namespace whose component contains a dot by a JSON-encoded key rather than the plain dotted string, and deliberately refuses the legacy key:Measured:
Three failures stack, none of them loud: the v0.6.0 table and its whole history are orphaned while
EnsureTablecorrectly creates a new one; the new directory__iceberg_namespace_v1__:[...].dbis not matched byisWarehouseDir, soMeasurements()walks the exporter's own metadata back in as a user database (the exact harm the/→.mapping exists to prevent, #634); andMetadataLocation()is percent-encoded while the directory is not, so noversion-hint.textis published and the declined fingerprint cache re-reconciles that measurement every tick forever.Mitigation here — two refusals, at the two scopes the problem has:
iceberg.namespace_prefixcontaining a dot is refused at config load, since it would affect every database on the node.checkNamespaceAddressable, called fromEnsureTable), logged and skipped, so the rest of the node keeps exporting — matching how a column-type conflict is already handled. This covers what a config check structurally cannot: an edge-sync spoke registering after startup.I deliberately did not add a node-wide startup refusal: one dotted spoke should not stop a hub from booting, and the per-measurement guard already prevents every new broken table.
Reachability. Arc database names are dot-free by their create-time rule. The one source that permits a dot is an edge-sync spoke ID —
validateSpokeIDrejects/,\,:and.., but not., sorocket.01is valid today, and the Iceberg source receives spoke namespaces unconditionally when both features are on.What is still unfixed: a table already published under a dotted namespace is now refused rather than served, and there is no migration. #1129 covers the permanent fix — emitting an addressable namespace and migrating existing tables, filed as one issue because shipping the first alone would orphan tables the same way the bump does.
Threshold re-derived; it stays at 12
Growth is now one manifest per append pass instead of two per removal pass, and the per-manifest cost fell ~6.7×.
T* = sqrt(C(N)/0.12)= 6.5 / 8.2 / 16.6 / 23.5 at 300 / 1000 / 5000 / 10000 live files, so 12 is within ~1.25× across the range — better behaved than on v0.6.0.Live measurement sharpened what the collapse is actually for. Only append-only passes grow the set: a removal-only pass usually adds nothing (v0.7.0 drops the manifest it empties), and a pass containing an in-place rewrite already merges the whole set as a side effect of #633's re-registration. The collapse is the backstop for append-only growth, and a table both written and deleted from reaches the threshold slowly or never.
Honesty about the tests
Two regression tests from #1124 no longer discriminate and are labelled as such rather than left looking like proofs.
EmptyingPassSucceedsandAllSurvivorsSkippedDoesNotCollapseboth pass with their guard removed, because v0.7.0 drops the entry-less manifests that made the merge illegal —ErrEmptyManifeststill exists and is still returned byManifestWriter.Close, but reaching it needs ≥2 inherited data manifests whose entries are all foreign-DELETED, and an emptying pass now inherits exactly one, takingmergeBin's single-manifest passthrough. The guards are kept: one comparison, they encode the intent, and reachability returns with a multi-spec table or any change to that drop.BelowThresholdLeavesManifestsAlonehad hard-coded v0.6.0's1 + 2*3growth. It now asserts the count never drops, which is what a collapse actually looks like, so it cannot encode a library constant again.Six comments corrected
v0.7.0 falsified them; all rewritten in this PR rather than left to rot: the #633 tombstone premise, the empty-manifest refusal,
collapseManifests' purpose, theRemovePropertiesnote (v0.7.0 adds it), the snapshot-age default (which was wrong for v0.6.0 — it defaulted to effectively forever, not 5 days — and is now correct), and the threshold derivation.Also commented:
tableDataFilesis correct only because it is a map keyed on path storing the file's size. v0.7.0 can plan several tasks for one Parquet file aboveread.split.target-size(128 MiB), solen(tasks), a slice, ortask.Lengthwould each double-count. Arc's own files are far below that; bulk-imported Parquet need not be.New dependencies
OpenTelemetry's API, RoaringBitmap v2, and geospatial encoders — for deletion vectors, geometry columns and remote scan planning, none of which Arc uses. No telemetry is registered or emitted: the OTel SDK is not in Arc's module graph, the only instrumented path requires a scan-planning mode Arc never selects, and the metrics reporter defaults to a no-op. No new network traffic, no new log output. Flagging it explicitly because Arc dropped
curlfrom the container to shed 22 CVEs, so added dependency surface is a deliberate trade here, not a free one.Configuration matrix
iceberg.enabled=false(default)if cfg.Iceberg.Enablediceberg.enabled=trueEnsureTable→checkNamespaceAddressablebeforetableIdentscheduler.go:142); no new cluster field touchediceberg.namespace_prefixdotted (NON-DEFAULT)iceberg.namespace_prefix=wh_nondefault(NON-DEFAULT, valid)isWarehouseDirkeys off the prefixwh_nondefault_hv.db/cpu,version-hint.textpublished, DuckDB reads iticeberg.retain_snapshots=3(NON-DEFAULT)WithOlderThan(0)iceberg.reconcile_interval=5(NON-DEFAULT)tableDataFilesTest plan
go test -tags=duckdb_arrow -race -count=1 ./...— 39 packages ok, 0 FAIL, 0 DATA RACE (internal/cluster156s,internal/api121s,internal/ingest88s, so the suite really ran)-race -count=20on the new and changed tests: ok, 143sinternal/iceberg/namespace_guard_test.go: the guard refusesrocket.01,site.a.b,rocket/telemetryand a dotted prefix, acceptsmydb/rocket-01/rocket_01; a reconcile for a dotted database errors, writes no catalog row and no warehouse directory, and a dot-free database on the same exporter still exports. Fails without the guard:reconcile accepted a dotted database: it would publish an unreadable tablemy.warehouse,.arc,arc.refused;arc,arc_wh,my-warehouseacceptedgo build ./cmd/... ./internal/...,gofmt -l,go vet: cleanmainand re-verifiediceberg.namespace_prefix=wh_nondefault+retain_snapshots=3+reconcile_interval=5(three non-default keys):wh_nondefault_hv.db/cpu;version-hint.textpublished under the non-default prefix2,3,4…12over twelve append passes, then the next pass collapses to1, logged onceiceberg_scanreads the table before and after; no Iceberg error or warningRefs #1106, #835. Follow-up: #1129.