1use std::future::Future;
16use std::sync::Arc;
17use std::time::Duration;
18
19use api::v1::meta::ddl_task_request::Task;
20use api::v1::meta::procedure_service_client::ProcedureServiceClient;
21use api::v1::meta::{
22 DdlTaskRequest, DdlTaskResponse, GcRegionsRequest, GcRegionsResponse, GcTableRequest,
23 GcTableResponse, MigrateRegionRequest, MigrateRegionResponse, ProcedureActor,
24 ProcedureDetailRequest, ProcedureDetailResponse, ProcedureEventContext, ProcedureId,
25 ProcedureStateResponse, QueryProcedureRequest, ReconcileRequest, ReconcileResponse,
26 RequestHeader, ResponseHeader, Role,
27};
28use common_grpc::channel_manager::ChannelManager;
29use common_meta::procedure_executor::ExecutorContext;
30use common_meta::rpc::ddl::{
31 CREATE_DATABASE_CREATOR_EXTENSION_KEY, CREATE_DATABASE_CREATOR_METADATA_KEY,
32};
33use common_meta::rpc::procedure::{
34 GcRegionsRequest as MetaGcRegionsRequest, GcResponse as MetaGcResponse,
35 GcTableRequest as MetaGcTableRequest,
36};
37use common_telemetry::tracing_context::TracingContext;
38use common_telemetry::{error, info, warn};
39use snafu::{ResultExt, ensure};
40use tokio::sync::RwLock;
41use tonic::transport::Channel;
42use tonic::{Request, Status};
43
44use crate::client::{Id, LeaderProviderRef, util};
45use crate::error;
46use crate::error::Result;
47
48pub(crate) fn procedure_event_context(context: &ExecutorContext) -> Option<ProcedureEventContext> {
53 context.event_input.as_ref().map(|input| {
54 let mut event_context = ProcedureEventContext::from(input);
55 event_context.protocol = context
56 .query_context
57 .as_ref()
58 .and_then(|query_context| query_context.protocol())
59 .unwrap_or_default();
60 event_context
61 })
62}
63
64pub(crate) fn procedure_actor(context: &ExecutorContext) -> Option<ProcedureActor> {
66 context.actor.as_ref().map(|username| ProcedureActor {
67 username: username.clone(),
68 })
69}
70
71#[derive(Clone, Debug)]
72pub struct Client {
73 inner: Arc<RwLock<Inner>>,
74}
75
76impl Client {
77 pub fn new(
78 id: Id,
79 role: Role,
80 channel_manager: ChannelManager,
81 max_retry: usize,
82 timeout: Duration,
83 ) -> Self {
84 let inner = Arc::new(RwLock::new(Inner {
85 id,
86 role,
87 channel_manager,
88 leader_provider: None,
89 max_retry,
90 timeout,
91 }));
92
93 Self { inner }
94 }
95
96 pub(crate) async fn start_with(&self, leader_provider: LeaderProviderRef) -> Result<()> {
98 let mut inner = self.inner.write().await;
99 inner.start_with(leader_provider)
100 }
101
102 pub async fn submit_ddl_task(&self, req: DdlTaskRequest) -> Result<DdlTaskResponse> {
103 let inner = self.inner.read().await;
104 inner.submit_ddl_task(req).await
105 }
106
107 pub async fn query_procedure_state(&self, pid: &str) -> Result<ProcedureStateResponse> {
109 let inner = self.inner.read().await;
110 inner.query_procedure_state(pid).await
111 }
112
113 pub async fn migrate_region(
119 &self,
120 context: &ExecutorContext,
121 region_id: u64,
122 from_peer: u64,
123 to_peer: u64,
124 timeout: Duration,
125 ) -> Result<MigrateRegionResponse> {
126 let inner = self.inner.read().await;
127 inner
128 .migrate_region(context, region_id, from_peer, to_peer, timeout)
129 .await
130 }
131
132 pub async fn reconcile(&self, request: ReconcileRequest) -> Result<ReconcileResponse> {
134 let inner = self.inner.read().await;
135 inner.reconcile(request).await
136 }
137
138 pub async fn list_procedures(&self) -> Result<ProcedureDetailResponse> {
139 let inner = self.inner.read().await;
140 inner.list_procedures().await
141 }
142
143 pub async fn gc_regions(
144 &self,
145 context: &ExecutorContext,
146 request: MetaGcRegionsRequest,
147 ) -> Result<MetaGcResponse> {
148 let inner = self.inner.read().await;
149 inner.gc_regions(context, request).await
150 }
151
152 pub async fn gc_table(
153 &self,
154 context: &ExecutorContext,
155 request: MetaGcTableRequest,
156 ) -> Result<MetaGcResponse> {
157 let inner = self.inner.read().await;
158 inner.gc_table(context, request).await
159 }
160}
161
162#[derive(Debug)]
163struct Inner {
164 id: Id,
165 role: Role,
166 channel_manager: ChannelManager,
167 leader_provider: Option<LeaderProviderRef>,
168 max_retry: usize,
169 timeout: Duration,
171}
172
173impl Inner {
174 fn start_with(&mut self, leader_provider: LeaderProviderRef) -> Result<()> {
175 ensure!(
176 !self.is_started(),
177 error::IllegalGrpcClientStateSnafu {
178 err_msg: "DDL client already started",
179 }
180 );
181 self.leader_provider = Some(leader_provider);
182 Ok(())
183 }
184
185 fn make_client(&self, addr: impl AsRef<str>) -> Result<ProcedureServiceClient<Channel>> {
186 let channel = self
187 .channel_manager
188 .get(addr)
189 .context(error::CreateChannelSnafu)?;
190
191 Ok(common_grpc::configure_tonic_client!(
192 ProcedureServiceClient::new(channel),
193 self.channel_manager,
194 ))
195 }
196
197 #[inline]
198 fn is_started(&self) -> bool {
199 self.leader_provider.is_some()
200 }
201
202 async fn with_retry<T, F, R, H>(&self, task: &str, body_fn: F, get_header: H) -> Result<T>
203 where
204 R: Future<Output = std::result::Result<T, Status>>,
205 F: Fn(ProcedureServiceClient<Channel>) -> R,
206 H: Fn(&T) -> &Option<ResponseHeader>,
207 {
208 let Some(leader_provider) = self.leader_provider.as_ref() else {
209 return error::IllegalGrpcClientStateSnafu {
210 err_msg: "not started",
211 }
212 .fail();
213 };
214
215 let mut times = 0;
216 let mut last_error = None;
217
218 while times < self.max_retry {
219 if let Some(leader) = &leader_provider.leader() {
220 let client = self.make_client(leader)?;
221 match body_fn(client).await {
222 Ok(res) => {
223 if util::is_not_leader(get_header(&res)) {
224 last_error = Some(format!("{leader} is not a leader"));
225 warn!("Failed to {task} to {leader}, not a leader");
226 let leader = leader_provider.ask_leader().await?;
227 info!("DDL client updated to new leader addr: {leader}");
228 times += 1;
229 continue;
230 }
231 return Ok(res);
232 }
233 Err(status) => {
234 if util::is_unreachable(&status) {
236 last_error = Some(status.to_string());
237 warn!("Failed to {task} to {leader}, source: {status}");
238 let leader = leader_provider.ask_leader().await?;
239 info!("Procedure client updated to new leader addr: {leader}");
240 times += 1;
241 continue;
242 } else {
243 error!("An error occurred in gRPC, status: {status:?}");
244 return Err(error::Error::from(status));
245 }
246 }
247 }
248 } else {
249 leader_provider.ask_leader().await?;
250 }
251 }
252
253 error::RetryTimesExceededSnafu {
254 msg: format!("Failed to {task}, last error: {:?}", last_error),
255 times: self.max_retry,
256 }
257 .fail()
258 }
259
260 async fn migrate_region(
261 &self,
262 context: &ExecutorContext,
263 region_id: u64,
264 from_peer: u64,
265 to_peer: u64,
266 timeout: Duration,
267 ) -> Result<MigrateRegionResponse> {
268 let mut req = MigrateRegionRequest {
269 region_id,
270 from_peer,
271 to_peer,
272 timeout_secs: timeout.as_secs() as u32,
273 event_context: procedure_event_context(context),
274 actor: procedure_actor(context),
275 ..Default::default()
276 };
277
278 req.set_header(
279 self.id,
280 self.role,
281 TracingContext::from_current_span().to_w3c(),
282 );
283
284 self.with_retry(
285 "migrate region",
286 move |mut client| {
287 let mut req = Request::new(req.clone());
288 req.set_timeout(self.timeout);
289
290 async move { client.migrate(req).await.map(|res| res.into_inner()) }
291 },
292 |resp: &MigrateRegionResponse| &resp.header,
293 )
294 .await
295 }
296
297 async fn reconcile(&self, request: ReconcileRequest) -> Result<ReconcileResponse> {
298 let mut req = request;
299 req.set_header(
300 self.id,
301 self.role,
302 TracingContext::from_current_span().to_w3c(),
303 );
304
305 self.with_retry(
306 "reconcile",
307 move |mut client| {
308 let mut req = Request::new(req.clone());
309 req.set_timeout(self.timeout);
310
311 async move { client.reconcile(req).await.map(|res| res.into_inner()) }
312 },
313 |resp: &ReconcileResponse| &resp.header,
314 )
315 .await
316 }
317
318 async fn gc_regions(
319 &self,
320 context: &ExecutorContext,
321 request: MetaGcRegionsRequest,
322 ) -> Result<MetaGcResponse> {
323 let timeout = request.timeout;
324 let req = GcRegionsRequest {
325 header: Some(RequestHeader {
326 protocol_version: 0,
327 member_id: self.id,
328 role: self.role as i32,
329 tracing_context: TracingContext::from_current_span().to_w3c(),
330 }),
331 region_ids: request.region_ids,
332 full_file_listing: request.full_file_listing,
333 timeout_secs: gc_timeout_secs(timeout),
334 event_context: procedure_event_context(context),
335 actor: procedure_actor(context),
336 };
337
338 let resp: GcRegionsResponse = self
339 .with_retry(
340 "gc_regions",
341 move |mut client| {
342 let mut req = Request::new(req.clone());
343 if let Some(timeout) = timeout {
344 req.set_timeout(timeout);
345 }
346 async move { client.gc_regions(req).await.map(|res| res.into_inner()) }
347 },
348 |resp: &GcRegionsResponse| &resp.header,
349 )
350 .await?;
351
352 let stats = resp.stats.unwrap_or_default();
353 Ok(MetaGcResponse {
354 processed_regions: stats.processed_regions,
355 need_retry_regions: stats.need_retry_regions,
356 deleted_files: stats.deleted_files,
357 deleted_indexes: stats.deleted_indexes,
358 })
359 }
360
361 async fn gc_table(
362 &self,
363 context: &ExecutorContext,
364 request: MetaGcTableRequest,
365 ) -> Result<MetaGcResponse> {
366 let timeout = request.timeout;
367 let req = GcTableRequest {
368 header: Some(RequestHeader {
369 protocol_version: 0,
370 member_id: self.id,
371 role: self.role as i32,
372 tracing_context: TracingContext::from_current_span().to_w3c(),
373 }),
374 catalog_name: request.catalog_name,
375 schema_name: request.schema_name,
376 table_name: request.table_name,
377 full_file_listing: request.full_file_listing,
378 timeout_secs: gc_timeout_secs(timeout),
379 event_context: procedure_event_context(context),
380 actor: procedure_actor(context),
381 };
382
383 let resp: GcTableResponse = self
384 .with_retry(
385 "gc_table",
386 move |mut client| {
387 let mut req = Request::new(req.clone());
388 if let Some(timeout) = timeout {
389 req.set_timeout(timeout);
390 }
391 async move { client.gc_table(req).await.map(|res| res.into_inner()) }
392 },
393 |resp: &GcTableResponse| &resp.header,
394 )
395 .await?;
396
397 let stats = resp.stats.unwrap_or_default();
398 Ok(MetaGcResponse {
399 processed_regions: stats.processed_regions,
400 need_retry_regions: stats.need_retry_regions,
401 deleted_files: stats.deleted_files,
402 deleted_indexes: stats.deleted_indexes,
403 })
404 }
405
406 async fn query_procedure_state(&self, pid: &str) -> Result<ProcedureStateResponse> {
407 let mut req = QueryProcedureRequest {
408 pid: Some(ProcedureId { key: pid.into() }),
409 ..Default::default()
410 };
411
412 req.set_header(
413 self.id,
414 self.role,
415 TracingContext::from_current_span().to_w3c(),
416 );
417
418 self.with_retry(
419 "query procedure state",
420 move |mut client| {
421 let mut req = Request::new(req.clone());
422 req.set_timeout(self.timeout);
423
424 async move { client.query(req).await.map(|res| res.into_inner()) }
425 },
426 |resp: &ProcedureStateResponse| &resp.header,
427 )
428 .await
429 }
430
431 async fn submit_ddl_task(&self, mut req: DdlTaskRequest) -> Result<DdlTaskResponse> {
432 let creator = create_database_creator_metadata_value(&req);
433 req.set_header(
434 self.id,
435 self.role,
436 TracingContext::from_current_span().to_w3c(),
437 );
438 let timeout = Duration::from_secs(req.timeout_secs.into());
439
440 self.with_retry(
441 "submit ddl task",
442 move |mut client| {
443 let mut req = Request::new(req.clone());
444 if let Some(value) = creator.as_deref() {
445 req.metadata_mut().insert_bin(
446 CREATE_DATABASE_CREATOR_METADATA_KEY,
447 tonic::metadata::MetadataValue::from_bytes(value.as_bytes()),
448 );
449 }
450 req.set_timeout(timeout);
451 async move { client.ddl(req).await.map(|res| res.into_inner()) }
452 },
453 |resp: &DdlTaskResponse| &resp.header,
454 )
455 .await
456 }
457
458 async fn list_procedures(&self) -> Result<ProcedureDetailResponse> {
459 let mut req = ProcedureDetailRequest::default();
460 req.set_header(
461 self.id,
462 self.role,
463 TracingContext::from_current_span().to_w3c(),
464 );
465
466 self.with_retry(
467 "list procedure",
468 move |mut client| {
469 let mut req = Request::new(req.clone());
470 req.set_timeout(self.timeout);
471 async move { client.details(req).await.map(|res| res.into_inner()) }
472 },
473 |resp: &ProcedureDetailResponse| &resp.header,
474 )
475 .await
476 }
477}
478
479fn create_database_creator_metadata_value(req: &DdlTaskRequest) -> Option<String> {
480 if !matches!(req.task, Some(Task::CreateDatabaseTask(_))) {
482 return None;
483 }
484
485 req.query_context
486 .as_ref()?
487 .extensions
488 .get(CREATE_DATABASE_CREATOR_EXTENSION_KEY)
489 .cloned()
490}
491
492fn gc_timeout_secs(timeout: Option<Duration>) -> u32 {
493 timeout
494 .map(|timeout| timeout.as_secs().max(1).try_into().unwrap_or(u32::MAX))
495 .unwrap_or(0)
496}
497
498#[cfg(test)]
499mod tests {
500 use std::time::{Duration, Instant};
501
502 use api::v1::meta::heartbeat_server::{Heartbeat, HeartbeatServer};
503 use api::v1::meta::procedure_service_server::{ProcedureService, ProcedureServiceServer};
504 use api::v1::meta::{
505 AskLeaderRequest, AskLeaderResponse, DdlTaskRequest, DdlTaskResponse, GcRegionsRequest,
506 GcRegionsResponse, GcTableRequest, GcTableResponse, HeartbeatRequest, HeartbeatResponse,
507 MigrateRegionRequest, MigrateRegionResponse, Peer, ProcedureDetailRequest,
508 ProcedureDetailResponse, ProcedureStateResponse, QueryProcedureRequest, ReconcileRequest,
509 ReconcileResponse, ResponseHeader, Role,
510 };
511 use async_trait::async_trait;
512 use common_base::protocol::Channel;
513 use common_error::status_code::StatusCode;
514 use common_event_recorder::{PersistentEventContext, ProcedureEventInput};
515 use common_meta::procedure_executor::{ExecutorContext, ProcedureExecutor};
516 use common_meta::rpc::ddl::{
517 CREATE_DATABASE_CREATOR_EXTENSION_KEY, CREATE_DATABASE_CREATOR_METADATA_KEY,
518 CommentObjectType, CommentOnTask, CreatorGrantIntent, DdlTask, QueryContext,
519 SubmitDdlTaskRequest, TriggerReason,
520 };
521 use common_telemetry::common_error::ext::ErrorExt;
522 use common_telemetry::info;
523 use tokio::net::TcpListener;
524 use tokio::sync::mpsc;
525 use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream};
526 use tonic::codec::CompressionEncoding;
527 use tonic::{Request, Response, Status};
528
529 use crate::client::MetaClientBuilder;
530 use crate::client::procedure::{gc_timeout_secs, procedure_event_context};
531
532 #[test]
533 fn test_gc_timeout_secs() {
534 assert_eq!(gc_timeout_secs(None), 0);
535 assert_eq!(gc_timeout_secs(Some(Duration::from_millis(1))), 1);
536 assert_eq!(gc_timeout_secs(Some(Duration::from_millis(999))), 1);
537 assert_eq!(gc_timeout_secs(Some(Duration::from_secs(1))), 1);
538 assert_eq!(gc_timeout_secs(Some(Duration::from_secs(10))), 10);
539 }
540
541 #[test]
542 fn test_procedure_event_context_derives_protocol_from_query_context() {
543 let context = ExecutorContext {
544 query_context: Some(QueryContext {
545 channel: Channel::Postgres as u8,
546 ..Default::default()
547 }),
548 event_input: Some(ProcedureEventInput::new(TriggerReason::Manual)),
549 ..Default::default()
550 };
551
552 assert_eq!(
553 procedure_event_context(&context).map(PersistentEventContext::from),
554 Some(PersistentEventContext::new(TriggerReason::Manual).with_protocol("postgres"))
555 );
556
557 let automatic_context = ExecutorContext {
558 event_input: Some(ProcedureEventInput::new(TriggerReason::ScheduledGc)),
559 ..Default::default()
560 };
561 assert_eq!(
562 procedure_event_context(&automatic_context).map(PersistentEventContext::from),
563 Some(PersistentEventContext::new(TriggerReason::ScheduledGc))
564 );
565
566 let unknown_channel_context = ExecutorContext {
567 query_context: Some(QueryContext::default()),
568 event_input: Some(ProcedureEventInput::new(TriggerReason::Manual)),
569 ..Default::default()
570 };
571 assert_eq!(
572 procedure_event_context(&unknown_channel_context).map(PersistentEventContext::from),
573 Some(PersistentEventContext::new(TriggerReason::Manual))
574 );
575 }
576
577 #[derive(Clone)]
578 struct MockHeartbeat {
579 leader_addr: String,
580 }
581
582 #[async_trait]
583 impl Heartbeat for MockHeartbeat {
584 type HeartbeatStream = ReceiverStream<Result<HeartbeatResponse, Status>>;
585
586 async fn heartbeat(
587 &self,
588 _request: Request<tonic::Streaming<HeartbeatRequest>>,
589 ) -> Result<Response<Self::HeartbeatStream>, Status> {
590 Err(Status::unimplemented(
591 "heartbeat stream is not used in this test",
592 ))
593 }
594
595 async fn ask_leader(
596 &self,
597 _request: Request<AskLeaderRequest>,
598 ) -> Result<Response<AskLeaderResponse>, Status> {
599 Ok(Response::new(AskLeaderResponse {
600 header: Some(ResponseHeader {
601 protocol_version: 0,
602 error: None,
603 }),
604 leader: Some(Peer {
605 id: 1,
606 addr: self.leader_addr.clone(),
607 }),
608 }))
609 }
610 }
611
612 #[derive(Clone)]
613 struct MockProcedure {
614 delay: Duration,
615 request_tx: Option<mpsc::UnboundedSender<Request<DdlTaskRequest>>>,
616 }
617
618 #[async_trait]
619 impl ProcedureService for MockProcedure {
620 async fn query(
621 &self,
622 _request: Request<QueryProcedureRequest>,
623 ) -> Result<Response<ProcedureStateResponse>, Status> {
624 Err(Status::unimplemented("query is not used in this test"))
625 }
626
627 async fn ddl(
628 &self,
629 request: Request<DdlTaskRequest>,
630 ) -> Result<Response<DdlTaskResponse>, Status> {
631 if let Some(request_tx) = &self.request_tx {
632 request_tx.send(request).unwrap();
633 }
634 tokio::time::sleep(self.delay).await;
635 Ok(Response::new(DdlTaskResponse {
636 header: Some(ResponseHeader {
637 protocol_version: 0,
638 error: None,
639 }),
640 ..Default::default()
641 }))
642 }
643
644 async fn reconcile(
645 &self,
646 _request: Request<ReconcileRequest>,
647 ) -> Result<Response<ReconcileResponse>, Status> {
648 Err(Status::unimplemented("reconcile is not used in this test"))
649 }
650
651 async fn migrate(
652 &self,
653 _request: Request<MigrateRegionRequest>,
654 ) -> Result<Response<MigrateRegionResponse>, Status> {
655 Err(Status::unimplemented("migrate is not used in this test"))
656 }
657
658 async fn details(
659 &self,
660 _request: Request<ProcedureDetailRequest>,
661 ) -> Result<Response<ProcedureDetailResponse>, Status> {
662 Err(Status::unimplemented("details is not used in this test"))
663 }
664
665 async fn gc_regions(
666 &self,
667 _request: Request<GcRegionsRequest>,
668 ) -> Result<Response<GcRegionsResponse>, Status> {
669 Err(Status::unimplemented("gc_regions is not used in this test"))
670 }
671
672 async fn gc_table(
673 &self,
674 _request: Request<GcTableRequest>,
675 ) -> Result<Response<GcTableResponse>, Status> {
676 Err(Status::unimplemented("gc_table is not used in this test"))
677 }
678 }
679
680 #[tokio::test(flavor = "multi_thread")]
681 async fn test_meta_client_forwards_create_database_creator_metadata() {
682 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
683 let addr_str = listener.local_addr().unwrap().to_string();
684 let (request_tx, mut request_rx) = mpsc::unbounded_channel();
685 let heartbeat = MockHeartbeat {
686 leader_addr: addr_str.clone(),
687 };
688 let procedure = MockProcedure {
689 delay: Duration::ZERO,
690 request_tx: Some(request_tx),
691 };
692 let server = tonic::transport::Server::builder()
693 .add_service(
694 HeartbeatServer::new(heartbeat).accept_compressed(CompressionEncoding::Zstd),
695 )
696 .add_service(
697 ProcedureServiceServer::new(procedure).accept_compressed(CompressionEncoding::Zstd),
698 )
699 .serve_with_incoming(TcpListenerStream::new(listener));
700 let server_handle = tokio::spawn(server);
701
702 let mut client = MetaClientBuilder::new(0, Role::Frontend)
703 .enable_heartbeat()
704 .enable_procedure()
705 .build();
706 client.start(&[addr_str.as_str()]).await.unwrap();
707
708 let creator = CreatorGrantIntent {
709 username: "alice".to_string(),
710 created_at_ns: 42,
711 };
712 let executor_context = ExecutorContext {
713 query_context: Some(QueryContext {
714 channel: Channel::Postgres as u8,
715 ..Default::default()
716 }),
717 actor: Some("effective-user".to_string()),
718 event_input: Some(ProcedureEventInput::new(TriggerReason::Manual)),
719 ..Default::default()
720 };
721 ProcedureExecutor::submit_ddl_task(
722 &client,
723 executor_context,
724 SubmitDdlTaskRequest::new(DdlTask::new_create_database(
725 "greptime".to_string(),
726 "metrics".to_string(),
727 false,
728 Default::default(),
729 Some(creator.clone()),
730 )),
731 )
732 .await
733 .unwrap();
734
735 let request = request_rx.recv().await.unwrap();
736 let encoded = serde_json::to_string(&creator).unwrap();
737 assert_eq!(
738 request
739 .metadata()
740 .get_bin(CREATE_DATABASE_CREATOR_METADATA_KEY)
741 .unwrap()
742 .to_bytes()
743 .unwrap()
744 .as_ref(),
745 encoded.as_bytes()
746 );
747 let request = request.into_inner();
748 assert_eq!(request.actor.unwrap().username, "effective-user");
749 assert_eq!(
750 PersistentEventContext::from(request.event_context.unwrap()),
751 PersistentEventContext::new(TriggerReason::Manual).with_protocol("postgres")
752 );
753 let extensions = &request.query_context.unwrap().extensions;
754 assert_eq!(extensions[CREATE_DATABASE_CREATOR_EXTENSION_KEY], encoded);
755
756 server_handle.abort();
757 }
758
759 #[tokio::test(flavor = "multi_thread")]
760 async fn test_meta_client_ddl_request_timeout() {
761 common_telemetry::init_default_ut_logging();
762
763 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
764 let addr = listener.local_addr().unwrap();
765 let addr_str = addr.to_string();
766
767 let heartbeat = MockHeartbeat {
768 leader_addr: addr_str.clone(),
769 };
770 let procedure = MockProcedure {
771 delay: Duration::from_secs(4),
772 request_tx: None,
773 };
774
775 let server = tonic::transport::Server::builder()
776 .add_service(
777 HeartbeatServer::new(heartbeat)
778 .accept_compressed(CompressionEncoding::Gzip)
779 .accept_compressed(CompressionEncoding::Zstd),
780 )
781 .add_service(
782 ProcedureServiceServer::new(procedure)
783 .accept_compressed(CompressionEncoding::Gzip)
784 .accept_compressed(CompressionEncoding::Zstd),
785 )
786 .serve_with_incoming(TcpListenerStream::new(listener));
787 let server_handle = tokio::spawn(server);
788
789 let mut client = MetaClientBuilder::new(0, Role::Frontend)
790 .enable_heartbeat()
791 .enable_procedure()
792 .build();
793 client.start(&[addr_str.as_str()]).await.unwrap();
794
795 let mut request = SubmitDdlTaskRequest::new(DdlTask::new_comment_on(CommentOnTask {
796 catalog_name: "greptime".to_string(),
797 schema_name: "public".to_string(),
798 object_type: CommentObjectType::Table,
799 object_name: "test_table".to_string(),
800 column_name: None,
801 object_id: None,
802 comment: Some("timeout".to_string()),
803 }));
804 request.timeout = Duration::from_secs(1);
805
806 let now = Instant::now();
807 let err = client
808 .submit_ddl_task(
809 ExecutorContext {
810 query_context: Some(QueryContext::default()),
811 ..Default::default()
812 },
813 request,
814 )
815 .await
816 .unwrap_err();
817 let elapsed = now.elapsed();
818 assert!(elapsed < Duration::from_secs(2));
820 info!("err: {err:?}, code: {}", err.status_code());
821 assert_eq!(err.status_code(), StatusCode::Cancelled);
822 let err_msg = err.to_string();
823 assert!(
824 err_msg.contains("Timeout expired"),
825 "unexpected error: {err_msg}"
826 );
827
828 server_handle.abort();
829 }
830}