Skip to content

Commit cb4d8ec

Browse files
[server][flink][test] polish orphan cleanup and broaden active snapshot set
1 parent 51e43b6 commit cb4d8ec

16 files changed

Lines changed: 374 additions & 212 deletions

File tree

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/OrphanCleanUtils.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apache.fluss.annotation.Internal;
2121
import org.apache.fluss.client.admin.Admin;
2222
import org.apache.fluss.config.ConfigOptions;
23+
import org.apache.fluss.config.Configuration;
2324
import org.apache.fluss.config.cluster.ConfigEntry;
2425
import org.apache.fluss.fs.FileStatus;
2526
import org.apache.fluss.fs.FileSystem;
@@ -36,6 +37,7 @@
3637
import java.util.ArrayList;
3738
import java.util.Collection;
3839
import java.util.HashMap;
40+
import java.util.LinkedHashSet;
3941
import java.util.List;
4042
import java.util.Map;
4143

@@ -108,12 +110,65 @@ public static String resolveRemoteDataDir(
108110
*/
109111
@Nullable
110112
public static String resolveClusterRemoteDataDir(Admin admin) throws Exception {
113+
return resolveClusterRemoteDataDir(fetchClusterConfigMap(admin));
114+
}
115+
116+
/** Extracts the single-root {@code remote.data.dir} from a pre-fetched config map. */
117+
@Nullable
118+
public static String resolveClusterRemoteDataDir(Map<String, String> configMap) {
119+
return configMap.get(ConfigOptions.REMOTE_DATA_DIR.key());
120+
}
121+
122+
/**
123+
* Resolves all cluster-level remote data directories by querying the coordinator's runtime
124+
* configuration. Reads both the single-root {@code remote.data.dir} and the multi-root {@code
125+
* remote.data.dirs}, deduplicates by normalized form, and returns the union as the canonical
126+
* root list.
127+
*
128+
* <p>This is the authoritative source for determining what storage roots the cleanup action is
129+
* allowed to touch.
130+
*
131+
* @return list of normalized roots (no trailing slash); never {@code null}, may be empty if the
132+
* cluster has neither config set (which should not happen because the coordinator requires
133+
* at least one remote data dir at startup).
134+
*/
135+
public static List<String> resolveClusterRemoteDataDirs(Admin admin) throws Exception {
136+
return resolveClusterRemoteDataDirs(fetchClusterConfigMap(admin));
137+
}
138+
139+
/** Extracts all remote data roots from a pre-fetched config map. */
140+
public static List<String> resolveClusterRemoteDataDirs(Map<String, String> configMap) {
141+
Configuration conf = Configuration.fromMap(configMap);
142+
LinkedHashSet<String> roots = new LinkedHashSet<String>();
143+
String singleDir = conf.get(ConfigOptions.REMOTE_DATA_DIR);
144+
if (singleDir != null && !singleDir.isEmpty()) {
145+
roots.add(normalizeRoot(singleDir));
146+
}
147+
List<String> multiDirs = conf.get(ConfigOptions.REMOTE_DATA_DIRS);
148+
if (multiDirs != null) {
149+
for (String dir : multiDirs) {
150+
if (dir != null && !dir.isEmpty()) {
151+
roots.add(normalizeRoot(dir));
152+
}
153+
}
154+
}
155+
return new ArrayList<String>(roots);
156+
}
157+
158+
/**
159+
* Fetches the coordinator's runtime configuration as a key-value map. Use this once and pass
160+
* the result to the map-based overloads of {@link #resolveClusterRemoteDataDir(Map)} and {@link
161+
* #resolveClusterRemoteDataDirs(Map)} to avoid duplicate RPCs.
162+
*/
163+
public static Map<String, String> fetchClusterConfigMap(Admin admin) throws Exception {
111164
Collection<ConfigEntry> entries = admin.describeClusterConfigs().get();
112165
Map<String, String> map = new HashMap<String, String>();
113166
for (ConfigEntry entry : entries) {
114-
map.put(entry.key(), entry.value());
167+
if (entry.value() != null) {
168+
map.put(entry.key(), entry.value());
169+
}
115170
}
116-
return map.get(ConfigOptions.REMOTE_DATA_DIR.key());
171+
return map;
117172
}
118173

119174
/** Constructs a remote sub-directory path, normalizing trailing slashes on the root. */

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/OrphanFilesCleanActionFactory.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,21 @@ public Optional<Action> create(MultipleParameterToolAdapter params) {
4444
public String help() {
4545
return "Usage: orphan_files_clean --bootstrap-server <host:port>\n"
4646
+ " (--database <db> [--table <name>] | --all-databases)\n"
47-
+ " [--scan-root <remoteDataDir>]...\n"
48-
+ " [--older-than 'yyyy-MM-dd HH:mm:ss']\n"
47+
+ " [--older-than '<ISO-8601 with offset>']\n"
4948
+ " [--delete-rate-limit-per-second 100] [--dry-run]\n"
5049
+ " [--allow-delete-manifest]\n"
5150
+ " [--allow-clean-orphan-tables]\n"
5251
+ " [--allow-clean-orphan-partitions]\n"
5352
+ " [--conf <key>=<value>]...\n"
5453
+ "\n"
5554
+ "Notes:\n"
56-
+ " --older-than is an absolute wall-clock cutoff (server local timezone). Files\n"
57-
+ " with mtime strictly less than the cutoff are deletion-eligible. Default:\n"
58-
+ " now - 3d, computed once at startup. The cutoff is frozen for the run, so a\n"
59-
+ " long scan cannot accidentally pull in files written after the action started.\n"
60-
+ " The cutoff must be at least 1d before now (closer cutoffs would race with\n"
61-
+ " mid-write files).\n"
55+
+ " --older-than is an absolute wall-clock cutoff in ISO-8601 with explicit\n"
56+
+ " offset (e.g. '2024-01-01T00:00:00+08:00' or '2024-01-01T00:00:00Z').\n"
57+
+ " Files with mtime strictly less than the cutoff are deletion-eligible.\n"
58+
+ " Default: now - 3d, computed once at startup. The cutoff is frozen for the\n"
59+
+ " run, so a long scan cannot accidentally pull in files written after the\n"
60+
+ " action started. The cutoff must be at least 1d before now (closer cutoffs\n"
61+
+ " would race with mid-write files).\n"
6262
+ " Orphan directory detection (table/partition) relies solely on ID guards\n"
6363
+ " (maxKnownTableId / maxKnownPartitionId), not mtime.\n"
6464
+ " --table also disables the orphan-table scan (no sibling orphan-table scan in\n"

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/audit/AuditLogger.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,15 @@ public void logSkipOrphanPartition(FsPath dir, String reason) {
196196
AUDIT.info(
197197
"action=skip_orphan_partition reason={} path={} ts={}", reason, dir, Instant.now());
198198
}
199+
200+
/** Skip a bucket target because its metadata-resolved root is outside cluster config. */
201+
public void logSkipBucketOutOfScope(long tableId, Long partitionId, String resolvedRoot) {
202+
AUDIT.info(
203+
"action=skip_bucket_target reason=out-of-scope-root table_id={} partition_id={}"
204+
+ " resolved_root={} ts={}",
205+
tableId,
206+
partitionId,
207+
resolvedRoot,
208+
Instant.now());
209+
}
199210
}

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/config/OrphanCleanConfig.java

Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,11 @@
2626
import java.io.Serializable;
2727
import java.time.Duration;
2828
import java.time.Instant;
29-
import java.time.LocalDateTime;
30-
import java.time.ZoneId;
31-
import java.time.format.DateTimeFormatter;
29+
import java.time.OffsetDateTime;
3230
import java.time.format.DateTimeParseException;
33-
import java.util.ArrayList;
3431
import java.util.Collection;
3532
import java.util.Collections;
3633
import java.util.HashMap;
37-
import java.util.List;
3834
import java.util.Map;
3935
import java.util.Optional;
4036

@@ -57,14 +53,6 @@ public final class OrphanCleanConfig implements Serializable {
5753

5854
private static final long DEFAULT_DELETE_RATE_LIMIT_PER_SECOND = 100L;
5955

60-
/**
61-
* Wall-clock timestamp format accepted on the CLI ({@code yyyy-MM-dd HH:mm:ss}, interpreted in
62-
* the server's local time zone). Matches Apache Paimon's {@code orphan_files_clean older_than}
63-
* grammar to minimize operator context-switching between systems.
64-
*/
65-
private static final DateTimeFormatter CUTOFF_FORMATTER =
66-
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
67-
6856
private final String bootstrapServer;
6957
private final boolean allDatabases;
7058
private final @Nullable String database;
@@ -73,7 +61,6 @@ public final class OrphanCleanConfig implements Serializable {
7361
private final boolean dryRun;
7462
private final long deleteRateLimitPerSecond;
7563
private final @Nullable Integer parallelism;
76-
private final List<String> scanRoots;
7764
private final boolean allowDeleteManifest;
7865
private final boolean allowCleanOrphanTables;
7966
private final boolean allowCleanOrphanPartitions;
@@ -88,7 +75,6 @@ private OrphanCleanConfig(
8875
boolean dryRun,
8976
long deleteRateLimitPerSecond,
9077
@Nullable Integer parallelism,
91-
List<String> scanRoots,
9278
boolean allowDeleteManifest,
9379
boolean allowCleanOrphanTables,
9480
boolean allowCleanOrphanPartitions,
@@ -101,7 +87,6 @@ private OrphanCleanConfig(
10187
this.dryRun = dryRun;
10288
this.deleteRateLimitPerSecond = deleteRateLimitPerSecond;
10389
this.parallelism = parallelism;
104-
this.scanRoots = Collections.unmodifiableList(new ArrayList<String>(scanRoots));
10590
this.allowDeleteManifest = allowDeleteManifest;
10691
this.allowCleanOrphanTables = allowCleanOrphanTables;
10792
this.allowCleanOrphanPartitions = allowCleanOrphanPartitions;
@@ -149,7 +134,6 @@ public static OrphanCleanConfig fromParams(MultipleParameterToolAdapter params)
149134
params.has("dry-run"),
150135
deleteRateLimitPerSecond,
151136
parallelism,
152-
parseScanRoots(params.getMultiParameter("scan-root")),
153137
allowDeleteManifest,
154138
allowCleanOrphanTables,
155139
allowCleanOrphanPartitions,
@@ -158,27 +142,28 @@ public static OrphanCleanConfig fromParams(MultipleParameterToolAdapter params)
158142

159143
/**
160144
* Parses a CLI cutoff value into an absolute epoch-ms timestamp. Empty input falls back to
161-
* {@code now - defaultGap}. Explicit input must parse as {@code yyyy-MM-dd HH:mm:ss} in the
162-
* Flink action JVM's local time zone and must be at least {@link #HARD_LOWER_BOUND} earlier
163-
* than {@code now} — closer-to-now cutoffs would race with active writes (see {@code
164-
* HARD_LOWER_BOUND} javadoc).
145+
* {@code now - defaultGap}. Explicit input must be ISO-8601 with an explicit offset (e.g.
146+
* {@code 2024-01-01T00:00:00+08:00} or {@code 2024-01-01T00:00:00Z}) and must be at least
147+
* {@link #HARD_LOWER_BOUND} earlier than {@code now} — closer-to-now cutoffs would race with
148+
* active writes (see {@code HARD_LOWER_BOUND} javadoc).
165149
*/
166150
private static long parseCutoff(
167151
String flag, @Nullable String value, long now, Duration defaultGap) {
168152
if (StringUtils.isNullOrWhitespaceOnly(value)) {
169153
return now - defaultGap.toMillis();
170154
}
171-
LocalDateTime parsed;
155+
OffsetDateTime parsed;
172156
try {
173-
parsed = LocalDateTime.parse(value, CUTOFF_FORMATTER);
157+
parsed = OffsetDateTime.parse(value);
174158
} catch (DateTimeParseException e) {
175159
throw new IllegalArgumentException(
176160
flag
177-
+ " must be a timestamp in 'yyyy-MM-dd HH:mm:ss' (server local TZ), got: "
161+
+ " must be an ISO-8601 timestamp with an explicit offset (e.g."
162+
+ " '2024-01-01T00:00:00+08:00' or '2024-01-01T00:00:00Z'); got: "
178163
+ value,
179164
e);
180165
}
181-
long parsedMillis = parsed.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
166+
long parsedMillis = parsed.toInstant().toEpochMilli();
182167
long maxAllowed = now - HARD_LOWER_BOUND.toMillis();
183168
if (parsedMillis > maxAllowed) {
184169
throw new IllegalArgumentException(
@@ -215,21 +200,6 @@ private static Integer parseParallelism(@Nullable String value) {
215200
return p;
216201
}
217202

218-
private static List<String> parseScanRoots(@Nullable Collection<String> values) {
219-
if (values == null || values.isEmpty()) {
220-
return Collections.emptyList();
221-
}
222-
223-
List<String> scanRoots = new ArrayList<String>(values.size());
224-
for (String value : values) {
225-
if (StringUtils.isNullOrWhitespaceOnly(value)) {
226-
throw new IllegalArgumentException("--scan-root must not be blank");
227-
}
228-
scanRoots.add(value);
229-
}
230-
return scanRoots;
231-
}
232-
233203
private static Map<String, String> parseExtraConfigs(@Nullable Collection<String> values) {
234204
if (values == null || values.isEmpty()) {
235205
return Collections.emptyMap();
@@ -291,11 +261,6 @@ public Optional<Integer> parallelism() {
291261
return Optional.ofNullable(parallelism);
292262
}
293263

294-
/** Returns additional remote.data.dir roots to scan. */
295-
public List<String> scanRoots() {
296-
return scanRoots;
297-
}
298-
299264
/**
300265
* Opt-in to delete {@code .manifest} files. Default {@code false}: mis-deleting an active
301266
* manifest leaves the coordinator's manifest pointer dangling and breaks the bucket's metadata

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/job/CleanStats.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
import java.util.List;
2525

2626
/**
27-
* Aggregatable cleanup statistics emitted by each {@link ScanAndCleanFunction} subtask. The {@code
28-
* touchedDirs} list is collected by the final aggregator for empty-directory sweeping after all
29-
* subtasks complete.
27+
* Per-task cleanup statistics emitted by each {@link ScanAndCleanFunction} subtask. The scalar
28+
* counters are accumulated by {@link StatsAggregateOperator} via simple addition; the short {@code
29+
* touchedDirs} list (typically 1–2 entries per task) is inserted into a {@code HashSet} for O(1)
30+
* deduplication — no list concatenation or O(n²) merge is needed.
3031
*/
3132
@Internal
3233
public final class CleanStats implements Serializable {
@@ -49,11 +50,11 @@ public CleanStats(
4950
this.deleted = deleted;
5051
this.deleteFailures = deleteFailures;
5152
this.bytesReclaimed = bytesReclaimed;
52-
this.touchedDirs = new ArrayList<>(touchedDirs);
53+
this.touchedDirs = new ArrayList<String>(touchedDirs);
5354
}
5455

5556
public static CleanStats empty() {
56-
return new CleanStats(0L, 0L, 0L, 0L, new ArrayList<String>());
57+
return new CleanStats(0L, 0L, 0L, 0L, new ArrayList<String>(0));
5758
}
5859

5960
public long scanned() {
@@ -75,15 +76,4 @@ public long bytesReclaimed() {
7576
public List<String> touchedDirs() {
7677
return touchedDirs;
7778
}
78-
79-
public CleanStats merge(CleanStats other) {
80-
List<String> mergedDirs = new ArrayList<>(this.touchedDirs);
81-
mergedDirs.addAll(other.touchedDirs);
82-
return new CleanStats(
83-
this.scanned + other.scanned,
84-
this.deleted + other.deleted,
85-
this.deleteFailures + other.deleteFailures,
86-
this.bytesReclaimed + other.bytesReclaimed,
87-
mergedDirs);
88-
}
8979
}

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/job/EmptyDirSweeper.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ public final class EmptyDirSweeper {
5454
private final RateLimiter rateLimiter;
5555
private final Set<FsPath> touchedRoots = new HashSet<FsPath>();
5656

57-
public EmptyDirSweeper(boolean dryRun, AuditLogger audit) {
58-
this(dryRun, audit, RateLimiter.create(100.0));
59-
}
60-
6157
public EmptyDirSweeper(boolean dryRun, AuditLogger audit, RateLimiter rateLimiter) {
6258
this.dryRun = dryRun;
6359
this.audit = audit;

fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/action/orphan/job/OrphanFilesCleanJob.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ public static CleanStats execute(
8787
stats.transform(
8888
"StatsAggregate",
8989
TypeInformation.of(new TypeHint<CleanStats>() {}),
90-
new StatsAggregateOperator(config.dryRun(), config.extraConfigs()))
90+
new StatsAggregateOperator(
91+
config.dryRun(),
92+
config.extraConfigs(),
93+
config.deleteRateLimitPerSecond()))
9194
.setParallelism(1)
9295
.setMaxParallelism(1);
9396

0 commit comments

Comments
 (0)