2121import static org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .base .Preconditions .checkState ;
2222
2323import java .io .IOException ;
24- import java .time .Duration ;
25- import java .time .Instant ;
2624import java .time .LocalDateTime ;
2725import java .time .YearMonth ;
2826import 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 /**
0 commit comments