// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::{ sync::{Arc, LazyLock}, time::Duration, }; use async_trait::async_trait; use derive_builder::Builder; use rustfs_s3select_api::{ QueryResult, query::{ Query, dispatcher::QueryDispatcher, execution::QueryStateMachineRef, logical_planner::Plan, session::{DEFAULT_S3SELECT_MEMORY_LIMIT_BYTES as DEFAULT_MEMORY_LIMIT_BYTES, QueryAdmission, SessionCtxFactory}, }, server::dbms::{DatabaseManagerSystem, QueryHandle}, }; use s3s::dto::SelectObjectContentInput; use tokio::sync::Semaphore; use crate::{ dispatcher::manager::SimpleQueryDispatcherBuilder, execution::{factory::SqlQueryExecutionFactory, scheduler::local::LocalScheduler}, function::simple_func_manager::SimpleFunctionMetadataManager, metadata::base_table::BaseTableProvider, sql::{optimizer::CascadeOptimizerBuilder, parser::DefaultParser}, }; const ENV_RUSTFS_S3SELECT_TARGET_PARTITIONS: &str = "RUSTFS_S3SELECT_TARGET_PARTITIONS"; const ENV_RUSTFS_S3SELECT_MEMORY_LIMIT_BYTES: &str = "RUSTFS_S3SELECT_MEMORY_LIMIT_BYTES"; const ENV_RUSTFS_S3SELECT_QUERY_TIMEOUT_SECS: &str = "RUSTFS_S3SELECT_QUERY_TIMEOUT_SECS"; const ENV_RUSTFS_S3SELECT_MAX_CONCURRENT_QUERIES: &str = "RUSTFS_S3SELECT_MAX_CONCURRENT_QUERIES"; pub(crate) const DEFAULT_QUERY_TIMEOUT_SECS: u64 = 300; pub(crate) const DEFAULT_MAX_CONCURRENT_QUERIES: usize = 4; const MAX_QUERY_TIMEOUT_SECS: u64 = 24 * 60 * 60; const TEST_MAX_CONCURRENT_QUERIES: usize = 1024; static QUERY_ADMISSION: LazyLock> = LazyLock::new(|| Arc::new(Semaphore::new(S3SelectRuntimeConfig::from_env().max_concurrent_queries))); #[derive(Builder)] pub struct RustFSms { // query dispatcher & query execution query_dispatcher: Arc, } #[async_trait] impl DatabaseManagerSystem for RustFSms where D: QueryDispatcher, { fn try_reserve_query(&self) -> QueryResult { self.query_dispatcher.try_reserve_query() } async fn execute(&self, query: &Query) -> QueryResult { let (query, result) = self.query_dispatcher.dispatch_query(query).await?; Ok(QueryHandle::new(query, result)) } async fn execute_admitted(&self, query: &Query, admission: QueryAdmission) -> QueryResult { let (query, result) = self.query_dispatcher.dispatch_query_admitted(query, admission).await?; Ok(QueryHandle::new(query, result)) } async fn build_query_state_machine(&self, query: Query) -> QueryResult { let query_state_machine = self.query_dispatcher.build_query_state_machine(query).await?; Ok(query_state_machine) } async fn build_logical_plan(&self, query_state_machine: QueryStateMachineRef) -> QueryResult> { let logical_plan = self.query_dispatcher.build_logical_plan(query_state_machine).await?; Ok(logical_plan) } async fn execute_logical_plan( &self, logical_plan: Plan, query_state_machine: QueryStateMachineRef, ) -> QueryResult { let query = query_state_machine.query.clone(); let result = self .query_dispatcher .execute_logical_plan(logical_plan, query_state_machine) .await?; Ok(QueryHandle::new(query.clone(), result)) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct S3SelectRuntimeConfig { target_partitions: usize, memory_limit_bytes: usize, query_timeout: Duration, max_concurrent_queries: usize, } impl Default for S3SelectRuntimeConfig { fn default() -> Self { Self { target_partitions: 0, memory_limit_bytes: DEFAULT_MEMORY_LIMIT_BYTES, query_timeout: Duration::from_secs(DEFAULT_QUERY_TIMEOUT_SECS), max_concurrent_queries: DEFAULT_MAX_CONCURRENT_QUERIES, } } } impl S3SelectRuntimeConfig { fn from_env() -> Self { Self { target_partitions: target_partitions_from_env_value( std::env::var(ENV_RUSTFS_S3SELECT_TARGET_PARTITIONS).ok().as_deref(), ), memory_limit_bytes: bounded_usize_from_env_value( std::env::var(ENV_RUSTFS_S3SELECT_MEMORY_LIMIT_BYTES).ok().as_deref(), DEFAULT_MEMORY_LIMIT_BYTES, usize::MAX, ), query_timeout: s3_select_query_timeout(), max_concurrent_queries: bounded_usize_from_env_value( std::env::var(ENV_RUSTFS_S3SELECT_MAX_CONCURRENT_QUERIES).ok().as_deref(), DEFAULT_MAX_CONCURRENT_QUERIES, Semaphore::MAX_PERMITS, ), } } } pub fn s3_select_query_timeout() -> Duration { Duration::from_secs(bounded_u64_from_env_value( std::env::var(ENV_RUSTFS_S3SELECT_QUERY_TIMEOUT_SECS).ok().as_deref(), DEFAULT_QUERY_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS, )) } fn target_partitions_from_env_value(value: Option<&str>) -> usize { value.and_then(|value| value.parse::().ok()).unwrap_or(0) } fn bounded_usize_from_env_value(value: Option<&str>, default: usize, max: usize) -> usize { value .and_then(|value| value.parse::().ok()) .filter(|value| (1..=max).contains(value)) .unwrap_or(default) } fn bounded_u64_from_env_value(value: Option<&str>, default: u64, max: u64) -> u64 { value .and_then(|value| value.parse::().ok()) .filter(|value| (1..=max).contains(value)) .unwrap_or(default) } fn query_admission(is_test: bool) -> Arc { if is_test { Arc::new(Semaphore::new(TEST_MAX_CONCURRENT_QUERIES)) } else { Arc::clone(&QUERY_ADMISSION) } } pub async fn make_rustfsms(input: Arc, is_test: bool) -> QueryResult { // init Function Manager, we can define some UDF if need let func_manager = SimpleFunctionMetadataManager::default(); let runtime_config = S3SelectRuntimeConfig::from_env(); let session_factory = Arc::new(SessionCtxFactory::new(is_test).with_target_partitions(runtime_config.target_partitions)); let parser = Arc::new(DefaultParser::default()); let optimizer = Arc::new(CascadeOptimizerBuilder::default().build()); let scheduler = Arc::new(LocalScheduler {}); let query_execution_factory = Arc::new(SqlQueryExecutionFactory::new(optimizer, scheduler)); let default_table_provider = Arc::new(BaseTableProvider::default()); let query_dispatcher = SimpleQueryDispatcherBuilder::default() .with_input(input) .with_func_manager(Arc::new(func_manager)) .with_default_table_provider(default_table_provider) .with_session_factory(session_factory) .with_memory_limit_bytes(runtime_config.memory_limit_bytes) .with_parser(parser) .with_query_execution_factory(query_execution_factory) .with_query_admission(query_admission(is_test)) .with_query_timeout(runtime_config.query_timeout) .build()?; let mut builder = RustFSmsBuilder::default(); let db_server = builder.query_dispatcher(query_dispatcher).build().expect("build db server"); Ok(db_server) } pub async fn make_rustfsms_with_components( input: Arc, is_test: bool, func_manager: Arc, parser: Arc, query_execution_factory: Arc, default_table_provider: Arc, ) -> QueryResult { let runtime_config = S3SelectRuntimeConfig::from_env(); let session_factory = Arc::new(SessionCtxFactory::new(is_test).with_target_partitions(runtime_config.target_partitions)); let query_dispatcher = SimpleQueryDispatcherBuilder::default() .with_input(input) .with_func_manager(func_manager) .with_default_table_provider(default_table_provider) .with_session_factory(session_factory) .with_memory_limit_bytes(runtime_config.memory_limit_bytes) .with_parser(parser) .with_query_execution_factory(query_execution_factory) .with_query_admission(query_admission(is_test)) .with_query_timeout(runtime_config.query_timeout) .build()?; let mut builder = RustFSmsBuilder::default(); let db_server = builder.query_dispatcher(query_dispatcher).build().expect("build db server"); Ok(db_server) } #[cfg(test)] mod tests { use std::sync::Arc; use async_trait::async_trait; use datafusion::{arrow::util::pretty, assert_batches_eq}; use parking_lot::Mutex; use rustfs_s3select_api::{ QueryResult, SelectInputMetrics, query::{ Context, Query, dispatcher::QueryDispatcher, execution::{Output, QueryStateMachine}, logical_planner::Plan, }, server::dbms::DatabaseManagerSystem, }; use s3s::dto::{ CSVInput, CSVOutput, ExpressionType, FieldDelimiter, FileHeaderInfo, InputSerialization, OutputSerialization, RecordDelimiter, SelectObjectContentInput, SelectObjectContentRequest, }; use crate::get_global_db; use super::{ DEFAULT_MAX_CONCURRENT_QUERIES, DEFAULT_MEMORY_LIMIT_BYTES, DEFAULT_QUERY_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS, RustFSms, S3SelectRuntimeConfig, bounded_u64_from_env_value, bounded_usize_from_env_value, target_partitions_from_env_value, }; #[derive(Default)] struct FreshMetricsDispatcher { executed_metrics: Mutex>>, } #[async_trait] impl QueryDispatcher for FreshMetricsDispatcher { async fn execute_query(&self, query: &Query) -> QueryResult { self.executed_metrics.lock().push(Arc::clone(query.input_metrics())); Ok(Output::Nil(())) } async fn build_logical_plan(&self, _query_state_machine: Arc) -> QueryResult> { unreachable!("fresh metrics test does not plan queries") } async fn execute_logical_plan( &self, _logical_plan: Plan, _query_state_machine: Arc, ) -> QueryResult { unreachable!("fresh metrics test does not execute plans") } async fn build_query_state_machine(&self, _query: Query) -> QueryResult> { unreachable!("fresh metrics test does not build state machines") } } fn metrics_test_query() -> Query { let expression = "SELECT * FROM S3Object"; let input = SelectObjectContentInput { bucket: "bucket".to_string(), expected_bucket_owner: None, key: "input.csv".to_string(), sse_customer_algorithm: None, sse_customer_key: None, sse_customer_key_md5: None, request: SelectObjectContentRequest { expression: expression.to_string(), expression_type: ExpressionType::from_static(ExpressionType::SQL), input_serialization: InputSerialization { csv: Some(CSVInput::default()), ..Default::default() }, output_serialization: OutputSerialization { csv: Some(CSVOutput::default()), ..Default::default() }, request_progress: None, scan_range: None, }, }; Query::new(Context { input: Arc::new(input) }, expression.to_string()) } #[test] fn parses_target_partitions_from_env_value() { assert_eq!(target_partitions_from_env_value(Some("4")), 4); assert_eq!(target_partitions_from_env_value(Some("0")), 0); assert_eq!(target_partitions_from_env_value(Some("not-a-number")), 0); assert_eq!(target_partitions_from_env_value(None), 0); } #[test] fn default_runtime_config_uses_datafusion_default_partitions() { let config = S3SelectRuntimeConfig::default(); assert_eq!(config.target_partitions, 0); assert_eq!(config.memory_limit_bytes, DEFAULT_MEMORY_LIMIT_BYTES); assert_eq!(config.query_timeout.as_secs(), DEFAULT_QUERY_TIMEOUT_SECS); assert_eq!(config.max_concurrent_queries, DEFAULT_MAX_CONCURRENT_QUERIES); } #[test] fn resource_limits_reject_invalid_and_out_of_range_values() { assert_eq!(bounded_usize_from_env_value(Some("1024"), 64, 2048), 1024); assert_eq!(bounded_usize_from_env_value(Some("0"), 64, 2048), 64); assert_eq!(bounded_usize_from_env_value(Some("4096"), 64, 2048), 64); assert_eq!(bounded_usize_from_env_value(Some("invalid"), 64, 2048), 64); assert_eq!(bounded_u64_from_env_value(Some("30"), 300, MAX_QUERY_TIMEOUT_SECS), 30); assert_eq!(bounded_u64_from_env_value(Some("0"), 300, MAX_QUERY_TIMEOUT_SECS), 300); assert_eq!(bounded_u64_from_env_value(Some("86401"), 300, MAX_QUERY_TIMEOUT_SECS), 300); assert_eq!(bounded_u64_from_env_value(None, 300, MAX_QUERY_TIMEOUT_SECS), 300); } #[tokio::test] async fn repeated_execute_returns_the_fresh_dispatched_query_metrics() { let dispatcher = Arc::new(FreshMetricsDispatcher::default()); let db = RustFSms { query_dispatcher: Arc::clone(&dispatcher), }; let query = metrics_test_query(); let first = db.execute(&query).await.expect("first execution should succeed"); let second = db.execute(&query).await.expect("second execution should succeed"); let executed_metrics = dispatcher.executed_metrics.lock(); assert_eq!(executed_metrics.len(), 2); assert!(Arc::ptr_eq(first.query().input_metrics(), &executed_metrics[0])); assert!(Arc::ptr_eq(second.query().input_metrics(), &executed_metrics[1])); assert!(!Arc::ptr_eq(first.query().input_metrics(), second.query().input_metrics())); assert!(!Arc::ptr_eq(first.query().input_metrics(), query.input_metrics())); assert!(!Arc::ptr_eq(second.query().input_metrics(), query.input_metrics())); } #[tokio::test] #[ignore = "requires a live RustFS store with a pre-seeded test object (bucket 'dandan')"] async fn test_simple_sql() { let sql = "select * from S3Object"; let input = SelectObjectContentInput { bucket: "dandan".to_string(), expected_bucket_owner: None, key: "test.csv".to_string(), sse_customer_algorithm: None, sse_customer_key: None, sse_customer_key_md5: None, request: SelectObjectContentRequest { expression: sql.to_string(), expression_type: ExpressionType::from_static("SQL"), input_serialization: InputSerialization { csv: Some(CSVInput { file_header_info: Some(FileHeaderInfo::from_static(FileHeaderInfo::USE)), ..Default::default() }), ..Default::default() }, output_serialization: OutputSerialization { csv: Some(CSVOutput::default()), ..Default::default() }, request_progress: None, scan_range: None, }, }; let db = get_global_db(input.clone(), true).await.expect("operation should succeed"); let query = Query::new(Context { input: Arc::new(input) }, sql.to_string()); let result = db.execute(&query).await.expect("operation should succeed"); let results = result .result() .chunk_result() .await .expect("operation should succeed") .to_vec(); let expected = [ "+----+---------+-----+------------+--------+", "| id | name | age | department | salary |", "+----+---------+-----+------------+--------+", "| 1 | Alice | 25 | HR | 05000 |", "| 2 | Bob | 30 | IT | 6000 |", "| 3 | Charlie | 35 | Finance | 7000 |", "| 4 | Diana | 22 | Marketing | 4500 |", "| 5 | Eve | 28 | IT | 5500 |", "| 6 | Frank | 40 | Finance | 8000 |", "| 7 | Grace | 26 | HR | 5200 |", "| 8 | Henry | 32 | IT | 6200 |", "| 9 | Ivy | 24 | Marketing | 4800 |", "| 10 | Jack | 38 | Finance | 7500 |", "+----+---------+-----+------------+--------+", ]; assert_batches_eq!(expected, &results); pretty::print_batches(&results).expect("operation should succeed"); } #[tokio::test] #[ignore = "requires a live RustFS store with a pre-seeded test object (bucket 'dandan')"] async fn test_func_sql() { let sql = "SELECT * FROM S3Object s"; let input = SelectObjectContentInput { bucket: "dandan".to_string(), expected_bucket_owner: None, key: "test.csv".to_string(), sse_customer_algorithm: None, sse_customer_key: None, sse_customer_key_md5: None, request: SelectObjectContentRequest { expression: sql.to_string(), expression_type: ExpressionType::from_static("SQL"), input_serialization: InputSerialization { csv: Some(CSVInput { file_header_info: Some(FileHeaderInfo::from_static(FileHeaderInfo::IGNORE)), field_delimiter: Some(FieldDelimiter::from("╦")), record_delimiter: Some(RecordDelimiter::from("\n")), ..Default::default() }), ..Default::default() }, output_serialization: OutputSerialization { csv: Some(CSVOutput::default()), ..Default::default() }, request_progress: None, scan_range: None, }, }; let db = get_global_db(input.clone(), true).await.expect("operation should succeed"); let query = Query::new(Context { input: Arc::new(input) }, sql.to_string()); let result = db.execute(&query).await.expect("operation should succeed"); let results = result .result() .chunk_result() .await .expect("operation should succeed") .to_vec(); pretty::print_batches(&results).expect("operation should succeed"); } }