diff --git a/config/config.md b/config/config.md
index 6544a28f93d..e380f075548 100644
--- a/config/config.md
+++ b/config/config.md
@@ -177,6 +177,7 @@
| `region_engine.mito.manifest_checkpoint_distance` | Integer | `10` | Number of meta action updated to trigger a new checkpoint for the manifest. |
| `region_engine.mito.compress_manifest` | Bool | `false` | Whether to compress manifest and checkpoint file by gzip (default false). |
| `region_engine.mito.experimental_enable_series_index` | Bool | `false` | Under development; do not enable. Whether to enable series indexes. Indexes are stored on the local filesystem under `{data_home}/series_index`. |
+| `region_engine.mito.experimental_series_index_max_size` | String | `5GiB` | Approximate series and range index size limit in open regions, shared across workers. Workers periodically refresh usage and skip maintenance when full. In-flight reconciliation can exceed the limit. Closed-region files, temporary output, catalogs, and old snapshots retained by readers are not counted. Minimum: 1KiB. Takes effect on restart. |
| `region_engine.mito.experimental_enable_range_index` | Bool | `false` | Whether to build and query range indexes when series indexes are enabled. Obsolete range-index metadata and files are still cleaned up when disabled. |
| `region_engine.mito.experimental_series_index_maintenance_interval` | String | `5m` | Interval between series-index maintenance runs. Zero uses the default of 5 min. |
| `region_engine.mito.experimental_series_index_bucket_width` | String | `5days` | Requested minimum series-index bucket width (default: 5 days), rounded up to an exact multiple of each region's compaction time window. |
@@ -639,6 +640,7 @@
| `region_engine.mito.experimental_manifest_keep_removed_file_ttl` | String | `1h` | How long to keep removed files in the `removed_files` field of manifest after they are removed from manifest. files will only be removed from `removed_files` field if both `keep_removed_file_count` and `keep_removed_file_ttl` is reached. |
| `region_engine.mito.compress_manifest` | Bool | `false` | Whether to compress manifest and checkpoint file by gzip (default false). |
| `region_engine.mito.experimental_enable_series_index` | Bool | `false` | Under development; do not enable. Whether to enable series indexes. Indexes are stored on the local filesystem under `{data_home}/series_index`. |
+| `region_engine.mito.experimental_series_index_max_size` | String | `5GiB` | Approximate series and range index size limit in open regions, shared across workers. Workers periodically refresh usage and skip maintenance when full. In-flight reconciliation can exceed the limit. Closed-region files, temporary output, catalogs, and old snapshots retained by readers are not counted. Minimum: 1KiB. Takes effect on restart. |
| `region_engine.mito.experimental_enable_range_index` | Bool | `false` | Whether to build and query range indexes when series indexes are enabled. Obsolete range-index metadata and files are still cleaned up when disabled. |
| `region_engine.mito.experimental_series_index_maintenance_interval` | String | `5m` | Interval between series-index maintenance runs. Zero uses the default of 5 min. |
| `region_engine.mito.experimental_series_index_bucket_width` | String | `5days` | Requested minimum series-index bucket width (default: 5 days), rounded up to an exact multiple of each region's compaction time window. |
diff --git a/config/datanode.example.toml b/config/datanode.example.toml
index 45baaefbdfb..da2d353fef8 100644
--- a/config/datanode.example.toml
+++ b/config/datanode.example.toml
@@ -502,6 +502,13 @@ compress_manifest = false
## Indexes are stored on the local filesystem under `{data_home}/series_index`.
#+ experimental_enable_series_index = false
+## Approximate series and range index size limit in open regions, shared across workers.
+## Workers periodically refresh usage and skip maintenance when full. In-flight reconciliation
+## can exceed the limit. Closed-region files, temporary output, catalogs, and old snapshots
+## retained by readers are not counted.
+## Minimum: 1KiB. Takes effect on restart.
+#+ experimental_series_index_max_size = "5GiB"
+
## Whether to build and query range indexes when series indexes are enabled.
## Obsolete range-index metadata and files are still cleaned up when disabled.
#+ experimental_enable_range_index = false
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index ef9dd561f6a..e0a8deb9c22 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -707,6 +707,13 @@ compress_manifest = false
## Indexes are stored on the local filesystem under `{data_home}/series_index`.
#+ experimental_enable_series_index = false
+## Approximate series and range index size limit in open regions, shared across workers.
+## Workers periodically refresh usage and skip maintenance when full. In-flight reconciliation
+## can exceed the limit. Closed-region files, temporary output, catalogs, and old snapshots
+## retained by readers are not counted.
+## Minimum: 1KiB. Takes effect on restart.
+#+ experimental_series_index_max_size = "5GiB"
+
## Whether to build and query range indexes when series indexes are enabled.
## Obsolete range-index metadata and files are still cleaned up when disabled.
#+ experimental_enable_range_index = false
diff --git a/src/cmd/tests/load_config_test.rs b/src/cmd/tests/load_config_test.rs
index 34c5bc8c9ba..d4b19812df2 100644
--- a/src/cmd/tests/load_config_test.rs
+++ b/src/cmd/tests/load_config_test.rs
@@ -159,6 +159,7 @@ fn test_load_datanode_example_config() {
},
region_engine: vec![
RegionEngineConfig::Mito(MitoConfig {
+ experimental_series_index_max_size: ReadableSize::gb(5),
auto_flush_interval: Duration::from_secs(10 * 60),
default_region_write_buffer_size: ReadableSize::mb(0),
write_cache_ttl: Some(Duration::from_secs(60 * 60 * 8)),
@@ -397,6 +398,7 @@ fn test_load_standalone_example_config() {
}),
region_engine: vec![
RegionEngineConfig::Mito(MitoConfig {
+ experimental_series_index_max_size: ReadableSize::gb(5),
auto_flush_interval: Duration::from_secs(10 * 60),
default_region_write_buffer_size: ReadableSize::mb(0),
write_cache_ttl: Some(Duration::from_secs(60 * 60 * 8)),
diff --git a/src/mito2/AGENTS.md b/src/mito2/AGENTS.md
index 583ec31bd4a..b8fd2f8892c 100644
--- a/src/mito2/AGENTS.md
+++ b/src/mito2/AGENTS.md
@@ -26,7 +26,7 @@ snapshot isolation). It implements the `RegionEngine` trait from `store-api`.
| `compaction` | `src/mito2/src/compaction/` | Compaction scheduler (`scheduler.rs` + `scheduler/`), TWCS picker, strict-window manual picker, compactor, memory control |
| `access_layer` | `src/mito2/src/access_layer.rs` | SST read/write over the object store |
| `sst` | `src/mito2/src/sst/` | Parquet format, file metadata, index layout |
-| `series_index` | `src/mito2/src/series_index/` | Series index writer/searcher, bucket planning, catalogs, worker-owned maintenance and purge tasks, and index lifecycle |
+| `series_index` | `src/mito2/src/series_index/` | Series index writer/searcher, bucket planning, catalogs, worker-owned reconciliation (`maintenance.rs`), shared approximate usage (`task.rs`), catalogs, and index lifecycle |
| `read` | `src/mito2/src/read/` | `ScanRegion`, merge, dedup, projection, streaming |
| `manifest` | `src/mito2/src/manifest/` | `RegionManifestManager`, manifest actions/edits |
| `cache` | `src/mito2/src/cache.rs` | Write/file/page caches |
@@ -62,6 +62,10 @@ filtered `RecordBatch` stream.
- **Manifest format** (`manifest/action.rs`): affects crash recovery and
follower replay. Keep it backward compatible.
- **SST/Parquet layout** (`sst/`): readers must stay compatible with existing files.
+- **Series-index disk limit**: tasks estimate usage from entry sizes in open-region snapshots
+ and defer new builds when the shared estimate is full. Cleanup still prunes obsolete
+ range entries and expires series indexes to reclaim published usage. Concurrent builds may
+ overshoot; closed-region files are not counted. Series handles and the SST purger own file deletion.
- **Series-index coverage** (`series_index/catalog.rs`): `SeriesIndexEntry` stores
compaction-window width and SST summaries keyed by aligned start in both catalogs and Parquet footers.
- **Request types** (`request.rs`): usually tied to proto definitions consumed by `datanode`.
diff --git a/src/mito2/src/config.rs b/src/mito2/src/config.rs
index 5164e2fd72b..035feb75a15 100644
--- a/src/mito2/src/config.rs
+++ b/src/mito2/src/config.rs
@@ -92,6 +92,10 @@ pub struct MitoConfig {
/// Under development; do not enable. Whether to enable series indexes (default false).
/// Indexes are stored on the local filesystem under `{data_home}/series_index`.
pub experimental_enable_series_index: bool,
+ /// Approximate series and range index size limit in open regions (default: 5 GiB).
+ /// Workers share a periodically refreshed estimate and skip maintenance when full.
+ /// In-flight reconciliation can exceed the limit; closed-region files are not counted.
+ pub experimental_series_index_max_size: ReadableSize,
/// Whether to build and query range indexes when series indexes are enabled (default false).
/// Obsolete range-index metadata and files are still cleaned up when disabled.
pub experimental_enable_range_index: bool,
@@ -221,6 +225,7 @@ impl Default for MitoConfig {
compress_manifest: false,
max_background_index_builds: divide_num_cpus(8),
experimental_enable_series_index: false,
+ experimental_series_index_max_size: ReadableSize::gb(5),
experimental_enable_range_index: false,
experimental_series_index_maintenance_interval:
DEFAULT_SERIES_INDEX_MAINTENANCE_INTERVAL,
@@ -280,6 +285,14 @@ impl MitoConfig {
///
/// Returns an error if there is a configuration that unable to sanitize.
pub fn sanitize(&mut self, data_home: &str) -> Result<()> {
+ if self.experimental_enable_series_index {
+ snafu::ensure!(
+ self.experimental_series_index_max_size.as_bytes() >= 1024,
+ crate::error::InvalidConfigSnafu {
+ reason: "experimental_series_index_max_size must be at least 1KiB"
+ }
+ );
+ }
// Use default value if `num_workers` is 0.
if self.num_workers == 0 {
self.num_workers = divide_num_cpus(2);
@@ -442,12 +455,17 @@ mod tests {
let mut config: MitoConfig = toml::from_str(
"experimental_enable_series_index = true
experimental_enable_range_index = false
+ experimental_series_index_max_size = '64MiB'
experimental_series_index_maintenance_interval = '30s'
experimental_series_index_bucket_width = '2days'",
)
.unwrap();
config.sanitize("/data").unwrap();
assert!(config.experimental_enable_series_index);
+ assert_eq!(
+ ReadableSize::mb(64),
+ config.experimental_series_index_max_size
+ );
assert!(!config.experimental_enable_range_index);
assert_eq!(
config.experimental_series_index_maintenance_interval,
@@ -459,6 +477,10 @@ mod tests {
);
let restored: MitoConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
assert_eq!(config, restored);
+ config.experimental_series_index_max_size = ReadableSize(1023);
+ assert!(config.sanitize("/data").is_err());
+ config.experimental_series_index_max_size = ReadableSize(1024);
+ config.sanitize("/data").unwrap();
let mut config: MitoConfig =
toml::from_str("experimental_series_index_maintenance_interval = '0s'").unwrap();
diff --git a/src/mito2/src/engine/index_build_test.rs b/src/mito2/src/engine/index_build_test.rs
index ce950f8a246..d2d59b038ff 100644
--- a/src/mito2/src/engine/index_build_test.rs
+++ b/src/mito2/src/engine/index_build_test.rs
@@ -1512,46 +1512,6 @@ async fn test_index_build_type_manual_consistency() {
assert_listener_counts(&listener, 2, 2);
}
-#[tokio::test]
-async fn test_gate_index_build_listener_smoke() {
- use store_api::storage::{FileId, RegionId};
-
- use crate::engine::listener::{EventListener, GateIndexBuildListener};
- use crate::sst::file::RegionFileId;
-
- let gate = Arc::new(GateIndexBuildListener::default());
-
- // Initial counts are zero.
- assert_eq!(gate.begin_count(), 0);
- assert_eq!(gate.finish_count(), 0);
- assert_eq!(gate.abort_count(), 0);
-
- // Spawn a task that will block in on_index_build_begin.
- let gate_clone = gate.clone();
- let handle = tokio::spawn(async move {
- gate_clone
- .on_index_build_begin(RegionFileId::new(RegionId::new(1, 1), FileId::random()))
- .await;
- });
-
- // Wait for begin to arrive.
- tokio::time::timeout(std::time::Duration::from_secs(5), gate.wait_begin(1))
- .await
- .unwrap();
- assert_eq!(gate.begin_count(), 1);
- assert_eq!(gate.finish_count(), 0);
- assert_eq!(gate.abort_count(), 0);
-
- // Release the blocked begin.
- gate.release_begin();
-
- // The spawned task should now complete.
- tokio::time::timeout(std::time::Duration::from_secs(5), handle)
- .await
- .unwrap()
- .unwrap();
-}
-
#[tokio::test]
async fn test_index_build_type_manual_duplicate_in_flight() {
let mut env = TestEnv::with_prefix("test_index_build_type_manual_duplicate_in_flight_").await;
diff --git a/src/mito2/src/engine/listener.rs b/src/mito2/src/engine/listener.rs
index 6ef4a293a2a..432e3367603 100644
--- a/src/mito2/src/engine/listener.rs
+++ b/src/mito2/src/engine/listener.rs
@@ -583,12 +583,12 @@ impl EventListener for GateIndexBuildListener {
info!("Region {} index build begin (gated)", region_file_id);
self.begin_count.fetch_add(1, Ordering::Relaxed);
self.begin_notify.notify_one();
- // Block until the test releases the gate.
- let _permit = self
- .begin_blocker
+ // Consume the release so subsequent builds remain blocked.
+ self.begin_blocker
.acquire()
.await
- .expect("gate semaphore should not be closed");
+ .expect("gate semaphore should not be closed")
+ .forget();
}
async fn on_index_build_finish(&self, region_file_id: RegionFileId) {
diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs
index bf089737f35..e10968d3a32 100644
--- a/src/mito2/src/engine/scan_test.rs
+++ b/src/mito2/src/engine/scan_test.rs
@@ -1340,6 +1340,7 @@ async fn check_two_phase_series_scan(
let store = region.series_index_store.clone().unwrap();
let sequence = region.flushed_sequence();
let entry = SeriesIndexEntry {
+ file_size: 0,
index_uuid: FileId::random(),
bucket_start: Timestamp::new_millisecond(0),
bucket_end: Timestamp::new_millisecond(2001),
@@ -1412,7 +1413,13 @@ async fn check_two_phase_series_scan(
.unwrap();
writer.write(0, &batch).await.unwrap();
writer.finish().await.unwrap();
- index_version.range_indexes.insert(file_id);
+ index_version.range_indexes.insert(
+ file_id,
+ crate::series_index::RangeIndexEntry {
+ file_id,
+ file_size: 0,
+ },
+ );
}
region
.series_index_version_control
@@ -1607,7 +1614,7 @@ async fn check_two_phase_series_scan(
// succeed even when that file is unavailable.
let region = engine.find_region(region_id).unwrap();
let version = region.series_index_version_control.current();
- let file_id = *version.range_indexes.iter().next().unwrap();
+ let file_id = *version.range_indexes.keys().next().unwrap();
region
.series_index_store
.as_ref()
diff --git a/src/mito2/src/metrics.rs b/src/mito2/src/metrics.rs
index 49e91d93efd..4c275c248c9 100644
--- a/src/mito2/src/metrics.rs
+++ b/src/mito2/src/metrics.rs
@@ -318,6 +318,14 @@ lazy_static! {
// Index metrics.
lazy_static! {
+ /// Approximate published index bytes in open regions, refreshed by maintenance.
+ pub static ref SERIES_INDEX_DISK_BYTES: IntGauge = register_int_gauge!(
+ "greptime_mito_series_index_disk_bytes", "estimated series and range index bytes in open regions"
+ ).unwrap();
+ /// Maintenance passes deferred by the estimated disk usage.
+ pub static ref SERIES_INDEX_CAPACITY_DEFERRED: IntCounter = register_int_counter!(
+ "greptime_mito_series_index_capacity_deferred_total", "series-index capacity deferrals"
+ ).unwrap();
// Index metrics.
/// Outcomes of series-index reconciliation passes.
pub static ref SERIES_INDEX_RECONCILE_TOTAL: IntCounterVec = register_int_counter_vec!(
diff --git a/src/mito2/src/read/series_candidate.rs b/src/mito2/src/read/series_candidate.rs
index f05f54e6662..3ba5512a7bd 100644
--- a/src/mito2/src/read/series_candidate.rs
+++ b/src/mito2/src/read/series_candidate.rs
@@ -778,6 +778,7 @@ mod tests {
));
let store = env.access_layer.object_store().clone();
let entry = SeriesIndexEntry {
+ file_size: 0,
index_uuid: FileId::random(),
bucket_start: Timestamp::new_millisecond(0),
bucket_end: Timestamp::new_millisecond(20),
diff --git a/src/mito2/src/series_index.rs b/src/mito2/src/series_index.rs
index 4a4133e1a9b..3aa70256709 100644
--- a/src/mito2/src/series_index.rs
+++ b/src/mito2/src/series_index.rs
@@ -39,7 +39,7 @@ use store_api::metric_engine_consts::{
use crate::error::Result;
#[cfg(test)]
-pub(crate) use crate::series_index::catalog::SeriesIndexEntry;
+pub(crate) use crate::series_index::catalog::{RangeIndexEntry, SeriesIndexEntry};
pub(crate) use crate::series_index::catalog::{
delete_catalogs, load_version_control, series_index_path,
};
diff --git a/src/mito2/src/series_index/bucket.rs b/src/mito2/src/series_index/bucket.rs
index 7fecb253441..446948838f6 100644
--- a/src/mito2/src/series_index/bucket.rs
+++ b/src/mito2/src/series_index/bucket.rs
@@ -334,6 +334,7 @@ impl SeriesBucket {
.filter_map(|file| file.meta_ref().sequence.map(|sequence| sequence.get()))
.min()?;
Some(SeriesIndexEntry {
+ file_size: 0,
index_uuid: FileId::random(),
bucket_start: self.start,
bucket_end: self.end,
diff --git a/src/mito2/src/series_index/builder.rs b/src/mito2/src/series_index/builder.rs
index 8d361be364e..63a51badaeb 100644
--- a/src/mito2/src/series_index/builder.rs
+++ b/src/mito2/src/series_index/builder.rs
@@ -21,7 +21,6 @@ use common_telemetry::warn;
use futures::TryStreamExt;
use object_store::ObjectStore;
use snafu::{OptionExt, ensure};
-use store_api::storage::FileId;
use crate::error::{Result, UnexpectedSnafu};
use crate::read::BoxedRecordBatchStream;
@@ -34,7 +33,7 @@ use crate::region::MitoRegionRef;
use crate::region::version::VersionRef;
use crate::series_index::bucket::SeriesBucket;
use crate::series_index::catalog::{
- SeriesIndexEntry, range_index_path, series_index_path, series_metadata,
+ RangeIndexEntry, SeriesIndexEntry, range_index_path, series_index_path, series_metadata,
};
use crate::series_index::purger::{IndexFilePurger, IndexFileType, file_operation};
use crate::series_index::version::SeriesIndexFileHandle;
@@ -68,7 +67,7 @@ pub(crate) async fn build_range_index(
region: &MitoRegionRef,
version: &VersionRef,
file: FileHandle,
-) -> Result