diff --git a/grafana/dashboards/metrics/cluster/dashboard.json b/grafana/dashboards/metrics/cluster/dashboard.json
index daa0f018836..2fffd27b25f 100644
--- a/grafana/dashboards/metrics/cluster/dashboard.json
+++ b/grafana/dashboards/metrics/cluster/dashboard.json
@@ -10249,7 +10249,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Compaction OPS per Instance.",
+ "description": "Published compaction units per second per Instance. A local unit is one input dependency group; a remote job counts as one unit.",
"fieldConfig": {
"defaults": {
"color": {
@@ -10335,9 +10335,9 @@
"uid": "${metrics}"
},
"editorMode": "code",
- "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))",
+ "expr": "sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))",
"instant": false,
- "legendFormat": "[{{ instance }}]-[{{pod}}]",
+ "legendFormat": "[{{ instance }}]-[{{pod}}]-[{{type}}]",
"range": true,
"refId": "A"
}
@@ -10350,7 +10350,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Ongoing compaction task count",
+ "description": "Ongoing compaction count: local units and remote jobs",
"fieldConfig": {
"defaults": {
"color": {
@@ -10451,7 +10451,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Compaction P99 and average per Instance.",
+ "description": "Compaction unit P99 and average elapsed time per Instance. Local units are timed from task start through worker apply; remote jobs from request start.",
"fieldConfig": {
"defaults": {
"color": {
@@ -10537,9 +10537,9 @@
"uid": "${metrics}"
},
"editorMode": "code",
- "expr": "histogram_quantile(0.99, sum by(instance, pod, le) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))",
+ "expr": "histogram_quantile(0.99, sum by(instance, pod, type, le) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~\"$datanode\"}[$__rate_interval])))",
"instant": false,
- "legendFormat": "[{{instance}}]-[{{pod}}]-p99",
+ "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]-p99",
"range": true,
"refId": "A"
},
@@ -10549,10 +10549,10 @@
"uid": "${metrics}"
},
"editorMode": "code",
- "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_sum{instance=~\"$datanode\"}[$__rate_interval])) / sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))",
+ "expr": "sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_sum{instance=~\"$datanode\"}[$__rate_interval])) / sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count{instance=~\"$datanode\"}[$__rate_interval]))",
"hide": false,
"instant": false,
- "legendFormat": "[{{instance}}]-[{{pod}}]-avg",
+ "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]-avg",
"range": true,
"refId": "B"
}
diff --git a/grafana/dashboards/metrics/cluster/dashboard.md b/grafana/dashboards/metrics/cluster/dashboard.md
index 3278db52bfd..455268cdd43 100644
--- a/grafana/dashboards/metrics/cluster/dashboard.md
+++ b/grafana/dashboards/metrics/cluster/dashboard.md
@@ -117,9 +117,9 @@
| Flush Elapsed Time | `histogram_quantile(0.95, sum by (instance, pod, le, type) (rate(greptime_mito_flush_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))`
`histogram_quantile(0.99, sum by (instance, pod, le, type) (rate(greptime_mito_flush_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))`
`sum by (instance, pod, type) (rate(greptime_mito_flush_elapsed_sum{instance=~"$datanode"}[$__rate_interval])) / sum by (instance, pod, type) (rate(greptime_mito_flush_elapsed_count{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Mito flush p95 and p99 elapsed time by datanode and flush type. Use this to identify slow flush jobs. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{type}}]-p95` |
| Flush Throughput | `sum by (instance, pod) (rate(greptime_mito_flush_bytes_total{instance=~"$datanode"}[$__rate_interval]))`
`sum by (instance, pod) (rate(greptime_mito_flush_file_total{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Mito flushed bytes and flushed file rates. Use this with flush elapsed time to distinguish slow jobs from large jobs. | `prometheus` | `Bps` | `[{{instance}}]-[{{pod}}]-bytes` |
| Inflight Flush | `greptime_mito_inflight_flush_count` | `timeseries` | Ongoing flush task count | `prometheus` | `none` | `[{{instance}}]-[{{pod}}]` |
-| Compaction OPS per Instance | `sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Compaction OPS per Instance. | `prometheus` | `ops` | `[{{ instance }}]-[{{pod}}]` |
-| Inflight Compaction | `greptime_mito_inflight_compaction_count` | `timeseries` | Ongoing compaction task count | `prometheus` | `none` | `[{{instance}}]-[{{pod}}]` |
-| Compaction P99 and Avg per Instance | `histogram_quantile(0.99, sum by(instance, pod, le) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))`
`sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_sum{instance=~"$datanode"}[$__rate_interval])) / sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Compaction P99 and average per Instance. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-p99` |
+| Compaction OPS per Instance | `sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Published compaction units per second per Instance. A local unit is one input dependency group; a remote job counts as one unit. | `prometheus` | `ops` | `[{{ instance }}]-[{{pod}}]-[{{type}}]` |
+| Inflight Compaction | `greptime_mito_inflight_compaction_count` | `timeseries` | Ongoing compaction count: local units and remote jobs | `prometheus` | `none` | `[{{instance}}]-[{{pod}}]` |
+| Compaction P99 and Avg per Instance | `histogram_quantile(0.99, sum by(instance, pod, type, le) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))`
`sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_sum{instance=~"$datanode"}[$__rate_interval])) / sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Compaction unit P99 and average elapsed time per Instance. Local units are timed from task start through worker apply; remote jobs from request start. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{type}}]-p99` |
| Compaction Elapsed Time per Instance by Stage | `histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_compaction_stage_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))`
`sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_sum{instance=~"$datanode"}[$__rate_interval]))/sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_count{instance=~"$datanode"}[$__rate_interval]))` | `timeseries` | Compaction latency by stage | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{stage}}]-p99` |
| Compaction Input/Output Bytes | `sum by(instance, pod) (rate(greptime_mito_compaction_input_bytes[$__rate_interval]))`
`sum by(instance, pod) (rate(greptime_mito_compaction_output_bytes[$__rate_interval]))` | `timeseries` | Compaction input and output bytes by datanode. Use this to correlate compaction latency with rewritten data volume. | `prometheus` | `Bps` | `[{{instance}}]-[{{pod}}]-input` |
# Index
diff --git a/grafana/dashboards/metrics/cluster/dashboard.yaml b/grafana/dashboards/metrics/cluster/dashboard.yaml
index bc659b401b5..7513c5e50e1 100644
--- a/grafana/dashboards/metrics/cluster/dashboard.yaml
+++ b/grafana/dashboards/metrics/cluster/dashboard.yaml
@@ -1417,17 +1417,17 @@ groups:
legendFormat: '[{{instance}}]-[{{pod}}]'
- title: Compaction OPS per Instance
type: timeseries
- description: Compaction OPS per Instance.
+ description: Published compaction units per second per Instance. A local unit is one input dependency group; a remote job counts as one unit.
unit: ops
queries:
- - expr: sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))
+ - expr: sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))
datasource:
type: prometheus
uid: ${metrics}
- legendFormat: '[{{ instance }}]-[{{pod}}]'
+ legendFormat: '[{{ instance }}]-[{{pod}}]-[{{type}}]'
- title: Inflight Compaction
type: timeseries
- description: Ongoing compaction task count
+ description: 'Ongoing compaction count: local units and remote jobs'
unit: none
queries:
- expr: greptime_mito_inflight_compaction_count
@@ -1437,19 +1437,19 @@ groups:
legendFormat: '[{{instance}}]-[{{pod}}]'
- title: Compaction P99 and Avg per Instance
type: timeseries
- description: Compaction P99 and average per Instance.
+ description: Compaction unit P99 and average elapsed time per Instance. Local units are timed from task start through worker apply; remote jobs from request start.
unit: s
queries:
- - expr: histogram_quantile(0.99, sum by(instance, pod, le) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))
+ - expr: histogram_quantile(0.99, sum by(instance, pod, type, le) (rate(greptime_mito_compaction_total_elapsed_bucket{instance=~"$datanode"}[$__rate_interval])))
datasource:
type: prometheus
uid: ${metrics}
- legendFormat: '[{{instance}}]-[{{pod}}]-p99'
- - expr: sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_sum{instance=~"$datanode"}[$__rate_interval])) / sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))
+ legendFormat: '[{{instance}}]-[{{pod}}]-[{{type}}]-p99'
+ - expr: sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_sum{instance=~"$datanode"}[$__rate_interval])) / sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count{instance=~"$datanode"}[$__rate_interval]))
datasource:
type: prometheus
uid: ${metrics}
- legendFormat: '[{{instance}}]-[{{pod}}]-avg'
+ legendFormat: '[{{instance}}]-[{{pod}}]-[{{type}}]-avg'
- title: Compaction Elapsed Time per Instance by Stage
type: timeseries
description: Compaction latency by stage
diff --git a/grafana/dashboards/metrics/standalone/dashboard.json b/grafana/dashboards/metrics/standalone/dashboard.json
index c1fda955d90..f17d40b4f96 100644
--- a/grafana/dashboards/metrics/standalone/dashboard.json
+++ b/grafana/dashboards/metrics/standalone/dashboard.json
@@ -10249,7 +10249,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Compaction OPS per Instance.",
+ "description": "Published compaction units per second per Instance. A local unit is one input dependency group; a remote job counts as one unit.",
"fieldConfig": {
"defaults": {
"color": {
@@ -10335,9 +10335,9 @@
"uid": "${metrics}"
},
"editorMode": "code",
- "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))",
+ "expr": "sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))",
"instant": false,
- "legendFormat": "[{{ instance }}]-[{{pod}}]",
+ "legendFormat": "[{{ instance }}]-[{{pod}}]-[{{type}}]",
"range": true,
"refId": "A"
}
@@ -10350,7 +10350,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Ongoing compaction task count",
+ "description": "Ongoing compaction count: local units and remote jobs",
"fieldConfig": {
"defaults": {
"color": {
@@ -10451,7 +10451,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Compaction P99 and average per Instance.",
+ "description": "Compaction unit P99 and average elapsed time per Instance. Local units are timed from task start through worker apply; remote jobs from request start.",
"fieldConfig": {
"defaults": {
"color": {
@@ -10537,9 +10537,9 @@
"uid": "${metrics}"
},
"editorMode": "code",
- "expr": "histogram_quantile(0.99, sum by(instance, pod, le) (rate(greptime_mito_compaction_total_elapsed_bucket[$__rate_interval])))",
+ "expr": "histogram_quantile(0.99, sum by(instance, pod, type, le) (rate(greptime_mito_compaction_total_elapsed_bucket[$__rate_interval])))",
"instant": false,
- "legendFormat": "[{{instance}}]-[{{pod}}]-p99",
+ "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]-p99",
"range": true,
"refId": "A"
},
@@ -10549,10 +10549,10 @@
"uid": "${metrics}"
},
"editorMode": "code",
- "expr": "sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_sum[$__rate_interval])) / sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))",
+ "expr": "sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_sum[$__rate_interval])) / sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))",
"hide": false,
"instant": false,
- "legendFormat": "[{{instance}}]-[{{pod}}]-avg",
+ "legendFormat": "[{{instance}}]-[{{pod}}]-[{{type}}]-avg",
"range": true,
"refId": "B"
}
diff --git a/grafana/dashboards/metrics/standalone/dashboard.md b/grafana/dashboards/metrics/standalone/dashboard.md
index 5dd6f1600e1..5c7a4042444 100644
--- a/grafana/dashboards/metrics/standalone/dashboard.md
+++ b/grafana/dashboards/metrics/standalone/dashboard.md
@@ -117,9 +117,9 @@
| Flush Elapsed Time | `histogram_quantile(0.95, sum by (instance, pod, le, type) (rate(greptime_mito_flush_elapsed_bucket[$__rate_interval])))`
`histogram_quantile(0.99, sum by (instance, pod, le, type) (rate(greptime_mito_flush_elapsed_bucket[$__rate_interval])))`
`sum by (instance, pod, type) (rate(greptime_mito_flush_elapsed_sum[$__rate_interval])) / sum by (instance, pod, type) (rate(greptime_mito_flush_elapsed_count[$__rate_interval]))` | `timeseries` | Mito flush p95 and p99 elapsed time by datanode and flush type. Use this to identify slow flush jobs. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{type}}]-p95` |
| Flush Throughput | `sum by (instance, pod) (rate(greptime_mito_flush_bytes_total[$__rate_interval]))`
`sum by (instance, pod) (rate(greptime_mito_flush_file_total[$__rate_interval]))` | `timeseries` | Mito flushed bytes and flushed file rates. Use this with flush elapsed time to distinguish slow jobs from large jobs. | `prometheus` | `Bps` | `[{{instance}}]-[{{pod}}]-bytes` |
| Inflight Flush | `greptime_mito_inflight_flush_count` | `timeseries` | Ongoing flush task count | `prometheus` | `none` | `[{{instance}}]-[{{pod}}]` |
-| Compaction OPS per Instance | `sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))` | `timeseries` | Compaction OPS per Instance. | `prometheus` | `ops` | `[{{ instance }}]-[{{pod}}]` |
-| Inflight Compaction | `greptime_mito_inflight_compaction_count` | `timeseries` | Ongoing compaction task count | `prometheus` | `none` | `[{{instance}}]-[{{pod}}]` |
-| Compaction P99 and Avg per Instance | `histogram_quantile(0.99, sum by(instance, pod, le) (rate(greptime_mito_compaction_total_elapsed_bucket[$__rate_interval])))`
`sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_sum[$__rate_interval])) / sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))` | `timeseries` | Compaction P99 and average per Instance. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-p99` |
+| Compaction OPS per Instance | `sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))` | `timeseries` | Published compaction units per second per Instance. A local unit is one input dependency group; a remote job counts as one unit. | `prometheus` | `ops` | `[{{ instance }}]-[{{pod}}]-[{{type}}]` |
+| Inflight Compaction | `greptime_mito_inflight_compaction_count` | `timeseries` | Ongoing compaction count: local units and remote jobs | `prometheus` | `none` | `[{{instance}}]-[{{pod}}]` |
+| Compaction P99 and Avg per Instance | `histogram_quantile(0.99, sum by(instance, pod, type, le) (rate(greptime_mito_compaction_total_elapsed_bucket[$__rate_interval])))`
`sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_sum[$__rate_interval])) / sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))` | `timeseries` | Compaction unit P99 and average elapsed time per Instance. Local units are timed from task start through worker apply; remote jobs from request start. | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{type}}]-p99` |
| Compaction Elapsed Time per Instance by Stage | `histogram_quantile(0.99, sum by(instance, pod, le, stage) (rate(greptime_mito_compaction_stage_elapsed_bucket[$__rate_interval])))`
`sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_sum[$__rate_interval]))/sum by(instance, pod, stage) (rate(greptime_mito_compaction_stage_elapsed_count[$__rate_interval]))` | `timeseries` | Compaction latency by stage | `prometheus` | `s` | `[{{instance}}]-[{{pod}}]-[{{stage}}]-p99` |
| Compaction Input/Output Bytes | `sum by(instance, pod) (rate(greptime_mito_compaction_input_bytes[$__rate_interval]))`
`sum by(instance, pod) (rate(greptime_mito_compaction_output_bytes[$__rate_interval]))` | `timeseries` | Compaction input and output bytes by datanode. Use this to correlate compaction latency with rewritten data volume. | `prometheus` | `Bps` | `[{{instance}}]-[{{pod}}]-input` |
# Index
diff --git a/grafana/dashboards/metrics/standalone/dashboard.yaml b/grafana/dashboards/metrics/standalone/dashboard.yaml
index beadb9c3215..627cd8b5d63 100644
--- a/grafana/dashboards/metrics/standalone/dashboard.yaml
+++ b/grafana/dashboards/metrics/standalone/dashboard.yaml
@@ -1417,17 +1417,17 @@ groups:
legendFormat: '[{{instance}}]-[{{pod}}]'
- title: Compaction OPS per Instance
type: timeseries
- description: Compaction OPS per Instance.
+ description: Published compaction units per second per Instance. A local unit is one input dependency group; a remote job counts as one unit.
unit: ops
queries:
- - expr: sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))
+ - expr: sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))
datasource:
type: prometheus
uid: ${metrics}
- legendFormat: '[{{ instance }}]-[{{pod}}]'
+ legendFormat: '[{{ instance }}]-[{{pod}}]-[{{type}}]'
- title: Inflight Compaction
type: timeseries
- description: Ongoing compaction task count
+ description: 'Ongoing compaction count: local units and remote jobs'
unit: none
queries:
- expr: greptime_mito_inflight_compaction_count
@@ -1437,19 +1437,19 @@ groups:
legendFormat: '[{{instance}}]-[{{pod}}]'
- title: Compaction P99 and Avg per Instance
type: timeseries
- description: Compaction P99 and average per Instance.
+ description: Compaction unit P99 and average elapsed time per Instance. Local units are timed from task start through worker apply; remote jobs from request start.
unit: s
queries:
- - expr: histogram_quantile(0.99, sum by(instance, pod, le) (rate(greptime_mito_compaction_total_elapsed_bucket[$__rate_interval])))
+ - expr: histogram_quantile(0.99, sum by(instance, pod, type, le) (rate(greptime_mito_compaction_total_elapsed_bucket[$__rate_interval])))
datasource:
type: prometheus
uid: ${metrics}
- legendFormat: '[{{instance}}]-[{{pod}}]-p99'
- - expr: sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_sum[$__rate_interval])) / sum by(instance, pod) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))
+ legendFormat: '[{{instance}}]-[{{pod}}]-[{{type}}]-p99'
+ - expr: sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_sum[$__rate_interval])) / sum by(instance, pod, type) (rate(greptime_mito_compaction_total_elapsed_count[$__rate_interval]))
datasource:
type: prometheus
uid: ${metrics}
- legendFormat: '[{{instance}}]-[{{pod}}]-avg'
+ legendFormat: '[{{instance}}]-[{{pod}}]-[{{type}}]-avg'
- title: Compaction Elapsed Time per Instance by Stage
type: timeseries
description: Compaction latency by stage
diff --git a/src/mito2/AGENTS.md b/src/mito2/AGENTS.md
index efee5f6cca1..f70aa622a27 100644
--- a/src/mito2/AGENTS.md
+++ b/src/mito2/AGENTS.md
@@ -91,6 +91,8 @@ Tests live next to the code as `*_test.rs` (e.g. `src/mito2/src/engine/flush_tes
- Loaded regions have resident compaction state, including `Idle` and handed-off
DDLs. Publish create/open/catchup results through the worker's `register_region`;
only close/drop unregisters them. See `compaction/scheduler/state.rs`.
+- Local compaction units use the snapshot-free `CompactionContext`. Retaining a
+ full `CompactionRegion` in a unit pins other units' inputs and delays SST purge.
## Maintenance contract
diff --git a/src/mito2/src/compaction.rs b/src/mito2/src/compaction.rs
index 1fbbfad5613..0f1b637e096 100644
--- a/src/mito2/src/compaction.rs
+++ b/src/mito2/src/compaction.rs
@@ -26,8 +26,10 @@ mod task;
#[cfg(test)]
mod test_util;
mod twcs;
+mod unit;
mod window;
+use std::any::Any;
use std::collections::HashMap;
use common_meta::key::SchemaMetadataManagerRef;
@@ -155,3 +157,14 @@ pub struct SerializedCompactionOutput {
filter_deleted: bool,
output_time_range: Option,
}
+
+/// Returns the printable reason carried by a caught panic payload.
+pub(crate) fn format_panic_reason(payload: &(dyn Any + Send + 'static)) -> String {
+ if let Some(message) = payload.downcast_ref::<&str>() {
+ message.to_string()
+ } else if let Some(message) = payload.downcast_ref::() {
+ message.clone()
+ } else {
+ "unknown panic".to_string()
+ }
+}
diff --git a/src/mito2/src/compaction/compactor.rs b/src/mito2/src/compaction/compactor.rs
index 1a025bcaeec..527750bf33e 100644
--- a/src/mito2/src/compaction/compactor.rs
+++ b/src/mito2/src/compaction/compactor.rs
@@ -38,6 +38,7 @@ use crate::access_layer::{
use crate::cache::{CacheManager, CacheManagerRef};
use crate::compaction::picker::PickerOutput;
use crate::compaction::reader::CompactionSstReaderBuilder;
+use crate::compaction::unit::CompactionUnit;
use crate::compaction::{CompactionOutput, find_dynamic_options};
use crate::config::MitoConfig;
use crate::engine::region_hook::{RegionHookRef, SstFileInfo};
@@ -123,6 +124,70 @@ pub struct CompactionRegion {
pub(crate) plugins: Plugins,
}
+/// Execution resources for compaction, without a region-wide SST snapshot.
+///
+/// Inputs are owned by each output or unit so a slow sibling cannot retain
+/// files that another unit has already replaced.
+#[derive(Clone)]
+pub struct CompactionContext {
+ /// Region whose files are being compacted.
+ pub region_id: RegionId,
+ /// Region options including resolved dynamic compaction options.
+ pub region_options: RegionOptions,
+ /// Options from the version used to plan the merge.
+ version_options: RegionOptions,
+ pub(crate) engine_config: Arc,
+ region_metadata: RegionMetadataRef,
+ pub(crate) cache_manager: CacheManagerRef,
+ pub(crate) access_layer: AccessLayerRef,
+ manifest_ctx: Arc,
+ plugins: Plugins,
+}
+
+impl From<&CompactionRegion> for CompactionContext {
+ fn from(region: &CompactionRegion) -> Self {
+ Self {
+ region_id: region.region_id,
+ region_options: region.region_options.clone(),
+ version_options: region.current_version.options.clone(),
+ engine_config: region.engine_config.clone(),
+ region_metadata: region.region_metadata.clone(),
+ cache_manager: region.cache_manager.clone(),
+ access_layer: region.access_layer.clone(),
+ manifest_ctx: region.manifest_ctx.clone(),
+ plugins: region.plugins.clone(),
+ }
+ }
+}
+
+/// Builds a minimal [`CompactionRegion`] for tests that do not touch the access layer.
+#[cfg(test)]
+pub(crate) async fn new_test_compaction_region() -> CompactionRegion {
+ let env = crate::test_util::scheduler_util::SchedulerEnv::new().await;
+ let metadata = crate::test_util::memtable_util::metadata_for_test();
+ let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
+ CompactionRegion {
+ region_id: RegionId::new(1, 1),
+ region_options: RegionOptions::default(),
+ engine_config: Arc::new(MitoConfig::default()),
+ region_metadata: metadata.clone(),
+ cache_manager: Arc::new(CacheManager::default()),
+ access_layer: env.access_layer.clone(),
+ manifest_ctx,
+ current_version: CompactionVersion {
+ metadata: metadata.clone(),
+ options: RegionOptions::default(),
+ ssts: Arc::new(SstVersion::new(metadata)),
+ memtable_min_sequence: None,
+ compaction_time_window: None,
+ },
+ file_purger: None,
+ ttl: None,
+ max_parallelism: 1,
+ plugins: Plugins::new(),
+ }
+}
+
/// OpenCompactionRegionRequest represents the request to open a compaction region.
#[derive(Clone)]
pub struct OpenCompactionRegionRequest {
@@ -273,9 +338,18 @@ impl CompactionRegion {
/// Fires [`RegionHook::on_sst_files_written`] for the freshly-merged SST
/// files in `merge_output`. Shared by both compaction paths, it must run
- /// before [`Compactor::update_manifest`], whose `on_manifest_updated`
- /// drains the per-region state this hook populates.
+ /// before this unit's [`Compactor::update_manifest`] so its committed edit
+ /// can be associated with the SST metadata reported here.
pub async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
+ CompactionContext::from(self)
+ .invoke_sst_hook(merge_output)
+ .await;
+ }
+}
+
+impl CompactionContext {
+ /// Reports this unit's output metadata before publishing its manifest edit.
+ pub(crate) async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
let Some(hook) = self.plugins.get::() else {
return;
};
@@ -319,6 +393,32 @@ impl CompactionRegion {
hook.on_sst_files_written(self.region_id, &self.region_metadata, &files)
.await;
}
+
+ /// Publishes outputs through the existing manifest validation and hook path.
+ pub(crate) async fn update_manifest(
+ &self,
+ merge_output: MergeOutput,
+ ) -> Result<(RegionEdit, ManifestVersion)> {
+ let edit = RegionEdit {
+ files_to_add: merge_output.files_to_add,
+ files_to_remove: merge_output.files_to_remove,
+ timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
+ compaction_time_window: merge_output
+ .compaction_time_window
+ .map(|seconds| Duration::from_secs(seconds as u64)),
+ flushed_entry_id: None,
+ flushed_sequence: None,
+ committed_sequence: None,
+ };
+
+ let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
+ let manifest_version = self
+ .manifest_ctx
+ .update_manifest_for_compaction(action_list)
+ .await?;
+
+ Ok((edit, manifest_version))
+ }
}
/// Builds an [`SstInfo`] for the remote-compaction path by copying the scalar
@@ -406,7 +506,7 @@ pub trait Compactor: Send + Sync + 'static {
pub trait SstMerger: Send + Sync + 'static {
async fn merge_single_output(
&self,
- compaction_region: CompactionRegion,
+ context: CompactionContext,
output: CompactionOutput,
write_opts: WriteOptions,
) -> Result<(Vec, Vec)>;
@@ -426,13 +526,13 @@ struct OutputSequenceMetadata {
/// without allocating a new admission marker. Retaining physical row sequences
/// does not by itself restore exact-read capability for untrusted inputs.
fn output_sequence_metadata(
- region: &CompactionRegion,
+ context: &CompactionContext,
inputs: &[FileHandle],
) -> OutputSequenceMetadata {
- let exact_sequence_trusted = region.region_options.preserve_row_sequence
+ let exact_sequence_trusted = context.region_options.preserve_row_sequence
&& inputs
.iter()
- .all(|f| f.is_effective_target_sequence_trusted(region.region_id));
+ .all(|f| f.is_effective_target_sequence_trusted(context.region_id));
OutputSequenceMetadata {
file_sequence: known_max_input_sequence(inputs),
exact_sequence_trusted,
@@ -456,39 +556,35 @@ fn known_max_input_sequence(inputs: &[FileHandle]) -> Option {
impl SstMerger for DefaultSstMerger {
async fn merge_single_output(
&self,
- compaction_region: CompactionRegion,
+ context: CompactionContext,
output: CompactionOutput,
write_opts: WriteOptions,
) -> Result<(Vec, Vec)> {
- let region_id = compaction_region.region_id;
- let index_options = compaction_region
- .current_version
- .options
- .index_options
- .clone();
- let append_mode = compaction_region.current_version.options.append_mode;
- let merge_mode = compaction_region.current_version.options.merge_mode();
- let flat_format = compaction_region
+ let region_id = context.region_id;
+ let index_options = context.version_options.index_options.clone();
+ let append_mode = context.version_options.append_mode;
+ let merge_mode = context.version_options.merge_mode();
+ let flat_format = context
.region_options
.sst_format
.map(|format| format == FormatType::Flat)
- .unwrap_or(compaction_region.engine_config.default_flat_format);
+ .unwrap_or(context.engine_config.default_flat_format);
- let index_config = compaction_region.engine_config.index.clone();
- let inverted_index_config = compaction_region.engine_config.inverted_index.clone();
- let fulltext_index_config = compaction_region.engine_config.fulltext_index.clone();
- let bloom_filter_index_config = compaction_region.engine_config.bloom_filter_index.clone();
+ let index_config = context.engine_config.index.clone();
+ let inverted_index_config = context.engine_config.inverted_index.clone();
+ let fulltext_index_config = context.engine_config.fulltext_index.clone();
+ let bloom_filter_index_config = context.engine_config.bloom_filter_index.clone();
let input_file_names = output
.inputs
.iter()
.map(|f| f.file_id().to_string())
.join(",");
- let sequence_metadata = output_sequence_metadata(&compaction_region, &output.inputs);
+ let sequence_metadata = output_sequence_metadata(&context, &output.inputs);
let builder = CompactionSstReaderBuilder {
- metadata: compaction_region.region_metadata.clone(),
- sst_layer: compaction_region.access_layer.clone(),
- cache: compaction_region.cache_manager.clone(),
+ metadata: context.region_metadata.clone(),
+ sst_layer: context.access_layer.clone(),
+ cache: context.cache_manager.clone(),
inputs: &output.inputs,
append_mode,
filter_deleted: output.filter_deleted,
@@ -498,15 +594,15 @@ impl SstMerger for DefaultSstMerger {
let source = builder.build_flat_sst_reader().await?;
let mut metrics = Metrics::new(WriteType::Compaction);
- let region_metadata = compaction_region.region_metadata.clone();
- let sst_infos = compaction_region
+ let region_metadata = context.region_metadata.clone();
+ let sst_infos = context
.access_layer
.write_sst(
SstWriteRequest {
op_type: OperationType::Compact,
metadata: region_metadata.clone(),
source,
- cache_manager: compaction_region.cache_manager.clone(),
+ cache_manager: context.cache_manager.clone(),
// Readers resolve file overrides before merge/dedup. Replacing
// their effective sequences here could promote old rows above
// versions in SSTs that were not part of this merge.
@@ -629,6 +725,50 @@ impl DefaultCompactor {
}
}
+impl DefaultCompactor {
+ /// Merges a complete atomic unit. No partial input replacement is returned.
+ pub(crate) async fn merge_unit(
+ &self,
+ context: &CompactionContext,
+ unit: &CompactionUnit,
+ ) -> Result {
+ let mut result = MergeOutput {
+ files_to_remove: unit.inputs.iter().map(|f| f.meta_ref().clone()).collect(),
+ compaction_time_window: Some(unit.time_window_size),
+ ..Default::default()
+ };
+ for output in &unit.outputs {
+ if self.cancel_handle.is_cancelled() {
+ return error::CompactionCancelledSnafu.fail();
+ }
+ // Let the writer finish and register its files before observing cancellation.
+ // Dropping it mid-write can lose ownership of already finalized SSTs.
+ let (files, infos) = self
+ .merger
+ .merge_single_output(
+ context.clone(),
+ output.clone(),
+ WriteOptions {
+ write_buffer_size: context.engine_config.sst_write_buffer_size,
+ max_file_size: unit.max_file_size,
+ row_group_size: context.region_options.row_group_size(),
+ float_field_encoding: context.region_options.float_field_encoding,
+ },
+ )
+ .await?;
+ if let Some(uncommitted) = &self.uncommitted {
+ uncommitted.track(&infos);
+ }
+ result.files_to_add.extend(files);
+ result.sst_infos.extend(infos);
+ }
+ if self.cancel_handle.is_cancelled() {
+ return error::CompactionCancelledSnafu.fail();
+ }
+ Ok(result)
+ }
+}
+
#[async_trait::async_trait]
impl Compactor for DefaultCompactor
where
@@ -642,6 +782,7 @@ where
let internal_parallelism = compaction_region.max_parallelism.max(1);
let compaction_time_window = picker_output.time_window_size;
let region_id = compaction_region.region_id;
+ let context = CompactionContext::from(compaction_region);
// Build tasks along with their input file metas so we can track which
// inputs correspond to each task.
@@ -657,11 +798,11 @@ where
float_field_encoding: compaction_region.region_options.float_field_encoding,
};
let merger = self.merger.clone();
- let compaction_region = compaction_region.clone();
+ let context = context.clone();
let uncommitted = self.uncommitted.clone();
let fut = async move {
let result = merger
- .merge_single_output(compaction_region, output, write_opts)
+ .merge_single_output(context, output, write_opts)
.await;
if let (Some(uncommitted), Ok((_, infos))) = (&uncommitted, &result) {
uncommitted.track(infos);
@@ -777,28 +918,9 @@ where
compaction_region: &CompactionRegion,
merge_output: MergeOutput,
) -> Result<(RegionEdit, ManifestVersion)> {
- // Write region edit to manifest.
- let edit = RegionEdit {
- files_to_add: merge_output.files_to_add,
- files_to_remove: merge_output.files_to_remove,
- // Use current timestamp as the edit timestamp.
- timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
- compaction_time_window: merge_output
- .compaction_time_window
- .map(|seconds| Duration::from_secs(seconds as u64)),
- flushed_entry_id: None,
- flushed_sequence: None,
- committed_sequence: None,
- };
-
- let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
- // TODO: We might leak files if we fail to update manifest. We can add a cleanup task to remove them later.
- let manifest_version = compaction_region
- .manifest_ctx
- .update_manifest_for_compaction(action_list)
- .await?;
-
- Ok((edit, manifest_version))
+ CompactionContext::from(compaction_region)
+ .update_manifest(merge_output)
+ .await
}
}
@@ -812,14 +934,10 @@ mod tests {
use tokio::time::sleep;
use super::{DefaultCompactor, *};
- use crate::cache::CacheManager;
use crate::compaction::picker::PickerOutput;
use crate::error::Result;
use crate::sst::file::FileHandle;
use crate::sst::file_purger::NoopFilePurger;
- use crate::sst::version::SstVersion;
- use crate::test_util::memtable_util::metadata_for_test;
- use crate::test_util::scheduler_util::SchedulerEnv;
fn dummy_file_meta() -> FileMeta {
FileMeta {
@@ -963,7 +1081,8 @@ mod tests {
for preserve in [false, true] {
region.region_options.preserve_row_sequence = preserve;
for (inputs, known_bound, inputs_trusted) in &cases {
- let metadata = output_sequence_metadata(®ion, inputs);
+ let metadata =
+ output_sequence_metadata(&CompactionContext::from(®ion), inputs);
let trusted = preserve && *inputs_trusted;
assert_eq!(trusted, metadata.exact_sequence_trusted);
assert_eq!(
@@ -1149,34 +1268,6 @@ mod tests {
}
}
- /// Build a minimal [`CompactionRegion`] suitable for tests where the
- /// [`SstMerger`] is mocked and never touches the access layer.
- async fn new_test_compaction_region() -> CompactionRegion {
- let env = SchedulerEnv::new().await;
- let metadata = metadata_for_test();
- let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
- CompactionRegion {
- region_id: RegionId::new(1, 1),
- region_options: RegionOptions::default(),
- engine_config: Arc::new(MitoConfig::default()),
- region_metadata: metadata.clone(),
- cache_manager: Arc::new(CacheManager::default()),
- access_layer: env.access_layer.clone(),
- manifest_ctx,
- current_version: CompactionVersion {
- metadata: metadata.clone(),
- options: RegionOptions::default(),
- ssts: Arc::new(SstVersion::new(metadata)),
- memtable_min_sequence: None,
- compaction_time_window: None,
- },
- file_purger: None,
- ttl: None,
- max_parallelism: 1,
- plugins: Plugins::new(),
- }
- }
-
/// An [`SstMerger`] that returns pre-configured results per call index.
///
/// Call 0 gets `results[0]`, call 1 gets `results[1]`, etc.
@@ -1199,7 +1290,7 @@ mod tests {
impl SstMerger for MockMerger {
async fn merge_single_output(
&self,
- _compaction_region: CompactionRegion,
+ _context: CompactionContext,
_output: CompactionOutput,
_write_opts: WriteOptions,
) -> Result<(Vec, Vec)> {
@@ -1215,6 +1306,56 @@ mod tests {
}
}
+ #[tokio::test]
+ async fn test_compaction_unit_group_failure_is_atomic() {
+ let context = CompactionContext::from(&new_test_compaction_region().await);
+ let input = new_file_handle(dummy_file_meta());
+ let unit = CompactionUnit::from_picker(PickerOutput {
+ outputs: (0..2)
+ .map(|_| CompactionOutput {
+ output_level: 1,
+ inputs: vec![input.clone()],
+ filter_deleted: false,
+ output_time_range: None,
+ })
+ .collect(),
+ ..Default::default()
+ })
+ .unwrap()
+ .pop()
+ .unwrap();
+ for fail_second in [false, true] {
+ let second = if fail_second {
+ error::InvalidMetaSnafu {
+ reason: "second output failed",
+ }
+ .fail()
+ } else {
+ Ok(vec![dummy_file_meta(), dummy_file_meta()])
+ };
+ let compactor =
+ DefaultCompactor::with_merger(MockMerger::new(vec![Ok(Vec::new()), second]));
+ let result = compactor.merge_unit(&context, &unit).await;
+ if fail_second {
+ assert!(
+ result.is_err(),
+ "cannot publish a successful subset of a dependency group"
+ );
+ } else {
+ let result = result.unwrap();
+ assert_eq!(2, result.files_to_add.len());
+ assert_eq!(
+ vec![input.meta_ref().file_id],
+ result
+ .files_to_remove
+ .iter()
+ .map(|f| f.file_id)
+ .collect::>()
+ );
+ }
+ }
+ }
+
#[tokio::test]
async fn test_partial_merge_failure_collects_only_successful_outputs() {
common_telemetry::init_default_ut_logging();
@@ -1371,7 +1512,7 @@ mod tests {
impl SstMerger for BlockingMerger {
async fn merge_single_output(
&self,
- _compaction_region: CompactionRegion,
+ _context: CompactionContext,
_output: CompactionOutput,
_write_opts: WriteOptions,
) -> Result<(Vec, Vec)> {
diff --git a/src/mito2/src/compaction/picker.rs b/src/mito2/src/compaction/picker.rs
index ec9fa91f4c8..3628953cc31 100644
--- a/src/mito2/src/compaction/picker.rs
+++ b/src/mito2/src/compaction/picker.rs
@@ -31,11 +31,6 @@ use crate::sst::file::{FileHandle, FileMeta};
use crate::sst::file_purger::FilePurger;
use crate::sst::version::LevelMeta;
-#[async_trait::async_trait]
-pub(crate) trait CompactionTask: Debug + Send + Sync + 'static {
- async fn run(&mut self);
-}
-
/// Picker picks input SST files for compaction.
/// Different compaction strategy may implement different pickers.
#[async_trait::async_trait]
diff --git a/src/mito2/src/compaction/scheduler.rs b/src/mito2/src/compaction/scheduler.rs
index e9da3002254..76459abf3bc 100644
--- a/src/mito2/src/compaction/scheduler.rs
+++ b/src/mito2/src/compaction/scheduler.rs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+mod local;
mod planning;
mod state;
@@ -324,28 +325,6 @@ impl CompactionScheduler {
.await
}
- /// Completes a cooperatively canceled execution.
- ///
- /// # Effects
- ///
- /// Resets the matching region to idle, notifies compaction waiters, and
- /// returns DDLs that were waiting for cancellation.
- ///
- /// # Constraints
- ///
- /// The owning worker must execute every returned DDL request. A stale
- /// cancellation is ignored.
- pub(crate) async fn on_execution_cancelled(
- &mut self,
- region_id: RegionId,
- execution: &CompactionExecution,
- ) -> Vec {
- if !self.is_current_execution(region_id, execution) {
- return Vec::new();
- }
- self.on_compaction_cancelled(region_id).await
- }
-
/// Records failure of the installed execution.
///
/// # Effects
@@ -684,6 +663,7 @@ impl CompactionScheduler {
}
/// Notifies the scheduler that the compaction job is cancelled cooperatively.
+ #[cfg(test)]
async fn on_compaction_cancelled(&mut self, region_id: RegionId) -> Vec {
self.finish_compaction_on_cancel(region_id)
}
diff --git a/src/mito2/src/compaction/scheduler/local.rs b/src/mito2/src/compaction/scheduler/local.rs
new file mode 100644
index 00000000000..8ff2a6cb150
--- /dev/null
+++ b/src/mito2/src/compaction/scheduler/local.rs
@@ -0,0 +1,341 @@
+// Copyright 2023 Greptime Team
+//
+// Licensed 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.
+
+use std::collections::{HashMap, VecDeque};
+use std::sync::Arc;
+
+use common_meta::key::SchemaMetadataManagerRef;
+use store_api::storage::RegionId;
+
+use crate::compaction::compactor::{CompactionContext, CompactionRegion};
+use crate::compaction::scheduler::state::{CompactingFiles, CompactionExecution, CompactionPhase};
+use crate::compaction::scheduler::{CompactionScheduler, CompactionTransition};
+use crate::compaction::task::CompactionTask;
+use crate::compaction::unit::CompactionUnit;
+use crate::error::{self, Error};
+use crate::metrics::INFLIGHT_COMPACTION_COUNT;
+use crate::region::ManifestContextRef;
+use crate::request::{
+ BackgroundNotify, CompactionUnitNotification, WorkerRequest, WorkerRequestWithTime,
+};
+use crate::schedule::{CancellableTaskState, RequestCancelResult};
+use crate::sst::file::UncommittedSsts;
+
+/// Tracks one unit through its terminal result and subsequent task exit.
+#[derive(Debug)]
+struct UnitExecution {
+ state: CancellableTaskState,
+ finished: bool,
+}
+
+/// A request's local units. All transitions run on the owning region worker.
+#[derive(Debug)]
+pub(crate) struct LocalCompaction {
+ /// All pending and submitted units, retained until task exit. Tasks own their input leases.
+ /// Keyed by each unit's execution plan ID, allocated from the scheduler's `next_plan_id`;
+ /// it equals `CompactionExecution::plan_id()` and the `plan_id` in unit notifications.
+ units: HashMap,
+ pending: VecDeque,
+ parallelism: usize,
+ error: Option>,
+ made_progress: bool,
+}
+
+impl LocalCompaction {
+ /// Requests cancellation for all units that have not started committing.
+ pub(crate) fn request_cancel(&self) -> RequestCancelResult {
+ let mut result = RequestCancelResult::TooLateToCancel;
+ for unit in self.units.values() {
+ match unit.state.request_cancel() {
+ RequestCancelResult::CancelIssued => result = RequestCancelResult::CancelIssued,
+ RequestCancelResult::AlreadyCancelling
+ if result != RequestCancelResult::CancelIssued =>
+ {
+ result = RequestCancelResult::AlreadyCancelling;
+ }
+ _ => {}
+ }
+ }
+ result
+ }
+
+ /// Counts submitted units that have not exited, including queued jobs.
+ fn num_running(&self) -> usize {
+ self.units.len() - self.pending.len()
+ }
+
+ /// Checks this attempt's phase without depending on any sibling's apply status.
+ fn can_apply(&self, id: u64) -> bool {
+ self.units
+ .get(&id)
+ .is_some_and(|unit| !unit.finished && unit.state.commit_started())
+ }
+
+ /// Records a terminal result once, but still awaits task exit.
+ fn finish(&mut self, id: u64, result: std::result::Result>) {
+ let Some(unit) = self.units.get_mut(&id) else {
+ return;
+ };
+ if unit.finished {
+ return;
+ }
+ unit.finished = true;
+ match result {
+ Ok(progress) => self.made_progress |= progress,
+ Err(err) => {
+ // A real failure must not be hidden by a sibling's cooperative cancellation.
+ if self.error.as_ref().is_none_or(|current| {
+ matches!(current.as_ref(), Error::CompactionCancelled { .. })
+ }) {
+ self.error = Some(err);
+ }
+ }
+ }
+ }
+}
+
+impl Drop for LocalCompaction {
+ /// Stops cancellable work when its region or compaction cycle is removed.
+ fn drop(&mut self) {
+ self.request_cancel();
+ }
+}
+
+impl CompactionScheduler {
+ /// Builds unit tasks with fresh execution IDs and shares their already-reserved inputs.
+ pub(crate) fn prepare_local_units(
+ &mut self,
+ region: CompactionRegion,
+ units: Vec,
+ reserved: CompactingFiles,
+ ) -> CompactionPhase {
+ let mut local = LocalCompaction {
+ units: HashMap::new(),
+ pending: VecDeque::new(),
+ parallelism: region.max_parallelism.max(1),
+ error: None,
+ made_progress: false,
+ };
+ let context = CompactionContext::from(®ion);
+ // Planning is complete; unit tasks must not pin the other units' inputs.
+ drop(region);
+ for unit in units {
+ let files = reserved.for_inputs(&unit.inputs);
+ let id = Self::next_plan_id(&mut self.next_plan_id);
+ let execution = CompactionExecution::new(id, files);
+ let state = CancellableTaskState::new();
+ local.units.insert(
+ id,
+ UnitExecution {
+ state: state.clone(),
+ finished: false,
+ },
+ );
+ local.pending.push_back(CompactionTask {
+ state,
+ execution,
+ compaction_context: context.clone(),
+ request_sender: self.request_sender.clone(),
+ listener: self.listener.clone(),
+ estimated_memory_bytes: unit.estimated_memory_bytes(),
+ unit,
+ memory_manager: self.memory_manager.clone(),
+ memory_policy: self.memory_policy,
+ uncommitted: UncommittedSsts::new(
+ context.region_id,
+ context.access_layer.clone(),
+ Some(context.cache_manager.clone()),
+ ),
+ });
+ }
+ CompactionPhase::Units(local)
+ }
+
+ /// Returns local unit state only while the region is in a local execution phase.
+ fn local_units_mut(&mut self, region_id: RegionId) -> Option<&mut LocalCompaction> {
+ match &mut self.region_status.get_mut(®ion_id)?.active.phase {
+ CompactionPhase::Units(local) => Some(local),
+ _ => None,
+ }
+ }
+
+ /// Submits pending units up to the request limit and arranges task-exit notifications.
+ pub(crate) fn dispatch_local_units(&mut self, region_id: RegionId) {
+ loop {
+ let Some(local) = self.local_units_mut(region_id) else {
+ return;
+ };
+ if local.num_running() >= local.parallelism {
+ return;
+ }
+ let Some(mut task) = local.pending.pop_front() else {
+ return;
+ };
+ let id = task.execution.plan_id();
+ let sender = self.request_sender.clone();
+ let completion = Box::pin(async move {
+ let _ = sender
+ .send(WorkerRequestWithTime::new(WorkerRequest::Background {
+ region_id,
+ notify: BackgroundNotify::CompactionUnit(
+ CompactionUnitNotification::Released { plan_id: id },
+ ),
+ }))
+ .await;
+ });
+ let job = Box::pin(async move {
+ INFLIGHT_COMPACTION_COUNT.inc();
+ task.run().await;
+ drop(task);
+ INFLIGHT_COMPACTION_COUNT.dec();
+ });
+ if let Err(err) = self.scheduler.schedule_with_completion(job, completion)
+ && let Some(local) = self.local_units_mut(region_id)
+ {
+ local.finish(id, Err(Arc::new(err)));
+ local.units.remove(&id);
+ }
+ }
+ }
+
+ /// Accepts an edit from any current unit that has started committing and is not finished.
+ pub(crate) fn can_apply_unit(&self, region_id: RegionId, id: u64) -> bool {
+ self.region_status.get(®ion_id).is_some_and(|status| {
+ matches!(&status.active.phase, CompactionPhase::Units(local)
+ if local.can_apply(id))
+ })
+ }
+
+ /// Records apply progress or failure without ending sibling executions.
+ pub(crate) fn on_unit_finished(
+ &mut self,
+ region_id: RegionId,
+ id: u64,
+ result: std::result::Result>,
+ ) {
+ if let Some(local) = self.local_units_mut(region_id) {
+ local.finish(id, result);
+ }
+ }
+
+ /// Accounts for task exit, starts pending units, and checks request completion.
+ pub(crate) async fn on_unit_released(
+ &mut self,
+ region_id: RegionId,
+ id: u64,
+ manifest: &ManifestContextRef,
+ schemas: SchemaMetadataManagerRef,
+ ) -> CompactionTransition {
+ let Some(local) = self.local_units_mut(region_id) else {
+ return CompactionTransition::NoAction;
+ };
+ if !local.units.contains_key(&id) {
+ return CompactionTransition::NoAction;
+ }
+ local.finish(
+ id,
+ Err(Arc::new(
+ error::UnexpectedSnafu {
+ reason: "Compaction unit exited without a terminal notification",
+ }
+ .build(),
+ )),
+ );
+ local.units.remove(&id);
+ self.dispatch_local_units(region_id);
+ self.finish_local_units(region_id, manifest, schemas).await
+ }
+
+ /// Completes the request only after all units exit, using the existing terminal paths.
+ pub(crate) async fn finish_local_units(
+ &mut self,
+ region_id: RegionId,
+ manifest: &ManifestContextRef,
+ schemas: SchemaMetadataManagerRef,
+ ) -> CompactionTransition {
+ let Some(local) = self.local_units_mut(region_id) else {
+ return CompactionTransition::NoAction;
+ };
+ if !local.units.is_empty() {
+ return CompactionTransition::NoAction;
+ }
+ if let Some(err) = local.error.take() {
+ if matches!(err.as_ref(), Error::CompactionCancelled { .. }) {
+ return CompactionTransition::from_pending_ddls(
+ self.finish_compaction_on_cancel(region_id),
+ );
+ }
+ self.on_compaction_failed(region_id, err);
+ return CompactionTransition::NoAction;
+ }
+ let progress = local.made_progress;
+ if let Some(status) = self.region_status.get_mut(®ion_id) {
+ for waiter in status.take_waiters() {
+ waiter.send(Ok(0));
+ }
+ }
+ self.on_compaction_finished(region_id, manifest, schemas, progress)
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_compaction_unit_apply_accepts_independent_executions() {
+ let units = [1, 2]
+ .into_iter()
+ .map(|id| {
+ (
+ id,
+ UnitExecution {
+ state: CancellableTaskState::new(),
+ finished: false,
+ },
+ )
+ })
+ .collect();
+ let mut local = LocalCompaction {
+ units,
+ pending: VecDeque::new(),
+ parallelism: 2,
+ error: None,
+ made_progress: false,
+ };
+ assert!(!local.can_apply(1));
+ assert!(!local.can_apply(2));
+ assert!(!local.can_apply(99));
+ for unit in local.units.values() {
+ assert!(unit.state.mark_commit_started());
+ }
+ assert!(local.can_apply(1) && local.can_apply(2));
+ local.finish(2, Ok(true));
+ assert!(local.can_apply(1));
+ assert!(!local.can_apply(2));
+ local.finish(2, Err(Arc::new(error::CompactionCancelledSnafu.build())));
+ local.finish(99, Err(Arc::new(error::CompactionCancelledSnafu.build())));
+ assert!(local.can_apply(1));
+ assert!(local.error.is_none());
+ assert!(local.made_progress);
+ local.finish(1, Ok(false));
+ assert!(!local.can_apply(1));
+ assert_eq!(
+ 2,
+ local.units.len(),
+ "terminal units remain tracked until task exit"
+ );
+ }
+}
diff --git a/src/mito2/src/compaction/scheduler/planning.rs b/src/mito2/src/compaction/scheduler/planning.rs
index 5553d6435da..d427044b955 100644
--- a/src/mito2/src/compaction/scheduler/planning.rs
+++ b/src/mito2/src/compaction/scheduler/planning.rs
@@ -14,7 +14,7 @@
use std::fmt;
use std::future::Future;
-use std::sync::{Arc, Mutex};
+use std::sync::Arc;
use std::time::Instant;
use api::v1::region::compact_request;
@@ -30,12 +30,12 @@ use tokio::sync::mpsc::{self, Sender};
use crate::access_layer::AccessLayerRef;
use crate::cache::CacheManagerRef;
-use crate::compaction::compactor::{CompactionRegion, CompactionVersion, DefaultCompactor};
-use crate::compaction::picker::{CompactionTask, PickerOutput, new_picker};
+use crate::compaction::compactor::{CompactionRegion, CompactionVersion};
+use crate::compaction::picker::{PickerOutput, new_picker};
use crate::compaction::scheduler::state::{CompactingFiles, CompactionExecution, CompactionPhase};
use crate::compaction::scheduler::{CompactionScheduler, CompactionTransition};
-use crate::compaction::task::CompactionTaskImpl;
-use crate::compaction::{CompactionOutput, find_dynamic_options};
+use crate::compaction::unit::CompactionUnit;
+use crate::compaction::{CompactionOutput, find_dynamic_options, format_panic_reason};
use crate::config::MitoConfig;
use crate::error::{CompactRegionSnafu, Error, RemoteCompactionSnafu, Result, UnexpectedSnafu};
use crate::metrics::{
@@ -44,11 +44,10 @@ use crate::metrics::{
use crate::region::ManifestContextRef;
use crate::region::options::RegionOptions;
use crate::request::{BackgroundNotify, OutputTx, WorkerRequest, WorkerRequestWithTime};
-use crate::schedule::CancellableTaskState;
use crate::schedule::remote_job_scheduler::{
CompactionJob, DefaultNotifier, RemoteJob, RemoteJobSchedulerRef,
};
-use crate::sst::file::{FileHandle, UncommittedSsts};
+use crate::sst::file::FileHandle;
use crate::sst::version::SstVersion;
use crate::worker::WorkerListener;
@@ -149,13 +148,7 @@ impl CompactionScheduler {
) {
// The idiomatic way to handle a panic result.
let result = std::panic::AssertUnwindSafe(planning).catch_unwind().await.unwrap_or_else(|payload| {
- let reason = if let Some(message) = payload.as_ref().downcast_ref::<&str>() {
- message.to_string()
- } else if let Some(message) = payload.as_ref().downcast_ref::() {
- message.clone()
- } else {
- "unknown panic".to_string()
- };
+ let reason = format_panic_reason(payload.as_ref());
CompactionPlanningResult::Error(Arc::new(
UnexpectedSnafu {
reason: format!(
@@ -333,16 +326,18 @@ impl CompactionScheduler {
};
let waiters = status.take_waiters();
match self
- .submit_prepared_compaction(prepared, files, waiters, plan_id)
+ .prepare_or_submit_compaction(prepared, files, waiters, plan_id)
.await
{
Ok(Some(phase)) => {
if let Some(status) = self.region_status.get_mut(®ion_id) {
status.set_phase(phase);
}
+ self.dispatch_local_units(region_id);
// The execution now owns the terminal transition. Any
// fenced DDLs remain queued until its callback arrives.
- CompactionTransition::NoAction
+ self.finish_local_units(region_id, manifest_ctx, schema_metadata_manager)
+ .await
}
Ok(None) => {
self.finish_compaction_planning(
@@ -427,12 +422,14 @@ impl CompactionScheduler {
CompactionTransition::NoAction
}
- async fn submit_prepared_compaction(
+ /// Submits a remote job, or prepares local unit tasks without submitting them.
+ /// The caller dispatches local units after installing the returned phase.
+ async fn prepare_or_submit_compaction(
&mut self,
prepared: PreparedCompaction,
files: CompactingFiles,
waiters: Vec,
- mut plan_id: u64,
+ plan_id: u64,
) -> Result