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