Skip to content

Discover and upload Iceberg tables alongside Hudi - #189

Closed
tiennguyen-onehouse wants to merge 9 commits into
mainfrom
iceberg-metrics-pr2-lakeview
Closed

Discover and upload Iceberg tables alongside Hudi#189
tiennguyen-onehouse wants to merge 9 commits into
mainfrom
iceberg-metrics-pr2-lakeview

Conversation

@tiennguyen-onehouse

@tiennguyen-onehouse tiennguyen-onehouse commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Teaches LakeView to detect Iceberg table roots (the metadata/ folder) in addition to Hudi (.hoodie/), and upload the current metadata.json pointer file to the Onehouse temp bucket on each iteration. The control plane parses the snapshot summary (which carries cumulative total-records, total-files-size, total-data-files, per-snapshot deltas, and the snapshot-log[] history) — gateway-controller side is a separate PR.

See docs/iceberg-support.md for the full design.

Abstractions

  • TableFormatDetector SPI with HudiTableFormatDetector (existing .hoodie/ check moved out of TableDiscoveryService.isHudiTableFolder) and IcebergTableFormatDetector (matches when a directory listing contains a metadata/ subdirectory and that subdirectory holds a *.metadata.json). TableDiscoveryService picks the single detector matching each Database's declared tableFormat and tags each discovered Table with its format.
  • IcebergMetadataUploaderService as a sibling of TableMetadataUploaderService rather than a branch inside it. Hudi's active/archived timeline distinction, hoodie.properties bootstrap, and LSM manifest plumbing don't apply to Iceberg, so a separate service is clearer than a branching megaclass. It reuses OnehouseApiClient, PresignedUrlFileUploader, AsyncStorageClient, and StorageUtils directly.
  • TableDiscoveryAndUploadJob.dispatchUpload partitions discovered tables by format and runs the two uploaders concurrently; both runOnce and processTables route through it.

Wire

  • New TableFormat enum (HUDI, ICEBERG) in api/models/request/, JSON-aliased to the proto enum names (TABLE_FORMAT_HUDI / TABLE_FORMAT_ICEBERG) so the control-plane parser accepts the value.
  • Table model gains tableFormat (defaults to HUDI for backward compat).
  • InitializeSingleTableMetricsCheckpointRequest gains a nullable tableFormat. Server treats absent as HUDI. tableType (COW/MOR) stays @NonNull to preserve the existing wire contract; Iceberg init passes COW as a meaningless placeholder since the server discriminates on tableFormat first.
  • s3a:// accepted by OBJECT_STORAGE_URI_PATTERN alongside s3:// (Glue metadata_location scheme).

Selecting the current metadata.json

Exactly one file is uploaded per iteration — the current metadata.json — resolved through three paths in order of preference:

  1. metadataLocationHint from the parser YAML's table hints (catalog-driven, e.g. Glue metadata_location): PUT the file directly, skip the listing. Only applies to #tableId-pinned base paths.
  2. metadata/version-hint.text (Hadoop catalog): its integer content names the current v{N}.metadata.json unambiguously.
  3. Numeric-aware filename comparison (last resort): the leading integer in the filename (after an optional v prefix) is the version number — correct for both v{N}.metadata.json (Hadoop) and 00000-<uuid>.metadata.json (Hive/Glue/Spark) without depending on zero-padding. ⚠️ This can select an uncommitted metadata.json if neither a hint nor version-hint.text is present; paths 1–2 are authoritative.

Checkpoint tracks the last uploaded filename; if it hasn't changed since last run, the iteration is a no-op.

Deployment ordering ⚠️

This change spans three repos and must roll out in order:

  1. idlsTableFormat proto enum / TableMetricsCheckpoint field (idls PR #1939, 80d81701).
  2. gateway-controllerIcebergCommitMetadataParser parses the uploaded metadata.json and emits to OpenSearch.
  3. LakeView (this PR).

If LakeView ships before the server understands tableFormat, the server-side protobuf JSON parser drops the unknown enum value and persists TABLE_FORMAT_INVALID, routing Iceberg uploads through the Hudi back-compat fallback. Do not deploy this until the running control plane tolerates tableFormat=ICEBERG cleanly. See the deploy-ordering section of docs/iceberg-support.md.

Test plan

  • ./gradlew :lakeview:test --tests "ai.onehouse.metadata_extractor.*" green locally
  • Instance-pipeline coverage for IcebergMetadataUploaderService (~97% line) + detector pairs + dispatchUpload routing
  • Existing TableDiscoveryServiceTest / TableDiscoveryAndUploadJobTest pass after constructor signature changes
  • CI green
  • Manual: end-to-end against s3://aadsharma-quanton-test/spark4_iceberg_variant.db/t_variant_1/ once control-plane parser lands

🤖 Generated with Claude Code

tiennguyen-onehouse and others added 2 commits May 12, 2026 19:30
Extends LakeView to find Iceberg tables (metadata/*.metadata.json) in addition
to Hudi tables (.hoodie/) and upload their current metadata.json to the
control plane for parsing. Iceberg upload is a parallel orchestrator rather
than a Hudi-coupled branch: the active/archived timeline, hoodie.properties
bootstrap, and LSM manifest plumbing don't apply.

Abstractions:
- TableFormatDetector SPI with HudiTableFormatDetector + IcebergTableFormatDetector
  implementations. TableDiscoveryService iterates registered detectors; the
  first match determines the format and tags the Table.
- New IcebergMetadataUploaderService sibling of TableMetadataUploaderService.
  Reuses OnehouseApiClient, PresignedUrlFileUploader, AsyncStorageClient.
- TableDiscoveryAndUploadJob.dispatchUpload partitions discovered tables by
  format and runs the two uploaders concurrently.

Wire:
- TableFormat enum (HUDI, ICEBERG) added to api/models/request.
- Table model carries tableFormat (default HUDI for backward compat).
- InitializeSingleTableMetricsCheckpointRequest gains tableFormat (nullable;
  server treats absent as HUDI). TableType (COW/MOR) stays @nonnull; Iceberg
  uploads pass COW as a meaningless placeholder since the server discriminates
  on tableFormat first.

Depends on idls PR #1939 for ObservedTableFormat on the proto side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LakeviewSyncTool builds TableDiscoveryService and TableDiscoveryAndUploadJob
manually (no Guice in the sync-tool entry path), so the constructor
signature changes from the parent commit broke its compile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-image iceberg-metrics-pr2-lakeview-may13

Adds the LakeView half of the fast-path that lets us skip the per-cycle
S3 LIST on Iceberg tables when the control plane already knows the
current metadata.json URI (e.g. from AWS Glue's metadata_location
parameter on the catalog entry).

- New TableHint POJO and Database.tableHints map (keyed on tableId).
  Older YAML versions don't carry this field; Jackson leaves it null
  and discovery falls through to the existing listing-based path.
- Table model gains metadataLocationHint (optional).
- TableDiscoveryService merges per-database tableHints into a single
  tableId -> hint map and attaches metadataLocationHint to each Table
  it discovers, when the tableId matches a hint.
- IcebergMetadataUploaderService.uploadIfNewMetadataJson branches on
  the hint: if set, derive the filename, compare against the
  checkpoint, and PUT directly to the hint URI. If the checkpoint
  already matches, no-op without touching S3 at all. Falls back to the
  existing LIST behavior when no hint is provided.

The control-plane producer (gw-agent MetricsExtractorFileUpdater) does
not yet emit the new field into the YAML — that needs a lakeview-config
artifact version bump on the gateway-controller side. Once that lands,
the fast-path activates automatically; until then, tableHints stays
null and behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-jar iceberg-metrics-pr2-lakeview-may13

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-jar iceberg-lakeview-may13

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-jar iceberg-lakeview-may13-v2

…tion

Concern 2: the previous IcebergTableFormatDetector matched any directory
containing a sub-directory named "metadata", which produced false positives
across customer warehouses (Spark checkpoint dirs, custom layouts, schema
folders, etc.). Replace the SPI ordering with per-Database declarative
routing — each Database in the parser YAML now declares its tableFormat
(default HUDI for backward compat), and TableDiscoveryService picks the
single matching detector for that database. The Iceberg detector also
becomes strict: requires metadata/ AND at least one *.metadata.json inside
it (one extra LIST during discovery only, not per upload cycle).

Concern 1: pickLatestMetadataJson lex-sorted filenames, so for the Hadoop
catalog naming v{N}.metadata.json (unpadded), v2.metadata.json beat
v10.metadata.json — a stale pointer. Resolve in three tiers: catalog hint,
then metadata/version-hint.text when present (canonical Iceberg lookup),
then numeric-aware sort on the leading integer. Empty metadata/ now
increments a NO_SUCH_KEY failure counter instead of silently returning
true, so phantom tables surface in dashboards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-jar iceberg-lakeview-may13-v2

S3 ListObjectsV2 surfaces "subdirectories" as CommonPrefixes whose
Prefix string carries a trailing slash (e.g. "metadata/"). The storage
client preserves that in File.filename, so the exact-string check
"metadata".equals(filename) fails and the detector returns false even
though the table layout is a valid Iceberg root.

Concrete repro in staging (Testing-Acme org, table
s3://acme-data-2/iceberg_tables/iceberg_table_test_may13/):
  - S3 list of the table base returns a single CommonPrefix "metadata/"
  - The agent emits tableFormat=ICEBERG + metadataLocationHint in the
    extractor YAML, the v2 lake-view image parses it fine, the
    discovery walks the base path
  - Detector returns false at the base, discoverTablesInPath recurses
    into metadata/, no Iceberg Table is ever emitted, dispatchUpload
    is called with an empty Iceberg set, IcebergMetadataUploaderService
    early-returns, nothing reaches the temp bucket

Strip a single trailing slash before comparing, so both "metadata" and
"metadata/" are accepted. Hudi's detector dodges this naturally via
startsWith(".hoodie"); the Iceberg one switched to equals() and lost
the same tolerance.

Add a unit test that hands the detector a File with filename="metadata/"
— the shape S3 actually produces — which fails on main and passes here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-jar iceberg-lakeview-may13-v3

@tiennguyen-onehouse tiennguyen-onehouse left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-image iceberg-lakeview-may13-v3

LakeView's InitializeTableMetricsCheckpoint request sends tableFormat
as a Jackson-serialized enum value. Without an explicit @JsonProperty
the wire string was the short Java identifier ("HUDI" / "ICEBERG"),
which protobuf-java-util's JSON parser on external-api cannot map
onto lake.TableFormat (the proto enum uses the canonical names
"TABLE_FORMAT_HUDI" / "TABLE_FORMAT_ICEBERG"). The mismatch is silent:
the parser falls back to enum-zero (TABLE_FORMAT_INVALID), the
checkpoint is persisted without the field, and GenerateCommitMetadata
UploadUrlHandler routes the table through the Hudi back-compat path.
For Hudi this happens to be correct so nothing surfaces; for Iceberg
the filename regex rejects the metadata.json with 400.

Annotate the enum values so the JSON wire string matches the proto
enum name on both sides of the contract. Java callsites remain
unchanged.

@dharmendersheshma dharmendersheshma left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-image iceberg-lakeview-may14-v1

StorageUtils.getBucketNameFromUri / getPathFromUrl were rejecting
s3a:// URIs with IllegalArgumentException, breaking
IcebergMetadataUploaderService for every newly-discovered iceberg
table on staging-acme. The agent stamps Firestore metadataLocationHint
as s3a:// (gateway-controller commit 0761ef368d, so the existing
GetTableMetricsHandler OpenSearch keyword filter matches), so every
metadata.json upload was throwing inside PresignedUrlFileUploader →
S3AsyncStorageClient.streamFileAsync → StorageUtils.getBucketNameFromUri.

Extend OBJECT_STORAGE_URI_PATTERN to accept the s3a:// scheme alongside
s3:// — both resolve to the same bucket/key, and the AWS SDK only ever
sees the parsed bucket + key, so this is purely a parser-tolerance
change.

Test: StorageUtilsTest now asserts s3a://bucket/path/to/file parses to
the same bucket name and path as s3://, plus the bare-bucket edge case.

@dharmendersheshma dharmendersheshma left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/push-image iceberg-lakeview-may14-v2

tiennguyen-onehouse and others added 2 commits May 21, 2026 22:17
Code review fixes:
- IcebergMetadataUploaderService: increment the table-processing failure
  counter on the four non-exception failure returns (checkpoint fetch,
  init, presigned-URL, upsert) so API-level failures show on dashboards,
  not just exceptions.
- Document that the numeric pickLatestMetadataJson fallback can select an
  uncommitted metadata.json; hint / version-hint.text are authoritative.
- Note that thenComposeAsync uses the default pool intentionally (non-
  blocking glue) and that metadataLocationHint only applies to #tableId-
  pinned base paths, not auto-discovery.

SonarCloud:
- S6204: Stream.collect(toList()) -> Stream.toList() in
  IcebergMetadataUploaderService and TableDiscoveryService (drop now-
  unused Collectors imports).
- S5411: avoid Boolean unboxing in processTable (Boolean.FALSE.equals).
- S6068: remove useless eq() matchers in TestIcebergTableFormatDetector.
- S5843 (regex complexity) skipped: reducing it 21->20 without loosening
  URL validation is not worth the risk for a non-gating smell.

Coverage (quality gate was failing new_coverage at 41.3%):
- Add instance-pipeline tests for IcebergMetadataUploaderService
  (uploadInstantsInTables, processTable, parse/init/upload/upsert,
  version-hint resolution, hint fast-path, failure branches) ->
  ~97% line coverage on the class.
- Add dispatchUpload format-routing test to TableDiscoveryAndUploadJobTest.

Docs: new docs/iceberg-support.md covering the detector SPI, tableFormat
declaration, three-tier metadata.json resolution, and the cross-repo
deploy ordering (idls -> gateway-controller -> LakeView).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Future

CI compiles on Java 8. Revert the two Sonar S6204 Stream.toList() changes
back to Collectors.toList() (toList is Java 16+), and replace Set.of /
CompletableFuture.failedFuture in the new tests with Guava ImmutableSet and
a local failedFuture helper. S6204 is won't-fix on this toolchain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@dharmendersheshma

Copy link
Copy Markdown
Contributor

#190
#191

merged to main as part of above two PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants