Skip to content

Commit 51e35e5

Browse files
authored
[IcebergIO] Improve TableCache (#38882)
* improve table cache * address comments * cleanups
1 parent 6062110 commit 51e35e5

17 files changed

Lines changed: 479 additions & 350 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run.",
3-
"modification": 2
3+
"modification": 1
44
}

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ public void processElement(
147147

148148
try {
149149
// see if table already exists with a spec
150-
spec = catalogConfig.catalog().loadTable(TableIdentifier.parse(tableIdentifier)).spec();
150+
spec =
151+
TableCache.getAndRefreshIfStale(
152+
catalogConfig, TableIdentifier.parse(tableIdentifier))
153+
.spec();
151154

152155
} catch (NoSuchTableException ignored) {
153156
// no partition to apply

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CreateReadTasksDoFn.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,13 @@ class CreateReadTasksDoFn
5252
this.scanConfig = scanConfig;
5353
}
5454

55-
@Setup
56-
public void setup() {
57-
TableCache.setup(scanConfig);
58-
}
59-
6055
@ProcessElement
6156
public void process(
6257
@Element KV<String, List<SnapshotInfo>> element,
6358
OutputReceiver<KV<ReadTaskDescriptor, ReadTask>> out)
6459
throws IOException, ExecutionException {
6560
// force refresh because the table must be updated before scanning snapshots
66-
Table table = TableCache.getRefreshed(element.getKey());
61+
Table table = TableCache.getRefreshed(scanConfig.getCatalogConfig(), element.getKey());
6762

6863
// scan snapshots individually and assign commit timestamp to files
6964
for (SnapshotInfo snapshot : element.getValue()) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ public PCollection<Row> expand(PBegin input) {
655655
TableIdentifier tableId =
656656
checkStateNotNull(getTableIdentifier(), "Must set a table to read from.");
657657

658-
Table table = getCatalogConfig().catalog().loadTable(tableId);
658+
Table table = TableCache.get(getCatalogConfig(), tableId);
659659

660660
IcebergScanConfig scanConfig =
661661
IcebergScanConfig.builder()

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ public enum ScanType {
6868
@Pure
6969
public Table getTable() {
7070
if (cachedTable == null) {
71-
cachedTable =
72-
getCatalogConfig().catalog().loadTable(TableIdentifier.parse(getTableIdentifier()));
71+
cachedTable = TableCache.get(getCatalogConfig(), TableIdentifier.parse(getTableIdentifier()));
7372
}
7473
return cachedTable;
7574
}

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,8 @@ class IncrementalScanSource extends PTransform<PBegin, PCollection<Row>> {
5353
@Override
5454
public PCollection<Row> expand(PBegin input) {
5555
Table table =
56-
scanConfig
57-
.getCatalogConfig()
58-
.catalog()
59-
.loadTable(TableIdentifier.parse(scanConfig.getTableIdentifier()));
56+
TableCache.get(
57+
scanConfig.getCatalogConfig(), TableIdentifier.parse(scanConfig.getTableIdentifier()));
6058

6159
PCollection<KV<String, List<SnapshotInfo>>> snapshots =
6260
MoreObjects.firstNonNull(scanConfig.getStreaming(), false)

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,14 @@ class ReadFromTasks extends DoFn<KV<ReadTaskDescriptor, ReadTask>, Row> {
5151
this.scanConfig = scanConfig;
5252
}
5353

54-
@Setup
55-
public void setup() {
56-
TableCache.setup(scanConfig);
57-
}
58-
5954
@ProcessElement
6055
public void process(
6156
@Element KV<ReadTaskDescriptor, ReadTask> element,
6257
RestrictionTracker<OffsetRange, Long> tracker,
6358
OutputReceiver<Row> out)
6459
throws IOException, ExecutionException, InterruptedException {
6560
ReadTask readTask = element.getValue();
66-
Table table = TableCache.get(scanConfig.getTableIdentifier());
61+
Table table = TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier());
6762

6863
List<FileScanTask> fileScanTasks = readTask.getFileScanTasks();
6964

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java

Lines changed: 49 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
2222

2323
import java.io.IOException;
24-
import java.time.Duration;
25-
import java.time.Instant;
2624
import java.time.LocalDateTime;
2725
import java.time.YearMonth;
2826
import java.time.ZoneOffset;
@@ -248,7 +246,7 @@ static String getPartitionDataPath(
248246
DateTimeFormatter.ofPattern("yyyy-MM-dd-HH");
249247
private static final LocalDateTime EPOCH = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC);
250248

251-
private final Catalog catalog;
249+
private final IcebergCatalogConfig catalogConfig;
252250
private final String filePrefix;
253251
private final long maxFileSize;
254252
private final int maxNumWriters;
@@ -260,46 +258,11 @@ static String getPartitionDataPath(
260258
private final Map<WindowedValue<IcebergDestination>, List<SerializableDataFile>>
261259
totalSerializableDataFiles = Maps.newHashMap();
262260

263-
static final class LastRefreshedTable {
264-
final Table table;
265-
volatile Instant lastRefreshTime;
266-
static final Duration STALENESS_THRESHOLD = Duration.ofMinutes(2);
267-
268-
LastRefreshedTable(Table table, Instant lastRefreshTime) {
269-
this.table = table;
270-
this.lastRefreshTime = lastRefreshTime;
271-
}
272-
273-
/**
274-
* Refreshes the table metadata if it is considered stale (older than 2 minutes).
275-
*
276-
* <p>This method first performs a non-synchronized check on the table's freshness. This
277-
* provides a lock-free fast path that avoids synchronization overhead in the common case where
278-
* the table does not need to be refreshed. If the table might be stale, it then enters a
279-
* synchronized block to ensure that only one thread performs the refresh operation.
280-
*/
281-
void refreshIfStale() {
282-
// Fast path: Avoid entering the synchronized block if the table is not stale.
283-
if (lastRefreshTime.isAfter(Instant.now().minus(STALENESS_THRESHOLD))) {
284-
return;
285-
}
286-
synchronized (this) {
287-
if (lastRefreshTime.isBefore(Instant.now().minus(STALENESS_THRESHOLD))) {
288-
table.refresh();
289-
lastRefreshTime = Instant.now();
290-
}
291-
}
292-
}
293-
}
294-
295-
@VisibleForTesting
296-
static final Cache<TableIdentifier, LastRefreshedTable> LAST_REFRESHED_TABLE_CACHE =
297-
CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build();
298-
299261
private boolean isClosed = false;
300262

301-
RecordWriterManager(Catalog catalog, String filePrefix, long maxFileSize, int maxNumWriters) {
302-
this.catalog = catalog;
263+
RecordWriterManager(
264+
IcebergCatalogConfig catalogConfig, String filePrefix, long maxFileSize, int maxNumWriters) {
265+
this.catalogConfig = catalogConfig;
303266
this.filePrefix = filePrefix;
304267
this.maxFileSize = maxFileSize;
305268
this.maxNumWriters = maxNumWriters;
@@ -308,9 +271,9 @@ void refreshIfStale() {
308271
/**
309272
* Returns an Iceberg {@link Table}.
310273
*
311-
* <p>First attempts to fetch the table from the {@link #LAST_REFRESHED_TABLE_CACHE}. If it's not
312-
* there, we attempt to load it using the Iceberg API. If the table doesn't exist at all, we
313-
* attempt to create it, inferring the table schema from the record schema.
274+
* <p>First attempts to fetch the table from the shared {@link TableCache}. If it's not there, we
275+
* attempt to load it using the Iceberg API. If the table doesn't exist at all, we attempt to
276+
* create it, inferring the table schema from the record schema.
314277
*
315278
* <p>Note that this is a best-effort operation that depends on the {@link Catalog}
316279
* implementation. Although it is expected, some implementations may not support creating a table
@@ -319,13 +282,13 @@ void refreshIfStale() {
319282
@VisibleForTesting
320283
Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
321284
TableIdentifier identifier = destination.getTableIdentifier();
322-
@Nullable
323-
LastRefreshedTable lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier);
324-
if (lastRefreshedTable != null && lastRefreshedTable.table != null) {
325-
lastRefreshedTable.refreshIfStale();
326-
return lastRefreshedTable.table;
327-
}
285+
return TableCache.getAndRefreshIfStale(
286+
catalogConfig, identifier, () -> loadOrCreateTable(destination, dataSchema));
287+
}
328288

289+
private Table loadOrCreateTable(IcebergDestination destination, Schema dataSchema) {
290+
Catalog catalog = catalogConfig.catalog();
291+
TableIdentifier identifier = destination.getTableIdentifier();
329292
Namespace namespace = identifier.namespace();
330293
@Nullable IcebergTableCreateConfig createConfig = destination.getTableCreateConfig();
331294
PartitionSpec partitionSpec =
@@ -336,53 +299,48 @@ Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
336299
? createConfig.getTableProperties()
337300
: Maps.newHashMap();
338301

339-
@Nullable Table table = null;
340-
synchronized (LAST_REFRESHED_TABLE_CACHE) {
341-
// Create namespace if it does not exist yet
342-
if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) {
343-
SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog;
344-
if (!supportsNamespaces.namespaceExists(namespace)) {
345-
try {
346-
supportsNamespaces.createNamespace(namespace);
347-
LOG.info("Created new namespace '{}'.", namespace);
348-
} catch (AlreadyExistsException ignored) {
349-
// race condition: another worker already created this namespace
350-
}
302+
// Create namespace if it does not exist yet
303+
if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) {
304+
SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog;
305+
if (!supportsNamespaces.namespaceExists(namespace)) {
306+
try {
307+
supportsNamespaces.createNamespace(namespace);
308+
LOG.info("Created new namespace '{}'.", namespace);
309+
} catch (AlreadyExistsException ignored) {
310+
// race condition: another worker already created this namespace
351311
}
352312
}
313+
}
353314

354-
// If table exists, just load it
355-
// Note: the implementation of catalog.tableExists() will load the table to check its
356-
// existence. We don't use it here to avoid double loadTable() calls.
315+
// If table exists, just load it
316+
// Note: the implementation of catalog.tableExists() will load the table to check its
317+
// existence. We don't use it here to avoid double loadTable() calls.
318+
try {
319+
return catalog.loadTable(identifier);
320+
} catch (NoSuchTableException e) { // Otherwise, create the table
321+
org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema);
357322
try {
358-
table = catalog.loadTable(identifier);
359-
} catch (NoSuchTableException e) { // Otherwise, create the table
360-
org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema);
361-
try {
362-
table =
363-
catalog
364-
.buildTable(identifier, tableSchema)
365-
.withPartitionSpec(partitionSpec)
366-
.withSortOrder(sortOrder)
367-
.withProperties(tableProperties)
368-
.create();
369-
LOG.info(
370-
"Created Iceberg table '{}' with schema: {}\n"
371-
+ ", partition spec: {}, sort order: {}, table properties: {}",
372-
identifier,
373-
tableSchema,
374-
partitionSpec,
375-
sortOrder,
376-
tableProperties);
377-
} catch (AlreadyExistsException ignored) {
378-
// race condition: another worker already created this table
379-
table = catalog.loadTable(identifier);
380-
}
323+
Table table =
324+
catalog
325+
.buildTable(identifier, tableSchema)
326+
.withPartitionSpec(partitionSpec)
327+
.withSortOrder(sortOrder)
328+
.withProperties(tableProperties)
329+
.create();
330+
LOG.info(
331+
"Created Iceberg table '{}' with schema: {}\n"
332+
+ ", partition spec: {}, sort order: {}, table properties: {}",
333+
identifier,
334+
tableSchema,
335+
partitionSpec,
336+
sortOrder,
337+
tableProperties);
338+
return table;
339+
} catch (AlreadyExistsException ignored) {
340+
// race condition: another worker already created this table
341+
return catalog.loadTable(identifier);
381342
}
382343
}
383-
lastRefreshedTable = new LastRefreshedTable(table, Instant.now());
384-
LAST_REFRESHED_TABLE_CACHE.put(identifier, lastRefreshedTable);
385-
return table;
386344
}
387345

388346
/**

sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public ScanSource(IcebergScanConfig scanConfig) {
4747
}
4848

4949
private TableScan getTableScan() {
50-
Table table = scanConfig.getTable();
50+
Table table =
51+
TableCache.getRefreshed(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier());
5152
TableScan tableScan = table.newScan().project(scanConfig.getProjectedSchema());
5253

5354
if (scanConfig.getFilter() != null) {

0 commit comments

Comments
 (0)