1use std::collections::HashMap;
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use common_error::ext::{BoxedError, ErrorExt};
20use common_procedure::error::{FromJsonSnafu, Result as ProcedureResult, ToJsonSnafu};
21use common_procedure::{
22 Context as ProcedureContext, EventContext, EventTrigger, LockKey, Procedure, ProcedureId,
23 Status,
24};
25use serde::{Deserialize, Serialize};
26use serde_with::{DefaultOnNull, serde_as};
27use snafu::{ResultExt, ensure};
28use strum::AsRefStr;
29
30use crate::ddl::DdlContext;
31use crate::ddl::event::database::{CREATE_DATABASE_EVENT_TYPE, DatabaseDdlEvent};
32use crate::ddl::utils::map_to_procedure_error;
33use crate::error::{self, Result};
34use crate::instruction::{CacheIdent, UserCacheIdent};
35use crate::key::schema_name::{SchemaNameKey, SchemaNameValue};
36use crate::lock_key::{CatalogLock, SchemaLock};
37use crate::rpc::ddl::CreatorGrantIntent;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum AtomicCreateAccess {
42 AddedExactAll,
44 AlreadyEffectiveAll,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum AtomicCreateOutcome {
51 Created(AtomicCreateAccess),
53 AlreadyCommitted,
55 UserChanged,
57 UserMissing,
59 SchemaConflict,
61}
62
63#[async_trait]
68pub trait CreateDatabaseMetadataCommitter: Send + Sync {
69 async fn commit(
70 &self,
71 catalog: &str,
72 schema: &str,
73 value: &SchemaNameValue,
74 creator: &CreatorGrantIntent,
75 ) -> std::result::Result<AtomicCreateOutcome, BoxedError>;
76}
77
78pub type CreateDatabaseMetadataCommitterRef = Arc<dyn CreateDatabaseMetadataCommitter>;
80
81pub struct CreateDatabaseProcedure {
83 pub context: DdlContext,
84 pub data: CreateDatabaseData,
85}
86
87impl CreateDatabaseProcedure {
88 pub const TYPE_NAME: &'static str = "metasrv-procedure::CreateDatabase";
89
90 pub fn new(
91 catalog: String,
92 schema: String,
93 create_if_not_exists: bool,
94 options: HashMap<String, String>,
95 creator: Option<CreatorGrantIntent>,
96 context: DdlContext,
97 ) -> Self {
98 Self {
99 context,
100 data: CreateDatabaseData {
101 state: CreateDatabaseState::Prepare,
102 catalog,
103 schema,
104 create_if_not_exists,
105 options,
106 creator,
107 },
108 }
109 }
110
111 pub fn from_json(json: &str, context: DdlContext) -> ProcedureResult<Self> {
112 let data = serde_json::from_str(json).context(FromJsonSnafu)?;
113
114 Ok(Self { context, data })
115 }
116
117 pub async fn on_prepare(&mut self) -> Result<Status> {
118 let exists = self
119 .context
120 .table_metadata_manager
121 .schema_manager()
122 .exists(SchemaNameKey::new(&self.data.catalog, &self.data.schema))
123 .await?;
124
125 if exists && self.data.create_if_not_exists {
126 return Ok(Status::done());
127 }
128
129 ensure!(
130 !exists,
131 error::SchemaAlreadyExistsSnafu {
132 catalog: &self.data.catalog,
133 schema: &self.data.schema,
134 }
135 );
136
137 self.data.state = CreateDatabaseState::CreateMetadata;
138 Ok(Status::executing(true))
139 }
140
141 pub async fn on_create_metadata(&mut self, procedure_id: ProcedureId) -> Result<Status> {
142 let mut value: SchemaNameValue = (&self.data.options).try_into()?;
143
144 if let Some(creator) = self.data.creator.as_ref() {
145 value.create_procedure_id = Some(procedure_id.to_string());
147 let Some(committer) = self.context.create_database_metadata_committer.as_ref() else {
148 return error::UnexpectedSnafu {
149 err_msg: "Create-database creator grant committer is not configured"
150 .to_string(),
151 }
152 .fail();
153 };
154 let _timer = crate::metrics::METRIC_META_CREATE_SCHEMA.start_timer();
155 let outcome = match committer
156 .commit(&self.data.catalog, &self.data.schema, &value, creator)
157 .await
158 {
159 Ok(outcome) => outcome,
160 Err(err) if err.is_retryable() => {
161 return Err(error::Error::RetryLater {
162 source: err,
163 clean_poisons: false,
164 });
165 }
166 Err(err) => return Err(err).context(error::ExternalSnafu),
167 };
168
169 match outcome {
170 AtomicCreateOutcome::Created(_) => {
171 crate::metrics::METRIC_META_CREATE_SCHEMA_COUNTER.inc();
172 }
173 AtomicCreateOutcome::AlreadyCommitted => {}
174 AtomicCreateOutcome::UserChanged => {
175 let source = error::UnexpectedSnafu {
176 err_msg: format!(
177 "User '{}' changed while creating database '{}.{}'",
178 creator.username, self.data.catalog, self.data.schema
179 ),
180 }
181 .build();
182 return Err(error::Error::retry_later(source));
183 }
184 AtomicCreateOutcome::UserMissing => {
185 return error::UnexpectedSnafu {
186 err_msg: format!(
187 "Creator '{}' no longer exists in catalog '{}'",
188 creator.username, self.data.catalog
189 ),
190 }
191 .fail();
192 }
193 AtomicCreateOutcome::SchemaConflict if self.data.create_if_not_exists => {
194 return Ok(Status::done());
195 }
196 AtomicCreateOutcome::SchemaConflict => {
197 return error::SchemaAlreadyExistsSnafu {
198 catalog: &self.data.catalog,
199 schema: &self.data.schema,
200 }
201 .fail();
202 }
203 }
204
205 self.data.state = CreateDatabaseState::InvalidateCreatorCache;
207 return Ok(Status::executing(true));
208 }
209
210 self.context
211 .table_metadata_manager
212 .schema_manager()
213 .create(
214 SchemaNameKey::new(&self.data.catalog, &self.data.schema),
215 Some(value),
216 self.data.create_if_not_exists,
217 )
218 .await?;
219
220 Ok(Status::done())
221 }
222
223 pub async fn on_invalidate_creator_cache(&mut self) -> Result<Status> {
224 let Some(creator) = self.data.creator.as_ref() else {
225 return Ok(Status::done());
226 };
227
228 let ident = CacheIdent::User(UserCacheIdent {
229 catalog: self.data.catalog.clone(),
230 username: creator.username.clone(),
231 });
232 let ctx = crate::cache_invalidator::Context {
233 subject: Some(format!(
234 "Invalidate creator cache after creating database '{}.{}' for '{}'",
235 self.data.catalog, self.data.schema, creator.username
236 )),
237 };
238 if let Err(source) = self
239 .context
240 .cache_invalidator
241 .invalidate(&ctx, &[ident])
242 .await
243 {
244 return Err(error::Error::retry_later(source));
245 }
246
247 Ok(Status::done())
248 }
249}
250
251#[async_trait]
252impl Procedure for CreateDatabaseProcedure {
253 fn type_name(&self) -> &str {
254 Self::TYPE_NAME
255 }
256
257 async fn execute(&mut self, ctx: &ProcedureContext) -> ProcedureResult<Status> {
258 let state = &self.data.state;
259
260 match state {
261 CreateDatabaseState::Prepare => self.on_prepare().await,
262 CreateDatabaseState::CreateMetadata => self.on_create_metadata(ctx.procedure_id).await,
263 CreateDatabaseState::InvalidateCreatorCache => self.on_invalidate_creator_cache().await,
264 }
265 .map_err(map_to_procedure_error)
266 }
267
268 fn dump(&self) -> ProcedureResult<String> {
269 serde_json::to_string(&self.data).context(ToJsonSnafu)
270 }
271
272 fn lock_key(&self) -> LockKey {
273 let lock_key = vec![
274 CatalogLock::Read(&self.data.catalog).into(),
275 SchemaLock::write(&self.data.catalog, &self.data.schema).into(),
276 ];
277
278 LockKey::new(lock_key)
279 }
280
281 fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
282 if !ctx.event_type_filter.allows(CREATE_DATABASE_EVENT_TYPE) {
283 return None;
284 }
285
286 let event = if matches!(&ctx.trigger, EventTrigger::Submitted) {
287 DatabaseDdlEvent::create_submitted(
288 &self.data.catalog,
289 &self.data.schema,
290 self.data.create_if_not_exists,
291 &self.data.options,
292 )
293 } else {
294 DatabaseDdlEvent::create_lifecycle(&self.data.catalog, &self.data.schema)
295 };
296 Some(Box::new(event))
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)]
302pub enum CreateDatabaseState {
303 Prepare,
305 CreateMetadata,
307 InvalidateCreatorCache,
309}
310
311#[serde_as]
313#[derive(Debug, Serialize, Deserialize)]
314pub struct CreateDatabaseData {
315 pub state: CreateDatabaseState,
316 pub catalog: String,
317 pub schema: String,
318 pub create_if_not_exists: bool,
319 #[serde_as(deserialize_as = "DefaultOnNull")]
320 pub options: HashMap<String, String>,
321 #[serde(default)]
323 pub creator: Option<CreatorGrantIntent>,
324}
325
326#[cfg(test)]
327mod tests {
328 use std::collections::VecDeque;
329 use std::sync::Mutex;
330 use std::sync::atomic::{AtomicUsize, Ordering};
331
332 use common_error::ext::ErrorExt;
333
334 use super::*;
335 use crate::cache_invalidator::{CacheInvalidator, Context};
336 use crate::test_util::{MockDatanodeManager, new_ddl_context};
337
338 const PROCEDURE_ID: &str = "4ee0ba94-11f0-4d4d-9468-5ebf732e3ab2";
339
340 fn procedure_id() -> ProcedureId {
341 ProcedureId::parse_str(PROCEDURE_ID).unwrap()
342 }
343
344 struct MockCommitter {
345 calls: AtomicUsize,
346 outcomes: Mutex<VecDeque<std::result::Result<AtomicCreateOutcome, BoxedError>>>,
347 }
348
349 impl MockCommitter {
350 fn new(
351 outcomes: impl IntoIterator<Item = std::result::Result<AtomicCreateOutcome, BoxedError>>,
352 ) -> Self {
353 Self {
354 calls: AtomicUsize::new(0),
355 outcomes: Mutex::new(outcomes.into_iter().collect()),
356 }
357 }
358 }
359
360 #[async_trait]
361 impl CreateDatabaseMetadataCommitter for MockCommitter {
362 async fn commit(
363 &self,
364 _catalog: &str,
365 _schema: &str,
366 value: &SchemaNameValue,
367 _creator: &CreatorGrantIntent,
368 ) -> std::result::Result<AtomicCreateOutcome, BoxedError> {
369 assert_eq!(value.create_procedure_id.as_deref(), Some(PROCEDURE_ID));
370 self.calls.fetch_add(1, Ordering::Relaxed);
371 self.outcomes
372 .lock()
373 .unwrap()
374 .pop_front()
375 .expect("unexpected commit call")
376 }
377 }
378
379 #[derive(Default)]
380 struct FailOnceInvalidator {
381 calls: AtomicUsize,
382 }
383
384 #[async_trait]
385 impl CacheInvalidator for FailOnceInvalidator {
386 async fn invalidate(&self, _ctx: &Context, _caches: &[CacheIdent]) -> Result<()> {
387 if self.calls.fetch_add(1, Ordering::Relaxed) == 0 {
388 return error::UnexpectedSnafu {
389 err_msg: "injected cache invalidation failure",
390 }
391 .fail();
392 }
393 Ok(())
394 }
395
396 fn invalidate_all(&self) -> Result<()> {
397 Ok(())
398 }
399 }
400
401 fn procedure(
402 outcomes: impl IntoIterator<Item = std::result::Result<AtomicCreateOutcome, BoxedError>>,
403 cache_invalidator: Arc<dyn CacheInvalidator>,
404 ) -> (CreateDatabaseProcedure, Arc<MockCommitter>) {
405 let mut context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
406 context.cache_invalidator = cache_invalidator;
407 let committer = Arc::new(MockCommitter::new(outcomes));
408 context.create_database_metadata_committer = Some(committer.clone());
409 let procedure = CreateDatabaseProcedure::new(
410 "greptime".to_string(),
411 "metrics".to_string(),
412 false,
413 HashMap::new(),
414 Some(CreatorGrantIntent {
415 username: "alice".to_string(),
416 created_at_ns: 1,
417 }),
418 context,
419 );
420 (procedure, committer)
421 }
422
423 #[tokio::test]
424 async fn created_and_recovered_commits_retry_only_cache_invalidation() {
425 let invalidator = Arc::new(FailOnceInvalidator::default());
426 let (mut procedure, committer) = procedure(
427 [Ok(AtomicCreateOutcome::Created(
428 AtomicCreateAccess::AddedExactAll,
429 ))],
430 invalidator.clone(),
431 );
432 procedure.data.state = CreateDatabaseState::CreateMetadata;
433
434 assert!(
435 !procedure
436 .on_create_metadata(procedure_id())
437 .await
438 .unwrap()
439 .is_done()
440 );
441 assert!(matches!(
442 procedure.data.state,
443 CreateDatabaseState::InvalidateCreatorCache
444 ));
445 assert_eq!(committer.calls.load(Ordering::Relaxed), 1);
446
447 assert!(
448 procedure
449 .on_invalidate_creator_cache()
450 .await
451 .unwrap_err()
452 .is_retryable()
453 );
454 assert!(
455 procedure
456 .on_invalidate_creator_cache()
457 .await
458 .unwrap()
459 .is_done()
460 );
461 assert_eq!(committer.calls.load(Ordering::Relaxed), 1);
462 assert_eq!(invalidator.calls.load(Ordering::Relaxed), 2);
463 }
464
465 #[tokio::test]
466 async fn user_change_is_explicitly_retryable() {
467 let (mut procedure, committer) = procedure(
468 [Ok(AtomicCreateOutcome::UserChanged)],
469 Arc::new(FailOnceInvalidator::default()),
470 );
471 procedure.data.state = CreateDatabaseState::CreateMetadata;
472
473 let err = procedure
474 .on_create_metadata(procedure_id())
475 .await
476 .unwrap_err();
477 assert!(err.is_retryable());
478 assert!(matches!(
479 procedure.data.state,
480 CreateDatabaseState::CreateMetadata
481 ));
482 assert_eq!(committer.calls.load(Ordering::Relaxed), 1);
483 }
484
485 #[tokio::test]
486 async fn lost_commit_response_replays_as_already_committed() {
487 let (mut procedure, committer) = procedure(
488 [
489 Err(BoxedError::new(
490 error::ElectionLeaderLeaseChangedSnafu.build(),
491 )),
492 Ok(AtomicCreateOutcome::AlreadyCommitted),
493 ],
494 Arc::new(FailOnceInvalidator::default()),
495 );
496 procedure.data.state = CreateDatabaseState::CreateMetadata;
497
498 let err = procedure
499 .on_create_metadata(procedure_id())
500 .await
501 .unwrap_err();
502 assert!(matches!(err, error::Error::RetryLater { .. }));
503 assert!(matches!(
504 procedure.data.state,
505 CreateDatabaseState::CreateMetadata
506 ));
507 assert!(
508 !procedure
509 .on_create_metadata(procedure_id())
510 .await
511 .unwrap()
512 .is_done()
513 );
514 assert!(matches!(
515 procedure.data.state,
516 CreateDatabaseState::InvalidateCreatorCache
517 ));
518 assert_eq!(committer.calls.load(Ordering::Relaxed), 2);
519 }
520
521 #[tokio::test]
522 async fn lost_state_transition_recovers_committed_business_state() {
523 let (mut procedure, committer) = procedure(
524 [
525 Ok(AtomicCreateOutcome::Created(
526 AtomicCreateAccess::AddedExactAll,
527 )),
528 Ok(AtomicCreateOutcome::AlreadyCommitted),
529 ],
530 Arc::new(FailOnceInvalidator::default()),
531 );
532 let context = procedure.context.clone();
533 procedure.data.state = CreateDatabaseState::CreateMetadata;
534 let stale_json = procedure.dump().unwrap();
535
536 assert!(
537 !procedure
538 .on_create_metadata(procedure_id())
539 .await
540 .unwrap()
541 .is_done()
542 );
543 assert!(matches!(
544 procedure.data.state,
545 CreateDatabaseState::InvalidateCreatorCache
546 ));
547
548 let mut recovered = CreateDatabaseProcedure::from_json(&stale_json, context).unwrap();
549 assert_eq!(recovered.data.creator.as_ref().unwrap().username, "alice");
550 assert!(
551 !recovered
552 .on_create_metadata(procedure_id())
553 .await
554 .unwrap()
555 .is_done()
556 );
557 assert!(matches!(
558 recovered.data.state,
559 CreateDatabaseState::InvalidateCreatorCache
560 ));
561 assert_eq!(committer.calls.load(Ordering::Relaxed), 2);
562 }
563
564 #[tokio::test]
565 async fn missing_creator_uses_legacy_schema_path() {
566 let mut context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
567 let committer = Arc::new(MockCommitter::new([Ok(AtomicCreateOutcome::Created(
568 AtomicCreateAccess::AddedExactAll,
569 ))]));
570 context.create_database_metadata_committer = Some(committer.clone());
571 let mut procedure = CreateDatabaseProcedure::new(
572 "greptime".to_string(),
573 "legacy".to_string(),
574 false,
575 HashMap::new(),
576 None,
577 context,
578 );
579 procedure.data.state = CreateDatabaseState::CreateMetadata;
580
581 assert!(
582 procedure
583 .on_create_metadata(procedure_id())
584 .await
585 .unwrap()
586 .is_done()
587 );
588 assert_eq!(committer.calls.load(Ordering::Relaxed), 0);
589 assert!(
590 procedure
591 .context
592 .table_metadata_manager
593 .schema_manager()
594 .exists(SchemaNameKey::new("greptime", "legacy"))
595 .await
596 .unwrap()
597 );
598 }
599
600 #[tokio::test]
601 async fn creator_without_committer_fails_closed() {
602 let context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
603 let mut procedure = CreateDatabaseProcedure::new(
604 "greptime".to_string(),
605 "missing_committer".to_string(),
606 false,
607 HashMap::new(),
608 Some(CreatorGrantIntent {
609 username: "alice".to_string(),
610 created_at_ns: 1,
611 }),
612 context,
613 );
614 procedure.data.state = CreateDatabaseState::CreateMetadata;
615
616 assert!(procedure.on_create_metadata(procedure_id()).await.is_err());
617 assert!(
618 !procedure
619 .context
620 .table_metadata_manager
621 .schema_manager()
622 .exists(SchemaNameKey::new("greptime", "missing_committer"))
623 .await
624 .unwrap()
625 );
626 }
627
628 #[test]
629 fn legacy_procedure_json_defaults_creator() {
630 let context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
631 let procedure = CreateDatabaseProcedure::from_json(
632 r#"{
633 "state":"CreateMetadata",
634 "catalog":"greptime",
635 "schema":"metrics",
636 "create_if_not_exists":false,
637 "options":{}
638 }"#,
639 context,
640 )
641 .unwrap();
642
643 assert!(procedure.data.creator.is_none());
644 }
645}