Skip to content

Commit e136cb0

Browse files
HDDS-14943. Implement rewrite logic for Iceberg's manifest-list files for path migration (#10190)
1 parent 7dd0072 commit e136cb0

2 files changed

Lines changed: 200 additions & 31 deletions

File tree

hadoop-ozone/iceberg/src/main/java/org/apache/hadoop/ozone/iceberg/RewriteTablePathOzoneAction.java

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ private boolean versionInFilePath(String path, String version) {
211211
}
212212

213213
private String rebuildMetadata() {
214-
//TODO need to implement rewrite of manifest list , manifest files and position delete files.
214+
//TODO need to implement rewrite of manifest files and position delete files.
215215
TableMetadata startMetadata = startVersionName != null
216216
? new StaticTableOperations(startVersionName, table.io()).current()
217217
: null;
@@ -227,12 +227,14 @@ private String rebuildMetadata() {
227227
Set<Long> deltaSnapshotIds = deltaSnapshots.stream().map(Snapshot::snapshotId).collect(Collectors.toSet());
228228
Set<Snapshot> validSnapshots = new HashSet<>(RewriteTablePathOzoneUtils.snapshotSet(endMetadata));
229229
validSnapshots.removeAll(RewriteTablePathOzoneUtils.snapshotSet(startMetadata));
230-
//TODO: manifestsToRewrite will be used while re-write of manifest-list files.
231230
Set<String> manifestsToRewrite = manifestsToRewrite(validSnapshots,
232231
startMetadata != null ? deltaSnapshotIds : null);
232+
RewriteResult<ManifestFile> rewriteManifestListResult =
233+
rewriteManifestLists(validSnapshots, endMetadata, manifestsToRewrite);
233234

234235
Set<Pair<String, String>> copyPlan = new HashSet<>();
235236
copyPlan.addAll(rewriteVersionResult.copyPlan());
237+
copyPlan.addAll(rewriteManifestListResult.copyPlan());
236238

237239
return RewriteTablePathOzoneUtils.saveFileList(copyPlan, stagingDir, table.io());
238240
}
@@ -362,6 +364,91 @@ private Set<String> manifestsToRewrite(Set<Snapshot> validSnapshots, Set<Long> d
362364
return manifestPaths;
363365
}
364366

367+
private RewriteResult<ManifestFile> rewriteManifestList(
368+
Snapshot snapshot, TableMetadata tableMetadata, Set<String> manifestsToRewrite) {
369+
RewriteResult<ManifestFile> result = new RewriteResult<>();
370+
371+
String path = snapshot.manifestListLocation();
372+
String outputPath = RewriteTablePathUtil.stagingPath(path, sourcePrefix, stagingDir);
373+
RewriteResult<ManifestFile> rewriteResult =
374+
RewriteTablePathUtil.rewriteManifestList(
375+
snapshot,
376+
table.io(),
377+
tableMetadata,
378+
manifestsToRewrite,
379+
sourcePrefix,
380+
targetPrefix,
381+
stagingDir,
382+
outputPath);
383+
384+
result.append(rewriteResult);
385+
result
386+
.copyPlan()
387+
.add(Pair.of(outputPath, RewriteTablePathUtil.newPath(path, sourcePrefix, targetPrefix)));
388+
return result;
389+
}
390+
391+
private RewriteResult<ManifestFile> rewriteManifestLists(Set<Snapshot> validSnapshots, TableMetadata endMetadata,
392+
Set<String> manifestsToRewrite) {
393+
394+
if (validSnapshots.isEmpty()) {
395+
return new RewriteResult<>();
396+
}
397+
398+
int maxInFlight = parallelism * MAX_INFLIGHT_MULTIPLIER;
399+
Semaphore semaphore = new Semaphore(maxInFlight);
400+
ExecutorCompletionService<RewriteResult<ManifestFile>> completionService =
401+
new ExecutorCompletionService<>(executorService);
402+
403+
RewriteResult<ManifestFile> combined = new RewriteResult<>();
404+
int submittedTasks = 0;
405+
int completedTasks = 0;
406+
407+
try {
408+
for (Snapshot snapshot : validSnapshots) {
409+
semaphore.acquire();
410+
411+
boolean taskSubmitted = false;
412+
try {
413+
completionService.submit(() -> {
414+
try {
415+
return rewriteManifestList(snapshot, endMetadata, manifestsToRewrite);
416+
} finally {
417+
semaphore.release();
418+
}
419+
});
420+
taskSubmitted = true;
421+
submittedTasks++;
422+
} finally {
423+
if (!taskSubmitted) {
424+
semaphore.release();
425+
}
426+
}
427+
428+
Future<RewriteResult<ManifestFile>> done;
429+
while ((done = completionService.poll()) != null) {
430+
combined.append(done.get());
431+
completedTasks++;
432+
}
433+
}
434+
435+
while (completedTasks < submittedTasks) {
436+
combined.append(completionService.take().get());
437+
completedTasks++;
438+
}
439+
440+
} catch (InterruptedException e) {
441+
Thread.currentThread().interrupt();
442+
executorService.shutdownNow();
443+
throw new RuntimeException("Interrupted while rewriting manifest lists", e);
444+
} catch (ExecutionException e) {
445+
executorService.shutdownNow();
446+
throw new RuntimeException("Failed to rewrite manifest list", e.getCause());
447+
}
448+
449+
return combined;
450+
}
451+
365452
private Set<Snapshot> deltaSnapshots(TableMetadata startMetadata, Set<Snapshot> allSnapshots) {
366453
if (startMetadata == null) {
367454
return allSnapshots;

hadoop-ozone/iceberg/src/test/java/org/apache/hadoop/ozone/iceberg/TestRewriteTablePathOzoneAction.java

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.BufferedReader;
2424
import java.io.InputStreamReader;
2525
import java.nio.charset.StandardCharsets;
26+
import java.nio.file.Path;
2627
import java.util.ArrayList;
2728
import java.util.HashMap;
2829
import java.util.HashSet;
@@ -33,7 +34,11 @@
3334
import org.apache.iceberg.DataFile;
3435
import org.apache.iceberg.DataFiles;
3536
import org.apache.iceberg.FileFormat;
37+
import org.apache.iceberg.GenericManifestFile;
38+
import org.apache.iceberg.GenericPartitionFieldSummary;
3639
import org.apache.iceberg.HasTableOperations;
40+
import org.apache.iceberg.InternalData;
41+
import org.apache.iceberg.ManifestFile;
3742
import org.apache.iceberg.PartitionSpec;
3843
import org.apache.iceberg.RewriteTablePathUtil;
3944
import org.apache.iceberg.Schema;
@@ -44,6 +49,7 @@
4449
import org.apache.iceberg.TableMetadata.MetadataLogEntry;
4550
import org.apache.iceberg.actions.RewriteTablePath;
4651
import org.apache.iceberg.hadoop.HadoopTables;
52+
import org.apache.iceberg.io.CloseableIterable;
4753
import org.apache.iceberg.types.Types;
4854
import org.apache.iceberg.util.Pair;
4955
import org.junit.jupiter.api.BeforeEach;
@@ -68,11 +74,11 @@ class TestRewriteTablePathOzoneAction {
6874
private Table table = null;
6975

7076
@TempDir
71-
private java.nio.file.Path tableDir;
77+
private Path tableDir;
7278
@TempDir
73-
private java.nio.file.Path targetDir;
79+
private Path targetDir;
7480
@TempDir
75-
private java.nio.file.Path stagingDir;
81+
private Path stagingDir;
7682

7783
@BeforeEach
7884
public void setupTableLocation() {
@@ -96,7 +102,9 @@ void fullTablePathRewrite() throws Exception {
96102
}
97103

98104
Set<Pair<String, String>> csvPairs = readCsvPairs(table, result.fileListLocation());
99-
assertEquals(expectedTargets, csvPairs.stream().map(Pair::second).collect(Collectors.toSet()));
105+
Set<String> actualTargets = csvPairs.stream().map(Pair::second).collect(Collectors.toSet());
106+
assertTrue(actualTargets.containsAll(expectedTargets),
107+
"Copy plan should contain all expected version file targets");
100108

101109
// Verify all internal paths inside each staged metadata file are rewritten to target.
102110
assertAllInternalPathsRewritten(csvPairs, targetPrefix);
@@ -124,7 +132,9 @@ void tablePathRewriteForStartAndNoEndVersionProvided() throws Exception {
124132
}
125133

126134
Set<Pair<String, String>> csvPairs = readCsvPairs(table, result.fileListLocation());
127-
assertEquals(expectedTargets, csvPairs.stream().map(Pair::second).collect(Collectors.toSet()));
135+
Set<String> actualTargets = csvPairs.stream().map(Pair::second).collect(Collectors.toSet());
136+
assertTrue(actualTargets.containsAll(expectedTargets),
137+
"Copy plan should contain all expected version file targets");
128138

129139
// Verify all internal paths inside each staged metadata file are rewritten to target
130140
assertAllInternalPathsRewritten(csvPairs, targetPrefix);
@@ -152,7 +162,9 @@ void tablePathRewriteForOnlyEndVersionProvided() throws Exception {
152162
}
153163

154164
Set<Pair<String, String>> csvPairs = readCsvPairs(table, result.fileListLocation());
155-
assertEquals(expectedTargets, csvPairs.stream().map(Pair::second).collect(Collectors.toSet()));
165+
Set<String> actualTargets = csvPairs.stream().map(Pair::second).collect(Collectors.toSet());
166+
assertTrue(actualTargets.containsAll(expectedTargets),
167+
"Copy plan should contain all expected version file targets");
156168

157169
// Verify all internal paths inside each staged metadata file are rewritten to target
158170
assertAllInternalPathsRewritten(csvPairs, targetPrefix);
@@ -182,49 +194,119 @@ void tablePathRewriteForStartAndEndVersionProvided() throws Exception {
182194
}
183195

184196
Set<Pair<String, String>> csvPairs = readCsvPairs(table, result.fileListLocation());
185-
assertEquals(expectedTargets, csvPairs.stream().map(Pair::second).collect(Collectors.toSet()));
197+
Set<String> actualTargets = csvPairs.stream().map(Pair::second).collect(Collectors.toSet());
198+
assertTrue(actualTargets.containsAll(expectedTargets),
199+
"Copy plan should contain all expected version file targets");
186200

187201
// Verify all internal paths inside each staged metadata file are rewritten to target
188202
assertAllInternalPathsRewritten(csvPairs, targetPrefix);
189203
}
190204

191205
/**
192-
* For every staged metadata JSON file in the CSV, parses the file and asserts that:
193-
* - The table location starts with target
194-
* - Every metadata-log entry path starts with target
195-
* - Every snapshot's manifest-list path starts with target
196-
* - Every statistics file path starts with target
197-
* - None of the above contain the source prefix.
206+
* For every staged file in the CSV copy plan, asserts that internal paths are rewritten
207+
* to the target prefix:
208+
* <ul>
209+
* <li><b>.metadata.json</b>: table location, metadata-log entries, and snapshot
210+
* manifest-list references all start with target.</li>
211+
* <li><b>snap-*.avro (manifest-list)</b>: target path starts with target, and every
212+
* manifest entry path inside the staged file starts with target.</li>
213+
* <li><b>*.avro (manifest)</b>: target path starts with target (content rewrite
214+
* is not yet implemented).</li>
215+
* </ul>
198216
*/
199-
private void assertAllInternalPathsRewritten(Set<Pair<String, String>> csvPairs, String target) {
217+
private void assertAllInternalPathsRewritten(Set<Pair<String, String>> csvPairs, String target) throws Exception {
218+
200219
for (Pair<String, String> pair : csvPairs) {
201220
String stagingPath = pair.first();
202221
String targetPath = pair.second();
203222

204-
// Only inspect .metadata.json files, manifest/data files and snapshots are not yet rewritten
205-
if (!stagingPath.endsWith(".metadata.json")) {
206-
continue;
223+
if (stagingPath.endsWith(".metadata.json")) {
224+
assertMetadataFileRewritten(stagingPath, targetPath, target);
225+
} else if (RewriteTablePathUtil.fileName(stagingPath).startsWith("snap-")) {
226+
assertManifestListRewritten(stagingPath, targetPath, target);
227+
} else if (RewriteTablePathUtil.fileName(stagingPath).endsWith(".avro")) {
228+
assertTrue(targetPath.startsWith(target),
229+
"Manifest file target path should start with target prefix: " + targetPath);
207230
}
231+
}
232+
}
208233

209-
assertTrue(targetPath.startsWith(target),
210-
"Target path in CSV should start with target prefix: " + targetPath);
234+
private void assertMetadataFileRewritten(String stagingPath, String targetPath, String target) {
235+
236+
assertTrue(targetPath.startsWith(target),
237+
"Target path in CSV should start with target prefix: " + targetPath);
238+
assertEquals(RewriteTablePathUtil.fileName(stagingPath), RewriteTablePathUtil.fileName(targetPath),
239+
"original and target metadata file should have the same filename");
240+
241+
TableMetadata rewritten = new StaticTableOperations(stagingPath, table.io()).current();
242+
TableMetadata original = new StaticTableOperations(
243+
targetPath.replace(targetPrefix, sourcePrefix), table.io()).current();
244+
Set<String> expectedMetadata = original.previousFiles().stream()
245+
.map(e -> RewriteTablePathUtil.fileName(e.file()))
246+
.collect(Collectors.toSet());
247+
Set<String> expectedManifestLists = original.snapshots().stream()
248+
.map(s -> RewriteTablePathUtil.fileName(s.manifestListLocation()))
249+
.collect(Collectors.toSet());
250+
Set<String> actualMetadata = new HashSet<>();
251+
Set<String> actualManifestLists = new HashSet<>();
252+
253+
assertTrue(rewritten.location().startsWith(target),
254+
"Metadata location should start with target: " + rewritten.location());
255+
256+
for (MetadataLogEntry entry : rewritten.previousFiles()) {
257+
assertTrue(entry.file().startsWith(target),
258+
"Metadata log entry should start with target: " + entry.file());
259+
actualMetadata.add(RewriteTablePathUtil.fileName(entry.file()));
260+
}
211261

212-
TableMetadata rewritten = new StaticTableOperations(stagingPath, table.io()).current();
262+
assertEquals(expectedMetadata, actualMetadata,
263+
"Rewritten metadata file should reference the same metadata files as the original");
213264

214-
assertTrue(rewritten.location().startsWith(target),
215-
"Metadata location should start with target: " + rewritten.location());
265+
for (Snapshot snapshot : rewritten.snapshots()) {
266+
String manifestList = snapshot.manifestListLocation();
267+
assertTrue(manifestList.startsWith(target),
268+
"Snapshot's manifest-list should start with target: " + manifestList);
269+
actualManifestLists.add(RewriteTablePathUtil.fileName(manifestList));
270+
}
271+
assertEquals(expectedManifestLists, actualManifestLists,
272+
"Rewritten metadata file should reference the same manifest-lists as the original");
273+
}
216274

217-
for (MetadataLogEntry entry : rewritten.previousFiles()) {
218-
assertTrue(entry.file().startsWith(target),
219-
"Metadata log entry should start with target: " + entry.file());
275+
private void assertManifestListRewritten(String stagingPath, String targetPath, String target) throws Exception {
276+
277+
assertTrue(targetPath.startsWith(target),
278+
"Manifest list target path should start with target prefix: " + targetPath);
279+
assertEquals(RewriteTablePathUtil.fileName(stagingPath), RewriteTablePathUtil.fileName(targetPath),
280+
"original and target manifest list should have the same filename");
281+
282+
Set<String> expectedManifests = new HashSet<>();
283+
Set<String> actualManifests = new HashSet<>();
284+
for (Snapshot s : table.snapshots()) {
285+
if (RewriteTablePathUtil.fileName(s.manifestListLocation()).equals(RewriteTablePathUtil.fileName(stagingPath))) {
286+
expectedManifests = s.allManifests(table.io())
287+
.stream()
288+
.map(m -> RewriteTablePathUtil.fileName(m.path()))
289+
.collect(Collectors.toSet());
290+
break;
220291
}
292+
}
221293

222-
for (Snapshot snapshot : rewritten.snapshots()) {
223-
String manifestList = snapshot.manifestListLocation();
224-
assertTrue(manifestList.startsWith(target),
225-
"Snapshot manifest-list should start with target: " + manifestList);
294+
try (CloseableIterable<ManifestFile> manifests =
295+
InternalData.read(FileFormat.AVRO, table.io().newInputFile(stagingPath))
296+
.setRootType(GenericManifestFile.class)
297+
.setCustomType(
298+
ManifestFile.PARTITION_SUMMARIES_ELEMENT_ID,
299+
GenericPartitionFieldSummary.class)
300+
.project(ManifestFile.schema())
301+
.build()) {
302+
for (ManifestFile manifest : manifests) {
303+
assertTrue(manifest.path().startsWith(target),
304+
"Manifest path inside staged manifest list should start with target prefix: " + manifest.path());
305+
actualManifests.add(RewriteTablePathUtil.fileName(manifest.path()));
226306
}
227307
}
308+
assertEquals(expectedManifests, actualManifests,
309+
"Rewritten manifest list should reference the same manifest files as the original");
228310
}
229311

230312
private static List<String> metadataLogEntryPaths(Table tbl) {

0 commit comments

Comments
 (0)