This is an automated email from the ASF dual-hosted git repository.
jerryshao 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 afb28398d6 [#13488] docs: Add Table Maintenance Service design (#13386)
afb28398d6 is described below
commit afb28398d6572f41d7337434f90e66d9f3102e6c
Author: MaSai <[email protected]>
AuthorDate: Mon Oct 12 19:39:52 2026 +0800
[#13488] docs: Add Table Maintenance Service design (#13386)
### What changes were proposed in this pull request?
Add `design-docs/table-maintenance-service-design.md` for the Table
Maintenance Service (TMS) as an **in-process main-server plugin** (port
**8090**), colocated with Iceberg REST.
The design covers:
- **db-scheduler** three task kinds: `policy-expand` / `cron-job` /
`commit-job` with expand / cron / commit pools
- **IRC in-process commit event** (no HTTP/Kafka); one Spark Job chains
`compaction` → `manifest-rewrite` → `snapshot-expiry` in-Job
- Reuse Govern Policy (`schedule`, `jobOptions`, optional `authConfig` /
`vendingConfig`) and optimizer core
- Persistence: `table_maintenance_job`, `table_snapshot_metrics` (keyed
by `table_id` + `policy_id` + `snapshot_id`, payload column
`metrics_value`)
### Why are the changes needed?
Optimizer execution already lives in `maintenance/optimizer`, but there
is no server-side path that reacts to Iceberg commits or schedules
maintenance inside Gravitino. This document records the agreed design
before implementation.
Fix: #13488
### Does this PR introduce _any_ user-facing change?
No. Design document only. No API, configuration, or runtime behavior
changes yet.
### How was this patch tested?
Documentation only. No code or tests in this PR.
---------
Co-authored-by: Cursor <[email protected]>
---
design-docs/table-maintenance-service-design.md | 569 ++++++++++++++++++++++++
1 file changed, 569 insertions(+)
diff --git a/design-docs/table-maintenance-service-design.md
b/design-docs/table-maintenance-service-design.md
new file mode 100644
index 0000000000..f1b768e79b
--- /dev/null
+++ b/design-docs/table-maintenance-service-design.md
@@ -0,0 +1,569 @@
+<!--
+ 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 of Table Maintenance Service in Gravitino
+
+## 1. Background
+
+The Table Maintenance Service (TMS) turns the existing `maintenance/optimizer`
core
+(`Updater`, `Recommender`, providers, `JobSubmitter`) into a long-running
capability on the
+Gravitino main server.
+
+Today that core can recommend and submit work, but nothing hosts it as a
service:
+
+1. Iceberg commits through IRC do not wake maintenance in-process.
+2. Ad hoc runs lack a single place for configuration, audit, and service
metrics.
+3. There is no embedded, multi-node-safe scheduler for expand and submit.
+
+This design closes those gaps by hosting that core in-process: policy-driven
triggers, a
+multi-node-safe scheduler for expand and submit, and Spark still on the Job
framework.
+
+---
+
+## 2. Goals
+
+1. Run TMS **in-process** on the main server so IRC can wake maintenance
without HTTP.
+2. After a successful Iceberg commit, **enqueue** commit-triggered work.
+3. **Reuse** the optimizer core (`Updater` / `Recommender` / job submit)
inside that process.
+4. Keep Spark execution on the **Gravitino Job framework**; record each run
for gates and
+ Validation.
+5. Store triggers and non-sensitive job settings on existing **Govern
Policies** — no parallel
+ policy store.
+6. Stay **multi-node safe**: one scheduler claim per task; in-flight work
gated by an unfinished
+ job row.
+7. Put **when** (`schedule`), **cooldown** (`minIntervalMs`), and **how to
run**
+ (`jobOptions` / `rewriteOptions`) in policy content; for each maintenance
type, the
+ nearest attachment wins.
+8. Keep **client auth** cluster-static in `gravitino.conf` (one Gravitino
identity, one IRC client
+ auth).
+
+---
+
+## 3. Non-Goals
+
+1. A standalone maintenance daemon or a dedicated aux Jetty port (for example
**9301**).
+2. Rewriting optimizer provider / submitter SPIs.
+3. Commit reports from engines that bypass Gravitino IRC.
+4. Commit-path HTTP or Kafka ingress (`POST …/events/iceberg-commit`, health
resource).
+5. Auto-creating a built-in TMS user or role. Operators grant privileges when
authorization is on.
+
+---
+
+## 4. Solution Investigations
+
+### 4.1 Deployment shape
+
+| Option | Decision
|
+| ----------------------------------- |
------------------------------------------------------ |
+| **In-process plugin (main server)** | **Chosen** — same JVM as IRC;
`extensionPackages` |
+| Separate long-running process | Rejected — extra deployable
|
+| Aux Jetty listener (`:9301`) | Rejected — extra port; diverges from
IdP-style plugins |
+
+**Chosen:** in-process plugin on the main server — same JVM as IRC, so commit
can wake
+maintenance without HTTP; loaded via `extensionPackages`. A separate process
adds another
+deployable and still needs a wake path into that process. An aux Jetty port
adds another
+listener and leaves the IdP-style plugin model used elsewhere on the main
server.
+
+### 4.2 Scheduler
+
+TMS schedules three kinds of work (policy expand, cron Spark submit,
commit-triggered Spark
+submit). Industry options compared below:
+
+| | **db-scheduler** (Chosen) |
JobRunr | ShedLock | Quartz JDBC |
+| ----------------------------------------- | ------------------------- |
------- | ---------- | ----------- |
+| License | Apache 2.0 | LGPL
v3 | Apache 2.0 | Apache 2.0 |
+| Cluster CAS + heartbeat | Yes | Yes
| Lock only | Yes (heavy) |
+| Fits long-lived expand + one-shot submits | Yes | Yes
| No | Yes |
+
+**Chosen:** db-scheduler — Apache 2.0, one `scheduled_tasks` table with
built-in heartbeat, and it
+fits both long-lived expand leases and one-shot cron/commit submits (three
pools: expand / cron /
+commit). JobRunr is LGPL. ShedLock only provides a lock, not the scheduler TMS
needs. Quartz JDBC
+can do the job but is heavier than needed here.
+
+### 4.3 Snapshot drift
+
+Question surveyed: if metrics were sampled at snapshot `S0` but
`currentSnapshotId` is already
+newer, do systems still trigger maintenance?
+
+| | **Amoro** | **Floe**
| **OpenHouse** |
+| ----------------------------------------------------- | --------- |
------------------- | ------------------- |
+| Gate when metrics `snapshotId` ≠ `currentSnapshotId`? | **Yes** | No (not
considered) | No (not considered) |
+
+**Amoro:** evaluates / plans against the then-current snapshot and binds that
id as
+`targetSnapshotId`. Optimizers run from that plan; at commit Amoro calls
Iceberg
+`validateFromSnapshot(targetSnapshotId)` (OCC). If HEAD has moved in a way
that invalidates the
+rewrite, **the commit fails** — inconsistency is caught at submit time, after
work may already have
+run.
+
+**Chosen:** TMS gates **before** continuing maintenance — after the in-job
sample at `S0`, if HEAD
+has moved, **do not continue** that type / cron job (end early; no re-sample
loop). The next cron /
+commit wake-up evaluates the fresher HEAD. Commit-time rewrite conflicts still
use Iceberg OCC.
+
+---
+
+## 5. Proposal
+
+TMS runs in-process on the Gravitino main server and uses db-scheduler. This
chapter has two
+parts:
+
+1. **Policy** — what to maintain, when to trigger, and non-sensitive job
settings.
+2. **Scheduling and commit** — how work is enqueued, claimed, and recorded.
+
+TMS has two trigger paths that stay separate:
+
+- **crontab** — expand a policy into one-shot Spark submits (one type per
submit).
+- **onCommit** — after an IRC commit, enqueue one per-table commit-job; one
pick runs a
+ chained Spark Job for the allowed types.
+
+Both paths end with a short control-plane `runJob` on the Gravitino Job
framework. Read
+policy first, then scheduling.
+
+| Store | Role
|
+| -------------------------------------------------------------- |
------------------------------------------------------------ |
+| `policy_meta` / `policy_relation_meta` / `policy_version_info` | Triggers +
`minIntervalMs` + `jobOptions` / `rewriteOptions` |
+| `scheduled_tasks` | Scheduler
rows: expand / cron-job / commit-job |
+| `table_maintenance_job` | Per-run
occupancy; before/after `snapshot_id` |
+| `table_snapshot_metrics` | Metrics
keyed by Iceberg `snapshot_id` |
+| `gravitino.conf` |
Cluster-static Gravitino / IRC client auth |
+| `job_run_meta` | Spark job
run record / `runtime_job_template` |
+
+### 5.1 Policy
+
+TMS reuses the existing metalake Policy APIs and does **not** add
+`/api/maintenance/table/policies`. Each policy covers **one** maintenance
type. The same
+policy may enable both `onCommit` and `crontab`.
+
+#### 5.1.1 Evaluate triggers (`onCommit` + `crontab`)
+
+Triggers live in **`policy_version_info.content.schedule`**, next to
`jobOptions`,
+`rewriteOptions`, and `minIntervalMs`. Operators edit that versioned content
through Policy
+APIs. `policy_meta` only holds identity and attachment. `scheduled_tasks` is
runtime state for db-scheduler — not where operators set triggers.
+
+TMS has two trigger modes. They never feed each other's queues:
+
+| Trigger | Path | Which types
|
+| ---------- | ---------------------------------------- |
----------------------------------------------- |
+| `onCommit` | IRC → **commit-job** only (skips expand) | Compaction /
manifest-rewrite / snapshot-expiry |
+| `crontab` | **policy-expand** → cron-job | All four types
|
+
+**`onCommit`** runs after a successful Iceberg commit through IRC. That fits
types that
+rewrite or expire data and metadata from the commit stream. **`orphan-cleanup`
does not:**
+it scans storage for unreferenced files on a schedule. Running it on every
commit would be
+too heavy. Create/alter rejects `schedule.onCommit = true` for orphan-cleanup;
that type is
+**crontab-only**.
+
+**`minIntervalMs`** sits in policy content **beside** `schedule` (not inside
it). It is the
+cooldown for **that policy's type** after `MAX(finished_at)` on
`table_maintenance_job` for
+`(table, policy)`. Server conf and code defaults are lower-priority fallbacks.
+
+Example content:
+
+```json
+{
+ "rewriteOptions": { "target-file-size-bytes": "536870912" },
+ "jobOptions": {
+ "spark.executor.memory": "7g",
+ "uri": "http://host:9001/iceberg",
+ "type": "rest"
+ },
+ "schedule": { "onCommit": true, "crontab": "0 2 * * *" },
+ "minIntervalMs": 3600000
+}
+```
+
+| Field | Runtime effect
|
+| ------------------- |
----------------------------------------------------------------- |
+| `schedule.onCommit` | IRC upserts commit-job; commit pool submits one
chained Job |
+| `schedule.crontab` | Sets / refreshes next `policy-expand` due time
|
+| `minIntervalMs` | Cooldown for this policy's type; highest when set
|
+| `jobOptions` | Non-sensitive Spark / IRC client props (`uri`, `type`,
memory, …) |
+
+Automated maintenance should set at least one of `onCommit` or `crontab`
(orphan-cleanup:
+**`crontab` only**).
+
+#### 5.1.2 Sensitive credentials
+
+Policy content is versioned, edited through Policy APIs, and readable with
`VIEW_POLICY`.
+**Secrets must not live there.** Put credential-shaped keys in `jobOptions` /
+`rewriteOptions` and create/alter **rejects** them. Non-sensitive Spark / IRC
client props
+(`uri`, `type`, memory, …) stay on the nearest policy; auth is cluster-static
in
+`gravitino.conf` (values may be SecretManager references). Conf keys: §7.4.
+
+TMS talks to **two** backends with **two** identities, so there are two
prefixes:
+
+| Prefix | Who uses it
|
+| -------------------------------------- |
----------------------------------------------------------------------- |
+| `gravitino.maintenance.gravitinoAuth.` | Control plane (expand / submit /
RBAC) and Jobs that call **Gravitino** |
+| `gravitino.maintenance.ircAuth.` | Spark Jobs that call **Iceberg
REST** |
+
+They are not interchangeable: one principal for metalake APIs, one for IRC.
+
+At `runJob`, the control plane builds the Job template’s two bags and
**overlays** auth onto
+each — it does not write secrets back into policy content:
+
+| Runtime bag | Non-sensitive (examples)
| Auth overlay |
+| ------------------- |
------------------------------------------------------------- |
--------------------------------------- |
+| **`updateOptions`** | `gravitino_uri`, `metalake`, updater impl names, …
| `gravitino.maintenance.gravitinoAuth.*` |
+| **`jobOptions`** | nearest policy `jobOptions` (`uri`, `type`, Spark
resources…) | `gravitino.maintenance.ircAuth.*` |
+
+### 5.2 Scheduling and commit
+
+db-scheduler runs **three** task kinds in `scheduled_tasks`:
+
+| Kind | `task_name` | Role |
+| ---- | --------------- | ----------------------------------------------- |
+| 1 | `policy-expand` | Long-lived lease per policy; fans out cron-jobs |
+| 2 | `cron-job` | One-shot crontab Spark submit |
+| 3 | `commit-job` | One-shot commit-triggered chained Spark submit |
+
+**policy-expand** stays after each pick. **cron-job** and **commit-job** are
deleted after a
+short `runJob`. Spark lifetime and in-flight checks use
`table_maintenance_job`, not those
+one-shot rows.
+
+Every node may poll; **exactly one claim wins** per due instance (CAS +
heartbeat). Missed
+heartbeats free only a hung **short** callback — not a long-running Spark Job.
+
+| Pool | Registers | Default threads | conf key
(`gravitino.maintenance.`) |
+| ------ | ------------- | --------------- |
----------------------------------- |
+| Expand | policy-expand | 4 | `scheduler.expand.threads`
|
+| Cron | cron-job | 8 | `scheduler.cron.threads`
|
+| Commit | commit-job | 4 | `scheduler.commit.threads`
|
+
+PK is `(task_name, task_instance)`. `scheduled_tasks` uses upstream
db-scheduler DDL.
+
+**TMS tables used by cron-job and commit-job**
+
+`job_run_meta` already records Spark runs. TMS still needs its own per-run row
for **in-flight
+gates**, **`minIntervalMs`**, and Iceberg **before/after snapshot ids**. That
is
+`table_maintenance_job`. Metric **values** live in `table_snapshot_metrics`.
+
+| Column | Type | Notes
|
+| -------------------- | -------------------------- |
------------------------------------------------------------------------------------------
|
+| `id` | `BIGINT UNSIGNED` PK | Auto increment
|
+| `job_id` | `BIGINT UNSIGNED NULL` | Set at `runJob`; maps to
`job_run_meta.job_run_id`; null if released before submit |
+| `table_id` | `BIGINT UNSIGNED NOT NULL` | `table_meta` PK
|
+| `policy_id` | `BIGINT UNSIGNED NOT NULL` | Cron: that Job's policy.
Commit chain: first type that ran; full chain in runtime template |
+| `before_snapshot_id` | `BIGINT NULL` | Set inside the Spark Job
when S0 is sampled |
+| `after_snapshot_id` | `BIGINT NULL` | Iceberg snapshot after
job terminal; null while pending |
+| `finished_at` | `BIGINT UNSIGNED NULL` | Epoch millis; **null =
in-flight** |
+
+```sql
+CREATE TABLE IF NOT EXISTS `table_maintenance_job` (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
+ `job_id` BIGINT(20) UNSIGNED NULL COMMENT 'job id; set at runJob; maps to
job_run_meta.job_run_id',
+ `table_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'table id from table_meta',
+ `policy_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'policy id from
policy_meta',
+ `before_snapshot_id` BIGINT(20) NULL COMMENT 'Iceberg snapshot id at
sample; null until sampled',
+ `after_snapshot_id` BIGINT(20) NULL COMMENT 'Iceberg snapshot id after
job; null while pending',
+ `finished_at` BIGINT(20) UNSIGNED NULL COMMENT 'end time; null =
in-flight',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_job_id` (`job_id`),
+ KEY `idx_tmj_table_policy_finished` (`table_id`, `policy_id`,
`finished_at`),
+ KEY `idx_tmj_before_sid` (`table_id`, `before_snapshot_id`),
+ KEY `idx_tmj_after_sid` (`table_id`, `after_snapshot_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
+ COMMENT 'per-run TMS job: snapshot pointers + submit gates';
+```
+
+Keep metrics in their own table so sample history can outlive a single run.
**One row = one
+`(table_id, policy_id, snapshot_id)`**. Include **`policy_id`** because the
four types may sample
+the same snapshot with different payloads.
+
+```sql
+CREATE TABLE IF NOT EXISTS `table_snapshot_metrics` (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
+ `table_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'table id from table_meta',
+ `policy_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'policy id from
policy_meta',
+ `snapshot_id` BIGINT(20) NOT NULL COMMENT 'Iceberg snapshot id',
+ `metrics_value` MEDIUMTEXT NOT NULL COMMENT 'JSON metrics for this
snapshot',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_tid_pid_sid` (`table_id`, `policy_id`, `snapshot_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
+ COMMENT 'per-policy table metrics by Iceberg snapshot_id';
+```
+
+Cron/commit claims **insert `table_maintenance_job` then `runJob`** — the
scheduler callback stays
+short. **Sampling is inside the Spark Job** (not on the control plane). On Job
terminal, set
+`after_snapshot_id` / `finished_at` (and `S1` metrics as needed).
+
+The three subsections below describe each task kind.
+
+#### 5.2.1 `policy-expand`
+
+Each policy has one long-lived expand row (`{policy_id}`). Expand does not run
Spark. When its
+crontab is due, it turns the policy into many short **cron-job** rows (one
candidate table at a
+time). The expand row itself stays; only its next due time moves.
+
+- **Create / enable:** insert the expand row, due at the next crontab.
+- **Alter schedule:** update the due time.
+- **Disable / drop:** delete the expand row.
+
+When expand is due (expand pool):
+
+```mermaid
+flowchart TD
+ A[List attached tables in batches] --> B["Enqueue up to
expand.enqueueBatchSize cron-jobs (default 100)"]
+ B --> C[Keep expand row; set next crontab]
+```
+
+Example with crontab `0 2 * * *`: create inserts expand due next 02:00; at
02:00 expand inserts
+many cron-jobs due now, then schedules itself for the next 02:00. Expand's due
time is when
+**enqueue** happens — not when Spark runs.
+
+#### 5.2.2 `cron-job`
+
+Expand writes these one-shot rows. Most types use one table per job
+(`table:{table_id}:{policy_id}`). Batched **snapshot-expiry** may use
+`batch:{batch_id}:{policy_id}`.
+
+When a cron-job is due (cron pool), keep the callback short — insert then
submit; do not sample here:
+
+```mermaid
+flowchart TD
+ A{Already in-flight?} -->|yes| Z[Delete cron-job; stop]
+ A -->|no| B[Insert table_maintenance_job]
+ B --> C[runJob]
+ C --> D[Delete cron-job; return]
+```
+
+Inside that Spark Job, sampling is part of the job:
+
+```mermaid
+flowchart TD
+ A[Sample S0 once; set before_snapshot_id] --> B{Snapshot drifted?}
+ B -->|yes| Z[End job early]
+ B -->|no| C{Recommender: maintain?}
+ C -->|no| Z
+ C -->|yes| D[Run maintenance]
+ D --> E[Spark Job terminal]
+ Z --> E
+```
+
+If the snapshot drifts after the in-job sample, end this job early — do not
re-sample in a loop.
+A later wake-up sees the fresher state.
+
+#### 5.2.3 `commit-job`
+
+After a successful IRC table update in the same JVM, an `EventListenerPlugin`
upserts one
+commit-job for that table (`{table_id}`). Register it with
`gravitino.eventListener.names` /
+`{name}.class`. The listener only enqueues — it does not submit Spark or build
the type chain.
+Bursts of commits on the same table coalesce to one row.
+
+When a commit-job is due (commit pool) — keep the callback short; Spark runs
elsewhere:
+
+```mermaid
+flowchart TD
+ A{Any type in chain in-flight?} -->|yes| Z[Delete commit-job; stop]
+ A -->|no| B["Collect onCommit types: compaction → manifest-rewrite →
snapshot-expiry"]
+ B --> C[Insert table_maintenance_job]
+ C --> D[Merge jobOptions + auth; one runJob]
+ D --> E[Delete commit-job]
+```
+
+Insert then `runJob` — sampling is **not** on the control plane.
+
+Inside that **one** Spark Job, types run in order in the same process. Each
type's full flow
+includes its own sample / recommend / work. If the snapshot drifts during a
type, **end that type**
+and continue with the next type in the same Job — do not abort the whole Job:
+
+```mermaid
+flowchart TD
+ A[Compaction: sample S0 → recommend → work] --> B{Snapshot drifted / type
done?}
+ B --> C[End compaction]
+ C --> D[Manifest-rewrite: sample → recommend → work]
+ D --> E{Snapshot drifted / type done?}
+ E --> F[End manifest-rewrite]
+ F --> G[Snapshot-expiry: sample → recommend → work]
+ G --> H{Snapshot drifted / type done?}
+ H --> I[End snapshot-expiry]
+ I --> J[Spark Job terminal]
+```
+
+When the Spark Job finishes (control plane Job listener):
+
+```mermaid
+flowchart TD
+ A[Sample S1 for policies the Job touched] --> B[Set after_snapshot_id and
finished_at]
+```
+
+---
+
+## 6. Authorization privileges
+
+TMS does **not** auto-create a metalake user, built-in role, or grants.
Deployments already bind
+identities through their own IdP and metalake RBAC; which catalogs TMS may
touch is an operator
+decision. A product-bundled privileged principal would surprise security
reviews and fight that
+existing model. Operators grant privileges when authorization is on.
+
+When `gravitino.authorization.enable = false`, no RBAC identity is required.
+
+When **authorization is enabled**, operators configure
**`gravitino.maintenance.gravitinoAuth.*`**
+in `gravitino.conf`. The Gravitino client authenticates with that cluster
identity; the RBAC
+principal is the identity from the **token**. Operators grant the privileges
below **to that token
+identity** so expand / submit can list metadata, read policies, resolve
secrets / vend credentials
+where needed, mutate tables, and run Jobs.
+
+| Privilege | Why
|
+| ------------------ |
---------------------------------------------------------------- |
+| `USE_CATALOG` | List / use catalogs
|
+| `USE_SCHEMA` | List / use schemas
|
+| `PROBE_TABLE_LIKE` | Probe / list table-like objects (with `USE_SCHEMA`)
|
+| `MODIFY_TABLE` | Rewrite / expire / orphan cleanup mutate table data and
metadata |
+| `USE_SECRET` | Retrieve secrets / vend credentials for objects the
Jobs touch |
+| `VIEW_POLICY` | Read attached maintenance policies and `jobOptions`
|
+| `USE_JOB_TEMPLATE` | Use built-in maintenance job templates
|
+| `RUN_JOB` | Submit Spark maintenance Jobs
|
+
+---
+
+## 7. Configuration
+
+### 7.1 Enablement
+
+| Key | Description
|
+| ----------------------------------------- |
------------------------------------------------------------------------------------------
|
+| `gravitino.server.rest.extensionPackages` | Include TMS Feature package
(e.g. `org.apache.gravitino.maintenance.web.rest.feature`) |
+| `gravitino.auxService.names` | Include `iceberg-rest` when
using IRC (same JVM) |
+| `gravitino.eventListener.names` | Include a TMS commit-job
listener name (e.g. `tms-commit-job`) |
+| `gravitino.eventListener.{name}.class` | FQCN of the TMS
`EventListenerPlugin` that upserts commit-job on `IcebergUpdateTableEvent` |
+
+```properties
+gravitino.server.rest.extensionPackages =
org.apache.gravitino.maintenance.web.rest.feature
+gravitino.auxService.names = iceberg-rest
+gravitino.eventListener.names = tms-commit-job
+gravitino.eventListener.tms-commit-job.class =
org.apache.gravitino.maintenance.listener.IcebergUpdateTableEventListener
+gravitino.maintenance.scheduler.expand.threads = 4
+gravitino.maintenance.scheduler.cron.threads = 8
+gravitino.maintenance.scheduler.commit.threads = 4
+```
+
+`extensionPackages` plus colocated IRC start the TMS Feature and db-scheduler
pools. The
+**event-listener** config wakes commit maintenance: on successful IRC
`IcebergUpdateTableEvent`,
+the listener upserts `commit-job` / `{table_id}`. Without it, the crontab path
still works;
+onCommit does not. HTTP / Kafka commit ingress is out of scope.
+
+### 7.2 Scheduler pools
+
+| Key | Default |
Description |
+| ------------------------------------------------------- | ------- |
----------------------------- |
+| `gravitino.maintenance.scheduler.expand.threads` | `4` | Expand
pool |
+| `gravitino.maintenance.scheduler.cron.threads` | `8` | Cron
pool |
+| `gravitino.maintenance.scheduler.commit.threads` | `4` | Commit
pool |
+| `gravitino.maintenance.scheduler.pollingIntervalMs` | `10000` | Poll
interval |
+| `gravitino.maintenance.scheduler.heartbeatIntervalMs` | `60000` |
Heartbeat while callback runs |
+| `gravitino.maintenance.scheduler.missedHeartbeatsLimit` | `6` | Misses
before dead |
+| `gravitino.maintenance.expand.enqueueBatchSize` | `100` | Max
cron-jobs per expand pick |
+
+### 7.3 Minimum interval
+
+`minIntervalMs` may be set on policy content **peer to `schedule`**. It
applies to **that
+policy’s maintenance type**. When omitted, fall back to `gravitino.conf` then
the code default.
+
+| Task type | Code-default `minIntervalMs` | Rationale
|
+| ------------------ | ---------------------------- |
------------------------------------------------------ |
+| `compaction` | `3600000` (1h) | Often `onCommit`;
cooldown after commit storms |
+| `manifest-rewrite` | `3600000` (1h) | Same — commit-driven
with compaction / snapshot-expiry |
+| `snapshot-expiry` | `3600000` (1h) | Same
|
+| `orphan-cleanup` | `86400000` (1d) | Crontab-only
|
+
+**Resolution** for a given type (first hit wins):
+
+1. effective policy content `minIntervalMs` (highest; that type’s policy)
+2. global `gravitino.maintenance.task.<type>.minIntervalMs`
+3. code default
+
+Checked on cron/commit submit. Policy content still owns trigger thresholds
(for example MSE); the
+interval only limits how often a type may run after the last finished job.
+
+### 7.4 Client auth (`gravitino.conf`)
+
+Cluster-static — **not** in policy content and **not** a DB table.
+
+| Prefix | Role
| Merged into `runJob` bag |
+| -------------------------------------- |
------------------------------------------------------------ |
----------------------------------------------- |
+| `gravitino.maintenance.gravitinoAuth.` | Expand / submit / RBAC client; also
jobs that call Gravitino | `updateOptions` (+ non-sensitive updater props) |
+| `gravitino.maintenance.ircAuth.` | Iceberg REST client auth for Spark
Jobs → IRC | `jobOptions` (+ policy non-sensitive props) |
+
+Illustrative auth keys (same shape under each prefix; values may be
SecretManager references where
+the deployment supports them). Auth keys are **overlaid** onto each bag; they
do not replace
+non-sensitive updater / policy props:
+
+| Key (suffix) | Example | Notes
|
+| ------------------- | -------------------------------------------- |
-------------------- |
+| `authType` | `none` / `basic` / `oauth2` / `kerberos` | Auth
mode |
+| `password` | secret ref or plaintext | basic
|
+| `oauth2ServerUri` | `https://idp/…/token` | oauth
token endpoint |
+| `oauthCredential` | `client_id:client_secret` | oauth
credential |
+| `oauthScope` | (IdP-defined; e.g. Azure `api://…/.default`) | oauth
scope |
+| `kerberosPrincipal` | `tms/_HOST@REALM` |
Kerberos |
+| `keytab` | path or secret ref |
Kerberos |
+
+```properties
+# Gravitino client (control plane)
+gravitino.maintenance.gravitinoAuth.authType = oauth2
+gravitino.maintenance.gravitinoAuth.oauth2ServerUri =
https://idp/realms/…/protocol/openid-connect/token
+gravitino.maintenance.gravitinoAuth.oauthCredential = client_id:client_secret
+
+# Iceberg REST client (Spark Jobs → IRC)
+gravitino.maintenance.ircAuth.authType = oauth2
+gravitino.maintenance.ircAuth.oauth2ServerUri =
https://idp/realms/…/protocol/openid-connect/token
+gravitino.maintenance.ircAuth.oauthCredential = client_id:client_secret
+```
+
+---
+
+## 8. Work Plan and Checklist
+
+| Phase | Work
|
+| ----- |
---------------------------------------------------------------------------------
|
+| 1 | `TableMaintenanceRESTFeature`; start/stop three db-scheduler pools
|
+| 2 | `PolicyExpandPipeline` + `MaintenanceSparkSubmitPipeline` + gates
|
+| 3 | Migrations: `scheduled_tasks`, `table_maintenance_job`,
`table_snapshot_metrics` |
+| 4 | IRC EventListener upserts commit-job; policy create → INSERT expand;
Job listener |
+| 5 | Metrics, shutdown, docs; `gravitino.conf` client auth; privilege
docs |
+
+Implementation checklist:
+
+- [ ] EventListener upserts commit-job only; crontab and onCommit paths do not
cross.
+- [ ] Expand → cron-jobs; cron/commit short submit then DELETE; occupancy on
+ `table_maintenance_job`.
+- [ ] Insert `table_maintenance_job` then `runJob`; sample / recommend / drift
handling run
+ **inside** the Spark Job; then DELETE the scheduled row.
+- [ ] Validation reads metrics per type in the run; purge unreferenced history
by TTL/N.
+- [ ] Commit path: one chained Job; `policy_id` = first type; full chain in
runtime template;
+ orphan crontab-only; coalesce on `{table_id}`; Spark `jobOptions` merge
uses max.
+- [ ] Client auth from `gravitino.conf` only; reject credential-shaped keys in
policy
+ `jobOptions`.
+- [ ] Authorization on ⇒ grants to the token identity; no auto user bootstrap.
+- [ ] Multi-node: one pick wins; reconcile closes stale occupancy without
inventing metrics.
+
+---
+
+## 9. References
+
+1. [Gravitino Iceberg REST service](../docs/iceberg-rest-service.md)
+2. [Manage policies in Gravitino](../docs/manage-policies-in-gravitino.md)
+3. [Iceberg compaction policy](../docs/iceberg-compaction-policy.md)
+4. [Table Maintenance optimizer
overview](../docs/table-maintenance-service/optimizer.md)
+5. [Credential vending](../docs/security/credential-vending.md)
+6. [db-scheduler](https://github.com/kagkarlsson/db-scheduler) — Apache 2.0
+7. [Apache Iceberg REST Catalog
OpenAPI](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml)
+8. [Amoro AIP-3 – Event-Triggered
Optimization](https://cwiki.apache.org/confluence/display/AMORO/AIP-3%3A+Event-Triggered+Optimization+of+Iceberg+Tables+in+Amoro)
+9. [OpenHouse
architecture](https://github.com/linkedin/openhouse/blob/main/ARCHITECTURE.md)
+10. [Iceberg reliability (optimistic
concurrency)](https://iceberg.apache.org/docs/latest/reliability/)