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 c2d2cd59cb [#14382] docs(tms): Document in-process table maintenance
(#14383)
c2d2cd59cb is described below
commit c2d2cd59cbf433c1523e3c09283a3edd4de5ce70
Author: MaSai <[email protected]>
AuthorDate: Mon Nov 2 21:09:35 2026 +0800
[#14382] docs(tms): Document in-process table maintenance (#14383)
### What changes were proposed in this pull request?
Document the in-process table maintenance service on its own page, and
correct the documented Gravitino callback URI.
- Add `docs/table-maintenance-service/service.md` for enabling the
service, `timeTrigger`, the cron path, and `commitTrigger`.
- Point `docs/table-maintenance-service/optimizer.md` at that page and
leave the CLI workflow there.
- Replace `gravitino.maintenance.gravitinoUri` with
`gravitino.maintenance.gravitinoAuth.uri` in the TMS configuration page,
and list the `uri` suffix in the `gravitinoAuth` table.
### Why are the changes needed?
The existing optimizer pages describe the manual CLI. Operators enabling
the in-process service had no matching usage guide, and that guide does
not belong inside the CLI walkthrough. The configuration page also named
`gravitino.maintenance.gravitinoUri`, which the server does not read.
The callback base URI is `gravitino.maintenance.gravitinoAuth.uri`.
Fixes #14382
### Does this PR introduce _any_ user-facing change?
Documentation only. No API or configuration key is added or removed. The
guide documents the existing `gravitino.maintenance.gravitinoAuth.uri`
key.
### How was this patch tested?
Docs-only change. Checked that links in
`docs/table-maintenance-service/` resolve, that
`gravitino.maintenance.gravitinoUri` is gone from the configuration
page, and that the quick-start cron is the six-field Quartz expression
`0 */5 * * * ?`.
---------
Co-authored-by: Cursor <[email protected]>
---
docs/index.md | 3 +-
.../optimizer-cli-reference.md | 84 +++++-----
.../optimizer-configuration.md | 27 ++--
.../optimizer-extension-guide.md | 30 ++--
.../optimizer-troubleshooting.md | 10 +-
docs/table-maintenance-service/optimizer.md | 4 +-
docs/table-maintenance-service/service.md | 173 +++++++++++++++++++++
7 files changed, 254 insertions(+), 77 deletions(-)
diff --git a/docs/index.md b/docs/index.md
index 4861407be7..ea844087e4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -115,7 +115,8 @@ To manage table and partition statistics, see [Manage
statistics in Gravitino](.
* [**Model catalog**](./model-catalog.md)
-If you want to automate table maintenance workflows, see [Table Maintenance
Service (Optimizer)](./table-maintenance-service/optimizer.md).
+To run maintenance inside the Gravitino server, see
[Service](./table-maintenance-service/service.md).
+For the manual CLI workflow, see [Table Maintenance Service
(Optimizer)](./table-maintenance-service/optimizer.md).
Start with Gravitino built-in policies and built-in job templates, and extend
via optimizer interfaces when needed.
Catalogs with an asterisk (\*) aren’t in the standard release tarball and
Docker image. Gravitino provides a
diff --git a/docs/table-maintenance-service/optimizer-cli-reference.md
b/docs/table-maintenance-service/optimizer-cli-reference.md
index 82fdec1ecc..b7f6ff8e2b 100644
--- a/docs/table-maintenance-service/optimizer-cli-reference.md
+++ b/docs/table-maintenance-service/optimizer-cli-reference.md
@@ -17,33 +17,33 @@ directory. Use `--conf-path` only when you need a custom
config file.
## Command Quick Reference
-| Command (`--type`) | Required options | Optional options | Purpose |
-| --- | --- | --- | --- |
-| `submit-strategy-jobs` | `--identifiers`, `--strategy-name` | `--dry-run`,
`--limit` | Recommend and optionally submit jobs |
-| `update-statistics` | `--calculator-name` | `--identifiers`,
`--statistics-payload`, `--file-path` | Calculate and persist statistics |
-| `append-metrics` | `--calculator-name` | `--identifiers`,
`--statistics-payload`, `--file-path` | Calculate and append metrics |
-| `monitor-metrics` | `--identifiers`, `--action-time` | `--range-seconds`,
`--partition-path` | Evaluate rules with before/after metrics |
-| `list-table-metrics` | `--identifiers` | `--partition-path` | Query stored
table or partition metrics |
-| `list-job-metrics` | `--identifiers` | None | Query stored job metrics |
-| `submit-update-stats-job` | `--identifiers` | `--dry-run`, `--update-mode`,
`--updater-options`, `--spark-conf` | Submit built-in Iceberg update
stats/metrics Spark jobs |
+| Command (`--type`) | Required options | Optional
options | Purpose
|
+|---------------------------|------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------|
+| `submit-strategy-jobs` | `--identifiers`, `--strategy-name` |
`--dry-run`, `--limit` | Recommend
and optionally submit jobs |
+| `update-statistics` | `--calculator-name` |
`--identifiers`, `--statistics-payload`, `--file-path` | Calculate
and persist statistics |
+| `append-metrics` | `--calculator-name` |
`--identifiers`, `--statistics-payload`, `--file-path` | Calculate
and append metrics |
+| `monitor-metrics` | `--identifiers`, `--action-time` |
`--range-seconds`, `--partition-path` | Evaluate
rules with before/after metrics |
+| `list-table-metrics` | `--identifiers` |
`--partition-path` | Query
stored table or partition metrics |
+| `list-job-metrics` | `--identifiers` | None
| Query stored job
metrics |
+| `submit-update-stats-job` | `--identifiers` |
`--dry-run`, `--update-mode`, `--updater-options`, `--spark-conf` | Submit
built-in Iceberg update stats/metrics Spark jobs |
## Option Field Meanings
-| Option | Meaning | Used by |
-| --- | --- | --- |
-| `--identifiers` | Comma-separated identifiers. See [Identifier
Rules](#identifier-rules) for command-specific formats. | All commands |
-| `--strategy-name` | Policy name to evaluate, for example
`iceberg_compaction_default`. | `submit-strategy-jobs` |
-| `--dry-run` | Preview mode. Prints recommendations or job configs without
submitting jobs. | `submit-strategy-jobs`, `submit-update-stats-job` |
-| `--limit` | Maximum number of strategy jobs to process. Must be `> 0`. |
`submit-strategy-jobs` |
-| `--calculator-name` | Statistics/metrics calculator implementation name (for
example `local-stats-calculator`). | `update-statistics`, `append-metrics` |
-| `--statistics-payload` | Inline JSON Lines content as input. Mutually
exclusive with `--file-path`. | `update-statistics`, `append-metrics` |
-| `--file-path` | Path to JSON Lines input file. Mutually exclusive with
`--statistics-payload`. | `update-statistics`, `append-metrics` |
-| `--action-time` | Action timestamp in epoch seconds used as evaluation
anchor. | `monitor-metrics` |
-| `--range-seconds` | Time window (seconds) for monitor evaluation. Default is
`86400` (24h). | `monitor-metrics` |
-| `--partition-path` | Partition path JSON array, for example
`'[{"dt":"2026-01-01"}]'`. Requires exactly one identifier. |
`monitor-metrics`, `list-table-metrics` |
-| `--update-mode` | Controls what built-in update job updates: `stats`,
`metrics`, or `all` (default). | `submit-update-stats-job` |
-| `--updater-options` | Flat JSON map passed to updater logic. For
`stats`/`all`, include `gravitino_uri` and `metalake`. |
`submit-update-stats-job` |
-| `--spark-conf` | Flat JSON map of Spark and Iceberg catalog configs used by
the job. | `submit-update-stats-job` |
+| Option | Meaning
| Used by
|
+|------------------------|------------------------------------------------------------------------------------------------------|---------------------------------------------------|
+| `--identifiers` | Comma-separated identifiers. See [Identifier
Rules](#identifier-rules) for command-specific formats. | All commands
|
+| `--strategy-name` | Policy name to evaluate, for example
`iceberg_compaction_default`. |
`submit-strategy-jobs` |
+| `--dry-run` | Preview mode. Prints recommendations or job configs
without submitting jobs. | `submit-strategy-jobs`,
`submit-update-stats-job` |
+| `--limit` | Maximum number of strategy jobs to process. Must be
`> 0`. | `submit-strategy-jobs`
|
+| `--calculator-name` | Statistics/metrics calculator implementation name
(for example `local-stats-calculator`). | `update-statistics`,
`append-metrics` |
+| `--statistics-payload` | Inline JSON Lines content as input. Mutually
exclusive with `--file-path`. | `update-statistics`,
`append-metrics` |
+| `--file-path` | Path to JSON Lines input file. Mutually exclusive
with `--statistics-payload`. | `update-statistics`,
`append-metrics` |
+| `--action-time` | Action timestamp in epoch seconds used as
evaluation anchor. | `monitor-metrics`
|
+| `--range-seconds` | Time window (seconds) for monitor evaluation.
Default is `86400` (24h). | `monitor-metrics`
|
+| `--partition-path` | Partition path JSON array, for example
`'[{"dt":"2026-01-01"}]'`. Requires exactly one identifier. |
`monitor-metrics`, `list-table-metrics` |
+| `--update-mode` | Controls what built-in update job updates: `stats`,
`metrics`, or `all` (default). | `submit-update-stats-job`
|
+| `--updater-options` | Flat JSON map passed to updater logic. For
`stats`/`all`, include `gravitino_uri` and `metalake`. |
`submit-update-stats-job` |
+| `--spark-conf` | Flat JSON map of Spark and Iceberg catalog configs
used by the job. | `submit-update-stats-job`
|
Global option:
@@ -242,13 +242,13 @@ EvaluationResult{scopeType=TABLE,
identifier=rest_catalog.db.t1, partitionPath=<
The following Iceberg job templates ship with the service. A full maintenance
pass collects statistics, compacts data files, expires the snapshot history
that compaction just created, consolidates manifests, and removes old orphan
files.
-| Job template | What it does
|
-|---------------------------------------|-------------------------------------------|
-| `builtin-iceberg-update-stats` | Collects file statistics and metrics
|
-| `builtin-iceberg-rewrite-data-files` | Compacts small data files
|
-| `builtin-iceberg-expire-snapshots` | Removes old snapshot metadata
|
-| `builtin-iceberg-remove-orphan-files` | Removes unreferenced files from
storage |
-| `builtin-iceberg-rewrite-manifests` | Consolidates small manifest files
|
+| Job template | What it does
|
+|-----------------------------------------|------------------------------------------|
+| `builtin-iceberg-update-stats` | Collects file statistics and
metrics |
+| `builtin-iceberg-rewrite-data-files` | Compacts small data files
|
+| `builtin-iceberg-expire-snapshots` | Removes old snapshot metadata
|
+| `builtin-iceberg-remove-orphan-files` | Removes unreferenced files from
storage |
+| `builtin-iceberg-rewrite-manifests` | Consolidates small manifest files
|
| `builtin-iceberg-update-manifest-stats` | Collects complete per-spec
manifest maps |
Each can be submitted directly over REST, and the first two are also what the
policy-driven workflow submits on your behalf. See [Quick
Start](./optimizer.md#walkthrough) for the policy-driven path.
@@ -303,9 +303,9 @@ Its `jobConf` is documented in
[Configuration](./optimizer-configuration.md#job-
`builtin-iceberg-update-manifest-stats` is a separate Spark job that collects
two table-level
statistics, including for partitioned tables:
-| Statistic | Object entry for each decimal spec ID |
-| --- | --- |
-| `custom-manifest-number-by-spec` | Long manifest count |
+| Statistic | Object entry for each decimal spec ID |
+|------------------------------------|---------------------------------------|
+| `custom-manifest-number-by-spec` | Long manifest count |
| `custom-avg-manifest-size-by-spec` | Double average manifest size in bytes |
The job reads manifest metadata without scanning file statistics. It works
after partition
@@ -428,14 +428,14 @@ The job calls Iceberg's `expire_snapshots` stored
procedure through Spark SQL.
`catalog_name` and `table_identifier` are required. The rest are optional.
-| Key | Description
| Default |
-|------------------|-----------------------------------------------------------------------|-----------------------------|
-| `catalog_name` | Iceberg catalog name as registered in Spark
| Required |
-| `table_identifier` | Fully qualified table name, such as `db.sample`
| Required |
-| `older_than` | Expire snapshots older than this `yyyy-MM-dd HH:mm:ss`
timestamp | Five days ago |
-| `retain_last` | Minimum number of recent snapshots to keep regardless of
age | `1` |
-| `stream_results` | Streams intermediate delete results when present
| Disabled |
-| `spark_conf` | JSON map of Spark configuration
| None |
+| Key | Description
| Default |
+|--------------------|------------------------------------------------------------------|---------------|
+| `catalog_name` | Iceberg catalog name as registered in Spark
| Required |
+| `table_identifier` | Fully qualified table name, such as `db.sample`
| Required |
+| `older_than` | Expire snapshots older than this `yyyy-MM-dd HH:mm:ss`
timestamp | Five days ago |
+| `retain_last` | Minimum number of recent snapshots to keep regardless
of age | `1` |
+| `stream_results` | Streams intermediate delete results when present
| Disabled |
+| `spark_conf` | JSON map of Spark configuration
| None |
`older_than` and `retain_last` work together, and `retain_last` wins. Setting
`older_than` to yesterday with `retain_last` at `5` keeps five snapshots even
if all five are older than yesterday.
diff --git a/docs/table-maintenance-service/optimizer-configuration.md
b/docs/table-maintenance-service/optimizer-configuration.md
index 99da41585a..599aeef16e 100644
--- a/docs/table-maintenance-service/optimizer-configuration.md
+++ b/docs/table-maintenance-service/optimizer-configuration.md
@@ -50,11 +50,12 @@ Cron also injects callback identity into `updater_options`:
- `metalake` — always from the policy namespace (policy
`updaterOptions.metalake` cannot redirect
sample/finish to another metalake)
-- `gravitino_uri` — from `gravitino.maintenance.gravitinoUri` when the policy
does not set
- `updaterOptions.gravitino_uri`. Submit fails closed if neither source
provides a URI.
+- `gravitino_uri` — from `gravitino.maintenance.gravitinoAuth.uri` when the
policy does not set
+ `updaterOptions.gravitino_uri`. A policy `updaterOptions.gravitino_uri`
overrides the cluster
+ value. Submit fails closed if neither source provides a URI.
```properties
-gravitino.maintenance.gravitinoUri = http://localhost:8090
+gravitino.maintenance.gravitinoAuth.uri = http://localhost:8090
```
### TMS commit-job (IRC update → chained Spark)
@@ -88,15 +89,16 @@ unknown or blank suffixes fail fast when the server loads
config. Supported `aut
exactly four: **`none`**, **`simple`**, **`basic`**, **`oauth`**. When any
Gravitino auth key is
set, `authType` is required, and `basic` / `oauth` also require their
mode-specific fields below.
-| Conf suffix | `updaterOptions` key | Used when `authType` is | Meaning
|
-| ----------------- | -------------------- | ----------------------- |
------------------------------------------- |
-| `authType` | `auth_type` | all modes | `none`,
`simple`, `basic`, or `oauth` |
-| `username` | `username` | `simple` / `basic` | User
principal (`simple` optional; `basic` required) |
-| `password` | `password` | `basic` |
Password or secret reference (required) |
-| `oauthServerUri` | `oauth_server_uri` | `oauth` | OAuth2
token server base URI (required) |
-| `oauthPath` | `oauth_path` | `oauth` | Token
path (for example `oauth2/token`) (required) |
+| Conf suffix | `updaterOptions` key | Used when `authType` is | Meaning
|
+| ----------------- | -------------------- | ----------------------- |
------------------------------------------------------ |
+| `uri` | `gravitino_uri` | any, including unset |
Gravitino callback base URI. Optional on its own. |
+| `authType` | `auth_type` | all modes | `none`,
`simple`, `basic`, or `oauth` |
+| `username` | `username` | `simple` / `basic` | User
principal (`simple` optional; `basic` required) |
+| `password` | `password` | `basic` |
Password or secret reference (required) |
+| `oauthServerUri` | `oauth_server_uri` | `oauth` | OAuth2
token server base URI (required) |
+| `oauthPath` | `oauth_path` | `oauth` | Token
path (for example `oauth2/token`) (required) |
| `oauthCredential` | `oauth_credential` | `oauth` | Client
credential, often `client_id:secret` (required) |
-| `oauthScope` | `oauth_scope` | `oauth` | Token
scope (IdP-specific) (required) |
+| `oauthScope` | `oauth_scope` | `oauth` | Token
scope (IdP-specific) (required) |
**1. `none`** — Gravitino authorization disabled; omit the prefix or set:
@@ -327,7 +329,8 @@ Four things are worth confirming before assuming a
configuration problem is a co
## Related
-- [Table Maintenance Service](./optimizer.md) for the concepts and the
walkthrough
+- [Service](./service.md) for the in-process service, `timeTrigger`, and the
cron path
+- [Table Maintenance Service](./optimizer.md) for the CLI walkthrough
- [CLI Reference](./optimizer-cli-reference.md) for every command and the
built-in job templates
- [Troubleshooting](./optimizer-troubleshooting.md) when a command or job fails
- [Extension Guide](./optimizer-extension-guide.md) for custom strategies and
providers
diff --git a/docs/table-maintenance-service/optimizer-extension-guide.md
b/docs/table-maintenance-service/optimizer-extension-guide.md
index 8193636414..ca1e15c844 100644
--- a/docs/table-maintenance-service/optimizer-extension-guide.md
+++ b/docs/table-maintenance-service/optimizer-extension-guide.md
@@ -20,21 +20,21 @@ The service supports three loading patterns:
## Extension Points and Config Keys
-| Area | Interface / type | Config key | Loading mode |
-| --- | --- | --- | --- |
-| Recommender statistics | `StatisticsProvider` |
`gravitino.optimizer.recommender.statisticsProvider` | `Provider` SPI by
`name()` |
-| Recommender strategy source | `StrategyProvider` |
`gravitino.optimizer.recommender.strategyProvider` | `Provider` SPI by `name()`
|
-| Recommender table metadata | `TableMetadataProvider` |
`gravitino.optimizer.recommender.tableMetaProvider` | `Provider` SPI by
`name()` |
-| Recommender job submission | `JobSubmitter` |
`gravitino.optimizer.recommender.jobSubmitter` | `Provider` SPI by `name()` |
-| Strategy evaluation logic | `StrategyHandler` |
`gravitino.optimizer.strategyHandler.<strategyType>.className` | Reflection by
class name |
-| Job template adaptation | `GravitinoJobAdapter` |
`gravitino.optimizer.jobAdapter.<jobTemplate>.className` | Reflection by class
name |
-| Update statistics sink | `StatisticsUpdater` |
`gravitino.optimizer.updater.statisticsUpdater` | `Provider` SPI by `name()` |
-| Update metrics sink | `MetricsUpdater` |
`gravitino.optimizer.updater.metricsUpdater` | `Provider` SPI by `name()` |
-| Monitor metrics source | `MetricsProvider` |
`gravitino.optimizer.monitor.metricsProvider` | `Provider` SPI by `name()` |
-| Monitor table-job relation | `TableJobRelationProvider` |
`gravitino.optimizer.monitor.tableJobRelationProvider` | `Provider` SPI by
`name()` |
-| Monitor evaluator | `MetricsEvaluator` |
`gravitino.optimizer.monitor.metricsEvaluator` | Typed SPI
(`ServiceLoader<MetricsEvaluator>`) |
-| Monitor callbacks | `MonitorCallback` |
`gravitino.optimizer.monitor.callbacks` | `Provider` SPI by `name()`
(comma-separated) |
-| CLI calculator | `StatisticsCalculator` | CLI `--calculator-name` | Typed
SPI (`ServiceLoader<StatisticsCalculator>`) |
+| Area | Interface / type | Config key
| Loading mode
|
+|-----------------------------|----------------------------|----------------------------------------------------------------|---------------------------------------------------|
+| Recommender statistics | `StatisticsProvider` |
`gravitino.optimizer.recommender.statisticsProvider` | `Provider` SPI
by `name()` |
+| Recommender strategy source | `StrategyProvider` |
`gravitino.optimizer.recommender.strategyProvider` | `Provider` SPI
by `name()` |
+| Recommender table metadata | `TableMetadataProvider` |
`gravitino.optimizer.recommender.tableMetaProvider` | `Provider` SPI
by `name()` |
+| Recommender job submission | `JobSubmitter` |
`gravitino.optimizer.recommender.jobSubmitter` | `Provider` SPI
by `name()` |
+| Strategy evaluation logic | `StrategyHandler` |
`gravitino.optimizer.strategyHandler.<strategyType>.className` | Reflection by
class name |
+| Job template adaptation | `GravitinoJobAdapter` |
`gravitino.optimizer.jobAdapter.<jobTemplate>.className` | Reflection by
class name |
+| Update statistics sink | `StatisticsUpdater` |
`gravitino.optimizer.updater.statisticsUpdater` | `Provider` SPI
by `name()` |
+| Update metrics sink | `MetricsUpdater` |
`gravitino.optimizer.updater.metricsUpdater` | `Provider` SPI
by `name()` |
+| Monitor metrics source | `MetricsProvider` |
`gravitino.optimizer.monitor.metricsProvider` | `Provider` SPI
by `name()` |
+| Monitor table-job relation | `TableJobRelationProvider` |
`gravitino.optimizer.monitor.tableJobRelationProvider` | `Provider` SPI
by `name()` |
+| Monitor evaluator | `MetricsEvaluator` |
`gravitino.optimizer.monitor.metricsEvaluator` | Typed SPI
(`ServiceLoader<MetricsEvaluator>`) |
+| Monitor callbacks | `MonitorCallback` |
`gravitino.optimizer.monitor.callbacks` | `Provider` SPI
by `name()` (comma-separated) |
+| CLI calculator | `StatisticsCalculator` | CLI
`--calculator-name` | Typed SPI
(`ServiceLoader<StatisticsCalculator>`) |
## Implement a Custom Provider
diff --git a/docs/table-maintenance-service/optimizer-troubleshooting.md
b/docs/table-maintenance-service/optimizer-troubleshooting.md
index 744ecf867d..1176f15fd5 100644
--- a/docs/table-maintenance-service/optimizer-troubleshooting.md
+++ b/docs/table-maintenance-service/optimizer-troubleshooting.md
@@ -107,11 +107,11 @@ Confirm it by checking the job run at
`/api/metalakes/{metalake}/jobs/runs/{job_
Observed in `1.2.0`:
-| Partitioning
| Rewrite |
-|-------------------------------------------------------------------------------|---------|
-| `p`, `p, c2`
| Works |
-| `p, years(ts)`, `p, months(ts)`, `p, days(ts)`, `p, hours(ts)`
| Fails |
-| `p, truncate(1, c2)`, `p, bucket(8, id)`
| Fails |
+| Partitioning | Rewrite |
+|----------------------------------------------------------------|---------|
+| `p`, `p, c2` | Works |
+| `p, years(ts)`, `p, months(ts)`, `p, days(ts)`, `p, hours(ts)` | Fails |
+| `p, truncate(1, c2)`, `p, bucket(8, id)` | Fails |
## Related
diff --git a/docs/table-maintenance-service/optimizer.md
b/docs/table-maintenance-service/optimizer.md
index d121226286..bcf94e048a 100644
--- a/docs/table-maintenance-service/optimizer.md
+++ b/docs/table-maintenance-service/optimizer.md
@@ -23,8 +23,7 @@ Confirm your environment matches this list before starting an
evaluation against
- Compaction and orphan file cleanup have built-in strategies. Snapshot
expiration is available as a directly submitted built-in job. Scheduling and
minimum run intervals remain separate from strategy evaluation. Sort and
cluster maintenance also require custom strategies.
- Compaction applies to Iceberg tables only, and only where every partition
uses an identity transform.
-- The service is driven through the CLI workflow rather than running on a
schedule of its own.
-
+- The in-process service, `timeTrigger`, and the cron path are documented in
[Service](./service.md). The rest of this page is the manual CLI workflow.
## How It Works
@@ -344,6 +343,7 @@ Policy-driven manifest maintenance through Policy ->
Strategy -> Adapter is a fo
## Related
+- [Service](./service.md) for the in-process service, `timeTrigger`, and the
cron path
- [Configuration](./optimizer-configuration.md) for the three configuration
layers
- [CLI Reference](./optimizer-cli-reference.md) for every command and the
built-in job templates
- [Troubleshooting](./optimizer-troubleshooting.md) when something above does
not behave
diff --git a/docs/table-maintenance-service/service.md
b/docs/table-maintenance-service/service.md
new file mode 100644
index 0000000000..9114c96ba0
--- /dev/null
+++ b/docs/table-maintenance-service/service.md
@@ -0,0 +1,173 @@
+---
+title: "Service"
+slug: "/table-maintenance-service/service"
+keywords:
+ - table maintenance
+ - time trigger
+ - cron
+license: "This software is licensed under the Apache License version 2."
+---
+
+## Overview
+
+The table maintenance service runs inside the Gravitino server. Associate a
policy with a tag and assign that tag to a catalog, schema, or table. The
service keeps the nearest policy of each built-in type and submits a Spark job.
A table tag beats a schema tag, and a schema tag beats a catalog tag. Two
policies of the same type on the same object are a conflict, and that table is
skipped.
+
+`timeTrigger` is a six-field Quartz cron (`second minute hour day-of-month
month day-of-week`) on the policy. When it is due, the expand pool lists
matching tables and inserts one cron-job per table. The cron pool then submits
Spark. Operators do not create cron-job rows.
+
+`commitTrigger` is the other switch on the same policy. A successful Iceberg
REST table update enqueues one chained job and does not pass through the expand
pool. See [Commit Trigger](#commit-trigger).
+
+Passwords, OAuth secrets, and keytab paths belong in `gravitino.conf`, not in
policy content.
+
+## Quick Start
+
+The steps below turn on a compaction schedule for one Iceberg table. They
assume a metalake `test`, an Iceberg catalog `rest_catalog`, and a table
`rest_catalog.db.t1` that already exists. Replace those names, and send
`Accept: application/vnd.gravitino.v1+json` on every request.
+
+**1. Enable the service.** Register the maintenance REST callbacks in
`gravitino.conf` and restart Gravitino. Client authentication and the job
executor are in
[Configuration](./optimizer-configuration.md#tms-cluster-static-client-auth).
+
+```properties
+gravitino.server.rest.extensionPackages =
org.apache.gravitino.maintenance.web.rest.feature
+```
+
+**2. Create a compaction policy with a time trigger.**
+
+```shell
+curl -s -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "nightly_compaction",
+ "policyType": "system_iceberg_compaction",
+ "comment": "Compact small Iceberg files",
+ "enabled": true,
+ "content": {
+ "timeTrigger": "0 */5 * * * ?",
+ "commitTrigger": false,
+ "minIntervalMs": 3600000
+ }
+ }' \
+ http://localhost:8090/api/metalakes/test/policies
+```
+
+**3. Associate the policy with a tag, then assign the tag to the table.**
+
+```shell
+curl -s -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "maintained", "comment": "Tables enrolled in TMS"}' \
+ http://localhost:8090/api/metalakes/test/tags
+
+curl -s -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+ -H "Content-Type: application/json" \
+ -d '{"selector": {"type": "ALL_VALUES"}}' \
+
http://localhost:8090/api/metalakes/test/tags/maintained/policies/nightly_compaction
+
+curl -s -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+ -H "Content-Type: application/json" \
+ -d '{"tagsToAdd": ["maintained"]}' \
+
http://localhost:8090/api/metalakes/test/objects/table/rest_catalog.db.t1/tags
+```
+
+**4. Confirm the derived policy, then wait for the next time trigger.**
+
+```shell
+curl -s -H "Accept: application/vnd.gravitino.v1+json" \
+
"http://localhost:8090/api/metalakes/test/objects/table/rest_catalog.db.t1/policies?details=true"
+```
+
+After the trigger is due, list job runs and look for template
`builtin-iceberg-maintenance-task`. See [Manage
jobs](../manage-jobs-in-gravitino.md#list-jobs-get-a-job-and-cancel).
+
+## Configuration
+
+Set the maintenance REST extension in `gravitino.conf`. Client authentication,
Iceberg REST auth, and the job executor are in
[Configuration](./optimizer-configuration.md#tms-cluster-static-client-auth).
+
+| Configuration item | Description
|
+|-------------------------------------------|------------------------------------------------------------------------------------------------------|
+| `gravitino.server.rest.extensionPackages` | Register the maintenance REST
callbacks. Set to `org.apache.gravitino.maintenance.web.rest.feature`. |
+
+A policy `updaterOptions.gravitino_uri` overrides the cluster callback URI.
Every other `gravitinoAuth` key from the cluster wins over the same key in the
policy.
+
+Scheduler pool sizes have these defaults:
+
+| Configuration item | Default |
+|-------------------------------------------------|---------|
+| `gravitino.maintenance.scheduler.expandThreads` | `4` |
+| `gravitino.maintenance.scheduler.cronThreads` | `8` |
+| `gravitino.maintenance.scheduler.commitThreads` | `4` |
+
+## Time Trigger
+
+`timeTrigger` stores the cron string on the policy. `0 */5 * * * ?` runs at
second 0 of every fifth minute. A five-field Unix cron is not accepted.
+
+Three objects decide which tables run:
+
+| Object | What to set
|
+|-----------------|----------------------------------------------------------------------------------------------------------|
+| Policy | `policyType`, `enabled`, and content fields `timeTrigger`,
`commitTrigger`, and `minIntervalMs`. |
+| Tag association | `POST
/api/metalakes/{metalake}/tags/{tag}/policies/{policy}` with selector
`ALL_VALUES` or `TAG_VALUE`. |
+| Tag assignment | Assign the tag to the table, or to a catalog or schema
ancestor. |
+
+`minIntervalMs` is the cooldown after a finished run of that policy on that
table. When the policy omits it, the server uses the per-type default in [Table
maintenance
cooldowns](../gravitino-server-config.md#table-maintenance-cooldowns).
+
+Built-in content types:
+
+| `policyType` | Time trigger | Commit trigger |
+|--------------------------------------|--------------|----------------|
+| `system_iceberg_compaction` | Yes | Yes |
+| `system_iceberg_rewrite_manifests` | Yes | Yes |
+| `system_iceberg_snapshot_expiration` | Yes | Yes |
+| `system_iceberg_orphan_file_removal` | Yes | No |
+
+Field lists for each type are in [Iceberg compaction
policy](../iceberg-compaction-policy.md), [Iceberg rewrite manifests
policy](../iceberg-rewrite-manifests-policy.md), and [Manage
policies](../manage-policies-in-gravitino.md).
+
+## Cron Path
+
+The cron path is what `timeTrigger` does after it is due.
+
+1. The expand pool wakes on the policy cron.
+2. It lists schemas and tables from the catalog. A missing Gravitino schema or
table is imported by `loadSchema` or `loadTable`.
+3. It drops tables that lost the nearest-policy check, that are already in
flight, or that are still inside `minIntervalMs`.
+4. It inserts one cron-job per remaining table. The cron pool submits one
`builtin-iceberg-maintenance-task` Spark job for that row.
+5. The job calls the maintenance REST callbacks below.
+
+TMS fills `action_ids` when it builds the job. Do not put `action_ids` in
policy `jobOptions`.
+
+The job reads two bags that were snapshotted at enqueue time:
+
+- `gravitinoAuth` becomes `updater_options`, including `gravitino_uri` and
`metalake`. `metalake` always comes from the policy namespace.
+- `ircAuth` becomes Iceberg REST catalog properties under
`spark.sql.catalog.<catalog>.*`.
+
+On Kubernetes, leave `spark_master` empty. See [Built-in
jobs](../k8s-job-executor.md#built-in-jobs).
+
+These endpoints are for the Spark job, not for operators. Schemas are in the
[maintenance OpenAPI definition](../open-api/maintenance/maintenance.yaml). The
commit path uses the same callbacks.
+
+| Callback | Method and path
|
+|-----------------------------|-----------------------------------------------------------------------------|
+| Before-maintenance metrics | `POST
/api/metalakes/{metalake}/maintenance/actions/{jobId}/metrics/before` |
+| Finish and release the slot | `POST
/api/metalakes/{metalake}/maintenance/actions/{jobId}/finish` |
+| After-maintenance metrics | `POST
/api/metalakes/{metalake}/maintenance/actions/{jobId}/metrics/after` |
+
+## Commit Trigger
+
+Set `commitTrigger` to `true` on a policy that should run after a successful
Iceberg REST table update. Orphan file removal cannot set `commitTrigger`. The
same `minIntervalMs` cooldown applies.
+
+Commit-triggered maintenance needs the Iceberg REST auxiliary service and the
listener:
+
+```properties
+gravitino.auxService.names = iceberg-rest
+gravitino.eventListener.names = post-commit-maintenance-listener
+gravitino.eventListener.post-commit-maintenance-listener.class =
org.apache.gravitino.maintenance.listener.PostCommitMaintenanceListener
+```
+
+Without the listener, time triggers still run. Commit-triggered maintenance
does not.
+
+1. An Iceberg REST table update reaches `PostCommitMaintenanceListener`.
+2. TMS loads the nearest policies with `commitTrigger` set to `true`, in
order: compaction, manifest rewrite, then snapshot expiry.
+3. A policy still inside `minIntervalMs` is dropped from the chain.
+4. If the effective `gravitinoAuth` or `ircAuth` bags disagree across the
remaining policies, the chain is not submitted.
+5. Otherwise one chained `builtin-iceberg-maintenance-task` job runs every
remaining policy.
+
+## Related
+
+- [Configuration](./optimizer-configuration.md) for client authentication,
Iceberg REST auth, and the job executor
+- [Table Maintenance Service](./optimizer.md) for the manual CLI workflow
+- [CLI Reference](./optimizer-cli-reference.md) for every command and the
built-in job templates
+- [Iceberg Compaction Policy](../iceberg-compaction-policy.md) for tuning the
built-in compaction strategy