1use api::v1::meta::ReconcileRequest;
16use async_trait::async_trait;
17use catalog::CatalogManagerRef;
18use common_error::ext::BoxedError;
19use common_function::handlers::ProcedureServiceHandler;
20use common_meta::key::TableMetadataManagerRef;
21use common_meta::procedure_executor::{ExecutorContext, ProcedureExecutorRef};
22use common_meta::rpc::ddl::{DdlTask, SubmitDdlTaskRequest, TriggerReason};
23use common_meta::rpc::procedure::{
24 GcRegionsRequest as MetaGcRegionsRequest, GcResponse as MetaGcResponse,
25 GcTableRequest as MetaGcTableRequest, ManageRegionFollowerRequest, MigrateRegionRequest,
26 ProcedureStateResponse,
27};
28use common_query::error as query_error;
29use common_query::error::Result as QueryResult;
30use session::context::QueryContextRef;
31use snafu::ResultExt;
32use table::table_name::TableName;
33
34use crate::error;
35use crate::utils::to_executor_context;
36
37#[derive(Clone)]
39pub struct ProcedureServiceOperator {
40 procedure_executor: ProcedureExecutorRef,
41 catalog_manager: CatalogManagerRef,
42 table_metadata_manager: TableMetadataManagerRef,
43}
44
45impl ProcedureServiceOperator {
46 pub fn new(
47 procedure_executor: ProcedureExecutorRef,
48 catalog_manager: CatalogManagerRef,
49 table_metadata_manager: TableMetadataManagerRef,
50 ) -> Self {
51 Self {
52 procedure_executor,
53 catalog_manager,
54 table_metadata_manager,
55 }
56 }
57}
58
59#[async_trait]
60impl ProcedureServiceHandler for ProcedureServiceOperator {
61 async fn purge_table(
62 &self,
63 query_ctx: QueryContextRef,
64 table_name: TableName,
65 ) -> QueryResult<()> {
66 let dropped = self
67 .table_metadata_manager
68 .get_dropped_table(&table_name)
69 .await
70 .map_err(BoxedError::new)
71 .context(query_error::ProcedureServiceSnafu)?
72 .ok_or_else(|| {
73 error::TableNotFoundSnafu {
74 table_name: table_name.to_string(),
75 }
76 .build()
77 })
78 .map_err(BoxedError::new)
79 .context(query_error::ProcedureServiceSnafu)?;
80 let executor_context = to_executor_context(query_ctx, TriggerReason::Manual);
81 let request = SubmitDdlTaskRequest::new(DdlTask::new_purge_dropped_table(dropped.table_id));
82 self.procedure_executor
83 .submit_ddl_task(executor_context, request)
84 .await
85 .map_err(BoxedError::new)
86 .context(query_error::ProcedureServiceSnafu)?;
87 Ok(())
88 }
89
90 async fn migrate_region(
91 &self,
92 query_ctx: QueryContextRef,
93 request: MigrateRegionRequest,
94 ) -> QueryResult<Option<String>> {
95 let executor_context = to_executor_context(query_ctx, TriggerReason::Manual);
96 Ok(self
97 .procedure_executor
98 .migrate_region(&executor_context, request)
99 .await
100 .map_err(BoxedError::new)
101 .context(query_error::ProcedureServiceSnafu)?
102 .pid
103 .map(|pid| String::from_utf8_lossy(&pid.key).to_string()))
104 }
105
106 async fn reconcile(&self, request: ReconcileRequest) -> QueryResult<Option<String>> {
107 Ok(self
108 .procedure_executor
109 .reconcile(&ExecutorContext::default(), request)
110 .await
111 .map_err(BoxedError::new)
112 .context(query_error::ProcedureServiceSnafu)?
113 .pid
114 .map(|pid| String::from_utf8_lossy(&pid.key).to_string()))
115 }
116
117 async fn query_procedure_state(&self, pid: &str) -> QueryResult<ProcedureStateResponse> {
118 self.procedure_executor
119 .query_procedure_state(&ExecutorContext::default(), pid)
120 .await
121 .map_err(BoxedError::new)
122 .context(query_error::ProcedureServiceSnafu)
123 }
124
125 async fn manage_region_follower(
126 &self,
127 query_ctx: QueryContextRef,
128 request: ManageRegionFollowerRequest,
129 ) -> QueryResult<()> {
130 let executor_context = to_executor_context(query_ctx, TriggerReason::Manual);
131 self.procedure_executor
132 .manage_region_follower(&executor_context, request)
133 .await
134 .map_err(BoxedError::new)
135 .context(query_error::ProcedureServiceSnafu)
136 }
137
138 fn catalog_manager(&self) -> &CatalogManagerRef {
139 &self.catalog_manager
140 }
141
142 async fn gc_regions(
143 &self,
144 query_ctx: QueryContextRef,
145 request: MetaGcRegionsRequest,
146 ) -> QueryResult<MetaGcResponse> {
147 let executor_context = to_executor_context(query_ctx, TriggerReason::Manual);
148 self.procedure_executor
149 .gc_regions(&executor_context, request)
150 .await
151 .map_err(BoxedError::new)
152 .context(query_error::ProcedureServiceSnafu)
153 }
154
155 async fn gc_table(
156 &self,
157 query_ctx: QueryContextRef,
158 request: MetaGcTableRequest,
159 ) -> QueryResult<MetaGcResponse> {
160 let executor_context = to_executor_context(query_ctx, TriggerReason::Manual);
161 self.procedure_executor
162 .gc_table(&executor_context, request)
163 .await
164 .map_err(BoxedError::new)
165 .context(query_error::ProcedureServiceSnafu)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use std::collections::HashMap;
172 use std::sync::{Arc, Mutex};
173
174 use api::v1::meta::{ProcedureDetailResponse, ReconcileResponse};
175 use common_meta::key::TableMetadataManager;
176 use common_meta::key::table_route::TableRouteValue;
177 use common_meta::key::test_utils::new_test_table_info_with_name;
178 use common_meta::kv_backend::memory::MemoryKvBackend;
179 use common_meta::procedure_executor::{
180 ExecutorContext, ProcedureExecutor, ProcedureExecutorRef,
181 };
182 use common_meta::rpc::ddl::{DdlTask, SubmitDdlTaskRequest, SubmitDdlTaskResponse};
183 use common_meta::rpc::procedure::{
184 AddRegionFollowerRequest, ManageRegionFollowerRequest, MigrateRegionResponse,
185 ProcedureStateResponse,
186 };
187 use session::context::{QueryContextBuilder, QueryContextRef};
188 use table::table_name::TableName;
189
190 use super::*;
191
192 #[derive(Default)]
193 struct RecordingProcedureExecutor {
194 requests: Mutex<Vec<SubmitDdlTaskRequest>>,
195 contexts: Mutex<Vec<ExecutorContext>>,
196 fail: bool,
197 }
198
199 #[async_trait]
200 impl ProcedureExecutor for RecordingProcedureExecutor {
201 async fn submit_ddl_task(
202 &self,
203 context: ExecutorContext,
204 request: SubmitDdlTaskRequest,
205 ) -> common_meta::error::Result<SubmitDdlTaskResponse> {
206 self.contexts.lock().unwrap().push(context);
207 self.requests.lock().unwrap().push(request);
208 if self.fail {
209 return common_meta::error::UnsupportedSnafu {
210 operation: "test purge failure",
211 }
212 .fail();
213 }
214 Ok(SubmitDdlTaskResponse::default())
215 }
216 async fn migrate_region(
217 &self,
218 _: &ExecutorContext,
219 _: MigrateRegionRequest,
220 ) -> common_meta::error::Result<MigrateRegionResponse> {
221 unimplemented!()
222 }
223 async fn reconcile(
224 &self,
225 _: &ExecutorContext,
226 _: ReconcileRequest,
227 ) -> common_meta::error::Result<ReconcileResponse> {
228 unimplemented!()
229 }
230 async fn query_procedure_state(
231 &self,
232 _: &ExecutorContext,
233 _: &str,
234 ) -> common_meta::error::Result<ProcedureStateResponse> {
235 unimplemented!()
236 }
237 async fn manage_region_follower(
238 &self,
239 context: &ExecutorContext,
240 _: ManageRegionFollowerRequest,
241 ) -> common_meta::error::Result<()> {
242 self.contexts.lock().unwrap().push(context.clone());
243 Ok(())
244 }
245 async fn list_procedures(
246 &self,
247 _: &ExecutorContext,
248 ) -> common_meta::error::Result<ProcedureDetailResponse> {
249 unimplemented!()
250 }
251 }
252
253 async fn operator_with_tombstone(
254 table_id: u32,
255 name: &TableName,
256 fail: bool,
257 ) -> (ProcedureServiceOperator, Arc<RecordingProcedureExecutor>) {
258 let manager = Arc::new(TableMetadataManager::new(Arc::new(
259 MemoryKvBackend::default(),
260 )));
261 let mut info = new_test_table_info_with_name(table_id, &name.table_name);
262 info.catalog_name = name.catalog_name.clone();
263 info.schema_name = name.schema_name.clone();
264 let route = TableRouteValue::physical(vec![]);
265 manager
266 .create_table_metadata(info, route.clone(), HashMap::new())
267 .await
268 .unwrap();
269 manager
270 .delete_table_metadata(table_id, name, &route, &HashMap::new(), None)
271 .await
272 .unwrap();
273 let executor = Arc::new(RecordingProcedureExecutor {
274 fail,
275 ..Default::default()
276 });
277 let operator = ProcedureServiceOperator::new(
278 executor.clone() as ProcedureExecutorRef,
279 catalog::memory::MemoryCatalogManager::new(),
280 manager,
281 );
282 (operator, executor)
283 }
284
285 #[tokio::test]
286 async fn test_purge_table_submits_tombstone_id_and_query_context() {
287 let name = TableName::new("catalog", "schema", "metrics");
288 let (operator, executor) = operator_with_tombstone(42, &name, false).await;
289 let manager = operator.table_metadata_manager.clone();
290 let mut live = new_test_table_info_with_name(99, "metrics");
291 live.catalog_name = "catalog".to_string();
292 live.schema_name = "schema".to_string();
293 manager
294 .create_table_metadata(live, TableRouteValue::physical(vec![]), HashMap::new())
295 .await
296 .unwrap();
297 let query_ctx = QueryContextBuilder::default()
298 .current_catalog("catalog".to_string())
299 .current_schema("schema".to_string())
300 .build()
301 .into();
302
303 operator.purge_table(query_ctx, name.clone()).await.unwrap();
304
305 let requests = executor.requests.lock().unwrap();
306 assert!(
307 matches!(&requests[0].task, DdlTask::PurgeDroppedTable(task) if task.table_id == 42)
308 );
309 let contexts = executor.contexts.lock().unwrap();
310 let query_context = contexts[0].query_context.as_ref().unwrap();
311 assert_eq!(query_context.current_catalog, "catalog");
312 assert_eq!(query_context.current_schema, "schema");
313 assert_eq!(
314 contexts[0].event_input.as_ref().map(|input| input.reason),
315 Some(TriggerReason::Manual)
316 );
317 }
318
319 #[tokio::test]
320 async fn test_manage_region_follower_forwards_query_context() {
321 let manager = Arc::new(TableMetadataManager::new(Arc::new(
322 MemoryKvBackend::default(),
323 )));
324 let executor = Arc::new(RecordingProcedureExecutor::default());
325 let operator = ProcedureServiceOperator::new(
326 executor.clone(),
327 catalog::memory::MemoryCatalogManager::new(),
328 manager,
329 );
330 let query_ctx: QueryContextRef = QueryContextBuilder::default().build().into();
331
332 operator
333 .manage_region_follower(
334 query_ctx,
335 ManageRegionFollowerRequest::AddRegionFollower(AddRegionFollowerRequest {
336 region_id: 1,
337 peer_id: 2,
338 }),
339 )
340 .await
341 .unwrap();
342
343 let contexts = executor.contexts.lock().unwrap();
344 assert_eq!(contexts.len(), 1);
345 assert_eq!(contexts[0].actor.as_deref(), Some("greptime"));
347 assert_eq!(
348 contexts[0].event_input.as_ref().map(|input| input.reason),
349 Some(TriggerReason::Manual)
350 );
351 }
352
353 #[tokio::test]
354 async fn test_purge_table_missing_tombstone_is_table_not_found() {
355 let manager = Arc::new(TableMetadataManager::new(Arc::new(
356 MemoryKvBackend::default(),
357 )));
358 let executor = Arc::new(RecordingProcedureExecutor::default());
359 let operator = ProcedureServiceOperator::new(
360 executor.clone(),
361 catalog::memory::MemoryCatalogManager::new(),
362 manager,
363 );
364 let error = operator
365 .purge_table(
366 QueryContextBuilder::default()
367 .current_catalog("catalog".to_string())
368 .current_schema("schema".to_string())
369 .build()
370 .into(),
371 TableName::new("catalog", "schema", "missing"),
372 )
373 .await
374 .unwrap_err();
375 assert_eq!(
376 common_error::status_code::StatusCode::TableNotFound,
377 common_error::ext::ErrorExt::status_code(&error)
378 );
379 assert!(executor.requests.lock().unwrap().is_empty());
380 }
381
382 #[tokio::test]
383 async fn test_purge_table_propagates_submission_failure() {
384 let name = TableName::new("catalog", "schema", "metrics");
385 let (operator, executor) = operator_with_tombstone(42, &name, true).await;
386 assert!(
387 operator
388 .purge_table(
389 QueryContextBuilder::default()
390 .current_catalog("catalog".to_string())
391 .current_schema("schema".to_string())
392 .build()
393 .into(),
394 name,
395 )
396 .await
397 .is_err()
398 );
399 assert_eq!(1, executor.requests.lock().unwrap().len());
400 }
401}