![JAR search and dependency download from the Maven repository](/logo.png)
org.dinky.shaded.paimon.operation.FileStoreCommitImpl Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dinky.shaded.paimon.operation;
import org.dinky.shaded.paimon.Snapshot;
import org.dinky.shaded.paimon.annotation.VisibleForTesting;
import org.dinky.shaded.paimon.data.BinaryRow;
import org.dinky.shaded.paimon.data.InternalRow;
import org.dinky.shaded.paimon.fs.FileIO;
import org.dinky.shaded.paimon.fs.Path;
import org.dinky.shaded.paimon.io.DataFileMeta;
import org.dinky.shaded.paimon.io.DataFilePathFactory;
import org.dinky.shaded.paimon.manifest.FileKind;
import org.dinky.shaded.paimon.manifest.IndexManifestEntry;
import org.dinky.shaded.paimon.manifest.IndexManifestFile;
import org.dinky.shaded.paimon.manifest.ManifestCommittable;
import org.dinky.shaded.paimon.manifest.ManifestEntry;
import org.dinky.shaded.paimon.manifest.ManifestFile;
import org.dinky.shaded.paimon.manifest.ManifestFileMeta;
import org.dinky.shaded.paimon.manifest.ManifestList;
import org.dinky.shaded.paimon.operation.metrics.CommitMetrics;
import org.dinky.shaded.paimon.operation.metrics.CommitStats;
import org.dinky.shaded.paimon.options.MemorySize;
import org.dinky.shaded.paimon.predicate.Predicate;
import org.dinky.shaded.paimon.predicate.PredicateBuilder;
import org.dinky.shaded.paimon.schema.SchemaManager;
import org.dinky.shaded.paimon.table.sink.CommitMessage;
import org.dinky.shaded.paimon.table.sink.CommitMessageImpl;
import org.dinky.shaded.paimon.types.RowType;
import org.dinky.shaded.paimon.utils.FileStorePathFactory;
import org.dinky.shaded.paimon.utils.Pair;
import org.dinky.shaded.paimon.utils.Preconditions;
import org.dinky.shaded.paimon.utils.RowDataToObjectArrayConverter;
import org.dinky.shaded.paimon.utils.SnapshotManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
/**
* Default implementation of {@link FileStoreCommit}.
*
* This class provides an atomic commit method to the user.
*
*
* - Before calling {@link FileStoreCommitImpl#commit}, if user cannot determine if this commit
* is done before, user should first call {@link FileStoreCommitImpl#filterCommitted}.
*
- Before committing, it will first check for conflicts by checking if all files to be removed
* currently exists, and if modified files have overlapping key ranges with existing files.
*
- After that it use the external {@link FileStoreCommitImpl#lock} (if provided) or the atomic
* rename of the file system to ensure atomicity.
*
- If commit fails due to conflicts or exception it tries its best to clean up and aborts.
*
- If atomic rename fails it tries again after reading the latest snapshot from step 2.
*
*
* NOTE: If you want to modify this class, any exception during commit MUST NOT BE IGNORED. They
* must be thrown to restart the job. It is recommended to run FileStoreCommitTest thousands of
* times to make sure that your changes is correct.
*/
public class FileStoreCommitImpl implements FileStoreCommit {
private static final Logger LOG = LoggerFactory.getLogger(FileStoreCommitImpl.class);
private final FileIO fileIO;
private final SchemaManager schemaManager;
private final String commitUser;
private final RowType partitionType;
private final RowDataToObjectArrayConverter partitionObjectConverter;
private final FileStorePathFactory pathFactory;
private final SnapshotManager snapshotManager;
private final ManifestFile manifestFile;
private final ManifestList manifestList;
private final IndexManifestFile indexManifestFile;
private final FileStoreScan scan;
private final int numBucket;
private final MemorySize manifestTargetSize;
private final MemorySize manifestFullCompactionSize;
private final int manifestMergeMinCount;
private final boolean dynamicPartitionOverwrite;
@Nullable private final Comparator keyComparator;
@Nullable private Lock lock;
private boolean ignoreEmptyCommit;
private CommitMetrics commitMetrics;
public FileStoreCommitImpl(
FileIO fileIO,
SchemaManager schemaManager,
String commitUser,
RowType partitionType,
FileStorePathFactory pathFactory,
SnapshotManager snapshotManager,
ManifestFile.Factory manifestFileFactory,
ManifestList.Factory manifestListFactory,
IndexManifestFile.Factory indexManifestFileFactory,
FileStoreScan scan,
int numBucket,
MemorySize manifestTargetSize,
MemorySize manifestFullCompactionSize,
int manifestMergeMinCount,
boolean dynamicPartitionOverwrite,
@Nullable Comparator keyComparator) {
this.fileIO = fileIO;
this.schemaManager = schemaManager;
this.commitUser = commitUser;
this.partitionType = partitionType;
this.partitionObjectConverter = new RowDataToObjectArrayConverter(partitionType);
this.pathFactory = pathFactory;
this.snapshotManager = snapshotManager;
this.manifestFile = manifestFileFactory.create();
this.manifestList = manifestListFactory.create();
this.indexManifestFile = indexManifestFileFactory.create();
this.scan = scan;
this.numBucket = numBucket;
this.manifestTargetSize = manifestTargetSize;
this.manifestFullCompactionSize = manifestFullCompactionSize;
this.manifestMergeMinCount = manifestMergeMinCount;
this.dynamicPartitionOverwrite = dynamicPartitionOverwrite;
this.keyComparator = keyComparator;
this.lock = null;
this.ignoreEmptyCommit = true;
this.commitMetrics = null;
}
@Override
public FileStoreCommit withLock(Lock lock) {
this.lock = lock;
return this;
}
@Override
public FileStoreCommit ignoreEmptyCommit(boolean ignoreEmptyCommit) {
this.ignoreEmptyCommit = ignoreEmptyCommit;
return this;
}
@Override
public Set filterCommitted(Set commitIdentifiers) {
// nothing to filter, fast exit
if (commitIdentifiers.isEmpty()) {
return commitIdentifiers;
}
Optional latestSnapshot = snapshotManager.latestSnapshotOfUser(commitUser);
if (latestSnapshot.isPresent()) {
Set result = new HashSet<>();
for (Long identifier : commitIdentifiers) {
// if committable is newer than latest snapshot, then it hasn't been committed
if (identifier > latestSnapshot.get().commitIdentifier()) {
result.add(identifier);
}
}
return result;
} else {
// if there is no previous snapshots then nothing should be filtered
return commitIdentifiers;
}
}
@Override
public void commit(ManifestCommittable committable, Map properties) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ready to commit\n" + committable.toString());
}
long started = System.nanoTime();
int generatedSnapshot = 0;
int attempts = 0;
Snapshot latestSnapshot = null;
Long safeLatestSnapshotId = null;
List baseEntries = new ArrayList<>();
List appendTableFiles = new ArrayList<>();
List appendChangelog = new ArrayList<>();
List compactTableFiles = new ArrayList<>();
List compactChangelog = new ArrayList<>();
List appendIndexFiles = new ArrayList<>();
collectChanges(
committable.fileCommittables(),
appendTableFiles,
appendChangelog,
compactTableFiles,
compactChangelog,
appendIndexFiles);
try {
if (!ignoreEmptyCommit
|| !appendTableFiles.isEmpty()
|| !appendChangelog.isEmpty()
|| !appendIndexFiles.isEmpty()) {
// Optimization for common path.
// Step 1:
// Read manifest entries from changed partitions here and check for conflicts.
// If there are no other jobs committing at the same time,
// we can skip conflict checking in tryCommit method.
// This optimization is mainly used to decrease the number of times we read from
// files.
latestSnapshot = snapshotManager.latestSnapshot();
if (latestSnapshot != null) {
// it is possible that some partitions only have compact changes,
// so we need to contain all changes
baseEntries.addAll(
readAllEntriesFromChangedPartitions(
latestSnapshot, appendTableFiles, compactTableFiles));
noConflictsOrFail(latestSnapshot.commitUser(), baseEntries, appendTableFiles);
safeLatestSnapshotId = latestSnapshot.id();
}
attempts +=
tryCommit(
appendTableFiles,
appendChangelog,
appendIndexFiles,
committable.identifier(),
committable.watermark(),
committable.logOffsets(),
Snapshot.CommitKind.APPEND,
safeLatestSnapshotId);
generatedSnapshot += 1;
}
if (!compactTableFiles.isEmpty() || !compactChangelog.isEmpty()) {
// Optimization for common path.
// Step 2:
// Add appendChanges to the manifest entries read above and check for conflicts.
// If there are no other jobs committing at the same time,
// we can skip conflict checking in tryCommit method.
// This optimization is mainly used to decrease the number of times we read from
// files.
if (safeLatestSnapshotId != null) {
baseEntries.addAll(appendTableFiles);
noConflictsOrFail(latestSnapshot.commitUser(), baseEntries, compactTableFiles);
// assume this compact commit follows just after the append commit created above
safeLatestSnapshotId += 1;
}
attempts +=
tryCommit(
compactTableFiles,
compactChangelog,
Collections.emptyList(),
committable.identifier(),
committable.watermark(),
committable.logOffsets(),
Snapshot.CommitKind.COMPACT,
safeLatestSnapshotId);
generatedSnapshot += 1;
}
} finally {
long commitDuration = (System.nanoTime() - started) / 1_000_000;
if (this.commitMetrics != null) {
reportCommit(
appendTableFiles,
appendChangelog,
compactTableFiles,
compactChangelog,
commitDuration,
generatedSnapshot,
attempts);
}
}
}
private void reportCommit(
List appendTableFiles,
List appendChangelogFiles,
List compactTableFiles,
List compactChangelogFiles,
long commitDuration,
int generatedSnapshots,
int attempts) {
CommitStats commitStats =
new CommitStats(
appendTableFiles,
appendChangelogFiles,
compactTableFiles,
compactChangelogFiles,
commitDuration,
generatedSnapshots,
attempts);
commitMetrics.reportCommit(commitStats);
}
@Override
public void overwrite(
Map partition,
ManifestCommittable committable,
Map properties) {
if (LOG.isDebugEnabled()) {
LOG.debug(
"Ready to overwrite partition {}\nManifestCommittable: {}\nProperties: {}",
partition,
committable,
properties);
}
long started = System.nanoTime();
int generatedSnapshot = 0;
int attempts = 0;
List appendTableFiles = new ArrayList<>();
List appendChangelog = new ArrayList<>();
List compactTableFiles = new ArrayList<>();
List compactChangelog = new ArrayList<>();
List appendIndexFiles = new ArrayList<>();
collectChanges(
committable.fileCommittables(),
appendTableFiles,
appendChangelog,
compactTableFiles,
compactChangelog,
appendIndexFiles);
if (!appendChangelog.isEmpty() || !compactChangelog.isEmpty()) {
StringBuilder warnMessage =
new StringBuilder(
"Overwrite mode currently does not commit any changelog.\n"
+ "Please make sure that the partition you're overwriting "
+ "is not being consumed by a streaming reader.\n"
+ "Ignored changelog files are:\n");
for (ManifestEntry entry : appendChangelog) {
warnMessage.append(" * ").append(entry.toString()).append("\n");
}
for (ManifestEntry entry : compactChangelog) {
warnMessage.append(" * ").append(entry.toString()).append("\n");
}
LOG.warn(warnMessage.toString());
}
try {
boolean skipOverwrite = false;
// partition filter is built from static or dynamic partition according to properties
Predicate partitionFilter = null;
if (dynamicPartitionOverwrite) {
if (appendTableFiles.isEmpty()) {
// in dynamic mode, if there is no changes to commit, no data will be deleted
skipOverwrite = true;
} else {
partitionFilter =
appendTableFiles.stream()
.map(ManifestEntry::partition)
.distinct()
// partition filter is built from new data's partitions
.map(p -> PredicateBuilder.equalPartition(p, partitionType))
.reduce(PredicateBuilder::or)
.orElseThrow(
() ->
new RuntimeException(
"Failed to get dynamic partition filter. This is unexpected."));
}
} else {
partitionFilter = PredicateBuilder.partition(partition, partitionType);
// sanity check, all changes must be done within the given partition
if (partitionFilter != null) {
for (ManifestEntry entry : appendTableFiles) {
if (!partitionFilter.test(
partitionObjectConverter.convert(entry.partition()))) {
throw new IllegalArgumentException(
"Trying to overwrite partition "
+ partition
+ ", but the changes in "
+ pathFactory.getPartitionString(entry.partition())
+ " does not belong to this partition");
}
}
}
}
// overwrite new files
if (!skipOverwrite) {
attempts +=
tryOverwrite(
partitionFilter,
appendTableFiles,
appendIndexFiles,
committable.identifier(),
committable.watermark(),
committable.logOffsets());
generatedSnapshot += 1;
}
if (!compactTableFiles.isEmpty()) {
attempts +=
tryCommit(
compactTableFiles,
Collections.emptyList(),
Collections.emptyList(),
committable.identifier(),
committable.watermark(),
committable.logOffsets(),
Snapshot.CommitKind.COMPACT,
null);
generatedSnapshot += 1;
}
} finally {
long commitDuration = (System.nanoTime() - started) / 1_000_000;
if (this.commitMetrics != null) {
reportCommit(
appendTableFiles,
Collections.emptyList(),
compactTableFiles,
Collections.emptyList(),
commitDuration,
generatedSnapshot,
attempts);
}
}
}
@Override
public void dropPartitions(List