Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@
import org.apache.beam.sdk.transforms.windowing.PaneInfo;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionView;
import org.apache.beam.sdk.values.Row;
import org.apache.beam.sdk.values.ValueInSingleWindow;
import org.apache.iceberg.PartitionKey;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.Schema;
import org.apache.iceberg.Table;
import org.apache.iceberg.exceptions.NoSuchTableException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
Expand All @@ -51,6 +53,7 @@ class AssignDestinationsAndPartitions

private final DynamicDestinations dynamicDestinations;
private final IcebergCatalogConfig catalogConfig;
private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> metadataView;

static final String DESTINATION = "destination";
static final String PARTITION = "partition";
Expand All @@ -63,14 +66,27 @@ class AssignDestinationsAndPartitions

public AssignDestinationsAndPartitions(
DynamicDestinations dynamicDestinations, IcebergCatalogConfig catalogConfig) {
this(dynamicDestinations, catalogConfig, null);
}

public AssignDestinationsAndPartitions(
DynamicDestinations dynamicDestinations,
IcebergCatalogConfig catalogConfig,
@Nullable PCollectionView<Map<String, SerializableTableSpec>> metadataView) {
this.dynamicDestinations = dynamicDestinations;
this.catalogConfig = catalogConfig;
this.metadataView = metadataView;
}

@Override
public PCollection<KV<Row, Row>> expand(PCollection<Row> input) {
ParDo.SingleOutput<Row, KV<Row, Row>> parDo =
ParDo.of(new AssignDoFn(dynamicDestinations, catalogConfig, metadataView));
if (metadataView != null) {
parDo = parDo.withSideInputs(metadataView);
}
return input
.apply(ParDo.of(new AssignDoFn(dynamicDestinations, catalogConfig)))
.apply(parDo)
.setCoder(
KvCoder.of(
RowCoder.of(OUTPUT_SCHEMA), RowCoder.of(dynamicDestinations.getDataSchema())));
Expand All @@ -83,24 +99,36 @@ static class AssignDoFn extends DoFn<Row, KV<Row, Row>> {
private transient @MonotonicNonNull Map<String, PartitionKey> partitionKeys;
private transient @MonotonicNonNull Map<String, BeamRowWrapper> wrappers;
private transient @MonotonicNonNull Map<String, Instant> lastRefreshTimes;
private transient @MonotonicNonNull Map<String, Integer> cachedSpecIds;

private final DynamicDestinations dynamicDestinations;
private final IcebergCatalogConfig catalogConfig;
private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> metadataView;

AssignDoFn(DynamicDestinations dynamicDestinations, IcebergCatalogConfig catalogConfig) {
this(dynamicDestinations, catalogConfig, null);
}

AssignDoFn(
DynamicDestinations dynamicDestinations,
IcebergCatalogConfig catalogConfig,
@Nullable PCollectionView<Map<String, SerializableTableSpec>> metadataView) {
this.dynamicDestinations = dynamicDestinations;
this.catalogConfig = catalogConfig;
this.metadataView = metadataView;
}

@Setup
public void setup() {
this.wrappers = new HashMap<>();
this.partitionKeys = new HashMap<>();
this.lastRefreshTimes = new HashMap<>();
this.cachedSpecIds = new HashMap<>();
}

@ProcessElement
public void processElement(
ProcessContext c,
@Element Row element,
BoundedWindow window,
PaneInfo paneInfo,
Expand All @@ -111,58 +139,83 @@ public void processElement(
dynamicDestinations.getTableStringIdentifier(
ValueInSingleWindow.of(element, timestamp, window, paneInfo));

String canonicalTableId;
try {
canonicalTableId =
IcebergUtils.tableIdentifierToString(
IcebergUtils.parseTableIdentifier(tableIdentifier));
} catch (Exception e) {
canonicalTableId = tableIdentifier.trim();
}

SerializableTableSpec tableSpec = null;
if (metadataView != null) {
Map<String, SerializableTableSpec> viewMap = c.sideInput(metadataView);
if (viewMap != null) {
tableSpec = viewMap.get(canonicalTableId);
if (tableSpec == null) {
tableSpec = viewMap.get(tableIdentifier);
}
}
}

Row data = dynamicDestinations.getData(element);

@Nullable PartitionKey partitionKey = checkStateNotNull(partitionKeys).get(tableIdentifier);

@Nullable BeamRowWrapper wrapper = checkStateNotNull(wrappers).get(tableIdentifier);

@Nullable Instant lastRefresh = checkStateNotNull(lastRefreshTimes).get(tableIdentifier);
@Nullable Integer cachedSpecId = checkStateNotNull(cachedSpecIds).get(tableIdentifier);

Instant now = Instant.now();

boolean specChanged =
tableSpec != null
&& (cachedSpecId == null || !cachedSpecId.equals(tableSpec.getSpecId()));

boolean shouldRefresh =
partitionKey == null
|| wrapper == null
|| lastRefresh == null
|| now.isAfter(lastRefresh.plus(REFRESH_INTERVAL));
|| specChanged
|| (tableSpec == null
&& (lastRefresh == null || now.isAfter(lastRefresh.plus(REFRESH_INTERVAL))));

if (shouldRefresh) {

PartitionSpec spec = PartitionSpec.unpartitioned();

Schema schema = IcebergUtils.beamSchemaToIcebergSchema(data.getSchema());

@Nullable IcebergTableCreateConfig createConfig =
dynamicDestinations.instantiateDestination(tableIdentifier).getTableCreateConfig();

if (createConfig != null && createConfig.getPartitionFields() != null) {

spec =
PartitionUtils.toPartitionSpec(createConfig.getPartitionFields(), data.getSchema());

} else if (tableSpec != null) {
spec = tableSpec.getPartitionSpec();
if (data.getSchema().getFieldCount() == tableSpec.getSchema().columns().size()) {
schema = tableSpec.getSchema();
}
checkStateNotNull(cachedSpecIds).put(tableIdentifier, tableSpec.getSpecId());
} else {

try {
// see if table already exists with a spec
spec =
Table table =
TableCache.getAndRefreshIfStale(
catalogConfig, IcebergUtils.parseTableIdentifier(tableIdentifier))
.spec();

catalogConfig, IcebergUtils.parseTableIdentifier(tableIdentifier));
spec = table.spec();
if (data.getSchema().getFieldCount() == table.schema().columns().size()) {
schema = table.schema();
}
} catch (NoSuchTableException ignored) {
// no partition to apply
}
}

partitionKey = new PartitionKey(spec, schema);

wrapper = new BeamRowWrapper(data.getSchema(), schema.asStruct());

checkStateNotNull(partitionKeys).put(tableIdentifier, partitionKey);

checkStateNotNull(wrappers).put(tableIdentifier, wrapper);

checkStateNotNull(lastRefreshTimes).put(tableIdentifier, now);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand Down Expand Up @@ -251,6 +252,7 @@ static String getPartitionDataPath(
private final long maxFileSize;
private final int maxNumWriters;
private final @Nullable Map<String, String> writeProperties;
private volatile @Nullable Map<String, SerializableTableSpec> sideInputTableSpecs;
@VisibleForTesting int openWriters = 0;

@VisibleForTesting
Expand All @@ -263,7 +265,7 @@ static String getPartitionDataPath(

RecordWriterManager(
IcebergCatalogConfig catalogConfig, String filePrefix, long maxFileSize, int maxNumWriters) {
this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, null);
this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, null, null);
}

RecordWriterManager(
Expand All @@ -272,11 +274,27 @@ static String getPartitionDataPath(
long maxFileSize,
int maxNumWriters,
@Nullable Map<String, String> writeProperties) {
this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, writeProperties, null);
}

RecordWriterManager(
IcebergCatalogConfig catalogConfig,
String filePrefix,
long maxFileSize,
int maxNumWriters,
@Nullable Map<String, String> writeProperties,
@Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
this.catalogConfig = catalogConfig;
this.filePrefix = filePrefix;
this.maxFileSize = maxFileSize;
this.maxNumWriters = maxNumWriters;
this.writeProperties = writeProperties;
this.sideInputTableSpecs = sideInputTableSpecs;
}

@VisibleForTesting
void setSideInputTableSpecs(@Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
this.sideInputTableSpecs = sideInputTableSpecs;
}

/**
Expand All @@ -291,12 +309,29 @@ static String getPartitionDataPath(
* using the Iceberg API.
*/
@VisibleForTesting
Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
Table getOrCreateTable(
IcebergDestination destination,
Schema dataSchema,
@Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
TableIdentifier identifier = destination.getTableIdentifier();
String tableIdString = IcebergUtils.tableIdentifierToString(identifier);
if (sideInputTableSpecs != null && sideInputTableSpecs.containsKey(tableIdString)) {
SerializableTableSpec spec = sideInputTableSpecs.get(tableIdString);
if (spec != null) {
Map<String, String> catalogProperties = catalogConfig.getCatalogProperties();
return new SideInputTable(
spec, catalogProperties != null ? catalogProperties : Collections.emptyMap());
}
}
return TableCache.getAndRefreshIfStale(
catalogConfig, identifier, () -> loadOrCreateTable(destination, dataSchema));
}

@VisibleForTesting
Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
return getOrCreateTable(destination, dataSchema, this.sideInputTableSpecs);
}

private Table loadOrCreateTable(IcebergDestination destination, Schema dataSchema) {
Catalog catalog = catalogConfig.catalog();
TableIdentifier identifier = destination.getTableIdentifier();
Expand Down Expand Up @@ -354,6 +389,23 @@ private Table loadOrCreateTable(IcebergDestination destination, Schema dataSchem
}
}

/**
* Fetches the appropriate {@link RecordWriter} for this destination and partition and writes the
* record, optionally updating the side-input table specs map.
*
* <p>If the {@link RecordWriterManager} is saturated (i.e. has hit the maximum limit of open
* writers), the record is rejected and {@code false} is returned.
*/
public boolean write(
WindowedValue<IcebergDestination> icebergDestination,
Row row,
@Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
if (sideInputTableSpecs != null) {
this.sideInputTableSpecs = sideInputTableSpecs;
}
return write(icebergDestination, row);
}

/**
* Fetches the appropriate {@link RecordWriter} for this destination and partition and writes the
* record.
Expand Down
Loading
Loading