This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 9a6f4b848a [#11195] docs: add design doc for iceberg
remove-orphan-files maintenance job (#11700)
9a6f4b848a is described below
commit 9a6f4b848ac697d7283df7780325bc80eeb935f7
Author: Akshay Thorat <[email protected]>
AuthorDate: Mon Sep 14 18:22:46 2026 -0700
[#11195] docs: add design doc for iceberg remove-orphan-files maintenance
job (#11700)
### What changes were proposed in this pull request?
Add a design document for the built-in Iceberg maintenance job
`builtin-iceberg-remove-orphan-files` that identifies and removes
orphaned data and metadata files from Iceberg table storage locations
via Spark's `remove_orphan_files` procedure.
The design doc covers:
- Full end-to-end architecture: Policy → Strategy → Adapter → Job
- Policy content class (`IcebergOrphanFileRemovalContent`) with
configurable parameters: `older_than` (timestamp), `location` (custom
path), `dry_run` (preview mode)
- Strategy handler for time-based trigger evaluation
- Job adapter for context-to-config conversion
- Safety considerations (3-day default, dry-run mode, policy-gated
execution)
- Comparison with existing compaction and snapshot expiration flows
- Proposed PR plan (3 incremental PRs)
### Why are the changes needed?
Orphan files accumulate from failed writes, incomplete transactions,
schema evolution, or concurrent operations. Without periodic cleanup,
these files waste significant storage. The existing built-in jobs cover
data compaction, statistics, and snapshot expiration but do not address
orphan file cleanup.
A design doc is needed before implementation to align on the approach,
safety mechanisms, and PR structure.
Fix: #11195
### Does this PR introduce any user-facing change?
No. This is a design document only.
### How was this patch tested?
N/A — design doc only, no code changes.
---
.../iceberg-remove-orphan-files-maintenance-job.md | 616 +++++++++++++++++++++
1 file changed, 616 insertions(+)
diff --git a/design-docs/iceberg-remove-orphan-files-maintenance-job.md
b/design-docs/iceberg-remove-orphan-files-maintenance-job.md
new file mode 100644
index 0000000000..885f78cb62
--- /dev/null
+++ b/design-docs/iceberg-remove-orphan-files-maintenance-job.md
@@ -0,0 +1,616 @@
+<!--
+ 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.
+-->
+
+# Design: Built-in Iceberg Remove Orphan Files Maintenance Job
+
+| Field | Value |
+| ------- | ---------------------------------------------------------- |
+| Status | Draft |
+| Authors | @laserninja |
+| Created | 2026-06-16 |
+| Issue | [#11195](https://github.com/apache/gravitino/issues/11195) |
+| Module | `api`, `maintenance/jobs`, `maintenance/optimizer` |
+
+---
+
+## 1. Background
+
+Orphan files accumulate in Iceberg table storage locations from failed writes,
+incomplete transactions, schema evolution, or concurrent operations. These
files
+are no longer referenced by any table snapshot but remain on disk, wasting
+significant storage - especially in high-write-volume environments.
+
+The existing built-in maintenance jobs (`builtin-iceberg-rewrite-data-files`
for
+data compaction, `builtin-iceberg-update-stats` for metrics, and
+`builtin-iceberg-expire-snapshots` for metadata cleanup) address data file
+optimization and snapshot lifecycle but do not cover orphan file removal.
+
+PR [#10500](https://github.com/apache/gravitino/pull/10500) added Trino-side
+delegation for `remove_orphan_files` as a procedure, but there is no
+server-side built-in job that can be triggered automatically via the Table
+Maintenance Service (Optimizer) policies.
+
+This design proposes adding full end-to-end support for Iceberg orphan file
+removal: from policy definition through strategy evaluation to Spark job
+execution.
+
+---
+
+## 2. Goals
+
+1. Add a new built-in policy type `system_iceberg_orphan_file_removal` for
+ declarative orphan file cleanup configuration.
+2. Add a strategy handler that evaluates when orphan file removal should run
+ based on configured trigger conditions.
+3. Add a job adapter that converts strategy evaluation results into job
+ configurations.
+4. Add the Spark job that executes Iceberg's `remove_orphan_files` procedure.
+5. Ensure the full flow works end-to-end: policy → strategy → job
+ submission → Spark execution.
+
+---
+
+## 3. Non-Goals
+
+- Snapshot expiration (separate Iceberg procedure, separate issue
[#11194](https://github.com/apache/gravitino/issues/11194)).
+- Automatic policy creation - users must explicitly create and attach
+ policies.
+- Changes to the Optimizer scheduling framework itself.
+- Custom file-level filtering beyond what Iceberg's procedure supports.
+
+---
+
+## 4. Existing Architecture Overview
+
+The Gravitino maintenance module follows a layered architecture for automated
+table maintenance. The existing Iceberg compaction flow establishes the
+pattern:
+
+```
+Policy Creation (REST API)
+ ↓
+GravitinoStrategyProvider (loads policies as strategies)
+ ↓
+CompactionStrategyHandler (evaluates trigger / score expressions)
+ ↓
+CompactionJobContext → GravitinoCompactionJobAdapter (converts to job config)
+ ↓
+GravitinoJobSubmitter (submits job via REST)
+ ↓
+IcebergRewriteDataFilesJob (Spark execution)
+```
+
+### 4.1 Layer Summary
+
+| Layer | Compaction Components
| Purpose |
+| ------------ |
------------------------------------------------------------------------ |
---------------------------------------------- |
+| **Policy** | `Policy.BuiltInType.ICEBERG_COMPACTION`,
`IcebergDataCompactionContent` | Define configuration, thresholds, expressions
|
+| **Strategy** | `CompactionStrategyHandler` extends
`BaseExpressionStrategyHandler` | Evaluate trigger conditions, score
partitions |
+| **Adapter** | `GravitinoCompactionJobAdapter`, `CompactionJobContext`
| Convert evaluation result to job configuration |
+| **Job** | `IcebergRewriteDataFilesJob`, registered in
`BuiltInJobTemplateProvider` | Execute Spark procedure |
+
+---
+
+## 5. Proposed Design
+
+We add the same four layers for orphan file removal, following the compaction
+pattern.
+
+### 5.1 Architecture Diagram
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ REST API: POST /metalakes/{m}/policies │
+│ type: "system_iceberg_orphan_file_removal" │
+│ content: IcebergOrphanFileRemovalContent │
+│ { olderThanDays, location, dryRun } │
+└──────────────────────────┬───────────────────────────────────┘
+ ↓
+┌──────────────────────────────────────────────────────────────┐
+│ GravitinoStrategyProvider │
+│ Loads policy → GravitinoStrategy │
+│ strategyType: "iceberg-orphan-file-removal" │
+│ jobTemplateName: "builtin-iceberg-remove-orphan-files" │
+└──────────────────────────┬───────────────────────────────────┘
+ ↓
+┌──────────────────────────────────────────────────────────────┐
+│ OrphanFileRemovalStrategyHandler │
+│ extends BaseExpressionStrategyHandler │
+│ dataRequirements: {TABLE_METADATA} │
+│ Evaluates: configured trigger conditions │
+│ Returns: StrategyEvaluation with score + context │
+└──────────────────────────┬───────────────────────────────────┘
+ ↓
+┌──────────────────────────────────────────────────────────────┐
+│ OrphanFileRemovalJobContext → JobAdapter │
+│ Extracts: older_than, location, dry_run │
+│ Builds: job config map for template substitution │
+└──────────────────────────┬───────────────────────────────────┘
+ ↓
+┌──────────────────────────────────────────────────────────────┐
+│ GravitinoJobSubmitter │
+│ Template: "builtin-iceberg-remove-orphan-files" │
+│ Submits via REST: POST /metalakes/{m}/jobs │
+└──────────────────────────┬───────────────────────────────────┘
+ ↓
+┌──────────────────────────────────────────────────────────────┐
+│ IcebergRemoveOrphanFilesJob (Spark) │
+│ CALL catalog.system.remove_orphan_files( │
+│ table => '…', older_than => TIMESTAMP '…', │
+│ location => '…', dry_run => bool) │
+└──────────────────────────────────────────────────────────────┘
+```
+
+---
+
+### 5.2 Layer 1 - Policy Definition (`api/`)
+
+#### 5.2.1 New Policy Type
+
+Add `ICEBERG_ORPHAN_FILE_REMOVAL` to `Policy.BuiltInType`:
+
+```java
+// api/src/main/java/org/apache/gravitino/policy/Policy.java
+enum BuiltInType {
+ ICEBERG_COMPACTION("system_iceberg_compaction",
+ IcebergDataCompactionContent.class),
+ ICEBERG_ORPHAN_FILE_REMOVAL("system_iceberg_orphan_file_removal",
+ IcebergOrphanFileRemovalContent.class), // NEW
+ CUSTOM("custom", CustomContent.class);
+}
+```
+
+#### 5.2.2 New Policy Content Class
+
+Create `IcebergOrphanFileRemovalContent` following the
+`IcebergDataCompactionContent` pattern:
+
+```java
+// NEW: api/src/main/java/org/apache/gravitino/policy/
+// IcebergOrphanFileRemovalContent.java
+public class IcebergOrphanFileRemovalContent implements PolicyContent {
+
+ // Strategy metadata
+ public static final String STRATEGY_TYPE_VALUE =
+ "iceberg-orphan-file-removal";
+ public static final String JOB_TEMPLATE_NAME_VALUE =
+ "builtin-iceberg-remove-orphan-files";
+
+ // Configurable fields
+ private final long olderThanDays; // default: 3
+ private final String location; // default: null (table location)
+ private final boolean dryRun; // default: false
+
+ // Trigger / score expressions will follow the shared scheduling design.
+
+ // Defaults
+ public static final long DEFAULT_OLDER_THAN_DAYS = 3;
+ public static final boolean DEFAULT_DRY_RUN = false;
+}
+```
+
+#### 5.2.3 Policy Content Fields
+
+| Field | Type | Default | Description
|
+| --------------- | --------- | ------- |
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
+| `olderThanDays` | `long` | 3 | Only remove orphan files older than
this many days. See [5.2.4](#524-why-olderthandays-defaults-to-3) for the
rationale.
|
+| `location` | `String` | null | Custom location to scan. When
specified, **only** this location is scanned instead of the table's default
location. Must be validated against the table's own location - see [Section
6.1](#61-location-validation). If null, the table's registered storage location
is used. |
+| `dryRun` | `boolean` | false | Preview-only mode - list orphan
files without deleting
|
+
+> **Note:** The minimum interval between runs is intentionally **not** a field
+> of this policy. A uniform minimum-interval mechanism will be defined across
+> all four system built-in policies and is out of scope for this design.
+
+#### 5.2.4 Why `olderThanDays` Defaults to 3
+
+The 3-day default mirrors Iceberg's own default for
+`remove_orphan_files` and exists to protect **in-flight writes**.
+
+Orphan file detection compares files on storage against files referenced by
+table metadata. A file written by a transaction that has not yet committed
+looks identical to an orphan file. If such a file is deleted, the in-flight
+commit fails or, worse, produces a table that references a missing file.
+A 3-day window is long enough to cover:
+
+- Long-running Spark / Flink write jobs that stage files before committing
+- Retried or paused jobs that resume hours or days later
+- Clock skew between the storage system and the job runtime
+
+**To remove all orphan files regardless of age**, set `olderThanDays` to `0`.
+The adapter then passes the current timestamp as `older_than`, so every
+unreferenced file is eligible for deletion.
+
+**This is unsafe while any writer is active** and should only be used when
+all writes to the table are known to be stopped - for example, during a
+maintenance window or when reclaiming storage from a decommissioned table.
+Run with `dryRun: true` first to review the file list.
+
+#### 5.2.5 Example Policy Creation
+
+```json
+POST /metalakes/default/policies
+{
+ "name": "remove_orphans_weekly",
+ "type": "system_iceberg_orphan_file_removal",
+ "comment": "Remove orphan files older than 3 days",
+ "enabled": true,
+ "content": {
+ "olderThanDays": 3,
+ "dryRun": false
+ }
+}
+```
+
+---
+
+### 5.3 Layer 2 - Strategy Handler (`maintenance/optimizer/`)
+
+#### 5.3.1 Strategy Handler
+
+```java
+// NEW: maintenance/optimizer/src/main/java/…/handler/orphan/
+// OrphanFileRemovalStrategyHandler.java
+public class OrphanFileRemovalStrategyHandler
+ extends BaseExpressionStrategyHandler {
+
+ public static final String NAME = "iceberg-orphan-file-removal";
+
+ @Override
+ public String strategyType() {
+ return NAME;
+ }
+
+ @Override
+ public Set<DataRequirement> dataRequirements() {
+ return ImmutableSet.of(DataRequirement.TABLE_METADATA);
+ }
+
+ @Override
+ protected JobExecutionContext buildJobExecutionContext(
+ NameIdentifier nameIdentifier,
+ Strategy strategy,
+ Table table,
+ List<PartitionPath> partitions,
+ Map<String, String> jobOptions) {
+ return new OrphanFileRemovalJobContext(
+ nameIdentifier, jobOptions, strategy.jobTemplateName());
+ }
+}
+```
+
+**Key design decision:** Orphan file removal operates at the **table level**,
+not partition level. Unlike compaction, which scores and selects individual
+partitions, `remove_orphan_files` scans the entire table's storage location.
+Therefore:
+
+- `dataRequirements()` excludes `PARTITION_STATISTICS`.
+- No partition scoring / selection logic is needed.
+
+**Trigger modes:** Gravitino supports two trigger mechanisms, and this
+design does not limit users to one:
+
+1. **Event trigger** - The strategy handler evaluates table metadata
+ (e.g., snapshot count changes, write events) and triggers cleanup when
+ conditions are met.
+2. **Time trigger** - The Optimizer's scheduling framework can invoke the
+ strategy handler periodically. Scheduling time and minimum run intervals
+ will be defined separately for all built-in strategies using job history.
+
+Both modes use the same strategy handler; the difference is in how
+often the handler is invoked.
+
+#### 5.3.2 Job Execution Context
+
+```java
+// NEW: maintenance/optimizer/src/main/java/…/handler/orphan/
+// OrphanFileRemovalJobContext.java
+public class OrphanFileRemovalJobContext implements JobExecutionContext {
+ private final NameIdentifier nameIdentifier;
+ private final Map<String, String> jobOptions;
+ private final String jobTemplateName;
+
+ // jobOptions keys:
+ // older_than, location, dry_run,
+ // catalog_name, table_identifier
+}
+```
+
+#### 5.3.3 Handler Registration
+
+The strategy handler type `"iceberg-orphan-file-removal"` must be registered so
+that the `Recommender` can instantiate it when it encounters a policy with that
+strategy type. This follows the existing pattern where handler classes are
+looked up by strategy type name.
+
+---
+
+### 5.4 Layer 3 - Job Adapter (`maintenance/optimizer/`)
+
+#### 5.4.1 Job Adapter
+
+```java
+// NEW: maintenance/optimizer/src/main/java/…/job/
+// GravitinoOrphanFileRemovalJobAdapter.java
+public class GravitinoOrphanFileRemovalJobAdapter
+ implements GravitinoJobAdapter {
+
+ @Override
+ public Map<String, String> jobConfig(JobExecutionContext context) {
+ OrphanFileRemovalJobContext ctx =
+ (OrphanFileRemovalJobContext) context;
+ Map<String, String> config = new HashMap<>();
+ config.put("catalog_name",
+ ctx.nameIdentifier().namespace()[0]);
+ config.put("table_identifier",
+ ctx.nameIdentifier().namespace()[1]
+ + "." + ctx.nameIdentifier().name());
+
+ // Convert olderThanDays → absolute timestamp
+ // olderThanDays == 0 means "now", i.e. remove all orphan files
+ Map<String, String> opts = ctx.jobOptions();
+ long days = Long.parseLong(
+ opts.getOrDefault("olderThanDays", "3"));
+ String ts = Instant.now()
+ .minus(Duration.ofDays(days))
+ .toString()
+ .replace("T", " ")
+ .substring(0, 19); // "yyyy-MM-dd HH:mm:ss"
+ config.put("older_than", ts);
+
+ // Validated against the table's own location before submission
+ if (opts.containsKey("location")) {
+ String location = opts.get("location");
+ validateLocation(ctx.nameIdentifier(), location);
+ config.put("location", location);
+ }
+
+ config.put("dry_run",
+ opts.getOrDefault("dryRun", "false"));
+ return config;
+ }
+}
+```
+
+#### 5.4.2 Register Adapter
+
+```java
+// UPDATE: GravitinoJobSubmitter.java
+private static final Map<String, Class<? extends GravitinoJobAdapter>>
+ jobAdapters = ImmutableMap.of(
+ "builtin-iceberg-rewrite-data-files",
+ GravitinoCompactionJobAdapter.class,
+ "builtin-iceberg-remove-orphan-files",
+ GravitinoOrphanFileRemovalJobAdapter.class // NEW
+);
+```
+
+---
+
+### 5.5 Layer 4 - Spark Job (`maintenance/jobs/`)
+
+#### 5.5.1 Job Class
+
+Create `IcebergRemoveOrphanFilesJob` following the same pattern as
+`IcebergRewriteDataFilesJob` and `IcebergExpireSnapshotsJob`:
+
+- **Template name:** `builtin-iceberg-remove-orphan-files`
+- **Version:** `v1`
+- **Parameters:** `--catalog`, `--table`, `--older-than`, `--location`,
+ `--dry-run`, `--spark-conf`
+
+#### 5.5.2 Procedure Call
+
+```sql
+CALL catalog.system.remove_orphan_files(
+ table => 'db.table_name',
+ older_than => TIMESTAMP '2026-06-13 00:00:00',
+ location => 's3://bucket/path/',
+ dry_run => true
+)
+```
+
+**Parameters (from Iceberg `remove_orphan_files` procedure):**
+
+| Parameter | Type | Required | Description
|
+| ------------ | ----------- | -------- |
----------------------------------------------------------------------------------------------------------------
|
+| `table` | `string` | Yes | Fully qualified table name
|
+| `older_than` | `timestamp` | No | Only remove files older than this
timestamp (default: 3 days) |
+| `location` | `string` | No | Custom directory to scan for orphans
(replaces table location when set; must be within the table's own location) |
+| `dry_run` | `boolean` | No | If true, list orphan files without
deleting them |
+
+#### 5.5.3 Output
+
+The procedure returns a result set with one column:
+
+| Column | Type | Description |
+| ---------------------- | -------- | -------------------------------------- |
+| `orphan_file_location` | `string` | Path of each orphan file found/removed |
+
+The job will log the count of orphan files removed (or found in dry-run mode).
+
+#### 5.5.4 Security
+
+- SQL injection prevention via `escapeSqlString()` / `escapeSqlIdentifier()`
+ (same utilities as `IcebergExpireSnapshotsJob`)
+- Input validation for `dry_run` (must be `true` or `false`)
+- `location` validated against the table's own storage location - see
+ [Section 6.1](#61-location-validation)
+
+#### 5.5.5 Job Registration
+
+```java
+// UPDATE: BuiltInJobTemplateProvider.java
+private static final List<BuiltInJob> BUILT_IN_JOBS =
+ ImmutableList.of(
+ new SparkPiJob(),
+ new IcebergRewriteDataFilesJob(),
+ new IcebergUpdateStatsAndMetricsJob(),
+ new IcebergRemoveOrphanFilesJob()); // NEW
+```
+
+---
+
+## 6. Safety Considerations
+
+Orphan file removal is inherently more dangerous than snapshot expiration or
+compaction because it **permanently deletes files**. Several safety mechanisms
+are built into the design:
+
+| Safety Mechanism | Description
|
+| --------------------------- |
----------------------------------------------------------------------------------------------------
|
+| **`older_than` default** | 3-day default ensures files from in-flight
writes are not deleted |
+| **`dry_run` mode** | Allows previewing which files would be deleted
before actual removal |
+| **Location validation** | Rejects any `location` outside the table's own
storage location - see [6.1](#61-location-validation) |
+| **Policy-gated** | Must be explicitly enabled by an administrator
via policy creation |
+| **Iceberg built-in safety** | The procedure itself only identifies files not
referenced by any snapshot |
+
+### 6.1 Location Validation
+
+The `location` parameter is the single most dangerous input in this job.
+Iceberg's `remove_orphan_files` deletes **every file under the given
+location that the target table does not reference**. If a caller passes the
+location of a *different* table, that table's data files are all unreferenced
+from the target table's point of view, and the procedure deletes them -
+silently destroying another table's data.
+
+The job must therefore validate `location` **before building the SQL**, not
+rely on Iceberg to reject it:
+
+```java
+static void validateLocation(NameIdentifier tableIdent, String location) {
+ String tableLocation = normalize(loadTableLocation(tableIdent));
+ String requested = normalize(location);
+
+ Preconditions.checkArgument(
+ requested.equals(tableLocation)
+ || requested.startsWith(tableLocation + "/"),
+ "location '%s' must be within the table's location '%s'",
+ location, tableLocation);
+}
+```
+
+Validation rules:
+
+1. Resolve the target table's own storage location from table metadata.
+2. Normalize both paths - canonicalize the scheme and authority, collapse
+ duplicate slashes, resolve `.` / `..` segments, and strip trailing slashes.
+3. Reject the request unless the normalized `location` is the table location
+ itself or a descendant of it.
+4. Reject symlinks or paths that resolve outside the table location after
+ normalization.
+
+Normalization must happen before the prefix check; comparing raw strings
+would let `s3://bucket/db/table/../other_table` pass a naive
+`startsWith` test.
+
+Validation happens in the job adapter (before submission) and again in the
+Spark job (before building the procedure call), so an ad-hoc job submission
+that bypasses the policy layer is still checked.
+
+### 6.2 Setting `older_than` Safely
+
+The `older_than` threshold should be set conservatively. Files from in-flight
+writes or concurrent operations may not yet be referenced by a committed
+snapshot. A minimum of 3 days is recommended.
+
+Setting `olderThanDays` to `0` removes all orphan files regardless of age.
+This is only safe when no writer is active against the table - see
+[Section 5.2.4](#524-why-olderthandays-defaults-to-3).
+
+---
+
+## 7. File Changes Summary
+
+### 7.1 New Files
+
+| File
| Layer | Description |
+|
------------------------------------------------------------------------------
| -------- | ----------------------------------------- |
+| `api/…/policy/IcebergOrphanFileRemovalContent.java`
| Policy | Policy content with removal configuration |
+|
`maintenance/optimizer/…/handler/orphan/OrphanFileRemovalStrategyHandler.java`
| Strategy | Trigger / score evaluation |
+| `maintenance/optimizer/…/handler/orphan/OrphanFileRemovalJobContext.java`
| Strategy | Job execution context |
+| `maintenance/optimizer/…/job/GravitinoOrphanFileRemovalJobAdapter.java`
| Adapter | Context → job config conversion |
+| `maintenance/jobs/…/iceberg/IcebergRemoveOrphanFilesJob.java`
| Job | Spark job |
+
+### 7.2 Modified Files
+
+| File | Change
|
+| -------------------------------------------------------- |
--------------------------------------------------------- |
+| `api/…/policy/Policy.java` | Add
`ICEBERG_ORPHAN_FILE_REMOVAL` to `BuiltInType` enum |
+| `maintenance/optimizer/…/job/GravitinoJobSubmitter.java` | Register
remove-orphan-files adapter in `jobAdapters` map |
+| `maintenance/jobs/…/BuiltInJobTemplateProvider.java` | Register
`IcebergRemoveOrphanFilesJob` |
+| Handler registry | Register
`OrphanFileRemovalStrategyHandler` |
+
+### 7.3 Test Files
+
+| File | Description
|
+| ----------------------------------------------- |
--------------------------- |
+| `TestIcebergOrphanFileRemovalContent.java` | Policy content unit tests
|
+| `TestOrphanFileRemovalStrategyHandler.java` | Strategy handler unit
tests |
+| `TestGravitinoOrphanFileRemovalJobAdapter.java` | Job adapter unit tests
|
+| `TestIcebergRemoveOrphanFilesJob.java` | Spark job unit tests
|
+
+---
+
+## 8. Proposed PR Plan
+
+| PR | Scope
| Dependencies |
+| -------- |
-------------------------------------------------------------------------------
| ------------ |
+| **PR 1** | Job layer: `IcebergRemoveOrphanFilesJob` +
`BuiltInJobTemplateProvider` + tests | None |
+| **PR 2** | Policy + Strategy + Adapter: all remaining layers + tests
| PR 1 |
+
+Since the total code size across the policy, strategy, and adapter layers is
+expected to be well under 1000 lines, PRs 2 and 3 from the original plan are
+combined into a single PR.
+
+---
+
+## 9. Open Questions
+
+1. **Cleanup scheduling** - Out of scope. A separate issue will define shared
+ scheduling logic using execution times from the Gravitino job table.
+2. ~~**Location parameter**~~ - Resolved: when `location` is specified,
+ only that location is scanned, and it must be within the table's own
+ storage location. See [Section 6.1](#61-location-validation).
+3. **Dry-run result persistence** - Should dry-run results be stored
+ somewhere (e.g., job output metadata) for review before actual deletion?
+4. ~~**PR granularity**~~ - Resolved: single PR for policy + strategy +
+ adapter layers since total code is expected to be under 1000 lines.
+5. ~~**`older_than` minimum**~~ - Resolved: no hard minimum is enforced.
+ `olderThanDays: 0` is a deliberate escape hatch for reclaiming storage
+ when no writer is active. See
+ [Section 5.2.4](#524-why-olderthandays-defaults-to-3).
+6. **Minimum run interval** - Out of scope. A uniform minimum-interval
+ mechanism will be defined across all four system built-in policies.
+
+---
+
+## 10. Comparison with Other Maintenance Flows
+
+| Aspect | Compaction
| Snapshot Expiration | Orphan File Removal
|
+| ----------------- |
-------------------------------------------------------------- |
--------------------------------------- |
-------------------------------------------------- |
+| Policy type | `system_iceberg_compaction`
| `system_iceberg_snapshot_expiration` |
`system_iceberg_orphan_file_removal` |
+| Strategy type | `iceberg-data-compaction`
| `iceberg-snapshot-expiration` | `iceberg-orphan-file-removal`
|
+| Job template | `builtin-iceberg-rewrite-data-files`
| `builtin-iceberg-expire-snapshots` |
`builtin-iceberg-remove-orphan-files` |
+| Scope | Per-partition (scored, top-N selected)
| Whole table | Whole table (or custom
location) |
+| Data requirements | `TABLE_METADATA` + `TABLE_STATISTICS` +
`PARTITION_STATISTICS` | `TABLE_METADATA` + `TABLE_STATISTICS` |
`TABLE_METADATA` |
+| Trigger metric | `custom-data-file-mse`, `custom-delete-file-number`
| `custom-snapshot-count` | Defined by shared scheduling
design (out of scope) |
+| Iceberg procedure | `rewrite_data_files`
| `expire_snapshots` | `remove_orphan_files`
|
+| Key parameters | strategy, sort-order, where, options
| older_than, retain_last, stream_results | older_than, location, dry_run
|
+| Destructiveness | Rewrites data (recoverable via snapshots)
| Removes metadata (irreversible) | Removes data files
(irreversible) |
+| Safety concern | Low - data is rewritten, not lost
| Medium - old snapshots are removed | High - files are permanently
deleted |