Skip to content

Commit d9de7fb

Browse files
authored
feat: throttling for query (#1766)
* feat: throttling for query Incoming query will stay in queue till the server's memory is under a set threshold. The timeout is set to 10s, failing faster is better than staying in limbo. * coderabbit suggestions
1 parent d9747e8 commit d9de7fb

4 files changed

Lines changed: 79 additions & 9 deletions

File tree

‎src/cli.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,10 +451,20 @@ pub struct Options {
451451
long,
452452
long = "query-mempool-size",
453453
env = "P_QUERY_MEMORY_LIMIT",
454-
help = "Set a fixed memory limit for query in GiB"
454+
help = "Set a fixed memory limit for query in Bytes"
455455
)]
456456
pub query_memory_pool_size: Option<usize>,
457457

458+
#[arg(
459+
long,
460+
long = "query-mem-threshold",
461+
value_parser = validation::validate_percentage,
462+
default_value = "80.0",
463+
env = "P_QUERY_MEMORY_THRESHOLD",
464+
help = "Set a threshold (percentage) for memory beyond which query will get queued for 10s to prevent OOM"
465+
)]
466+
pub query_mem_threshold: f32,
467+
458468
#[arg(
459469
long,
460470
env = "P_PARQUET_METADATA_CACHE_SIZE",

‎src/handlers/http/query.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,8 @@ Description: {0}"#
730730
impl actix_web::ResponseError for QueryError {
731731
fn status_code(&self) -> StatusCode {
732732
match self {
733-
QueryError::Execute(_) | QueryError::JsonParse(_) => StatusCode::INTERNAL_SERVER_ERROR,
733+
QueryError::JsonParse(_) => StatusCode::INTERNAL_SERVER_ERROR,
734+
QueryError::Execute(e) => e.status_code(),
734735
QueryError::MetastoreError(e) => e.status_code(),
735736
_ => StatusCode::BAD_REQUEST,
736737
}

‎src/handlers/http/resource_check.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ pub fn spawn_resource_monitor(shutdown_rx: tokio::sync::oneshot::Receiver<()>) {
6363
refresh_sys_info();
6464
let (used_memory, total_memory, cpu_usage) = tokio::task::spawn_blocking(|| {
6565
let sys = SYS_INFO.lock().unwrap();
66-
let used_memory = sys.used_memory() as f32;
67-
let total_memory = sys.total_memory() as f32;
66+
let (used_memory, total_memory) = if let Some(cgroup) = sys.cgroup_limits() {
67+
(cgroup.rss as f32,cgroup.total_memory as f32)
68+
} else {
69+
(sys.used_memory() as f32,sys.total_memory() as f32)
70+
};
6871
let cpu_usage = sys.global_cpu_usage();
6972
(used_memory, total_memory, cpu_usage)
7073
}).await.unwrap();

‎src/query/mod.rs‎

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ use std::pin::Pin;
5353
use std::sync::atomic::{AtomicUsize, Ordering};
5454
use std::sync::{Arc, RwLock};
5555
use std::task::{Context, Poll};
56-
use sysinfo::System;
56+
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
5757
use tokio::runtime::Runtime;
5858
use tokio_stream::wrappers::UnboundedReceiverStream;
5959
use tracing::Instrument;
@@ -82,9 +82,6 @@ type BoxedBatchStream = SendableRecordBatchStream;
8282
type QueryResult = Result<(Either<Vec<RecordBatch>, BoxedBatchStream>, Vec<String>), ExecuteError>;
8383
const DEFAULT_COUNTS_TOP_K: usize = 10;
8484

85-
// pub static QUERY_SESSION: Lazy<SessionContext> =
86-
// Lazy::new(|| Query::create_session_context(PARSEABLE.storage()));
87-
8885
pub static QUERY_SESSION_STATE: Lazy<SessionState> =
8986
Lazy::new(|| Query::create_session_state(PARSEABLE.storage()));
9087

@@ -147,10 +144,42 @@ impl InMemorySessionContext {
147144
}
148145
}
149146

147+
async fn enough_available_memory() -> Result<(), ExecuteError> {
148+
let mut s = System::new_with_specifics(
149+
RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
150+
);
151+
s.refresh_all();
152+
let threshold = (PARSEABLE.options.query_mem_threshold / 100.0) as f64;
153+
154+
let f = async {
155+
loop {
156+
if let Some(cgroup) = s.cgroup_limits() {
157+
if (cgroup.rss as f64) < threshold * (cgroup.total_memory as f64) {
158+
return;
159+
}
160+
} else {
161+
s.refresh_memory();
162+
if (s.used_memory() as f64) < threshold * (s.total_memory() as f64) {
163+
return;
164+
}
165+
}
166+
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
167+
}
168+
};
169+
if let Err(e) = tokio::time::timeout(tokio::time::Duration::from_secs(10), f).await {
170+
Err(ExecuteError::ServerBusy(e))
171+
} else {
172+
Ok(())
173+
}
174+
}
175+
150176
/// This function executes a query on the dedicated runtime, ensuring that the query is not isolated to a single thread/CPU
151177
/// at a time and has access to the entire thread pool, enabling better concurrent processing, and thus quicker results.
152178
pub async fn execute(query: Query, is_streaming: bool, tenant_id: &Option<String>) -> QueryResult {
153179
let id = tenant_id.clone();
180+
181+
// before executing query, check whether enough memory is available or not
182+
enough_available_memory().await?;
154183
QUERY_RUNTIME
155184
.spawn(async move {
156185
tokio::time::timeout(
@@ -220,7 +249,11 @@ impl Query {
220249
None => {
221250
let mut system = System::new();
222251
system.refresh_memory();
223-
let available_mem = system.available_memory();
252+
let available_mem = if let Some(cgroup) = system.cgroup_limits() {
253+
cgroup.total_memory
254+
} else {
255+
system.total_memory()
256+
};
224257
(available_mem as usize, 0.85)
225258
}
226259
};
@@ -270,6 +303,8 @@ impl Query {
270303
.parquet
271304
.schema_force_view_types = true;
272305

306+
config.options_mut().explain.show_statistics = true;
307+
273308
SessionStateBuilder::new()
274309
.with_default_features()
275310
.with_config(config)
@@ -958,6 +993,7 @@ pub fn flatten_objects_for_count(objects: Vec<Value>) -> Vec<Value> {
958993

959994
pub mod error {
960995
use crate::{parseable::StreamNotFound, storage::ObjectStorageError};
996+
use actix_web::http::{StatusCode, header::ContentType};
961997
use datafusion::error::DataFusionError;
962998
use tokio::time::error::Elapsed;
963999

@@ -971,6 +1007,26 @@ pub mod error {
9711007
StreamNotFound(#[from] StreamNotFound),
9721008
#[error("{0}: {1}s")]
9731009
Timeout(Elapsed, u64),
1010+
#[error("{0}: Not enough memory available to serve the request")]
1011+
ServerBusy(Elapsed),
1012+
}
1013+
1014+
impl actix_web::ResponseError for ExecuteError {
1015+
fn status_code(&self) -> StatusCode {
1016+
match self {
1017+
ExecuteError::ObjectStorage(_) => StatusCode::INTERNAL_SERVER_ERROR,
1018+
ExecuteError::Datafusion(_) => StatusCode::INTERNAL_SERVER_ERROR,
1019+
ExecuteError::StreamNotFound(_) => StatusCode::NOT_FOUND,
1020+
ExecuteError::Timeout(_, _) => StatusCode::REQUEST_TIMEOUT,
1021+
ExecuteError::ServerBusy(_) => StatusCode::SERVICE_UNAVAILABLE,
1022+
}
1023+
}
1024+
1025+
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
1026+
actix_web::HttpResponse::build(self.status_code())
1027+
.insert_header(ContentType::plaintext())
1028+
.body(self.to_string())
1029+
}
9741030
}
9751031
}
9761032

0 commit comments

Comments
 (0)