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::{INSERT_SKIP_WAL_HINT, 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        channel: Channel,
82    ) -> Result<Output> {
83        let header = request.header.as_ref();
84        let hints = crate::hint_headers::validate_public_hints(hints)?;
85        let query_ctx = create_query_context(channel, header, hints, HashMap::new())?;
86        let query = request.request.context(InvalidQuerySnafu {
87            reason: "Expecting non-empty GreptimeRequest.",
88        })?;
89        self.authenticate_request_with_query_ctx(request.header.as_ref(), &query_ctx)
90            .await?;
91        self.handle_request_with_query_ctx(query, query_ctx).await
92    }
93
94    pub(crate) async fn authenticate_request_with_query_ctx(
95        &self,
96        header: Option<&RequestHeader>,
97        query_ctx: &QueryContextRef,
98    ) -> Result<()> {
99        let user_info = context_auth::auth(self.user_provider.clone(), header, query_ctx).await?;
100        query_ctx.set_current_user(user_info);
101        Ok(())
102    }
103
104    pub(crate) fn handle_request_with_query_ctx(
105        &self,
106        query: QueryRequest,
107        query_ctx: QueryContextRef,
108    ) -> impl Future<Output = Result<Output>> + Send + 'static {
109        let handler = self.handler.clone();
110        let runtime = self.runtime.clone();
111        let request_type = request_type(&query).to_string();
112        let db = query_ctx.get_db_string();
113        let timer = RequestTimer::new(db.clone(), request_type);
114        let tracing_context = TracingContext::from_current_span();
115
116        async move {
117            let result_future = async move {
118                handler
119                .do_query(query, query_ctx)
120                .trace(tracing_context.attach(tracing::info_span!(
121                    "GreptimeRequestHandler::handle_request_runtime"
122                )))
123                .await
124                .map_err(|e| {
125                    if e.status_code().should_log_error() {
126                        let root_error = e.root_cause().unwrap_or(&e);
127                        error!(e; "Failed to handle request, error: {}", root_error.to_string());
128                    } else {
129                        // Currently, we still print a debug log.
130                        debug!("Failed to handle request, err: {:?}", e);
131                    }
132                    e
133                })
134            };
135
136            match runtime {
137                Some(runtime) => {
138                    // Executes requests in another runtime to
139                    // 1. prevent the execution from being cancelled unexpected by Tonic runtime;
140                    //   - Refer to our blog for the rational behind it:
141                    //     https://www.greptime.com/blogs/2023-01-12-hidden-control-flow.html
142                    //   - Obtaining a `JoinHandle` to get the panic message (if there's any).
143                    //     From its docs, `JoinHandle` is cancel safe. The task keeps running even it's handle been dropped.
144                    // 2. avoid the handler blocks the gRPC runtime incidentally.
145                    runtime
146                        .spawn(result_future)
147                        .await
148                        .context(JoinTaskSnafu)
149                        .inspect_err(|e| {
150                            timer.record(e.status_code());
151                        })?
152                }
153                None => result_future.await,
154            }
155        }
156    }
157
158    pub(crate) async fn put_record_batches(
159        &self,
160        stream: PutRecordBatchRequestStream,
161        result_sender: mpsc::Sender<TonicResult<DoPutResponse>>,
162        query_ctx: QueryContextRef,
163    ) {
164        let handler = self.handler.clone();
165        let runtime = self
166            .runtime
167            .clone()
168            .unwrap_or_else(common_runtime::global_runtime);
169        runtime.spawn(async move {
170            let mut result_stream = handler.handle_put_record_batch_stream(stream, query_ctx);
171
172            while let Some(result) = result_stream.next().await {
173                match &result {
174                    Ok(response) => {
175                        // Record the elapsed time metric from the response
176                        metrics::GRPC_BULK_INSERT_ELAPSED.observe(response.elapsed_secs());
177                    }
178                    Err(e) => {
179                        error!(e; "Failed to handle flight record batches");
180                    }
181                }
182
183                if let Err(e) = result_sender.try_send(result.map_err(Status::from))
184                    && let TrySendError::Closed(_) = e
185                {
186                    warn!(r#""DoPut" client maybe unreachable, abort handling its message"#);
187                    break;
188                }
189            }
190        });
191    }
192}
193
194pub fn get_request_type(request: &GreptimeRequest) -> &'static str {
195    request
196        .request
197        .as_ref()
198        .map(request_type)
199        .unwrap_or_default()
200}
201
202/// Creates a new `QueryContext` from the provided request header and extensions.
203/// Strongly recommend setting an appropriate channel, as this is very helpful for statistics.
204pub(crate) fn create_query_context(
205    channel: Channel,
206    header: Option<&RequestHeader>,
207    extensions: Vec<(String, String)>,
208    snapshot_seqs: HashMap<u64, u64>,
209) -> Result<QueryContextRef> {
210    let (catalog, schema) = header
211        .map(|header| {
212            // We provide dbname field in newer versions of protos/sdks
213            // parse dbname from header in priority
214            if !header.dbname.is_empty() {
215                parse_catalog_and_schema_from_db_string(&header.dbname)
216            } else {
217                (
218                    if !header.catalog.is_empty() {
219                        header.catalog.to_lowercase()
220                    } else {
221                        DEFAULT_CATALOG_NAME.to_string()
222                    },
223                    if !header.schema.is_empty() {
224                        header.schema.to_lowercase()
225                    } else {
226                        DEFAULT_SCHEMA_NAME.to_string()
227                    },
228                )
229            }
230        })
231        .unwrap_or_else(|| {
232            (
233                DEFAULT_CATALOG_NAME.to_string(),
234                DEFAULT_SCHEMA_NAME.to_string(),
235            )
236        });
237    let timezone = parse_timezone(header.map(|h| h.timezone.as_str()));
238    let mut ctx_builder = QueryContextBuilder::default()
239        .current_catalog(catalog)
240        .current_schema(schema)
241        .timezone(timezone)
242        .channel(channel)
243        .snapshot_seqs(Arc::new(RwLock::new(snapshot_seqs)));
244
245    for (key, value) in extensions {
246        match key.as_str() {
247            READ_PREFERENCE_HINT => {
248                let Ok(read_preference) = ReadPreference::from_str(&value) else {
249                    return UnknownHintSnafu {
250                        hint: format!("{key}={value}"),
251                    }
252                    .fail();
253                };
254                ctx_builder = ctx_builder.read_preference(read_preference);
255            }
256            INSERT_SKIP_WAL_HINT => {
257                let skip_wal = value.parse::<bool>().map_err(|_| {
258                    UnknownHintSnafu {
259                        hint: format!("{key}={value}"),
260                    }
261                    .build()
262                })?;
263                ctx_builder = ctx_builder.skip_wal(skip_wal);
264            }
265            _ if is_reserved_extension_key(&key) => {
266                debug!(
267                    key = key.as_str(),
268                    "Ignoring reserved external query context extension key"
269                );
270            }
271            _ => {
272                ctx_builder = ctx_builder.set_extension(key, value);
273            }
274        }
275    }
276    Ok(ctx_builder.build().into())
277}
278
279/// Histogram timer for handling gRPC request.
280///
281/// The timer records the elapsed time with [StatusCode::Success] on drop.
282pub(crate) struct RequestTimer {
283    start: Instant,
284    db: String,
285    request_type: String,
286    status_code: StatusCode,
287}
288
289impl RequestTimer {
290    /// Returns a new timer.
291    pub fn new(db: String, request_type: String) -> RequestTimer {
292        RequestTimer {
293            start: Instant::now(),
294            db,
295            request_type,
296            status_code: StatusCode::Success,
297        }
298    }
299
300    /// Consumes the timer and record the elapsed time with specific `status_code`.
301    pub fn record(mut self, status_code: StatusCode) {
302        self.status_code = status_code;
303    }
304}
305
306impl Drop for RequestTimer {
307    fn drop(&mut self) {
308        METRIC_SERVER_GRPC_DB_REQUEST_TIMER
309            .with_label_values(&[
310                self.db.as_str(),
311                self.request_type.as_str(),
312                self.status_code.as_ref(),
313            ])
314            .observe(self.start.elapsed().as_secs_f64());
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use chrono::FixedOffset;
321    use common_error::ext::BoxedError;
322    use common_error::{GREPTIME_DB_HEADER_ERROR_CODE, GREPTIME_DB_HEADER_ERROR_RETRY_HINT};
323    use common_time::Timezone;
324    use query::options::FLOW_SCHEDULED_TIME_MILLIS;
325    use session::hints::{
326        INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY, READ_PREFERENCE_EXTENSION_KEY,
327        REMOTE_QUERY_ID_EXTENSION_KEY,
328    };
329    use snafu::ResultExt;
330    use tonic::Code;
331
332    use super::*;
333    use crate::error::{ExecuteGrpcRequestSnafu, InvalidParameterSnafu};
334
335    #[test]
336    fn test_create_query_context_typed_skip_wal() {
337        let ctx = create_query_context(Channel::Grpc, None, vec![], HashMap::new()).unwrap();
338        assert!(!ctx.skip_wal());
339        let legacy = create_query_context(
340            Channel::Grpc,
341            None,
342            vec![("skip_wal".to_string(), "true".to_string())],
343            HashMap::new(),
344        )
345        .unwrap();
346        assert!(!legacy.skip_wal());
347        assert_eq!(legacy.extension("skip_wal"), Some("true"));
348        for (value, expected) in [("true", true), ("false", false)] {
349            let ctx = create_query_context(
350                Channel::Grpc,
351                None,
352                vec![(INSERT_SKIP_WAL_HINT.to_string(), value.to_string())],
353                HashMap::new(),
354            )
355            .unwrap();
356            assert_eq!(ctx.skip_wal(), expected);
357            assert_eq!(ctx.extension(INSERT_SKIP_WAL_HINT), None);
358        }
359        for value in ["", "TRUE", "1", "invalid"] {
360            assert!(
361                create_query_context(
362                    Channel::Grpc,
363                    None,
364                    vec![(INSERT_SKIP_WAL_HINT.to_string(), value.to_string())],
365                    HashMap::new()
366                )
367                .is_err()
368            );
369        }
370        let ctx = create_query_context(
371            Channel::Grpc,
372            None,
373            vec![
374                (INSERT_SKIP_WAL_HINT.to_string(), "true".to_string()),
375                (INSERT_SKIP_WAL_HINT.to_string(), "false".to_string()),
376            ],
377            HashMap::new(),
378        )
379        .unwrap();
380        assert!(!ctx.skip_wal());
381        assert_eq!(ctx.extension(INSERT_SKIP_WAL_HINT), None);
382    }
383
384    #[test]
385    fn test_create_query_context_read_preference_duplicates() {
386        for (values, valid) in [
387            (["leader", "LEADER"], true),
388            (["invalid", "leader"], false),
389            (["leader", "invalid"], false),
390        ] {
391            let result = create_query_context(
392                Channel::Grpc,
393                None,
394                values
395                    .into_iter()
396                    .map(|value| (READ_PREFERENCE_HINT.to_string(), value.to_string()))
397                    .collect(),
398                HashMap::new(),
399            );
400            if valid {
401                let ctx = result.unwrap();
402                assert!(matches!(ctx.read_preference(), ReadPreference::Leader));
403                assert_eq!(ctx.extension(READ_PREFERENCE_HINT), None);
404            } else {
405                assert!(result.is_err());
406            }
407        }
408    }
409
410    #[test]
411    fn test_create_query_context() {
412        let header = RequestHeader {
413            catalog: "cat-a-log".to_string(),
414            timezone: "+01:00".to_string(),
415            ..Default::default()
416        };
417        let query_context = create_query_context(
418            Channel::Unknown,
419            Some(&header),
420            vec![
421                ("auto_create_table".to_string(), "true".to_string()),
422                ("read_preference".to_string(), "leader".to_string()),
423                (
424                    READ_PREFERENCE_EXTENSION_KEY.to_string(),
425                    "follower".to_string(),
426                ),
427                (
428                    REMOTE_QUERY_ID_EXTENSION_KEY.to_string(),
429                    "spoofed".to_string(),
430                ),
431                (
432                    INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY.to_string(),
433                    "spoofed-regs".to_string(),
434                ),
435                (
436                    FLOW_SCHEDULED_TIME_MILLIS.to_string(),
437                    "1700000000000".to_string(),
438                ),
439            ],
440            HashMap::from([(7, 88)]),
441        )
442        .unwrap();
443        assert_eq!(query_context.get_snapshot(7), Some(88));
444        assert_eq!(query_context.current_catalog(), "cat-a-log");
445        assert_eq!(query_context.current_schema(), DEFAULT_SCHEMA_NAME);
446        assert_eq!(
447            query_context.timezone(),
448            Timezone::Offset(FixedOffset::east_opt(3600).unwrap())
449        );
450        assert!(matches!(
451            query_context.read_preference(),
452            ReadPreference::Leader
453        ));
454        assert_eq!(query_context.extension("auto_create_table"), Some("true"));
455        assert_eq!(query_context.extension(READ_PREFERENCE_HINT), None);
456        assert_eq!(query_context.extension(READ_PREFERENCE_EXTENSION_KEY), None);
457        assert_ne!(query_context.remote_query_id(), Some("spoofed"));
458        assert!(
459            query_context
460                .extension(INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY)
461                .is_none()
462        );
463        assert_eq!(
464            query_context.extension(FLOW_SCHEDULED_TIME_MILLIS),
465            Some("1700000000000")
466        );
467    }
468
469    #[test]
470    fn test_create_query_context_ignores_remote_query_id_extension() {
471        let query_context = create_query_context(
472            Channel::Grpc,
473            None,
474            vec![(
475                REMOTE_QUERY_ID_EXTENSION_KEY.to_string(),
476                "spoofed-query-id".to_string(),
477            )],
478            HashMap::new(),
479        )
480        .unwrap();
481
482        assert_ne!(query_context.remote_query_id(), Some("spoofed-query-id"));
483        assert_eq!(
484            query_context.extension(REMOTE_QUERY_ID_EXTENSION_KEY),
485            query_context.remote_query_id()
486        );
487    }
488
489    #[test]
490    fn test_record_batch_error_to_status_preserves_error_details() {
491        let inner = InvalidParameterSnafu {
492            reason: "Column not found, column: new_col",
493        }
494        .build();
495        let err = Err::<(), _>(BoxedError::new(inner))
496            .context(ExecuteGrpcRequestSnafu)
497            .unwrap_err();
498
499        let status = Status::from(err);
500
501        assert_eq!(status.code(), Code::InvalidArgument);
502        assert!(
503            status
504                .message()
505                .contains("Column not found, column: new_col")
506        );
507        assert!(
508            status
509                .message()
510                .contains("Invalid request parameter: Column not found")
511        );
512        assert!(
513            status
514                .metadata()
515                .contains_key(GREPTIME_DB_HEADER_ERROR_CODE)
516        );
517        assert!(
518            status
519                .metadata()
520                .contains_key(GREPTIME_DB_HEADER_ERROR_RETRY_HINT)
521        );
522    }
523}