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 = hint_headers::validate_public_hints(hints)?;
202 hints.extend(extract_flow_extensions(request.metadata())?);
203 let snapshot_seqs = extract_snapshot_seqs(request.metadata())?;
204 let channel = request
205 .extensions()
206 .get::<Channel>()
207 .copied()
208 .unwrap_or(Channel::Grpc);
209
210 let ticket = request.into_inner().ticket;
211 let request =
212 GreptimeRequest::decode(ticket.as_ref()).context(error::InvalidFlightTicketSnafu)?;
213 let query_ctx =
214 create_query_context(channel, request.header.as_ref(), hints, snapshot_seqs)?;
215 let flow_extensions = FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())
219 .map_err(|e| Status::invalid_argument(e.output_msg()))?;
220 let should_emit_terminal_metrics = flow_extensions
221 .as_ref()
222 .is_some_and(|extensions| extensions.should_collect_region_watermark());
223
224 let span = info_span!(
226 "GreptimeRequestHandler::do_get",
227 protocol = "grpc",
228 request_type = get_request_type(&request)
229 );
230 let flight_compression = self.flight_compression;
231 async {
232 let query = request.request.context(InvalidQuerySnafu {
233 reason: "Expecting non-empty GreptimeRequest.",
234 })?;
235 self.authenticate_request_with_query_ctx(request.header.as_ref(), &query_ctx)
236 .await?;
237 let output = self.handle_request_with_query_ctx(query, query_ctx.clone());
238 let stream = to_flight_data_stream(
239 output,
240 TracingContext::from_current_span(),
241 flight_compression,
242 query_ctx,
243 should_emit_terminal_metrics,
244 );
245 Ok(Response::new(stream))
246 }
247 .trace(span)
248 .await
249 }
250
251 async fn do_put(
252 &self,
253 request: Request<Streaming<FlightData>>,
254 ) -> TonicResult<Response<TonicStream<PutResult>>> {
255 let (headers, extensions, stream) = request.into_parts();
256
257 let limiter = extensions.get::<ServerMemoryLimiter>().cloned();
258
259 let mut query_ctx =
260 context_auth::create_query_context_from_grpc_metadata(&headers, &extensions)?;
261 for (key, value) in hint_headers::extract_hints(&headers) {
263 if key == AUTO_CREATE_TABLE_KEY {
264 Arc::make_mut(&mut query_ctx).set_extension(key, value);
265 }
266 }
267 context_auth::check_auth(self.user_provider.clone(), &headers, query_ctx.clone()).await?;
268
269 const MAX_PENDING_RESPONSES: usize = 32;
270 let (tx, rx) = mpsc::channel::<TonicResult<DoPutResponse>>(MAX_PENDING_RESPONSES);
271
272 let stream = PutRecordBatchRequestStream::new(
273 stream,
274 query_ctx.current_catalog().to_string(),
275 query_ctx.current_schema(),
276 limiter,
277 )
278 .await?;
279 let _ = tx.send(Ok(DoPutResponse::new(0, 0, 0.0))).await;
281 self.put_record_batches(stream, tx, query_ctx).await;
282
283 let response = ReceiverStream::new(rx)
284 .and_then(|response| {
285 future::ready({
286 serde_json::to_vec(&response)
287 .context(ToJsonSnafu)
288 .map(|x| PutResult {
289 app_metadata: Bytes::from(x),
290 })
291 .map_err(Into::into)
292 })
293 })
294 .boxed();
295 Ok(Response::new(response))
296 }
297}
298
299pub struct PutRecordBatchRequest {
300 pub table_name: TableName,
301 pub request_id: i64,
302 pub timestamp_range: Option<(i64, i64)>,
303 pub record_batch: DfRecordBatch,
304 pub schema_bytes: Bytes,
305 pub flight_data: FlightData,
306 pub(crate) _guard: Option<MemoryGuard<RequestMemoryMetrics>>,
307}
308
309impl PutRecordBatchRequest {
310 fn try_new(
311 table_name: TableName,
312 record_batch: DfRecordBatch,
313 request_id: i64,
314 timestamp_range: Option<(i64, i64)>,
315 schema_bytes: Bytes,
316 flight_data: FlightData,
317 limiter: Option<&ServerMemoryLimiter>,
318 ) -> Result<Self> {
319 let memory_usage = flight_data.data_body.len()
320 + flight_data.app_metadata.len()
321 + flight_data.data_header.len();
322
323 let _guard = if let Some(limiter) = limiter {
324 let guard = limiter.try_acquire(memory_usage as u64).ok_or_else(|| {
325 let inner_err = common_memory_manager::Error::MemoryLimitExceeded {
326 requested_bytes: memory_usage as u64,
327 limit_bytes: limiter.limit_bytes(),
328 };
329 error::MemoryLimitExceededSnafu.into_error(inner_err)
330 })?;
331 Some(guard)
332 } else {
333 None
334 };
335
336 Ok(Self {
337 table_name,
338 request_id,
339 timestamp_range,
340 record_batch,
341 schema_bytes,
342 flight_data,
343 _guard,
344 })
345 }
346}
347
348pub struct PutRecordBatchRequestStream {
349 flight_data_stream: Streaming<FlightData>,
350 catalog: String,
351 schema_name: String,
352 limiter: Option<ServerMemoryLimiter>,
353 state: StreamState,
356}
357
358enum StreamState {
359 Init,
360 Ready {
361 table_name: TableName,
362 schema: SchemaRef,
363 schema_bytes: Bytes,
364 decoder: FlightDecoder,
365 },
366}
367
368impl PutRecordBatchRequestStream {
369 pub async fn new(
372 flight_data_stream: Streaming<FlightData>,
373 catalog: String,
374 schema: String,
375 limiter: Option<ServerMemoryLimiter>,
376 ) -> TonicResult<Self> {
377 Ok(Self {
378 flight_data_stream,
379 catalog,
380 schema_name: schema,
381 limiter,
382 state: StreamState::Init,
383 })
384 }
385
386 pub fn table_name(&self) -> Option<&TableName> {
389 match &self.state {
390 StreamState::Init => None,
391 StreamState::Ready { table_name, .. } => Some(table_name),
392 }
393 }
394
395 pub fn schema(&self) -> Option<&SchemaRef> {
398 match &self.state {
399 StreamState::Init => None,
400 StreamState::Ready { schema, .. } => Some(schema),
401 }
402 }
403
404 pub fn schema_bytes(&self) -> Option<&Bytes> {
407 match &self.state {
408 StreamState::Init => None,
409 StreamState::Ready { schema_bytes, .. } => Some(schema_bytes),
410 }
411 }
412
413 fn extract_table_name(mut descriptor: FlightDescriptor) -> Result<String> {
414 ensure!(
415 descriptor.r#type == arrow_flight::flight_descriptor::DescriptorType::Path as i32,
416 InvalidParameterSnafu {
417 reason: "expect FlightDescriptor::type == 'Path' only",
418 }
419 );
420 ensure!(
421 descriptor.path.len() == 1,
422 InvalidParameterSnafu {
423 reason: "expect FlightDescriptor::path has only one table name",
424 }
425 );
426 Ok(descriptor.path.remove(0))
427 }
428}
429
430impl Stream for PutRecordBatchRequestStream {
431 type Item = TonicResult<PutRecordBatchRequest>;
432
433 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
434 loop {
435 let poll = ready!(self.flight_data_stream.poll_next_unpin(cx));
436
437 match poll {
438 Some(Ok(flight_data)) => {
439 let limiter = self.limiter.clone();
440
441 match &mut self.state {
442 StreamState::Init => {
443 let flight_descriptor = match flight_data.flight_descriptor.as_ref() {
445 Some(descriptor) => descriptor.clone(),
446 None => {
447 return Poll::Ready(Some(Err(Status::failed_precondition(
448 "table to put is not found in flight descriptor",
449 ))));
450 }
451 };
452
453 let table_name_str = match Self::extract_table_name(flight_descriptor) {
454 Ok(name) => name,
455 Err(e) => {
456 return Poll::Ready(Some(Err(Status::invalid_argument(
457 e.to_string(),
458 ))));
459 }
460 };
461 let table_name = TableName::new(
462 self.catalog.clone(),
463 self.schema_name.clone(),
464 table_name_str,
465 );
466
467 let mut decoder = FlightDecoder::default();
469 let schema_message = decoder.try_decode(&flight_data).map_err(|e| {
470 Status::invalid_argument(format!("Failed to decode schema: {}", e))
471 })?;
472
473 match schema_message {
474 Some(FlightMessage::Schema(schema)) => {
475 let schema_bytes = decoder.schema_bytes().ok_or_else(|| {
476 Status::internal(
477 "decoder should have schema bytes after decoding schema",
478 )
479 })?;
480
481 self.state = StreamState::Ready {
483 table_name,
484 schema,
485 schema_bytes,
486 decoder,
487 };
488 continue;
490 }
491 _ => {
492 return Poll::Ready(Some(Err(Status::failed_precondition(
493 "first message must be a Schema message",
494 ))));
495 }
496 }
497 }
498 StreamState::Ready {
499 table_name,
500 schema: _,
501 schema_bytes,
502 decoder,
503 } => {
504 let metadata = if !flight_data.app_metadata.is_empty() {
506 serde_json::from_slice::<DoPutMetadata>(&flight_data.app_metadata)
507 .ok()
508 } else {
509 None
510 };
511 let request_id = metadata
512 .as_ref()
513 .map(|meta| meta.request_id())
514 .unwrap_or_default();
515 let timestamp_range = metadata.and_then(|meta| meta.timestamp_range());
516
517 match decoder.try_decode(&flight_data) {
519 Ok(Some(FlightMessage::RecordBatch(record_batch))) => {
520 let table_name = table_name.clone();
521 let schema_bytes = schema_bytes.clone();
522 return Poll::Ready(Some(
523 PutRecordBatchRequest::try_new(
524 table_name,
525 record_batch,
526 request_id,
527 timestamp_range,
528 schema_bytes,
529 flight_data,
530 limiter.as_ref(),
531 )
532 .map_err(|e| Status::invalid_argument(e.to_string())),
533 ));
534 }
535 Ok(Some(other)) => {
536 debug!("Unexpected flight message: {:?}", other);
537 return Poll::Ready(Some(Err(Status::invalid_argument(
538 "Expected RecordBatch message, got other message type",
539 ))));
540 }
541 Ok(None) => {
542 continue;
544 }
545 Err(e) => {
546 return Poll::Ready(Some(Err(Status::invalid_argument(
547 format!("Failed to decode RecordBatch: {}", e),
548 ))));
549 }
550 }
551 }
552 }
553 }
554 Some(Err(e)) => {
555 return Poll::Ready(Some(Err(e)));
556 }
557 None => {
558 return Poll::Ready(None);
559 }
560 }
561 }
562 }
563}
564
565fn extract_flow_extensions(
566 metadata: &tonic::metadata::MetadataMap,
567) -> TonicResult<Vec<(String, String)>> {
568 Ok(extract_json_metadata(metadata, FLOW_EXTENSIONS_METADATA_KEY)?.unwrap_or_default())
569}
570
571fn extract_snapshot_seqs(
572 metadata: &tonic::metadata::MetadataMap,
573) -> TonicResult<HashMap<u64, u64>> {
574 Ok(extract_json_metadata(metadata, SNAPSHOT_SEQS_METADATA_KEY)?.unwrap_or_default())
575}
576
577fn extract_json_metadata<T: serde::de::DeserializeOwned>(
578 metadata: &tonic::metadata::MetadataMap,
579 key: &'static str,
580) -> TonicResult<Option<T>> {
581 let Some(value) = metadata.get(key) else {
582 return Ok(None);
583 };
584
585 let value = value
586 .to_str()
587 .map_err(|e| Status::invalid_argument(format!("Invalid {key} metadata value: {e}")))?;
588
589 let parsed = serde_json::from_str::<T>(value)
590 .map_err(|e| Status::invalid_argument(format!("Invalid {key} metadata JSON: {e}")))?;
591 Ok(Some(parsed))
592}
593
594fn to_flight_data_stream<F>(
595 output: F,
596 tracing_context: TracingContext,
597 flight_compression: FlightCompression,
598 query_ctx: QueryContextRef,
599 should_emit_terminal_metrics: bool,
600) -> TonicStream<FlightData>
601where
602 F: std::future::Future<Output = Result<Output>> + Send + 'static,
603{
604 let initializer = async move {
605 let output = output.await.map_err(Status::from)?;
606 output_to_flight_record_batch_source(output, should_emit_terminal_metrics)
607 };
608 let stream = FlightRecordBatchStream::new(
609 FlightRecordBatchStreamInput::initializer(initializer),
610 tracing_context,
611 flight_compression,
612 query_ctx,
613 );
614 Box::pin(stream) as _
615}
616
617fn output_to_flight_record_batch_source(
618 output: Output,
619 should_emit_terminal_metrics: bool,
620) -> TonicResult<FlightRecordBatchSource> {
621 match output.data {
622 OutputData::Stream(stream) => Ok(FlightRecordBatchSource::RecordBatches(stream)),
623 OutputData::RecordBatches(x) => Ok(FlightRecordBatchSource::RecordBatches(x.as_stream())),
624 OutputData::AffectedRows(rows) => {
625 let terminal_metrics = match terminal_recordbatch_metrics_from_plan_if_requested(
626 output.meta.plan,
627 should_emit_terminal_metrics,
628 ) {
629 Some(metrics) => match serde_json::to_string(&metrics) {
630 Ok(metrics) => Some(metrics),
631 Err(e) => {
632 return Err(Status::internal(format!(
633 "Failed to serialize terminal metrics: {e}"
634 )));
635 }
636 },
637 None => None,
638 };
639 Ok(FlightRecordBatchSource::AffectedRows {
640 rows,
641 metrics: terminal_metrics,
642 })
643 }
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use query::options::{
650 FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_RETURN_REGION_SEQ,
651 FLOW_SCHEDULED_TIME_MILLIS, FLOW_SINK_TABLE_ID, FlowIncrementalMode,
652 };
653 use tonic::metadata::{AsciiMetadataValue, MetadataMap};
654
655 use super::*;
656
657 #[test]
658 fn test_extract_flow_extensions_preserves_comma_bearing_values() {
659 let mut metadata = MetadataMap::new();
660 metadata.insert(
661 FLOW_EXTENSIONS_METADATA_KEY,
662 AsciiMetadataValue::try_from(
663 r#"[["flow.return_region_seq","true"],["flow.incremental_after_seqs","{\"1\":10,\"2\":20}"]]"#,
664 )
665 .unwrap(),
666 );
667
668 let extensions = extract_flow_extensions(&metadata).unwrap();
669 assert_eq!(
670 extensions,
671 vec![
672 ("flow.return_region_seq".to_string(), "true".to_string()),
673 (
674 "flow.incremental_after_seqs".to_string(),
675 r#"{"1":10,"2":20}"#.to_string()
676 ),
677 ]
678 );
679 }
680
681 #[test]
682 fn test_flow_extensions_can_carry_scheduled_time() {
683 let mut metadata = MetadataMap::new();
684 metadata.insert(
685 FLOW_EXTENSIONS_METADATA_KEY,
686 AsciiMetadataValue::try_from(r#"[["flow.scheduled_time_millis","1700000000000"]]"#)
687 .unwrap(),
688 );
689
690 let flow_extensions = extract_flow_extensions(&metadata).unwrap();
691 let query_ctx =
692 create_query_context(Channel::Grpc, None, flow_extensions, HashMap::new()).unwrap();
693
694 assert_eq!(
695 query_ctx.extension(FLOW_SCHEDULED_TIME_MILLIS),
696 Some("1700000000000")
697 );
698 }
699
700 #[test]
701 fn test_flow_extensions_forward_sequence_range_to_query_context() {
702 let mut metadata = MetadataMap::new();
703 metadata.insert(
704 FLOW_EXTENSIONS_METADATA_KEY,
705 AsciiMetadataValue::try_from(
706 r#"[["flow.return_region_seq","true"],["flow.incremental_mode","sequence_range"],["flow.incremental_after_seqs","{\"1\":10,\"2\":20}"],["flow.sink_table_id","42"]]"#,
707 )
708 .unwrap(),
709 );
710
711 let flow_extensions = extract_flow_extensions(&metadata).unwrap();
712 let query_ctx =
713 create_query_context(Channel::Grpc, None, flow_extensions, HashMap::new()).unwrap();
714 let parsed =
715 query::options::FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())
716 .unwrap()
717 .unwrap();
718
719 assert_eq!(
720 parsed.incremental_mode,
721 Some(FlowIncrementalMode::SequenceRange)
722 );
723 assert_eq!(
724 parsed.incremental_after_seqs,
725 Some(HashMap::from([(1, 10), (2, 20)]))
726 );
727 assert!(parsed.return_region_seq);
728 assert_eq!(parsed.sink_table_id, Some(42));
729 assert_eq!(
730 query_ctx.extension(FLOW_INCREMENTAL_MODE),
731 Some("sequence_range")
732 );
733 assert_eq!(
734 query_ctx.extension(FLOW_INCREMENTAL_AFTER_SEQS),
735 Some(r#"{"1":10,"2":20}"#)
736 );
737 assert_eq!(query_ctx.extension(FLOW_RETURN_REGION_SEQ), Some("true"));
738 assert_eq!(query_ctx.extension(FLOW_SINK_TABLE_ID), Some("42"));
739 }
740
741 #[test]
742 fn test_extract_flow_extensions_rejects_invalid_json() {
743 let mut metadata = MetadataMap::new();
744 metadata.insert(
745 FLOW_EXTENSIONS_METADATA_KEY,
746 AsciiMetadataValue::try_from("not-json").unwrap(),
747 );
748
749 let err = extract_flow_extensions(&metadata).unwrap_err();
750 assert_eq!(err.code(), tonic::Code::InvalidArgument);
751 }
752}