Skip to main content

servers/grpc/
greptime_handler.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
15//! Handler for Greptime Database service. It's implemented by frontend.
16
17use std::collections::HashMap;
18use std::future::Future;
19use std::str::FromStr;
20use std::sync::{Arc, RwLock};
21use std::time::Instant;
22
23use api::helper::request_type;
24use api::v1::greptime_request::Request as QueryRequest;
25use api::v1::{GreptimeRequest, RequestHeader};
26use auth::UserProviderRef;
27use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
28use common_catalog::parse_catalog_and_schema_from_db_string;
29use common_error::ext::ErrorExt;
30use common_error::status_code::StatusCode;
31use common_grpc::flight::do_put::DoPutResponse;
32use common_query::Output;
33use common_runtime::Runtime;
34use common_runtime::runtime::RuntimeTrait;
35use common_session::ReadPreference;
36use common_telemetry::tracing_context::{FutureExt, TracingContext};
37use common_telemetry::{debug, error, tracing, warn};
38use common_time::timezone::parse_timezone;
39use futures_util::StreamExt;
40use session::context::{Channel, QueryContextBuilder, QueryContextRef};
41use session::hints::{READ_PREFERENCE_HINT, is_reserved_extension_key};
42use snafu::{OptionExt, ResultExt};
43use tokio::sync::mpsc;
44use tokio::sync::mpsc::error::TrySendError;
45use tonic::Status;
46
47use crate::error::{InvalidQuerySnafu, JoinTaskSnafu, Result, UnknownHintSnafu};
48use crate::grpc::flight::PutRecordBatchRequestStream;
49use crate::grpc::{FlightCompression, TonicResult, context_auth};
50use crate::metrics::{self, METRIC_SERVER_GRPC_DB_REQUEST_TIMER};
51use crate::query_handler::grpc::ServerGrpcQueryHandlerRef;
52
53#[derive(Clone)]
54pub struct GreptimeRequestHandler {
55    handler: ServerGrpcQueryHandlerRef,
56    pub(crate) user_provider: Option<UserProviderRef>,
57    runtime: Option<Runtime>,
58    pub(crate) flight_compression: FlightCompression,
59}
60
61impl GreptimeRequestHandler {
62    pub fn new(
63        handler: ServerGrpcQueryHandlerRef,
64        user_provider: Option<UserProviderRef>,
65        runtime: Option<Runtime>,
66        flight_compression: FlightCompression,
67    ) -> Self {
68        Self {
69            handler,
70            user_provider,
71            runtime,
72            flight_compression,
73        }
74    }
75
76    #[tracing::instrument(skip_all, fields(protocol = "grpc", request_type = get_request_type(&request)))]
77    pub(crate) async fn handle_request(
78        &self,
79        request: GreptimeRequest,
80        hints: Vec<(String, String)>,
81    ) -> Result<Output> {
82        let header = request.header.as_ref();
83        let query_ctx = create_query_context(Channel::Grpc, header, hints, HashMap::new())?;
84        let query = request.request.context(InvalidQuerySnafu {
85            reason: "Expecting non-empty GreptimeRequest.",
86        })?;
87        self.authenticate_request_with_query_ctx(request.header.as_ref(), &query_ctx)
88            .await?;
89        self.handle_request_with_query_ctx(query, query_ctx).await
90    }
91
92    pub(crate) async fn authenticate_request_with_query_ctx(
93        &self,
94        header: Option<&RequestHeader>,
95        query_ctx: &QueryContextRef,
96    ) -> Result<()> {
97        let user_info = context_auth::auth(self.user_provider.clone(), header, query_ctx).await?;
98        query_ctx.set_current_user(user_info);
99        Ok(())
100    }
101
102    pub(crate) fn handle_request_with_query_ctx(
103        &self,
104        query: QueryRequest,
105        query_ctx: QueryContextRef,
106    ) -> impl Future<Output = Result<Output>> + Send + 'static {
107        let handler = self.handler.clone();
108        let runtime = self.runtime.clone();
109        let request_type = request_type(&query).to_string();
110        let db = query_ctx.get_db_string();
111        let timer = RequestTimer::new(db.clone(), request_type);
112        let tracing_context = TracingContext::from_current_span();
113
114        async move {
115            let result_future = async move {
116                handler
117                .do_query(query, query_ctx)
118                .trace(tracing_context.attach(tracing::info_span!(
119                    "GreptimeRequestHandler::handle_request_runtime"
120                )))
121                .await
122                .map_err(|e| {
123                    if e.status_code().should_log_error() {
124                        let root_error = e.root_cause().unwrap_or(&e);
125                        error!(e; "Failed to handle request, error: {}", root_error.to_string());
126                    } else {
127                        // Currently, we still print a debug log.
128                        debug!("Failed to handle request, err: {:?}", e);
129                    }
130                    e
131                })
132            };
133
134            match runtime {
135                Some(runtime) => {
136                    // Executes requests in another runtime to
137                    // 1. prevent the execution from being cancelled unexpected by Tonic runtime;
138                    //   - Refer to our blog for the rational behind it:
139                    //     https://www.greptime.com/blogs/2023-01-12-hidden-control-flow.html
140                    //   - Obtaining a `JoinHandle` to get the panic message (if there's any).
141                    //     From its docs, `JoinHandle` is cancel safe. The task keeps running even it's handle been dropped.
142                    // 2. avoid the handler blocks the gRPC runtime incidentally.
143                    runtime
144                        .spawn(result_future)
145                        .await
146                        .context(JoinTaskSnafu)
147                        .inspect_err(|e| {
148                            timer.record(e.status_code());
149                        })?
150                }
151                None => result_future.await,
152            }
153        }
154    }
155
156    pub(crate) async fn put_record_batches(
157        &self,
158        stream: PutRecordBatchRequestStream,
159        result_sender: mpsc::Sender<TonicResult<DoPutResponse>>,
160        query_ctx: QueryContextRef,
161    ) {
162        let handler = self.handler.clone();
163        let runtime = self
164            .runtime
165            .clone()
166            .unwrap_or_else(common_runtime::global_runtime);
167        runtime.spawn(async move {
168            let mut result_stream = handler.handle_put_record_batch_stream(stream, query_ctx);
169
170            while let Some(result) = result_stream.next().await {
171                match &result {
172                    Ok(response) => {
173                        // Record the elapsed time metric from the response
174                        metrics::GRPC_BULK_INSERT_ELAPSED.observe(response.elapsed_secs());
175                    }
176                    Err(e) => {
177                        error!(e; "Failed to handle flight record batches");
178                    }
179                }
180
181                if let Err(e) = result_sender.try_send(result.map_err(Status::from))
182                    && let TrySendError::Closed(_) = e
183                {
184                    warn!(r#""DoPut" client maybe unreachable, abort handling its message"#);
185                    break;
186                }
187            }
188        });
189    }
190}
191
192pub fn get_request_type(request: &GreptimeRequest) -> &'static str {
193    request
194        .request
195        .as_ref()
196        .map(request_type)
197        .unwrap_or_default()
198}
199
200/// Creates a new `QueryContext` from the provided request header and extensions.
201/// Strongly recommend setting an appropriate channel, as this is very helpful for statistics.
202pub(crate) fn create_query_context(
203    channel: Channel,
204    header: Option<&RequestHeader>,
205    mut extensions: Vec<(String, String)>,
206    snapshot_seqs: HashMap<u64, u64>,
207) -> Result<QueryContextRef> {
208    let (catalog, schema) = header
209        .map(|header| {
210            // We provide dbname field in newer versions of protos/sdks
211            // parse dbname from header in priority
212            if !header.dbname.is_empty() {
213                parse_catalog_and_schema_from_db_string(&header.dbname)
214            } else {
215                (
216                    if !header.catalog.is_empty() {
217                        header.catalog.to_lowercase()
218                    } else {
219                        DEFAULT_CATALOG_NAME.to_string()
220                    },
221                    if !header.schema.is_empty() {
222                        header.schema.to_lowercase()
223                    } else {
224                        DEFAULT_SCHEMA_NAME.to_string()
225                    },
226                )
227            }
228        })
229        .unwrap_or_else(|| {
230            (
231                DEFAULT_CATALOG_NAME.to_string(),
232                DEFAULT_SCHEMA_NAME.to_string(),
233            )
234        });
235    let timezone = parse_timezone(header.map(|h| h.timezone.as_str()));
236    let mut ctx_builder = QueryContextBuilder::default()
237        .current_catalog(catalog)
238        .current_schema(schema)
239        .timezone(timezone)
240        .channel(channel)
241        .snapshot_seqs(Arc::new(RwLock::new(snapshot_seqs)));
242
243    if let Some(x) = extensions
244        .iter()
245        .position(|(k, _)| k == READ_PREFERENCE_HINT)
246    {
247        let (k, v) = extensions.swap_remove(x);
248        let Ok(read_preference) = ReadPreference::from_str(&v) else {
249            return UnknownHintSnafu {
250                hint: format!("{k}={v}"),
251            }
252            .fail();
253        };
254        ctx_builder = ctx_builder.read_preference(read_preference);
255    }
256
257    for (key, value) in extensions {
258        if is_reserved_extension_key(&key) {
259            debug!(
260                key = key.as_str(),
261                "Ignoring reserved external query context extension key"
262            );
263            continue;
264        }
265        ctx_builder = ctx_builder.set_extension(key, value);
266    }
267    Ok(ctx_builder.build().into())
268}
269
270/// Histogram timer for handling gRPC request.
271///
272/// The timer records the elapsed time with [StatusCode::Success] on drop.
273pub(crate) struct RequestTimer {
274    start: Instant,
275    db: String,
276    request_type: String,
277    status_code: StatusCode,
278}
279
280impl RequestTimer {
281    /// Returns a new timer.
282    pub fn new(db: String, request_type: String) -> RequestTimer {
283        RequestTimer {
284            start: Instant::now(),
285            db,
286            request_type,
287            status_code: StatusCode::Success,
288        }
289    }
290
291    /// Consumes the timer and record the elapsed time with specific `status_code`.
292    pub fn record(mut self, status_code: StatusCode) {
293        self.status_code = status_code;
294    }
295}
296
297impl Drop for RequestTimer {
298    fn drop(&mut self) {
299        METRIC_SERVER_GRPC_DB_REQUEST_TIMER
300            .with_label_values(&[
301                self.db.as_str(),
302                self.request_type.as_str(),
303                self.status_code.as_ref(),
304            ])
305            .observe(self.start.elapsed().as_secs_f64());
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use chrono::FixedOffset;
312    use common_error::ext::BoxedError;
313    use common_error::{GREPTIME_DB_HEADER_ERROR_CODE, GREPTIME_DB_HEADER_ERROR_RETRY_HINT};
314    use common_time::Timezone;
315    use query::options::FLOW_SCHEDULED_TIME_MILLIS;
316    use session::hints::{
317        INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY, REMOTE_QUERY_ID_EXTENSION_KEY,
318    };
319    use snafu::ResultExt;
320    use tonic::Code;
321
322    use super::*;
323    use crate::error::{ExecuteGrpcRequestSnafu, InvalidParameterSnafu};
324
325    #[test]
326    fn test_create_query_context() {
327        let header = RequestHeader {
328            catalog: "cat-a-log".to_string(),
329            timezone: "+01:00".to_string(),
330            ..Default::default()
331        };
332        let query_context = create_query_context(
333            Channel::Unknown,
334            Some(&header),
335            vec![
336                ("auto_create_table".to_string(), "true".to_string()),
337                ("read_preference".to_string(), "leader".to_string()),
338                (
339                    REMOTE_QUERY_ID_EXTENSION_KEY.to_string(),
340                    "spoofed".to_string(),
341                ),
342                (
343                    INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY.to_string(),
344                    "spoofed-regs".to_string(),
345                ),
346                (
347                    FLOW_SCHEDULED_TIME_MILLIS.to_string(),
348                    "1700000000000".to_string(),
349                ),
350            ],
351            HashMap::from([(7, 88)]),
352        )
353        .unwrap();
354        assert_eq!(query_context.get_snapshot(7), Some(88));
355        assert_eq!(query_context.current_catalog(), "cat-a-log");
356        assert_eq!(query_context.current_schema(), DEFAULT_SCHEMA_NAME);
357        assert_eq!(
358            query_context.timezone(),
359            Timezone::Offset(FixedOffset::east_opt(3600).unwrap())
360        );
361        assert!(matches!(
362            query_context.read_preference(),
363            ReadPreference::Leader
364        ));
365        assert_eq!(query_context.extension("auto_create_table"), Some("true"));
366        assert_ne!(query_context.remote_query_id(), Some("spoofed"));
367        assert!(
368            query_context
369                .extension(INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY)
370                .is_none()
371        );
372        assert_eq!(
373            query_context.extension(FLOW_SCHEDULED_TIME_MILLIS),
374            Some("1700000000000")
375        );
376    }
377
378    #[test]
379    fn test_create_query_context_ignores_remote_query_id_extension() {
380        let query_context = create_query_context(
381            Channel::Grpc,
382            None,
383            vec![(
384                REMOTE_QUERY_ID_EXTENSION_KEY.to_string(),
385                "spoofed-query-id".to_string(),
386            )],
387            HashMap::new(),
388        )
389        .unwrap();
390
391        assert_ne!(query_context.remote_query_id(), Some("spoofed-query-id"));
392        assert_eq!(
393            query_context.extension(REMOTE_QUERY_ID_EXTENSION_KEY),
394            query_context.remote_query_id()
395        );
396    }
397
398    #[test]
399    fn test_record_batch_error_to_status_preserves_error_details() {
400        let inner = InvalidParameterSnafu {
401            reason: "Column not found, column: new_col",
402        }
403        .build();
404        let err = Err::<(), _>(BoxedError::new(inner))
405            .context(ExecuteGrpcRequestSnafu)
406            .unwrap_err();
407
408        let status = Status::from(err);
409
410        assert_eq!(status.code(), Code::InvalidArgument);
411        assert!(
412            status
413                .message()
414                .contains("Column not found, column: new_col")
415        );
416        assert!(
417            status
418                .message()
419                .contains("Invalid request parameter: Column not found")
420        );
421        assert!(
422            status
423                .metadata()
424                .contains_key(GREPTIME_DB_HEADER_ERROR_CODE)
425        );
426        assert!(
427            status
428                .metadata()
429                .contains_key(GREPTIME_DB_HEADER_ERROR_RETRY_HINT)
430        );
431    }
432}