1mod stream;
16
17use std::collections::HashMap;
18use std::pin::Pin;
19use std::sync::Arc;
20use std::task::{Context, Poll};
21
22use api::v1::GreptimeRequest;
23use arrow_flight::flight_service_server::FlightService;
24use arrow_flight::{
25 Action, ActionType, Criteria, Empty, FlightData, FlightDescriptor, FlightInfo,
26 HandshakeRequest, HandshakeResponse, PollInfo, PutResult, SchemaResult, Ticket,
27};
28use async_trait::async_trait;
29use bytes::{self, Bytes};
30use common_error::ext::ErrorExt;
31use common_grpc::flight::do_put::{DoPutMetadata, DoPutResponse};
32use common_grpc::flight::{
33 FLOW_EXTENSIONS_METADATA_KEY, FlightDecoder, FlightMessage, SNAPSHOT_SEQS_METADATA_KEY,
34};
35use common_memory_manager::MemoryGuard;
36use common_query::{Output, OutputData};
37use common_recordbatch::DfRecordBatch;
38use common_telemetry::debug;
39use common_telemetry::tracing::info_span;
40use common_telemetry::tracing_context::{FutureExt, TracingContext};
41use datatypes::arrow::datatypes::SchemaRef;
42use futures::{Stream, future, ready};
43use futures_util::{StreamExt, TryStreamExt};
44use prost::Message;
45use query::metrics::terminal_recordbatch_metrics_from_plan_if_requested;
46use query::options::FlowQueryExtensions;
47use session::context::{Channel, QueryContextRef};
48use snafu::{IntoError, OptionExt, ResultExt, ensure};
49use table::requests::AUTO_CREATE_TABLE_KEY;
50use table::table_name::TableName;
51use tokio::sync::mpsc;
52use tokio_stream::wrappers::ReceiverStream;
53use tonic::{Request, Response, Status, Streaming};
54
55use crate::error::{InvalidParameterSnafu, InvalidQuerySnafu, Result, ToJsonSnafu};
56pub use crate::grpc::flight::stream::{
57 FlightRecordBatchSource, FlightRecordBatchStream, FlightRecordBatchStreamInput,
58};
59use crate::grpc::greptime_handler::{
60 GreptimeRequestHandler, create_query_context, get_request_type,
61};
62use crate::grpc::{FlightCompression, TonicResult, context_auth};
63use crate::request_memory_limiter::ServerMemoryLimiter;
64use crate::request_memory_metrics::RequestMemoryMetrics;
65use crate::{error, hint_headers};
66
67pub type TonicStream<T> = Pin<Box<dyn Stream<Item = TonicResult<T>> + Send + 'static>>;
68
69#[async_trait]
71pub trait FlightCraft: Send + Sync + 'static {
72 async fn do_get(
73 &self,
74 request: Request<Ticket>,
75 ) -> TonicResult<Response<TonicStream<FlightData>>>;
76
77 async fn do_put(
78 &self,
79 request: Request<Streaming<FlightData>>,
80 ) -> TonicResult<Response<TonicStream<PutResult>>> {
81 let _ = request;
82 Err(Status::unimplemented("Not yet implemented"))
83 }
84}
85
86pub type FlightCraftRef = Arc<dyn FlightCraft>;
87
88pub struct FlightCraftWrapper<T: FlightCraft>(pub T);
89
90impl<T: FlightCraft> From<T> for FlightCraftWrapper<T> {
91 fn from(t: T) -> Self {
92 Self(t)
93 }
94}
95
96#[async_trait]
97impl FlightCraft for FlightCraftRef {
98 async fn do_get(
99 &self,
100 request: Request<Ticket>,
101 ) -> TonicResult<Response<TonicStream<FlightData>>> {
102 (**self).do_get(request).await
103 }
104
105 async fn do_put(
106 &self,
107 request: Request<Streaming<FlightData>>,
108 ) -> TonicResult<Response<TonicStream<PutResult>>> {
109 self.as_ref().do_put(request).await
110 }
111}
112
113#[async_trait]
114impl<T: FlightCraft> FlightService for FlightCraftWrapper<T> {
115 type HandshakeStream = TonicStream<HandshakeResponse>;
116
117 async fn handshake(
118 &self,
119 _: Request<Streaming<HandshakeRequest>>,
120 ) -> TonicResult<Response<Self::HandshakeStream>> {
121 Err(Status::unimplemented("Not yet implemented"))
122 }
123
124 type ListFlightsStream = TonicStream<FlightInfo>;
125
126 async fn list_flights(
127 &self,
128 _: Request<Criteria>,
129 ) -> TonicResult<Response<Self::ListFlightsStream>> {
130 Err(Status::unimplemented("Not yet implemented"))
131 }
132
133 async fn get_flight_info(
134 &self,
135 _: Request<FlightDescriptor>,
136 ) -> TonicResult<Response<FlightInfo>> {
137 Err(Status::unimplemented("Not yet implemented"))
138 }
139
140 async fn poll_flight_info(
141 &self,
142 _: Request<FlightDescriptor>,
143 ) -> TonicResult<Response<PollInfo>> {
144 Err(Status::unimplemented("Not yet implemented"))
145 }
146
147 async fn get_schema(
148 &self,
149 _: Request<FlightDescriptor>,
150 ) -> TonicResult<Response<SchemaResult>> {
151 Err(Status::unimplemented("Not yet implemented"))
152 }
153
154 type DoGetStream = TonicStream<FlightData>;
155
156 async fn do_get(&self, request: Request<Ticket>) -> TonicResult<Response<Self::DoGetStream>> {
157 self.0.do_get(request).await
158 }
159
160 type DoPutStream = TonicStream<PutResult>;
161
162 async fn do_put(
163 &self,
164 request: Request<Streaming<FlightData>>,
165 ) -> TonicResult<Response<Self::DoPutStream>> {
166 self.0.do_put(request).await
167 }
168
169 type DoExchangeStream = TonicStream<FlightData>;
170
171 async fn do_exchange(
172 &self,
173 _: Request<Streaming<FlightData>>,
174 ) -> TonicResult<Response<Self::DoExchangeStream>> {
175 Err(Status::unimplemented("Not yet implemented"))
176 }
177
178 type DoActionStream = TonicStream<arrow_flight::Result>;
179
180 async fn do_action(&self, _: Request<Action>) -> TonicResult<Response<Self::DoActionStream>> {
181 Err(Status::unimplemented("Not yet implemented"))
182 }
183
184 type ListActionsStream = TonicStream<ActionType>;
185
186 async fn list_actions(
187 &self,
188 _: Request<Empty>,
189 ) -> TonicResult<Response<Self::ListActionsStream>> {
190 Err(Status::unimplemented("Not yet implemented"))
191 }
192}
193
194#[async_trait]
195impl FlightCraft for GreptimeRequestHandler {
196 async fn do_get(
197 &self,
198 request: Request<Ticket>,
199 ) -> TonicResult<Response<TonicStream<FlightData>>> {
200 let mut hints = hint_headers::extract_hints(request.metadata());
201 hints.extend(extract_flow_extensions(request.metadata())?);
202 let snapshot_seqs = extract_snapshot_seqs(request.metadata())?;
203 let channel = request
204 .extensions()
205 .get::<Channel>()
206 .copied()
207 .unwrap_or(Channel::Grpc);
208
209 let ticket = request.into_inner().ticket;
210 let request =
211 GreptimeRequest::decode(ticket.as_ref()).context(error::InvalidFlightTicketSnafu)?;
212 let query_ctx =
213 create_query_context(channel, request.header.as_ref(), hints, snapshot_seqs)?;
214 let flow_extensions = FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())
218 .map_err(|e| Status::invalid_argument(e.output_msg()))?;
219 let should_emit_terminal_metrics = flow_extensions
220 .as_ref()
221 .is_some_and(|extensions| extensions.should_collect_region_watermark());
222
223 let span = info_span!(
225 "GreptimeRequestHandler::do_get",
226 protocol = "grpc",
227 request_type = get_request_type(&request)
228 );
229 let flight_compression = self.flight_compression;
230 async {
231 let query = request.request.context(InvalidQuerySnafu {
232 reason: "Expecting non-empty GreptimeRequest.",
233 })?;
234 self.authenticate_request_with_query_ctx(request.header.as_ref(), &query_ctx)
235 .await?;
236 let output = self.handle_request_with_query_ctx(query, query_ctx.clone());
237 let stream = to_flight_data_stream(
238 output,
239 TracingContext::from_current_span(),
240 flight_compression,
241 query_ctx,
242 should_emit_terminal_metrics,
243 );
244 Ok(Response::new(stream))
245 }
246 .trace(span)
247 .await
248 }
249
250 async fn do_put(
251 &self,
252 request: Request<Streaming<FlightData>>,
253 ) -> TonicResult<Response<TonicStream<PutResult>>> {
254 let (headers, extensions, stream) = request.into_parts();
255
256 let limiter = extensions.get::<ServerMemoryLimiter>().cloned();
257
258 let mut query_ctx =
259 context_auth::create_query_context_from_grpc_metadata(&headers, &extensions)?;
260 for (key, value) in hint_headers::extract_hints(&headers) {
262 if key == AUTO_CREATE_TABLE_KEY {
263 Arc::make_mut(&mut query_ctx).set_extension(key, value);
264 }
265 }
266 context_auth::check_auth(self.user_provider.clone(), &headers, query_ctx.clone()).await?;
267
268 const MAX_PENDING_RESPONSES: usize = 32;
269 let (tx, rx) = mpsc::channel::<TonicResult<DoPutResponse>>(MAX_PENDING_RESPONSES);
270
271 let stream = PutRecordBatchRequestStream::new(
272 stream,
273 query_ctx.current_catalog().to_string(),
274 query_ctx.current_schema(),
275 limiter,
276 )
277 .await?;
278 let _ = tx.send(Ok(DoPutResponse::new(0, 0, 0.0))).await;
280 self.put_record_batches(stream, tx, query_ctx).await;
281
282 let response = ReceiverStream::new(rx)
283 .and_then(|response| {
284 future::ready({
285 serde_json::to_vec(&response)
286 .context(ToJsonSnafu)
287 .map(|x| PutResult {
288 app_metadata: Bytes::from(x),
289 })
290 .map_err(Into::into)
291 })
292 })
293 .boxed();
294 Ok(Response::new(response))
295 }
296}
297
298pub struct PutRecordBatchRequest {
299 pub table_name: TableName,
300 pub request_id: i64,
301 pub timestamp_range: Option<(i64, i64)>,
302 pub record_batch: DfRecordBatch,
303 pub schema_bytes: Bytes,
304 pub flight_data: FlightData,
305 pub(crate) _guard: Option<MemoryGuard<RequestMemoryMetrics>>,
306}
307
308impl PutRecordBatchRequest {
309 fn try_new(
310 table_name: TableName,
311 record_batch: DfRecordBatch,
312 request_id: i64,
313 timestamp_range: Option<(i64, i64)>,
314 schema_bytes: Bytes,
315 flight_data: FlightData,
316 limiter: Option<&ServerMemoryLimiter>,
317 ) -> Result<Self> {
318 let memory_usage = flight_data.data_body.len()
319 + flight_data.app_metadata.len()
320 + flight_data.data_header.len();
321
322 let _guard = if let Some(limiter) = limiter {
323 let guard = limiter.try_acquire(memory_usage as u64).ok_or_else(|| {
324 let inner_err = common_memory_manager::Error::MemoryLimitExceeded {
325 requested_bytes: memory_usage as u64,
326 limit_bytes: limiter.limit_bytes(),
327 };
328 error::MemoryLimitExceededSnafu.into_error(inner_err)
329 })?;
330 Some(guard)
331 } else {
332 None
333 };
334
335 Ok(Self {
336 table_name,
337 request_id,
338 timestamp_range,
339 record_batch,
340 schema_bytes,
341 flight_data,
342 _guard,
343 })
344 }
345}
346
347pub struct PutRecordBatchRequestStream {
348 flight_data_stream: Streaming<FlightData>,
349 catalog: String,
350 schema_name: String,
351 limiter: Option<ServerMemoryLimiter>,
352 state: StreamState,
355}
356
357enum StreamState {
358 Init,
359 Ready {
360 table_name: TableName,
361 schema: SchemaRef,
362 schema_bytes: Bytes,
363 decoder: FlightDecoder,
364 },
365}
366
367impl PutRecordBatchRequestStream {
368 pub async fn new(
371 flight_data_stream: Streaming<FlightData>,
372 catalog: String,
373 schema: String,
374 limiter: Option<ServerMemoryLimiter>,
375 ) -> TonicResult<Self> {
376 Ok(Self {
377 flight_data_stream,
378 catalog,
379 schema_name: schema,
380 limiter,
381 state: StreamState::Init,
382 })
383 }
384
385 pub fn table_name(&self) -> Option<&TableName> {
388 match &self.state {
389 StreamState::Init => None,
390 StreamState::Ready { table_name, .. } => Some(table_name),
391 }
392 }
393
394 pub fn schema(&self) -> Option<&SchemaRef> {
397 match &self.state {
398 StreamState::Init => None,
399 StreamState::Ready { schema, .. } => Some(schema),
400 }
401 }
402
403 pub fn schema_bytes(&self) -> Option<&Bytes> {
406 match &self.state {
407 StreamState::Init => None,
408 StreamState::Ready { schema_bytes, .. } => Some(schema_bytes),
409 }
410 }
411
412 fn extract_table_name(mut descriptor: FlightDescriptor) -> Result<String> {
413 ensure!(
414 descriptor.r#type == arrow_flight::flight_descriptor::DescriptorType::Path as i32,
415 InvalidParameterSnafu {
416 reason: "expect FlightDescriptor::type == 'Path' only",
417 }
418 );
419 ensure!(
420 descriptor.path.len() == 1,
421 InvalidParameterSnafu {
422 reason: "expect FlightDescriptor::path has only one table name",
423 }
424 );
425 Ok(descriptor.path.remove(0))
426 }
427}
428
429impl Stream for PutRecordBatchRequestStream {
430 type Item = TonicResult<PutRecordBatchRequest>;
431
432 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
433 loop {
434 let poll = ready!(self.flight_data_stream.poll_next_unpin(cx));
435
436 match poll {
437 Some(Ok(flight_data)) => {
438 let limiter = self.limiter.clone();
439
440 match &mut self.state {
441 StreamState::Init => {
442 let flight_descriptor = match flight_data.flight_descriptor.as_ref() {
444 Some(descriptor) => descriptor.clone(),
445 None => {
446 return Poll::Ready(Some(Err(Status::failed_precondition(
447 "table to put is not found in flight descriptor",
448 ))));
449 }
450 };
451
452 let table_name_str = match Self::extract_table_name(flight_descriptor) {
453 Ok(name) => name,
454 Err(e) => {
455 return Poll::Ready(Some(Err(Status::invalid_argument(
456 e.to_string(),
457 ))));
458 }
459 };
460 let table_name = TableName::new(
461 self.catalog.clone(),
462 self.schema_name.clone(),
463 table_name_str,
464 );
465
466 let mut decoder = FlightDecoder::default();
468 let schema_message = decoder.try_decode(&flight_data).map_err(|e| {
469 Status::invalid_argument(format!("Failed to decode schema: {}", e))
470 })?;
471
472 match schema_message {
473 Some(FlightMessage::Schema(schema)) => {
474 let schema_bytes = decoder.schema_bytes().ok_or_else(|| {
475 Status::internal(
476 "decoder should have schema bytes after decoding schema",
477 )
478 })?;
479
480 self.state = StreamState::Ready {
482 table_name,
483 schema,
484 schema_bytes,
485 decoder,
486 };
487 continue;
489 }
490 _ => {
491 return Poll::Ready(Some(Err(Status::failed_precondition(
492 "first message must be a Schema message",
493 ))));
494 }
495 }
496 }
497 StreamState::Ready {
498 table_name,
499 schema: _,
500 schema_bytes,
501 decoder,
502 } => {
503 let metadata = if !flight_data.app_metadata.is_empty() {
505 serde_json::from_slice::<DoPutMetadata>(&flight_data.app_metadata)
506 .ok()
507 } else {
508 None
509 };
510 let request_id = metadata
511 .as_ref()
512 .map(|meta| meta.request_id())
513 .unwrap_or_default();
514 let timestamp_range = metadata.and_then(|meta| meta.timestamp_range());
515
516 match decoder.try_decode(&flight_data) {
518 Ok(Some(FlightMessage::RecordBatch(record_batch))) => {
519 let table_name = table_name.clone();
520 let schema_bytes = schema_bytes.clone();
521 return Poll::Ready(Some(
522 PutRecordBatchRequest::try_new(
523 table_name,
524 record_batch,
525 request_id,
526 timestamp_range,
527 schema_bytes,
528 flight_data,
529 limiter.as_ref(),
530 )
531 .map_err(|e| Status::invalid_argument(e.to_string())),
532 ));
533 }
534 Ok(Some(other)) => {
535 debug!("Unexpected flight message: {:?}", other);
536 return Poll::Ready(Some(Err(Status::invalid_argument(
537 "Expected RecordBatch message, got other message type",
538 ))));
539 }
540 Ok(None) => {
541 continue;
543 }
544 Err(e) => {
545 return Poll::Ready(Some(Err(Status::invalid_argument(
546 format!("Failed to decode RecordBatch: {}", e),
547 ))));
548 }
549 }
550 }
551 }
552 }
553 Some(Err(e)) => {
554 return Poll::Ready(Some(Err(e)));
555 }
556 None => {
557 return Poll::Ready(None);
558 }
559 }
560 }
561 }
562}
563
564fn extract_flow_extensions(
565 metadata: &tonic::metadata::MetadataMap,
566) -> TonicResult<Vec<(String, String)>> {
567 Ok(extract_json_metadata(metadata, FLOW_EXTENSIONS_METADATA_KEY)?.unwrap_or_default())
568}
569
570fn extract_snapshot_seqs(
571 metadata: &tonic::metadata::MetadataMap,
572) -> TonicResult<HashMap<u64, u64>> {
573 Ok(extract_json_metadata(metadata, SNAPSHOT_SEQS_METADATA_KEY)?.unwrap_or_default())
574}
575
576fn extract_json_metadata<T: serde::de::DeserializeOwned>(
577 metadata: &tonic::metadata::MetadataMap,
578 key: &'static str,
579) -> TonicResult<Option<T>> {
580 let Some(value) = metadata.get(key) else {
581 return Ok(None);
582 };
583
584 let value = value
585 .to_str()
586 .map_err(|e| Status::invalid_argument(format!("Invalid {key} metadata value: {e}")))?;
587
588 let parsed = serde_json::from_str::<T>(value)
589 .map_err(|e| Status::invalid_argument(format!("Invalid {key} metadata JSON: {e}")))?;
590 Ok(Some(parsed))
591}
592
593fn to_flight_data_stream<F>(
594 output: F,
595 tracing_context: TracingContext,
596 flight_compression: FlightCompression,
597 query_ctx: QueryContextRef,
598 should_emit_terminal_metrics: bool,
599) -> TonicStream<FlightData>
600where
601 F: std::future::Future<Output = Result<Output>> + Send + 'static,
602{
603 let initializer = async move {
604 let output = output.await.map_err(Status::from)?;
605 output_to_flight_record_batch_source(output, should_emit_terminal_metrics)
606 };
607 let stream = FlightRecordBatchStream::new(
608 FlightRecordBatchStreamInput::initializer(initializer),
609 tracing_context,
610 flight_compression,
611 query_ctx,
612 );
613 Box::pin(stream) as _
614}
615
616fn output_to_flight_record_batch_source(
617 output: Output,
618 should_emit_terminal_metrics: bool,
619) -> TonicResult<FlightRecordBatchSource> {
620 match output.data {
621 OutputData::Stream(stream) => Ok(FlightRecordBatchSource::RecordBatches(stream)),
622 OutputData::RecordBatches(x) => Ok(FlightRecordBatchSource::RecordBatches(x.as_stream())),
623 OutputData::AffectedRows(rows) => {
624 let terminal_metrics = match terminal_recordbatch_metrics_from_plan_if_requested(
625 output.meta.plan,
626 should_emit_terminal_metrics,
627 ) {
628 Some(metrics) => match serde_json::to_string(&metrics) {
629 Ok(metrics) => Some(metrics),
630 Err(e) => {
631 return Err(Status::internal(format!(
632 "Failed to serialize terminal metrics: {e}"
633 )));
634 }
635 },
636 None => None,
637 };
638 Ok(FlightRecordBatchSource::AffectedRows {
639 rows,
640 metrics: terminal_metrics,
641 })
642 }
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use query::options::{
649 FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_RETURN_REGION_SEQ,
650 FLOW_SCHEDULED_TIME_MILLIS, FLOW_SINK_TABLE_ID, FlowIncrementalMode,
651 };
652 use tonic::metadata::{AsciiMetadataValue, MetadataMap};
653
654 use super::*;
655
656 #[test]
657 fn test_extract_flow_extensions_preserves_comma_bearing_values() {
658 let mut metadata = MetadataMap::new();
659 metadata.insert(
660 FLOW_EXTENSIONS_METADATA_KEY,
661 AsciiMetadataValue::try_from(
662 r#"[["flow.return_region_seq","true"],["flow.incremental_after_seqs","{\"1\":10,\"2\":20}"]]"#,
663 )
664 .unwrap(),
665 );
666
667 let extensions = extract_flow_extensions(&metadata).unwrap();
668 assert_eq!(
669 extensions,
670 vec![
671 ("flow.return_region_seq".to_string(), "true".to_string()),
672 (
673 "flow.incremental_after_seqs".to_string(),
674 r#"{"1":10,"2":20}"#.to_string()
675 ),
676 ]
677 );
678 }
679
680 #[test]
681 fn test_flow_extensions_can_carry_scheduled_time() {
682 let mut metadata = MetadataMap::new();
683 metadata.insert(
684 FLOW_EXTENSIONS_METADATA_KEY,
685 AsciiMetadataValue::try_from(r#"[["flow.scheduled_time_millis","1700000000000"]]"#)
686 .unwrap(),
687 );
688
689 let flow_extensions = extract_flow_extensions(&metadata).unwrap();
690 let query_ctx =
691 create_query_context(Channel::Grpc, None, flow_extensions, HashMap::new()).unwrap();
692
693 assert_eq!(
694 query_ctx.extension(FLOW_SCHEDULED_TIME_MILLIS),
695 Some("1700000000000")
696 );
697 }
698
699 #[test]
700 fn test_flow_extensions_forward_sequence_range_to_query_context() {
701 let mut metadata = MetadataMap::new();
702 metadata.insert(
703 FLOW_EXTENSIONS_METADATA_KEY,
704 AsciiMetadataValue::try_from(
705 r#"[["flow.return_region_seq","true"],["flow.incremental_mode","sequence_range"],["flow.incremental_after_seqs","{\"1\":10,\"2\":20}"],["flow.sink_table_id","42"]]"#,
706 )
707 .unwrap(),
708 );
709
710 let flow_extensions = extract_flow_extensions(&metadata).unwrap();
711 let query_ctx =
712 create_query_context(Channel::Grpc, None, flow_extensions, HashMap::new()).unwrap();
713 let parsed =
714 query::options::FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())
715 .unwrap()
716 .unwrap();
717
718 assert_eq!(
719 parsed.incremental_mode,
720 Some(FlowIncrementalMode::SequenceRange)
721 );
722 assert_eq!(
723 parsed.incremental_after_seqs,
724 Some(HashMap::from([(1, 10), (2, 20)]))
725 );
726 assert!(parsed.return_region_seq);
727 assert_eq!(parsed.sink_table_id, Some(42));
728 assert_eq!(
729 query_ctx.extension(FLOW_INCREMENTAL_MODE),
730 Some("sequence_range")
731 );
732 assert_eq!(
733 query_ctx.extension(FLOW_INCREMENTAL_AFTER_SEQS),
734 Some(r#"{"1":10,"2":20}"#)
735 );
736 assert_eq!(query_ctx.extension(FLOW_RETURN_REGION_SEQ), Some("true"));
737 assert_eq!(query_ctx.extension(FLOW_SINK_TABLE_ID), Some("42"));
738 }
739
740 #[test]
741 fn test_extract_flow_extensions_rejects_invalid_json() {
742 let mut metadata = MetadataMap::new();
743 metadata.insert(
744 FLOW_EXTENSIONS_METADATA_KEY,
745 AsciiMetadataValue::try_from("not-json").unwrap(),
746 );
747
748 let err = extract_flow_extensions(&metadata).unwrap_err();
749 assert_eq!(err.code(), tonic::Code::InvalidArgument);
750 }
751}