Skip to content

Address wrap-up items for Iceberg side input table cache - #40222

Merged
jrmccluskey merged 7 commits into
apache:masterfrom
jrmccluskey:feat/iceberg-side-input-table-cache-wrapup
Sep 23, 2026
Merged

jrmccluskey merged 7 commits into
apache:masterfrom
jrmccluskey:feat/iceberg-side-input-table-cache-wrapup

Conversation

@jrmccluskey

Copy link
Copy Markdown
Contributor

Completes a handful of cleanup tasks for the side input cache:

  • Binds historical sort orders unchecked via SortOrderParser.fromJson(schema, node, getOrderId()) in SerializableTableSpec.
  • Exposes Hadoop Configuration via IcebergCatalogConfig.getHadoopConfiguration() and propagates to SerializableTableSpec.getFileIO(conf) through SideInputTable in write transforms.
  • Adds worker-local transient Guava cache to ExtractTableIdsDoFn to eliminate hot-key shuffle bottlenecks

Fixes #39723


Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md

GitHub Actions Tests Status (on master branch)

Build python source distribution and wheels
Python tests
Java tests
Go tests

See CI.md for more information about GitHub Actions CI or the workflows README to see a list of phrases to trigger workflows.

- Bind historical sort orders unchecked via SortOrderParser.fromJson(schema, node, getOrderId()) in SerializableTableSpec.
- Expose Hadoop Configuration via IcebergCatalogConfig.getHadoopConfiguration() and propagate to SerializableTableSpec.getFileIO(conf) through SideInputTable in write transforms.
- Add worker-local transient Guava cache to ExtractTableIdsDoFn to eliminate hot-key shuffle bottlenecks on streaming writes.
- Add comprehensive unit and integration tests.
@github-actions github-actions Bot added java io iceberg Iceberg IO connector labels Sep 22, 2026
…tion in tests

- Ensure TableMetadataDriver passes its Clock to ExtractTableIdsDoFn so virtual-time advancement in tests (e.g. testUnusedTablesEvictedFromStreamingCache) correctly expires worker-local table ID caches instead of using wall-clock time.
- Fixes testUnusedTablesEvictedFromStreamingCache flakiness on CI environments with limited concurrency.
… cache TTL

- Advance ControllableTestClock on trigger rows in testMetadataRefreshedAcrossIntervals, testMetadataRefreshedAcrossIntervalsAsSideInput, and testMetadataRefreshedAcrossIntervalsAsSideInputWithMultipleTables so worker-local table ID caches expire as intended across virtual intervals.
- In testStreamingSpecEvolutionWithoutPipelineRestart, wait for the worker-local cache TTL (500ms) to elapse after spec evolution before emitting the second element.
- Add library.java.jackson_databind to dependencies in sdks/java/io/iceberg/build.gradle to resolve usedUndeclaredArtifacts dependency analysis warning caused by JsonNode usage in SerializableTableSpec.

@VisibleForTesting
public abstract @Nullable Clock getClock();

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.

nit: make this and setClock package private

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.

done

ValueInSingleWindow.of(element, timestamp, window, paneInfo));
if (tableIdentifier != null && !tableIdentifier.trim().isEmpty()) {
out.output(tableIdentifier.trim());
String canonicalId = tableIdentifier.trim();

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.

readability nit: I don't think we need the outer if (tableIdentifier != null && !tableIdentifier.trim().isEmpty()) check.

Also let's work directly with tableIdentifier instead of trimming it into a "canonical" version

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.

done

String canonicalId = tableIdentifier.trim();
if (localTableIdCache == null && refreshInterval != null) {
initCache();
}

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.

readability nit: Do we need this if we're already calling it in setup()?

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.

removed, was a codegen add-in because a unit test directly called ProcessElement() without calling setup() first and the obvious "fix" was to check that the cache was initialized in ProcessElement()

table.updateSpec().addField("city").commit();
// Ensure worker-local table ID cache TTL (interval / 2 = 500ms) has elapsed
try {
Thread.sleep(600);

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.

give this a little extra time to avoid potential flakes?

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.

done

Comment on lines +349 to +351
private final @Nullable Clock clock;
private transient @Nullable Ticker ticker;
private transient @Nullable Cache<String, Boolean> localTableIdCache;

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.

Lots of complexity here. I feel like we can simplify with just a simple LinkedHashMap<String, Long> LRU that tracks tableIds and the last emitted millis. So only emit when (now - lastEmitted) > refreshInterval and update lastEmitted=now.
Both ways probably work though so will leave it up to you

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.

Let's go with the simpler implementation

@github-actions

Copy link
Copy Markdown
Contributor

Assigning reviewers:

R: @ahmedabu98 for label java.
R: @chamikaramj for label iceberg.

Note: If you would like to opt out of this review, comment assign to next reviewer.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

The PR bot will only process comments in the main thread (not review comments).

…p LRU

- Replace Guava Cache, CacheBuilder, and Ticker in ExtractTableIdsDoFn with a standard LinkedHashMap-based LRU cache capped at 10,000 entries.
- Track lastEmitted timestamp per table identifier and emit when (now - lastEmitted) >= refreshInterval / 2.
- Remove getClock() and setClock() from TableMetadataDriver and its Builder to keep the AutoValue API clean and free of testing methods.
- Provide testing hooks via static globalTestClock and instance setClock on ExtractTableIdsDoFn.

@ahmedabu98 ahmedabu98 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.

LGTM, thank you!

@jrmccluskey

Copy link
Copy Markdown
Contributor Author

Test failure was a PubSubToKafkaE2E test flake, merging

@jrmccluskey
jrmccluskey merged commit c462e49 into apache:master Sep 23, 2026
22 of 24 checks passed
jrmccluskey added a commit that referenced this pull request Sep 23, 2026
…#40248)

* Expose Public API for Iceberg Side Input Cache (#40140)

* Expose Public API for Iceberg Side Input Cache

* Update sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java

Co-authored-by: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com>

* Streamline API, remove redundant checks

* Check polled table metric

* Evolve Iceberg schema mid-execution in streaming side input cache test

* Throw if sub-options are enabled without side input cache enabled

* test with partition spec evolution

---------

Co-authored-by: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com>

* Address wrap-up items for Iceberg side input table cache (#40222)

* Address wrap-up items for Iceberg side input table cache

- Bind historical sort orders unchecked via SortOrderParser.fromJson(schema, node, getOrderId()) in SerializableTableSpec.
- Expose Hadoop Configuration via IcebergCatalogConfig.getHadoopConfiguration() and propagate to SerializableTableSpec.getFileIO(conf) through SideInputTable in write transforms.
- Add worker-local transient Guava cache to ExtractTableIdsDoFn to eliminate hot-key shuffle bottlenecks on streaming writes.
- Add comprehensive unit and integration tests.

* Propagate Clock to ExtractTableIdsDoFn for deterministic cache expiration in tests

- Ensure TableMetadataDriver passes its Clock to ExtractTableIdsDoFn so virtual-time advancement in tests (e.g. testUnusedTablesEvictedFromStreamingCache) correctly expires worker-local table ID caches instead of using wall-clock time.
- Fixes testUnusedTablesEvictedFromStreamingCache flakiness on CI environments with limited concurrency.

* Make streaming refresh and spec evolution tests robust against worker cache TTL

- Advance ControllableTestClock on trigger rows in testMetadataRefreshedAcrossIntervals, testMetadataRefreshedAcrossIntervalsAsSideInput, and testMetadataRefreshedAcrossIntervalsAsSideInputWithMultipleTables so worker-local table ID caches expire as intended across virtual intervals.
- In testStreamingSpecEvolutionWithoutPipelineRestart, wait for the worker-local cache TTL (500ms) to elapse after spec evolution before emitting the second element.

* Declare jackson-databind dependency in Iceberg IO module

- Add library.java.jackson_databind to dependencies in sdks/java/io/iceberg/build.gradle to resolve usedUndeclaredArtifacts dependency analysis warning caused by JsonNode usage in SerializableTableSpec.

* Avoid potential flakes in EvolveSpec DoFn, address nits

* Simplify worker-local cache in TableMetadataDriver using LinkedHashMap LRU

- Replace Guava Cache, CacheBuilder, and Ticker in ExtractTableIdsDoFn with a standard LinkedHashMap-based LRU cache capped at 10,000 entries.
- Track lastEmitted timestamp per table identifier and emit when (now - lastEmitted) >= refreshInterval / 2.
- Remove getClock() and setClock() from TableMetadataDriver and its Builder to keep the AutoValue API clean and free of testing methods.
- Provide testing hooks via static globalTestClock and instance setClock on ExtractTableIdsDoFn.

* simplify dofn + fix unit test

---------

Co-authored-by: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Implement Side-Input Table Cache for IcebergIO

2 participants