Skip to main content

query/query_engine/
state.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::fmt;
17use std::num::NonZeroUsize;
18use std::sync::{Arc, RwLock};
19
20use async_trait::async_trait;
21use catalog::CatalogManagerRef;
22use common_base::Plugins;
23use common_function::aggrs::aggr_wrapper::fix_order::FixStateUdafOrderingAnalyzer;
24use common_function::function_factory::ScalarFunctionFactory;
25use common_function::function_registry::FUNCTION_REGISTRY;
26use common_function::handlers::{
27    FlowServiceHandlerRef, ProcedureServiceHandlerRef, TableMutationHandlerRef,
28};
29use common_function::state::FunctionState;
30use common_stat::get_total_memory_bytes;
31use common_telemetry::warn;
32use datafusion::catalog::{Session, TableFunction};
33use datafusion::dataframe::DataFrame;
34use datafusion::error::Result as DfResult;
35use datafusion::execution::SessionStateBuilder;
36use datafusion::execution::context::{QueryPlanner, SessionConfig, SessionContext, SessionState};
37use datafusion::execution::memory_pool::{
38    FairSpillPool, GreedyMemoryPool, MemoryConsumer, MemoryLimit, MemoryPool, MemoryReservation,
39    TrackConsumersPool,
40};
41use datafusion::physical_optimizer::PhysicalOptimizerRule;
42use datafusion::physical_optimizer::optimizer::PhysicalOptimizer;
43use datafusion::physical_optimizer::sanity_checker::SanityCheckPlan;
44use datafusion::physical_plan::ExecutionPlan;
45use datafusion::physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner};
46use datafusion_expr::{AggregateUDF, LogicalPlan as DfLogicalPlan, WindowUDF};
47use datafusion_optimizer::Analyzer;
48use datafusion_optimizer::analyzer::function_rewrite::ApplyFunctionRewrites;
49use datafusion_optimizer::optimizer::Optimizer;
50use partition::manager::PartitionRuleManagerRef;
51use promql::extension_plan::PromExtensionPlanner;
52use session::context::QueryContextRef;
53use table::TableRef;
54use table::table::adapter::DfTableProviderAdapter;
55
56use crate::QueryEngineContext;
57use crate::dist_plan::{
58    DistExtensionPlanner, DistPlannerAnalyzer, DistPlannerOptions, DynFilterRegistryManager,
59    MergeSortExtensionPlanner, RemoteDynFilterReceiverExtensionPlanner,
60    RemoteDynFilterRegistryLease,
61};
62use crate::metrics::{QUERY_MEMORY_POOL_REJECTED_TOTAL, QUERY_MEMORY_POOL_USAGE_BYTES};
63use crate::optimizer::ExtensionAnalyzerRule;
64use crate::optimizer::const_normalization::ConstNormalizationRule;
65use crate::optimizer::constant_term::MatchesConstantTermOptimizer;
66use crate::optimizer::count_nest_aggr::CountNestAggrRule;
67use crate::optimizer::count_wildcard::CountWildcardToTimeIndexRule;
68use crate::optimizer::enforce_sorting::EnforceSorting;
69use crate::optimizer::expr_depth;
70use crate::optimizer::global_limit::EnsureGlobalLimitForFetch;
71use crate::optimizer::json_schema_concretize::JsonSchemaConcretizeRule;
72use crate::optimizer::json_type_concretize::JsonTypeConcretizeRule;
73use crate::optimizer::parallelize_scan::ParallelizeScan;
74use crate::optimizer::promql_tsid_narrow_join::PromqlTsidNarrowJoin;
75use crate::optimizer::remove_duplicate::RemoveDuplicate;
76use crate::optimizer::scan_hint::ScanHintRule;
77use crate::optimizer::string_normalization::StringNormalizationRule;
78use crate::optimizer::transcribe_atat::TranscribeAtatRule;
79use crate::optimizer::type_conversion::TypeConversionRule;
80use crate::optimizer::windowed_sort::WindowedSortPhysicalRule;
81use crate::options::{QueryMemoryPoolPolicy, QueryOptions as QueryOptionsNew};
82use crate::query_engine::DefaultSerializer;
83use crate::query_engine::options::QueryOptions;
84use crate::query_engine::runtime::{
85    DefaultQueryRuntimeProvider, QueryRuntimeContext, QueryRuntimeProvider, QueryRuntimeProviderRef,
86};
87use crate::range_select::planner::RangeSelectPlanner;
88use crate::region_query::RegionQueryHandlerRef;
89
90/// Query engine global state
91#[derive(Clone)]
92pub struct QueryEngineState {
93    df_context: SessionContext,
94    catalog_manager: CatalogManagerRef,
95    dyn_filter_registry_manager: Arc<DynFilterRegistryManager>,
96    function_state: Arc<FunctionState>,
97    scalar_functions: Arc<RwLock<HashMap<String, ScalarFunctionFactory>>>,
98    aggr_functions: Arc<RwLock<HashMap<String, AggregateUDF>>>,
99    table_functions: Arc<RwLock<HashMap<String, Arc<TableFunction>>>>,
100    extension_rules: Vec<Arc<dyn ExtensionAnalyzerRule + Send + Sync>>,
101    plugins: Plugins,
102}
103
104impl fmt::Debug for QueryEngineState {
105    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106        f.debug_struct("QueryEngineState")
107            .field("state", &self.df_context.state())
108            .finish()
109    }
110}
111
112impl QueryEngineState {
113    #[allow(clippy::too_many_arguments)]
114    pub fn new(
115        catalog_list: CatalogManagerRef,
116        partition_rule_manager: Option<PartitionRuleManagerRef>,
117        region_query_handler: Option<RegionQueryHandlerRef>,
118        table_mutation_handler: Option<TableMutationHandlerRef>,
119        procedure_service_handler: Option<ProcedureServiceHandlerRef>,
120        flow_service_handler: Option<FlowServiceHandlerRef>,
121        with_dist_planner: bool,
122        plugins: Plugins,
123        options: QueryOptionsNew,
124    ) -> Self {
125        Self::try_new(
126            catalog_list,
127            partition_rule_manager,
128            region_query_handler,
129            table_mutation_handler,
130            procedure_service_handler,
131            flow_service_handler,
132            with_dist_planner,
133            plugins,
134            options,
135        )
136        .expect("Failed to build query engine state")
137    }
138
139    #[allow(clippy::too_many_arguments)]
140    pub fn try_new(
141        catalog_list: CatalogManagerRef,
142        partition_rule_manager: Option<PartitionRuleManagerRef>,
143        region_query_handler: Option<RegionQueryHandlerRef>,
144        table_mutation_handler: Option<TableMutationHandlerRef>,
145        procedure_service_handler: Option<ProcedureServiceHandlerRef>,
146        flow_service_handler: Option<FlowServiceHandlerRef>,
147        with_dist_planner: bool,
148        plugins: Plugins,
149        options: QueryOptionsNew,
150    ) -> DfResult<Self> {
151        let total_memory = get_total_memory_bytes().max(0) as u64;
152        let memory_pool_size = options.memory_pool_size.resolve(total_memory) as usize;
153        let runtime_provider = plugins.get::<QueryRuntimeProviderRef>();
154        let mut session_config = SessionConfig::new().with_create_default_catalog_and_schema(false);
155        if options.parallelism > 0 {
156            session_config = session_config.with_target_partitions(options.parallelism);
157        }
158        if options.allow_query_fallback {
159            session_config
160                .options_mut()
161                .extensions
162                .insert(DistPlannerOptions {
163                    allow_query_fallback: true,
164                });
165        }
166
167        // todo(hl): This serves as a workaround for https://github.com/GreptimeTeam/greptimedb/issues/5659
168        // and we can add that check back once we upgrade datafusion.
169        session_config
170            .options_mut()
171            .execution
172            .skip_physical_aggregate_schema_check = true;
173
174        let runtime_context = QueryRuntimeContext::new(&options, memory_pool_size);
175        let default_runtime_provider = DefaultQueryRuntimeProvider;
176        default_runtime_provider.configure_session_config(runtime_context, &mut session_config);
177        if let Some(provider) = runtime_provider.as_ref() {
178            provider.configure_session_config(runtime_context, &mut session_config);
179        }
180        let runtime_builder = DefaultQueryRuntimeProvider::runtime_env_builder(runtime_context);
181        let runtime_env = match runtime_provider {
182            Some(provider) => provider.build_runtime_env(runtime_context, runtime_builder)?,
183            None => default_runtime_provider.build_runtime_env(runtime_context, runtime_builder)?,
184        };
185
186        // Apply extension rules
187        let mut extension_rules = Vec::new();
188
189        // The [`TypeConversionRule`] must be at first
190        extension_rules.insert(0, Arc::new(TypeConversionRule) as _);
191        extension_rules.push(Arc::new(CountNestAggrRule) as _);
192
193        // Apply the datafusion rules
194        let mut analyzer = Analyzer::new();
195        analyzer.rules.insert(0, Arc::new(TranscribeAtatRule));
196        analyzer.rules.insert(0, Arc::new(StringNormalizationRule));
197        analyzer
198            .rules
199            .insert(0, Arc::new(CountWildcardToTimeIndexRule));
200        analyzer.rules.push(Arc::new(ConstNormalizationRule));
201
202        // Add ApplyFunctionRewrites rule,
203        // Note we cannot use `analyzer.add_function_rewrite`
204        // because only rules are copied into session_state
205        analyzer.rules.insert(
206            0,
207            Arc::new(ApplyFunctionRewrites::new(
208                FUNCTION_REGISTRY.function_rewrites(),
209            )),
210        );
211        if with_dist_planner {
212            analyzer.rules.push(Arc::new(DistPlannerAnalyzer));
213            analyzer.rules.push(Arc::new(JsonSchemaConcretizeRule));
214        }
215        analyzer.rules.push(Arc::new(FixStateUdafOrderingAnalyzer));
216        analyzer.rules.push(Arc::new(expr_depth::ExprDepthRule));
217
218        // Note: Postgres oid-alias string coercion (`'x'::regclass`,
219        // `'public'::regnamespace`, ...) is handled by the
220        // `PostgresCompatibilityParser`'s built-in `RewriteRegCastToSubquery`
221        // rule at SQL-parse time, so no analyzer rule is registered here. The
222        // implicit `oid_col = 'name'` form is not resolved (it needs schema
223        // awareness the parser lacks); the few client queries that use it are
224        // handled by the parser's blacklist.
225
226        let mut optimizer = Optimizer::new();
227        optimizer.rules.push(Arc::new(ScanHintRule));
228        optimizer.rules.push(Arc::new(JsonTypeConcretizeRule));
229
230        // add physical optimizer
231        let mut physical_optimizer = PhysicalOptimizer::new();
232        // Change TableScan's partition right before enforcing distribution
233        physical_optimizer
234            .rules
235            .insert(5, Arc::new(ParallelizeScan));
236        // Prefer collecting narrow PromQL build sides over repartitioning wide label streams.
237        physical_optimizer
238            .rules
239            .insert(6, Arc::new(PromqlTsidNarrowJoin));
240        // Re-enforce sorting after custom rules update scan partitioning and distribution.
241        // Keep it immediately before DataFusion's default EnsureRequirements.
242        physical_optimizer.rules.insert(7, Arc::new(EnforceSorting));
243        // Add rule for windowed sort
244        physical_optimizer
245            .rules
246            .push(Arc::new(WindowedSortPhysicalRule));
247        // explicitly not do filter pushdown for windowed sort&part sort
248        // (notice that `PartSortExec` create another new dyn filter that need to be pushdown if want to use dyn filter optimization)
249        // benchmark shows it can cause performance regression due to useless filtering and extra shuffle.
250        // We can add a rule to do filter pushdown for windowed sort in the future if we find a way to avoid the performance regression.
251        physical_optimizer
252            .rules
253            .push(Arc::new(MatchesConstantTermOptimizer));
254        physical_optimizer
255            .rules
256            .push(Arc::new(EnsureGlobalLimitForFetch));
257        // Add rule to remove duplicate nodes generated by other rules. Run this in the last.
258        physical_optimizer.rules.push(Arc::new(RemoveDuplicate));
259        // Place SanityCheckPlan at the end of the list to ensure that it runs after all other rules.
260        Self::remove_physical_optimizer_rule(
261            &mut physical_optimizer.rules,
262            SanityCheckPlan {}.name(),
263        );
264        physical_optimizer.rules.push(Arc::new(SanityCheckPlan {}));
265
266        let session_state = SessionStateBuilder::new()
267            .with_config(session_config)
268            .with_runtime_env(runtime_env)
269            .with_default_features()
270            .with_analyzer_rules(analyzer.rules)
271            .with_serializer_registry(Arc::new(DefaultSerializer))
272            .with_query_planner(Arc::new(DfQueryPlanner::new(
273                catalog_list.clone(),
274                partition_rule_manager,
275                region_query_handler.clone(),
276                options.enable_per_region_metrics,
277            )))
278            .with_optimizer_rules(optimizer.rules)
279            .with_physical_optimizer_rules(physical_optimizer.rules)
280            .build();
281        let df_context = SessionContext::new_with_state(session_state);
282        register_function_aliases(&df_context);
283        register_pg_catalog_compat(&df_context);
284
285        Ok(Self {
286            df_context,
287            catalog_manager: catalog_list,
288            dyn_filter_registry_manager: Arc::new(DynFilterRegistryManager::default()),
289            function_state: Arc::new(FunctionState {
290                plugins: plugins.clone(),
291                table_mutation_handler,
292                procedure_service_handler,
293                flow_service_handler,
294            }),
295            aggr_functions: Arc::new(RwLock::new(HashMap::new())),
296            table_functions: Arc::new(RwLock::new(HashMap::new())),
297            extension_rules,
298            plugins,
299            scalar_functions: Arc::new(RwLock::new(HashMap::new())),
300        })
301    }
302
303    fn remove_physical_optimizer_rule(
304        rules: &mut Vec<Arc<dyn PhysicalOptimizerRule + Send + Sync>>,
305        name: &str,
306    ) {
307        rules.retain(|rule| rule.name() != name);
308    }
309
310    /// Optimize the logical plan by the extension analyzer rules.
311    pub fn optimize_by_extension_rules(
312        &self,
313        plan: DfLogicalPlan,
314        context: &QueryEngineContext,
315    ) -> DfResult<DfLogicalPlan> {
316        expr_depth::validate_plan(&plan)?;
317        self.extension_rules
318            .iter()
319            .try_fold(plan, |acc_plan, rule| {
320                let plan =
321                    rule.analyze(acc_plan, context, self.session_state().config_options())?;
322                expr_depth::validate_plan(&plan)?;
323                Ok(plan)
324            })
325    }
326
327    /// Run the full logical plan optimize phase for the given plan.
328    pub fn optimize_logical_plan(&self, plan: DfLogicalPlan) -> DfResult<DfLogicalPlan> {
329        expr_depth::validate_plan(&plan)?;
330        self.session_state().optimize(&plan)
331    }
332
333    /// Retrieve the scalar function by name
334    pub fn scalar_function(&self, function_name: &str) -> Option<ScalarFunctionFactory> {
335        self.scalar_functions
336            .read()
337            .unwrap()
338            .get(function_name)
339            .cloned()
340    }
341
342    /// Retrieve scalar function names.
343    pub fn scalar_names(&self) -> Vec<String> {
344        self.scalar_functions
345            .read()
346            .unwrap()
347            .keys()
348            .cloned()
349            .collect()
350    }
351
352    /// Retrieve the aggregate function by name
353    pub fn aggr_function(&self, function_name: &str) -> Option<AggregateUDF> {
354        self.aggr_functions
355            .read()
356            .unwrap()
357            .get(function_name)
358            .cloned()
359    }
360
361    /// Retrieve aggregate function names.
362    pub fn aggr_names(&self) -> Vec<String> {
363        self.aggr_functions
364            .read()
365            .unwrap()
366            .keys()
367            .cloned()
368            .collect()
369    }
370
371    /// Retrieve table function by name
372    pub fn table_function(&self, function_name: &str) -> Option<Arc<TableFunction>> {
373        self.table_functions
374            .read()
375            .unwrap()
376            .get(function_name)
377            .cloned()
378    }
379
380    /// Retrieve table function names.
381    pub fn table_function_names(&self) -> Vec<String> {
382        self.table_functions
383            .read()
384            .unwrap()
385            .keys()
386            .cloned()
387            .collect()
388    }
389
390    /// Register an scalar function.
391    /// Will override if the function with same name is already registered.
392    pub fn register_scalar_function(&self, func: ScalarFunctionFactory) {
393        let name = func.name().to_string();
394        let x = self
395            .scalar_functions
396            .write()
397            .unwrap()
398            .insert(name.clone(), func);
399
400        if x.is_some() {
401            warn!("Already registered scalar function '{name}'");
402        }
403    }
404
405    /// Register an aggregate function.
406    ///
407    /// # Panics
408    /// Will panic if the function with same name is already registered.
409    ///
410    /// Panicking consideration: currently the aggregated functions are all statically registered,
411    /// user cannot define their own aggregate functions on the fly. So we can panic here. If that
412    /// invariant is broken in the future, we should return an error instead of panicking.
413    pub fn register_aggr_function(&self, func: AggregateUDF) {
414        let name = func.name().to_string();
415        let x = self
416            .aggr_functions
417            .write()
418            .unwrap()
419            .insert(name.clone(), func);
420        assert!(
421            x.is_none(),
422            "Already registered aggregate function '{name}'"
423        );
424    }
425
426    pub fn register_table_function(&self, func: Arc<TableFunction>) {
427        let name = func.name();
428        let x = self
429            .table_functions
430            .write()
431            .unwrap()
432            .insert(name.to_string(), func.clone());
433
434        if x.is_some() {
435            warn!("Already registered table function '{name}'");
436        }
437    }
438
439    /// Register a window function (UDWF) directly on the DataFusion SessionContext.
440    ///
441    /// This makes the function visible via `session_state.window_functions()`,
442    /// which is used by `DfContextProviderAdapter::get_window_meta`.
443    pub fn register_window_function(&self, func: WindowUDF) {
444        self.df_context.register_udwf(func);
445    }
446
447    pub fn catalog_manager(&self) -> &CatalogManagerRef {
448        &self.catalog_manager
449    }
450
451    pub fn dyn_filter_registry_manager(&self) -> Arc<DynFilterRegistryManager> {
452        self.dyn_filter_registry_manager.clone()
453    }
454
455    pub fn acquire_remote_dyn_filter_registry_lease(
456        &self,
457        query_ctx: &QueryContextRef,
458    ) -> Option<RemoteDynFilterRegistryLease> {
459        let query_id = query_ctx.remote_query_id_value()?;
460        Some(
461            self.dyn_filter_registry_manager
462                .clone()
463                .acquire_lease(query_id),
464        )
465    }
466
467    pub fn function_state(&self) -> Arc<FunctionState> {
468        self.function_state.clone()
469    }
470
471    /// Returns the [`TableMutationHandlerRef`] in state.
472    pub fn table_mutation_handler(&self) -> Option<&TableMutationHandlerRef> {
473        self.function_state.table_mutation_handler.as_ref()
474    }
475
476    /// Returns the [`ProcedureServiceHandlerRef`] in state.
477    pub fn procedure_service_handler(&self) -> Option<&ProcedureServiceHandlerRef> {
478        self.function_state.procedure_service_handler.as_ref()
479    }
480
481    pub(crate) fn disallow_cross_catalog_query(&self) -> bool {
482        self.plugins
483            .map::<QueryOptions, _, _>(|x| x.disallow_cross_catalog_query)
484            .unwrap_or(false)
485    }
486
487    pub fn session_state(&self) -> SessionState {
488        self.df_context.state()
489    }
490
491    /// Create a DataFrame for a table
492    pub fn read_table(&self, table: TableRef) -> DfResult<DataFrame> {
493        self.df_context
494            .read_table(Arc::new(DfTableProviderAdapter::new(table)))
495    }
496}
497
498struct DfQueryPlanner {
499    physical_planner: DefaultPhysicalPlanner,
500}
501
502impl fmt::Debug for DfQueryPlanner {
503    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
504        f.debug_struct("DfQueryPlanner").finish()
505    }
506}
507
508#[async_trait]
509impl QueryPlanner for DfQueryPlanner {
510    async fn create_physical_plan(
511        &self,
512        logical_plan: &DfLogicalPlan,
513        session: &dyn Session,
514    ) -> DfResult<Arc<dyn ExecutionPlan>> {
515        self.physical_planner
516            .create_physical_plan(logical_plan, session)
517            .await
518    }
519}
520
521/// MySQL-compatible scalar function aliases: (target_name, alias)
522const SCALAR_FUNCTION_ALIASES: &[(&str, &str)] = &[
523    ("upper", "ucase"),
524    ("lower", "lcase"),
525    ("ceil", "ceiling"),
526    ("substr", "mid"),
527    ("random", "rand"),
528];
529
530/// MySQL-compatible aggregate function aliases: (target_name, alias)
531const AGGREGATE_FUNCTION_ALIASES: &[(&str, &str)] =
532    &[("stddev_pop", "std"), ("var_pop", "variance")];
533
534/// Register function aliases.
535///
536/// This function adds aliases like `ucase` -> `upper`, `lcase` -> `lower`, etc.
537/// to make GreptimeDB more compatible with MySQL syntax.
538fn register_function_aliases(ctx: &SessionContext) {
539    let state = ctx.state();
540
541    for (target, alias) in SCALAR_FUNCTION_ALIASES {
542        if let Some(func) = state.scalar_functions().get(*target) {
543            let aliased = func.as_ref().clone().with_aliases([*alias]);
544            ctx.register_udf(aliased);
545        }
546    }
547
548    for (target, alias) in AGGREGATE_FUNCTION_ALIASES {
549        if let Some(func) = state.aggregate_functions().get(*target) {
550            let aliased = func.as_ref().clone().with_aliases([*alias]);
551            ctx.register_udaf(aliased);
552        }
553    }
554}
555
556/// Register the session-level (not function-registry) Postgres-compatibility
557/// extension sourced from `datafusion-pg-catalog`: widen Postgres `int4`
558/// bounds to `int8` for `generate_series`/`range` (their stock implementations
559/// require `int8` literals, and DF invokes them at planning time, before any
560/// analyzer rule can run).
561///
562/// The oid-alias type planner is supplied per SQL-planner context by
563/// [`DfContextProviderAdapter::get_type_planner`](crate::datafusion::planner::DfContextProviderAdapter::get_type_planner).
564/// Forward oid-alias name->oid resolution is handled at SQL-parse time by the
565/// `PostgresCompatibilityParser`'s built-in rewrite rule, not here.
566fn register_pg_catalog_compat(ctx: &SessionContext) {
567    datafusion_pg_catalog::pg_catalog::generate_series_arg_coercion::CoerceIntArgsToBigInt::widen(
568        ctx,
569    );
570}
571
572impl DfQueryPlanner {
573    fn new(
574        catalog_manager: CatalogManagerRef,
575        partition_rule_manager: Option<PartitionRuleManagerRef>,
576        region_query_handler: Option<RegionQueryHandlerRef>,
577        enable_per_region_metrics: bool,
578    ) -> Self {
579        let mut planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>> = vec![
580            Arc::new(PromExtensionPlanner),
581            Arc::new(RangeSelectPlanner),
582            Arc::new(RemoteDynFilterReceiverExtensionPlanner),
583        ];
584        if let (Some(region_query_handler), Some(partition_rule_manager)) =
585            (region_query_handler, partition_rule_manager)
586        {
587            planners.push(Arc::new(DistExtensionPlanner::new(
588                catalog_manager,
589                partition_rule_manager,
590                region_query_handler,
591                enable_per_region_metrics,
592            )));
593            planners.push(Arc::new(MergeSortExtensionPlanner {}));
594        }
595        Self {
596            physical_planner: DefaultPhysicalPlanner::with_extension_planners(planners),
597        }
598    }
599}
600
601/// A wrapper around a memory pool that records metrics.
602///
603/// This wrapper intercepts all memory pool operations and updates
604/// Prometheus metrics for monitoring query memory usage and rejections.
605///
606/// The inner pool is wrapped with `TrackConsumersPool` to preserve
607/// top-consumer error context on rejection.
608#[derive(Debug)]
609pub(super) struct MetricsMemoryPool {
610    inner: Arc<dyn MemoryPool>,
611}
612
613impl MetricsMemoryPool {
614    // Number of top memory consumers to report in OOM error messages
615    const TOP_CONSUMERS_TO_REPORT: usize = 5;
616
617    /// Create a new metrics-wrapped memory pool with the given size limit and
618    /// allocation policy.
619    pub(super) fn new(limit: usize, policy: QueryMemoryPoolPolicy) -> Self {
620        let top_n = NonZeroUsize::new(Self::TOP_CONSUMERS_TO_REPORT).unwrap();
621        let inner: Arc<dyn MemoryPool> = match policy {
622            QueryMemoryPoolPolicy::Greedy => {
623                Arc::new(TrackConsumersPool::new(GreedyMemoryPool::new(limit), top_n))
624            }
625            QueryMemoryPoolPolicy::Fair => {
626                Arc::new(TrackConsumersPool::new(FairSpillPool::new(limit), top_n))
627            }
628        };
629        Self { inner }
630    }
631
632    #[inline]
633    fn update_metrics(&self) {
634        QUERY_MEMORY_POOL_USAGE_BYTES.set(self.inner.reserved() as i64);
635    }
636}
637
638impl fmt::Display for MetricsMemoryPool {
639    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640        write!(f, "{}(inner_pool: {})", self.name(), self.inner)
641    }
642}
643
644impl MemoryPool for MetricsMemoryPool {
645    fn name(&self) -> &str {
646        "metrics"
647    }
648
649    fn register(&self, consumer: &MemoryConsumer) {
650        self.inner.register(consumer);
651    }
652
653    fn unregister(&self, consumer: &MemoryConsumer) {
654        self.inner.unregister(consumer);
655    }
656
657    fn grow(&self, reservation: &MemoryReservation, additional: usize) {
658        self.inner.grow(reservation, additional);
659        self.update_metrics();
660    }
661
662    fn shrink(&self, reservation: &MemoryReservation, shrink: usize) {
663        self.inner.shrink(reservation, shrink);
664        self.update_metrics();
665    }
666
667    fn try_grow(
668        &self,
669        reservation: &MemoryReservation,
670        additional: usize,
671    ) -> datafusion_common::Result<()> {
672        let result = self.inner.try_grow(reservation, additional);
673        if result.is_err() {
674            QUERY_MEMORY_POOL_REJECTED_TOTAL.inc();
675        }
676        self.update_metrics();
677        result
678    }
679
680    fn reserved(&self) -> usize {
681        self.inner.reserved()
682    }
683
684    fn memory_limit(&self) -> MemoryLimit {
685        self.inner.memory_limit()
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use std::sync::atomic::{AtomicBool, Ordering};
692
693    use common_base::Plugins;
694    use common_base::memory_limit::MemoryLimit;
695    use common_base::readable_size::ReadableSize;
696    use datafusion::error::DataFusionError;
697    use datafusion::execution::memory_pool::{GreedyMemoryPool, MemoryLimit as DfMemoryLimit};
698    use datafusion::execution::runtime_env::{RuntimeEnv, RuntimeEnvBuilder};
699    use datafusion_common::config::SpillCompression;
700    use session::context::QueryContext;
701
702    use super::*;
703    use crate::options::{QueryOptions, QuerySpillCompression, QuerySpillMode};
704    use crate::query_engine::runtime::{
705        DefaultQueryRuntimeProvider, QueryRuntimeContext, QueryRuntimeProvider,
706        QueryRuntimeProviderRef,
707    };
708
709    fn new_query_engine_state() -> QueryEngineState {
710        new_query_engine_state_with(Plugins::default(), QueryOptions::default())
711    }
712
713    fn new_query_engine_state_with(plugins: Plugins, options: QueryOptions) -> QueryEngineState {
714        QueryEngineState::new(
715            catalog::memory::new_memory_catalog_manager().unwrap(),
716            None,
717            None,
718            None,
719            None,
720            None,
721            false,
722            plugins,
723            options,
724        )
725    }
726
727    struct TestRuntimeProvider {
728        build_called: AtomicBool,
729        configure_called: AtomicBool,
730    }
731
732    impl TestRuntimeProvider {
733        fn new() -> Self {
734            Self {
735                build_called: AtomicBool::new(false),
736                configure_called: AtomicBool::new(false),
737            }
738        }
739    }
740
741    impl QueryRuntimeProvider for TestRuntimeProvider {
742        fn configure_session_config(
743            &self,
744            ctx: QueryRuntimeContext<'_>,
745            config: &mut SessionConfig,
746        ) {
747            assert_eq!(ctx.resolved_memory_pool_size, 1024);
748            self.configure_called.store(true, Ordering::SeqCst);
749            *config = config.clone().with_target_partitions(7);
750        }
751
752        fn build_runtime_env(
753            &self,
754            ctx: QueryRuntimeContext<'_>,
755            builder: RuntimeEnvBuilder,
756        ) -> DfResult<Arc<RuntimeEnv>> {
757            assert_eq!(ctx.resolved_memory_pool_size, 1024);
758            self.build_called.store(true, Ordering::SeqCst);
759            builder
760                .with_memory_pool(Arc::new(GreedyMemoryPool::new(2048)))
761                .build()
762                .map(Arc::new)
763        }
764    }
765
766    struct ErrorRuntimeProvider;
767
768    impl QueryRuntimeProvider for ErrorRuntimeProvider {
769        fn build_runtime_env(
770            &self,
771            _ctx: QueryRuntimeContext<'_>,
772            _builder: RuntimeEnvBuilder,
773        ) -> DfResult<Arc<RuntimeEnv>> {
774            Err(DataFusionError::Execution("runtime provider error".into()))
775        }
776    }
777
778    #[test]
779    fn query_runtime_default_provider_keeps_bounded_memory_pool() {
780        let state = new_query_engine_state_with(
781            Plugins::default(),
782            QueryOptions {
783                memory_pool_size: MemoryLimit::Size(ReadableSize(1024)),
784                ..Default::default()
785            },
786        );
787
788        assert!(matches!(
789            state
790                .session_state()
791                .runtime_env()
792                .memory_pool
793                .memory_limit(),
794            DfMemoryLimit::Finite(1024)
795        ));
796    }
797
798    #[test]
799    fn query_runtime_provider_from_plugins_builds_runtime_env() {
800        let plugins = Plugins::default();
801        let provider = Arc::new(TestRuntimeProvider::new());
802        plugins.insert::<QueryRuntimeProviderRef>(provider.clone());
803
804        let state = new_query_engine_state_with(
805            plugins,
806            QueryOptions {
807                memory_pool_size: MemoryLimit::Size(ReadableSize(1024)),
808                ..Default::default()
809            },
810        );
811
812        assert!(provider.build_called.load(Ordering::SeqCst));
813        assert!(matches!(
814            state
815                .session_state()
816                .runtime_env()
817                .memory_pool
818                .memory_limit(),
819            DfMemoryLimit::Finite(2048)
820        ));
821    }
822
823    #[test]
824    fn query_runtime_provider_from_plugins_configures_session_config() {
825        let plugins = Plugins::default();
826        let provider = Arc::new(TestRuntimeProvider::new());
827        plugins.insert::<QueryRuntimeProviderRef>(provider.clone());
828
829        let state = new_query_engine_state_with(
830            plugins,
831            QueryOptions {
832                memory_pool_size: MemoryLimit::Size(ReadableSize(1024)),
833                experimental_spill_mode: QuerySpillMode::Custom,
834                experimental_spill_compression: QuerySpillCompression::Zstd,
835                ..Default::default()
836            },
837        );
838
839        assert!(provider.configure_called.load(Ordering::SeqCst));
840        assert_eq!(7, state.session_state().config().target_partitions());
841        assert_eq!(
842            SpillCompression::Zstd,
843            state
844                .session_state()
845                .config()
846                .options()
847                .execution
848                .spill_compression
849        );
850    }
851
852    #[test]
853    fn query_runtime_provider_error_is_returned_by_try_new() {
854        let plugins = Plugins::default();
855        plugins.insert::<QueryRuntimeProviderRef>(Arc::new(ErrorRuntimeProvider));
856
857        let err = match QueryEngineState::try_new(
858            catalog::memory::new_memory_catalog_manager().unwrap(),
859            None,
860            None,
861            None,
862            None,
863            None,
864            false,
865            plugins,
866            QueryOptions::default(),
867        ) {
868            Err(err) => err,
869            Ok(_) => panic!("expected runtime provider error"),
870        };
871
872        assert!(
873            matches!(err, DataFusionError::Execution(message) if message == "runtime provider error")
874        );
875    }
876
877    #[test]
878    fn query_engine_state_reuses_query_scoped_dyn_filter_registry_lease() {
879        let state = new_query_engine_state();
880        let query_ctx = QueryContext::arc();
881
882        let first = state
883            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
884            .unwrap();
885        let second = state
886            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
887            .unwrap();
888
889        assert!(first.ptr_eq(&second));
890        assert_eq!(state.dyn_filter_registry_manager().registry_count(), 1);
891        assert_eq!(
892            first.registry().query_id(),
893            query_ctx.remote_query_id_value().unwrap()
894        );
895    }
896
897    #[test]
898    fn query_engine_state_relies_on_query_context_remote_query_id_contract() {
899        let state = new_query_engine_state();
900        let query_ctx = QueryContext::arc();
901
902        assert!(query_ctx.remote_query_id_value().is_some());
903
904        let lease = state
905            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
906            .unwrap();
907
908        assert_eq!(
909            lease.registry().query_id(),
910            query_ctx.remote_query_id_value().unwrap()
911        );
912        assert_eq!(state.dyn_filter_registry_manager().registry_count(), 1);
913    }
914
915    #[test]
916    fn query_engine_state_separates_registries_for_different_query_contexts() {
917        let state = new_query_engine_state();
918        let first_query_ctx = QueryContext::arc();
919        let second_query_ctx = QueryContext::arc();
920
921        let first = state
922            .acquire_remote_dyn_filter_registry_lease(&first_query_ctx)
923            .unwrap();
924        let second = state
925            .acquire_remote_dyn_filter_registry_lease(&second_query_ctx)
926            .unwrap();
927
928        assert!(!first.ptr_eq(&second));
929        assert_eq!(state.dyn_filter_registry_manager().registry_count(), 2);
930        assert_eq!(
931            first.registry().query_id(),
932            first_query_ctx.remote_query_id_value().unwrap()
933        );
934        assert_eq!(
935            second.registry().query_id(),
936            second_query_ctx.remote_query_id_value().unwrap()
937        );
938    }
939
940    /// Builds a runtime env through the default provider seam, mirroring what
941    /// [`QueryEngineState::try_new`] does.
942    fn build_runtime_env(options: &QueryOptions, memory_pool_size: usize) -> Arc<RuntimeEnv> {
943        let ctx = QueryRuntimeContext::new(options, memory_pool_size);
944        let builder = DefaultQueryRuntimeProvider::runtime_env_builder(ctx);
945        DefaultQueryRuntimeProvider
946            .build_runtime_env(ctx, builder)
947            .expect("Failed to build RuntimeEnv")
948    }
949
950    #[test]
951    fn test_build_runtime_env_custom_mode_with_path() {
952        // Use a temp directory managed manually so we don't need the `tempfile` crate.
953        let spill_dir = std::env::temp_dir().join(format!("df_spill_test_{}", std::process::id()));
954        let _ = std::fs::remove_dir_all(&spill_dir);
955        let opts = QueryOptions {
956            experimental_spill_mode: QuerySpillMode::Custom,
957            experimental_spill_path: Some(spill_dir.clone()),
958            experimental_spill_max_temp_directory_size: ReadableSize::gb(1),
959            ..Default::default()
960        };
961        let env = build_runtime_env(&opts, 0);
962
963        assert!(env.disk_manager.tmp_files_enabled());
964        let tmp_file = env.disk_manager.create_tmp_file("test spill");
965        assert!(tmp_file.is_ok());
966        assert!(spill_dir.exists());
967
968        let _ = std::fs::remove_dir_all(&spill_dir);
969    }
970
971    #[test]
972    fn test_build_runtime_env_disabled_mode() {
973        let opts = QueryOptions {
974            experimental_spill_mode: QuerySpillMode::Disabled,
975            ..Default::default()
976        };
977        let env = build_runtime_env(&opts, 0);
978
979        assert!(!env.disk_manager.tmp_files_enabled());
980        let result = env.disk_manager.create_tmp_file("test spill");
981        assert!(result.is_err());
982        if let Err(error) = result {
983            assert!(format!("{error}").contains("DiskManager is disabled"));
984        }
985    }
986
987    #[test]
988    fn test_metrics_memory_pool_policy_differs_with_multiple_spillable_consumers() {
989        for (policy, greedy) in [
990            (QueryMemoryPoolPolicy::Greedy, true),
991            (QueryMemoryPoolPolicy::Fair, false),
992        ] {
993            let pool: Arc<dyn MemoryPool> = Arc::new(MetricsMemoryPool::new(100, policy));
994            let first = MemoryConsumer::new("first")
995                .with_can_spill(true)
996                .register(&pool);
997            let second = MemoryConsumer::new("second")
998                .with_can_spill(true)
999                .register(&pool);
1000
1001            let first_result = first.try_grow(75);
1002            assert_eq!(first_result.is_ok(), greedy);
1003            if greedy {
1004                assert!(second.try_grow(26).is_err());
1005            } else {
1006                first.try_grow(50).unwrap();
1007                second.try_grow(50).unwrap();
1008            }
1009        }
1010    }
1011
1012    /// Builds a runtime with custom spill configuration and a bounded memory
1013    /// pool, then runs sort queries that probe spill-to-disk behaviour:
1014    ///
1015    /// - A pool large enough for merge chunks but still much smaller
1016    ///   than the total data.  Executes the physical plan directly so we can
1017    ///   walk the plan tree afterwards and sum `spill_count` / `spilled_rows` /
1018    ///   `spilled_bytes` on `SortExec` nodes.  All three must be > 0.
1019    #[tokio::test]
1020    async fn test_sort_spill_smoke_with_custom_runtime() {
1021        use arrow::array::Int32Array;
1022        use arrow::datatypes::{DataType, Field, Schema};
1023        use arrow::record_batch::RecordBatch;
1024        use datafusion::datasource::MemTable;
1025        use datafusion::physical_plan::collect as df_collect;
1026
1027        let spill_dir =
1028            std::env::temp_dir().join(format!("greptime_spill_smoke_{}", std::process::id()));
1029        let _ = std::fs::remove_dir_all(&spill_dir);
1030
1031        let opts = QueryOptions {
1032            experimental_spill_mode: QuerySpillMode::Custom,
1033            experimental_spill_path: Some(spill_dir.clone()),
1034            experimental_spill_max_temp_directory_size: ReadableSize::gb(1),
1035            experimental_memory_pool_policy: QueryMemoryPoolPolicy::Greedy,
1036            ..Default::default()
1037        };
1038
1039        let session_config = SessionConfig::new()
1040            .with_target_partitions(1)
1041            .with_sort_in_place_threshold_bytes(0)
1042            .with_sort_spill_reservation_bytes(64 * 1024);
1043
1044        let schema = Arc::new(Schema::new(vec![
1045            Field::new("id", DataType::Int32, false),
1046            Field::new("val", DataType::Int32, false),
1047        ]));
1048        let n_rows: i32 = 200_000;
1049        let batch_size: i32 = 5_000;
1050        let partitions = 1usize;
1051
1052        let mut table_partitions: Vec<Vec<RecordBatch>> = Vec::with_capacity(partitions);
1053        for _ in 0..partitions {
1054            let mut batches = Vec::new();
1055            let mut row_offset: i32 = 0;
1056            while row_offset < n_rows {
1057                let chunk_end = (row_offset + batch_size).min(n_rows);
1058                let chunk_len = (chunk_end - row_offset) as usize;
1059                let mut id_builder = Int32Array::builder(chunk_len);
1060                let mut val_builder = Int32Array::builder(chunk_len);
1061                for i in row_offset..chunk_end {
1062                    id_builder.append_value(i);
1063                    val_builder.append_value(n_rows - 1 - i);
1064                }
1065                batches.push(
1066                    RecordBatch::try_new(
1067                        Arc::clone(&schema),
1068                        vec![
1069                            Arc::new(id_builder.finish()),
1070                            Arc::new(val_builder.finish()),
1071                        ],
1072                    )
1073                    .unwrap(),
1074                );
1075                row_offset = chunk_end;
1076            }
1077            table_partitions.push(batches);
1078        }
1079
1080        fn build_ctx(
1081            session_config: &SessionConfig,
1082            runtime: &Arc<RuntimeEnv>,
1083            schema: &Arc<Schema>,
1084            partitions: &[Vec<RecordBatch>],
1085        ) -> SessionContext {
1086            let session_state = SessionStateBuilder::new()
1087                .with_config(session_config.clone())
1088                .with_runtime_env(Arc::clone(runtime))
1089                .with_default_features()
1090                .build();
1091            let ctx = SessionContext::new_with_state(session_state);
1092            let table = MemTable::try_new(Arc::clone(schema), partitions.to_vec()).unwrap();
1093            ctx.register_table("t", Arc::new(table)).unwrap();
1094            ctx
1095        }
1096
1097        {
1098            let mem_limit: usize = 512 * 1024; // 512 KB pool, 64 KB reserved for merge
1099
1100            let runtime = build_runtime_env(&opts, mem_limit);
1101            assert_eq!(runtime.memory_pool.reserved(), 0);
1102            let ctx = build_ctx(&session_config, &runtime, &schema, &table_partitions);
1103
1104            let df = ctx
1105                .sql("SELECT val FROM t ORDER BY val ASC")
1106                .await
1107                .expect("planning ORDER BY");
1108            let plan = df
1109                .create_physical_plan()
1110                .await
1111                .expect("creating physical plan");
1112            let task_ctx = ctx.task_ctx();
1113
1114            let batches = df_collect(Arc::clone(&plan), task_ctx)
1115                .await
1116                .expect("executing ORDER BY");
1117
1118            let (spill_count, spilled_rows, spilled_bytes) = sum_sort_spill_metrics(&plan);
1119            assert!(
1120                spill_count > 0,
1121                "expected SortExec spill_count > 0 (pool={} KB, reservation=64 KB), got 0",
1122                mem_limit / 1024,
1123            );
1124            assert!(
1125                spilled_rows > 0,
1126                "expected SortExec spilled_rows > 0, got 0 \
1127                 (spill_count={spill_count}, spilled_bytes={spilled_bytes})",
1128            );
1129            assert!(
1130                spilled_bytes > 0,
1131                "expected SortExec spilled_bytes > 0, got 0 \
1132                 (spill_count={spill_count}, spilled_rows={spilled_rows})",
1133            );
1134
1135            let vals: Vec<i32> = batches
1136                .iter()
1137                .flat_map(|b| {
1138                    let col = b.column(0).as_any().downcast_ref::<Int32Array>().unwrap();
1139                    (0..b.num_rows()).map(move |i| col.value(i))
1140                })
1141                .collect();
1142            assert_eq!(vals.len(), n_rows as usize);
1143            for w in vals.windows(2) {
1144                assert!(w[0] <= w[1], "sort order violation: {} > {}", w[0], w[1]);
1145            }
1146
1147            assert!(
1148                std::fs::read_dir(&spill_dir)
1149                    .ok()
1150                    .map(|mut entries| entries.any(|e| {
1151                        e.as_ref()
1152                            .map(|de| de.file_name().to_string_lossy().starts_with("datafusion-"))
1153                            .unwrap_or(false)
1154                    }))
1155                    .unwrap_or(false),
1156                "Expected 'datafusion-*' directory in spill path {:?} \
1157                 (DiskManager should create it on build).",
1158                spill_dir,
1159            );
1160
1161            assert_eq!(runtime.memory_pool.reserved(), 0);
1162        }
1163
1164        let _ = std::fs::remove_dir_all(&spill_dir);
1165    }
1166
1167    /// Walk a physical plan tree, summing spill metrics for every node whose
1168    /// name starts with `"SortExec"`.
1169    fn sum_sort_spill_metrics(plan: &Arc<dyn ExecutionPlan>) -> (usize, usize, usize) {
1170        let mut spill_count = 0usize;
1171        let mut spilled_rows = 0usize;
1172        let mut spilled_bytes = 0usize;
1173
1174        fn walk(plan: &Arc<dyn ExecutionPlan>, sc: &mut usize, sr: &mut usize, sb: &mut usize) {
1175            let name = plan.name();
1176            if name.starts_with("SortExec")
1177                && let Some(m) = plan.metrics()
1178            {
1179                *sc += m.spill_count().unwrap_or(0);
1180                *sr += m.spilled_rows().unwrap_or(0);
1181                *sb += m.spilled_bytes().unwrap_or(0);
1182            }
1183            for child in plan.children() {
1184                walk(child, sc, sr, sb);
1185            }
1186        }
1187
1188        walk(
1189            plan,
1190            &mut spill_count,
1191            &mut spilled_rows,
1192            &mut spilled_bytes,
1193        );
1194        (spill_count, spilled_rows, spilled_bytes)
1195    }
1196}