diff --git a/src/catalog/tests/open.rs b/src/catalog/tests/open.rs index f09f6b4d89a45..a34093f60a57e 100644 --- a/src/catalog/tests/open.rs +++ b/src/catalog/tests/open.rs @@ -19,7 +19,8 @@ use mz_catalog::durable::objects::{DurableType, Snapshot}; use mz_catalog::durable::{ BUILTIN_MIGRATION_SHARD_KEY, CATALOG_VERSION, CatalogError, Database, DurableCatalogError, DurableCatalogState, EXPRESSION_CACHE_SHARD_KEY, Epoch, FenceError, - MOCK_AUTHENTICATION_NONCE_KEY, Schema, TestCatalogStateBuilder, test_bootstrap_args, + MOCK_AUTHENTICATION_NONCE_KEY, Schema, TestCatalogStateBuilder, Transaction, + test_bootstrap_args, }; use mz_catalog_protos::objects::{SettingKey, SettingValue}; use mz_ore::cast::usize_to_u64; @@ -30,7 +31,9 @@ use mz_persist_client::{PersistClient, PersistLocation}; use mz_persist_types::ShardId; use mz_proto::RustType; use mz_repr::role_id::RoleId; +use mz_repr::{CatalogItemId, GlobalId}; use mz_sql::catalog::{RoleAttributesRaw, RoleMembership, RoleVars}; +use mz_sql::names::SchemaId; use uuid::Uuid; /// A new type for [`Snapshot`] that excludes fields that change often from the debug output. It's @@ -482,6 +485,295 @@ async fn test_open_read_only(state_builder: TestCatalogStateBuilder) { Box::new(state).expire().await; } +/// The ids of all items in a catalog snapshot. +fn item_ids(snapshot: &Snapshot) -> Vec { + snapshot + .items + .keys() + .map(|key| CatalogItemId::from_proto(key.gid.clone()).unwrap()) + .collect() +} + +/// Inserts a view item. A `Some` owner session tags the item as temporary. +fn insert_view( + txn: &mut Transaction<'_>, + id: CatalogItemId, + schema_id: SchemaId, + name: &str, + ephemeral_owner_session: Option, +) { + let CatalogItemId::User(raw_id) = id else { + panic!("tests only insert user items"); + }; + txn.insert_item( + id, + u32::try_from(20_000 + raw_id).expect("small"), + GlobalId::User(raw_id), + schema_id, + name, + format!("CREATE VIEW {name} AS SELECT 1"), + RoleId::User(1), + vec![], + BTreeMap::new(), + ephemeral_owner_session, + ) + .unwrap(); +} + +#[mz_ore::test(tokio::test)] +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` +async fn test_persist_open_reclaims_ephemeral_items() { + let persist_client = PersistClient::new_for_tests().await; + let state_builder = TestCatalogStateBuilder::new(persist_client); + test_open_reclaims_ephemeral_items(state_builder).await; +} + +/// Temporary items are durable, tagged with the UUID of the session that +/// created them, so a process that dies without running its session-close +/// cleanup leaves them behind. Opening the catalog with write intent fences out +/// every previous owner, which means every session that could own one is dead, +/// so that open reclaims them. This is the only thing standing between a +/// `kill -9` and a permanently leaked catalog item. +/// +/// A read-only open must not reclaim anything: during a zero-downtime deploy the +/// follower reads the leader's catalog while the leader's sessions are still +/// live and still own their temporary items. +async fn test_open_reclaims_ephemeral_items(state_builder: TestCatalogStateBuilder) { + let state_builder = state_builder.with_default_deploy_generation(); + let owner_session = Uuid::from_u128(1); + let ephemeral_id = CatalogItemId::User(200); + let normal_id = CatalogItemId::User(100); + + // A session creates a temporary item, next to a normal one, and the process + // then dies without closing the session. + { + let mut state = state_builder + .clone() + .unwrap_build() + .await + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + let _ = state + .sync_to_current_updates() + .await + .expect("unable to sync"); + + let mut txn = state.transaction().await.unwrap(); + insert_view(&mut txn, normal_id, SchemaId::User(1), "keep", None); + // Temporary items are parented to a sentinel schema id shared by + // every session. + insert_view( + &mut txn, + ephemeral_id, + SchemaId::User(0), + "tt", + Some(owner_session), + ); + let _ = txn.get_and_commit_op_updates(); + let commit_ts = txn.upper(); + txn.commit(commit_ts).await.unwrap(); + + let snapshot = state.snapshot().await.unwrap(); + assert!(item_ids(&snapshot).contains(&ephemeral_id)); + Box::new(state).expire().await; + } + + // A read-only open leaves it alone. Checked before the writable open below, + // which is what removes it. + { + let mut read_only_state = state_builder + .clone() + .unwrap_build() + .await + .open_read_only(&test_bootstrap_args()) + .await + .unwrap(); + let ids = item_ids(&read_only_state.snapshot().await.unwrap()); + assert!( + ids.contains(&ephemeral_id), + "read-only open reclaimed an ephemeral item" + ); + Box::new(read_only_state).expire().await; + } + + // Opening with write intent reclaims it, and only it. + { + let mut state = state_builder + .unwrap_build() + .await + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + let ids = item_ids(&state.snapshot().await.unwrap()); + assert!( + !ids.contains(&ephemeral_id), + "writable open did not reclaim the ephemeral item" + ); + assert!( + ids.contains(&normal_id), + "writable open reclaimed a non-ephemeral item" + ); + Box::new(state).expire().await; + } +} + +#[mz_ore::test(tokio::test)] +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` +async fn test_persist_open_reclaims_late_ephemeral_items() { + let persist_client = PersistClient::new_for_tests().await; + let state_builder = TestCatalogStateBuilder::new(persist_client); + test_open_reclaims_late_ephemeral_items(state_builder).await; +} + +/// During a zero-downtime deploy the outgoing leader keeps serving sessions +/// while the incoming generation opens the catalog, so a temporary item can +/// become durable after the incoming opener has synced its initial snapshot +/// but before its fence lands. The open must reclaim items it did not know +/// about when the opener was built, since reclamation runs on the state synced +/// by the fence loop, not on the build-time snapshot. +async fn test_open_reclaims_late_ephemeral_items(state_builder: TestCatalogStateBuilder) { + let owner_session = Uuid::from_u128(1); + let ephemeral_id = CatalogItemId::User(200); + let deploy_generation = 0; + + // The outgoing leader. + let mut state = state_builder + .clone() + .with_deploy_generation(deploy_generation) + .unwrap_build() + .await + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + let _ = state + .sync_to_current_updates() + .await + .expect("unable to sync"); + + // The incoming generation syncs its snapshot of the catalog here, before + // the temporary item below exists. Building does not fence anyone, only + // the open below does. + let unopened = state_builder + .with_deploy_generation(deploy_generation + 1) + .unwrap_build() + .await; + + // A session on the still unfenced leader creates a temporary item. + let mut txn = state.transaction().await.unwrap(); + insert_view( + &mut txn, + ephemeral_id, + SchemaId::User(0), + "tt", + Some(owner_session), + ); + let _ = txn.get_and_commit_op_updates(); + let commit_ts = txn.upper(); + txn.commit(commit_ts).await.unwrap(); + assert!( + item_ids(&state.snapshot().await.unwrap()).contains(&ephemeral_id), + "the temporary item must be durable before the promotion" + ); + + // Promotion. The open fences the leader and reclaims the item the opener + // never saw at build time. + let mut new_state = unopened + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + let ids = item_ids(&new_state.snapshot().await.unwrap()); + assert!( + !ids.contains(&ephemeral_id), + "open did not reclaim an ephemeral item committed after the opener was built" + ); + + // The outgoing leader is fenced. + let err = state.transaction().await.unwrap_err(); + assert!( + matches!( + err, + CatalogError::Durable(DurableCatalogError::Fence( + FenceError::DeployGeneration { .. } + )) + ), + "unexpected err: {err:?}" + ); + Box::new(new_state).expire().await; +} + +#[mz_ore::test(tokio::test)] +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` +async fn test_persist_fenced_ephemeral_item_write() { + let persist_client = PersistClient::new_for_tests().await; + let state_builder = TestCatalogStateBuilder::new(persist_client); + test_fenced_ephemeral_item_write(state_builder).await; +} + +/// A fenced-out catalog cannot durably create a temporary item. Once the fence +/// from a newer deploy generation lands, the old owner's in-flight commit is +/// rejected wholesale, so the item never becomes durable and there is nothing +/// for the new generation to reclaim. +async fn test_fenced_ephemeral_item_write(state_builder: TestCatalogStateBuilder) { + let owner_session = Uuid::from_u128(1); + let ephemeral_id = CatalogItemId::User(200); + let deploy_generation = 0; + + let mut state = state_builder + .clone() + .with_deploy_generation(deploy_generation) + .unwrap_build() + .await + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + let _ = state + .sync_to_current_updates() + .await + .expect("unable to sync"); + + // A session starts creating a temporary item but has not committed yet. + let mut txn = state.transaction().await.unwrap(); + insert_view( + &mut txn, + ephemeral_id, + SchemaId::User(0), + "tt", + Some(owner_session), + ); + let _ = txn.get_and_commit_op_updates(); + + // A newer generation opens before the commit lands. + let mut new_state = state_builder + .with_deploy_generation(deploy_generation + 1) + .unwrap_build() + .await + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + + // The commit is rejected by the fence. + let commit_ts = txn.upper(); + let err = txn.commit(commit_ts).await.unwrap_err(); + assert!( + matches!( + err, + CatalogError::Durable(DurableCatalogError::Fence( + FenceError::DeployGeneration { .. } + )) + ), + "unexpected err: {err:?}" + ); + + // The item never became durable. + let ids = item_ids(&new_state.snapshot().await.unwrap()); + assert!( + !ids.contains(&ephemeral_id), + "rejected ephemeral item write became durable" + ); + Box::new(new_state).expire().await; +} + #[mz_ore::test(tokio::test)] #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` async fn test_persist_open() { diff --git a/src/catalog/tests/read-write.rs b/src/catalog/tests/read-write.rs index c312590a60258..90f1bc480e24b 100644 --- a/src/catalog/tests/read-write.rs +++ b/src/catalog/tests/read-write.rs @@ -31,6 +31,7 @@ use mz_repr::role_id::RoleId; use mz_repr::{CatalogItemId, GlobalId}; use mz_sql::catalog::{RoleAttributesRaw, RoleMembership, RoleVars}; use mz_sql::names::{DatabaseId, ResolvedDatabaseSpecifier, SchemaId}; +use uuid::Uuid; #[mz_ore::test(tokio::test)] #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` @@ -517,6 +518,118 @@ async fn test_items(state_builder: TestCatalogStateBuilder) { Box::new(state).expire().await; } +#[mz_ore::test(tokio::test)] +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` +async fn test_persist_ephemeral_items() { + let persist_client = PersistClient::new_for_tests().await; + let state_builder = TestCatalogStateBuilder::new(persist_client); + test_ephemeral_items(state_builder).await; +} + +/// Temporary items are durable items tagged with the UUID of the session that +/// created them. Two properties hold them together: +/// +/// - Name uniqueness is scoped by that tag, because every session's temporary +/// schema shares one sentinel schema id, so without the scoping two sessions +/// could not both hold a `tt`. +/// - `remove_ephemeral_items` reclaims all of them and nothing else. It is what +/// a writable catalog open uses to clean up after a crash, so an over-broad +/// filter here would silently delete real user items. +async fn test_ephemeral_items(state_builder: TestCatalogStateBuilder) { + let state_builder = state_builder.with_default_deploy_generation(); + let session_a = Uuid::from_u128(1); + let session_b = Uuid::from_u128(2); + // The sentinel schema id that every session's temporary schema shares. + let temp_schema = SchemaId::User(0); + + let mut state = state_builder + .unwrap_build() + .await + .open(SYSTEM_TIME().into(), &test_bootstrap_args()) + .await + .unwrap(); + // Drain initial updates. + let _ = state + .sync_to_current_updates() + .await + .expect("unable to sync"); + + let mut txn = state.transaction().await.unwrap(); + + let insert = |txn: &mut mz_catalog::durable::Transaction, + id: u64, + schema_id: SchemaId, + name: &str, + owner_session: Option| { + txn.insert_item( + CatalogItemId::User(id), + u32::try_from(20_000 + id).expect("small"), + GlobalId::User(id), + schema_id, + name, + format!("CREATE VIEW {name} AS SELECT 1"), + RoleId::User(1), + vec![], + BTreeMap::new(), + owner_session, + ) + }; + + // A normal item, plus one temporary item per session sharing a name. + insert(&mut txn, 100, SchemaId::User(1), "keep", None).unwrap(); + insert(&mut txn, 200, temp_schema, "tt", Some(session_a)).unwrap(); + insert(&mut txn, 300, temp_schema, "tt", Some(session_b)).unwrap(); + + // One session may not hold the same name twice, though. + let err = insert(&mut txn, 400, temp_schema, "tt", Some(session_a)).unwrap_err(); + assert!( + matches!( + err, + CatalogError::Catalog(mz_sql::catalog::CatalogError::ItemAlreadyExists(_, ref name)) + if name == "tt" + ), + "expected ItemAlreadyExists, got {err:?}" + ); + + txn.remove_ephemeral_items(); + + // Drain txn updates. + let _ = txn.get_and_commit_op_updates(); + let commit_ts = txn.upper(); + txn.commit(commit_ts).await.unwrap(); + + let snapshot_items: Vec = state + .snapshot() + .await + .unwrap() + .items + .into_iter() + .map(RustType::from_proto) + .map_ok(|(k, v)| Item::from_key_value(k, v)) + .collect::>() + .unwrap(); + + // Nothing ephemeral survives, and the normal item is untouched. + assert!( + !snapshot_items + .iter() + .any(|item| item.ephemeral_owner_session.is_some()), + "ephemeral items survived: {:?}", + snapshot_items + .iter() + .filter(|item| item.ephemeral_owner_session.is_some()) + .collect::>() + ); + assert!( + snapshot_items + .iter() + .any(|item| item.id == CatalogItemId::User(100) && item.name == "keep"), + "non-ephemeral item was removed: {snapshot_items:?}" + ); + + Box::new(state).expire().await; +} + #[mz_ore::test(tokio::test)] #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux` async fn test_persist_schemas() { diff --git a/test/0dt/mzcompose.py b/test/0dt/mzcompose.py index 84b520e0cf296..f20afd9a30d11 100644 --- a/test/0dt/mzcompose.py +++ b/test/0dt/mzcompose.py @@ -3112,3 +3112,119 @@ def workflow_ddl_detection_with_id_pool(c: Composition) -> None: > SELECT * FROM pool_mv; 1 """)) + + +def workflow_ddl_detection_ephemeral_items(c: Composition) -> None: + """Verify that temporary items do not count as reactable DDL in preflight. + + Temporary items are durable catalog items tagged with their owning + session's UUID and draw ids from the normal user-id allocator. + Ensure that creation of them during 0dt preflight does not halt the + read-only environment. + """ + c.down(destroy_volumes=True) + c.up("mz_old") + + PREFLIGHT_STARTED = "waiting for deployment to be caught up" + + def count_preflight_starts() -> int: + """Count mz_new boots via the preflight start line in its log.""" + logs = c.invoke("logs", "mz_new", capture=True).stdout + return sum(PREFLIGHT_STARTED in line for line in logs.splitlines()) + + def await_preflight_start() -> None: + deadline = time.time() + 120 + while time.time() < deadline: + if count_preflight_starts() >= 1: + return + time.sleep(0.5) + raise RuntimeError("timed out waiting for mz_new preflight to start") + + # The DDL check defaults to every 5 minutes plus once right before + # ready-to-promote. Tighten it so the temporary items below sit through + # many checks. Read at mz_new's boot from the catalog. + c.sql( + """ + ALTER SYSTEM SET with_0dt_deployment_ddl_check_interval = '1s'; + ALTER SYSTEM SET cluster = quickstart; + """, + service="mz_old", + port=6877, + user="mz_system", + ) + + # Start mz_new in read-only mode (deploy_generation=1) and wait for it + # to start the preflight process. + c.up("mz_new") + await_preflight_start() + + # A session on the leader creates the temporary items. The connection + # stays open so the items stay durable. + conn = c.sql_connection(service="mz_old") + cur = conn.cursor() + cur.execute("CREATE TEMPORARY TABLE temp_t (a int)") + cur.execute("CREATE TEMPORARY VIEW temp_v AS SELECT * FROM temp_t") + cur.execute("INSERT INTO temp_t VALUES (1)") + + # Prove the temporary items are durable catalog rows on the leader while + # mz_new's checks tick + ephemeral = c.sql_query( + """SELECT count(*) FROM mz_internal.mz_catalog_raw + WHERE data->>'kind' = 'Item' + AND data->'value'->>'ephemeral_owner_session' IS NOT NULL""", + service="mz_old", + port=6877, + user="mz_system", + ) + assert ephemeral == [(2,)], f"temporary items are not durable: {ephemeral}" + + # the temporary items must exist while mz_new is still checking for DDL + # i.e. before it announces ready. if we're caught up before we've created + # temporary items, fail loudly. + deadline = time.time() + 120 + status = None + while time.time() < deadline: + try: + status = _leader_status(c, "mz_new") + break + except Exception: + time.sleep(1) + assert ( + status == DeploymentStatus.INITIALIZING.value + ), f"mz_new reached status {status} before the temporary items were created" + + # Sit through several 1s-interval DDL checks with the temporary items in + # the catalog, then let mz_new run the final check on its way to + # ready-to-promote. Assert we only see one preflight start throughout + # promotion which means we never halted. + time.sleep(5) + assert ( + count_preflight_starts() == 1 + ), "mz_new rebooted with only temporary items created" + c.await_mz_deployment_status(DeploymentStatus.READY_TO_PROMOTE, "mz_new") + assert ( + count_preflight_starts() == 1 + ), "mz_new rebooted on the final DDL check with only temporary items created" + + c.promote_mz("mz_new") + c.await_mz_deployment_status(DeploymentStatus.IS_LEADER, "mz_new", sleep_time=None) + + # The takeover opened the catalog with write intent, which fences the old + # leader (killing the session that owned the temporary items) and + # reclaims every ephemeral item. Only mz_catalog_raw shows whether the + # durable rows themselves are gone. + ephemeral = c.sql_query( + """SELECT count(*) FROM mz_internal.mz_catalog_raw + WHERE data->>'kind' = 'Item' + AND data->'value'->>'ephemeral_owner_session' IS NOT NULL""", + service="mz_new", + port=6877, + user="mz_system", + ) + assert ephemeral == [(0,)], f"ephemeral items survived promotion: {ephemeral}" + + # The old leader died with the session's socket; closing is bookkeeping. + try: + conn.close() + except Exception: + pass diff --git a/test/restart/mzcompose.py b/test/restart/mzcompose.py index 2f5ad65fc52b9..70f2e389e7789 100644 --- a/test/restart/mzcompose.py +++ b/test/restart/mzcompose.py @@ -1234,6 +1234,145 @@ def wait_for_full_sample(names: list[str]) -> None: ) +def workflow_temporary_item_cleanup(c: Composition) -> None: + """Temporary tables and views are durable catalog items tagged with the + UUID of the session that created them (SQL-150), so they need explicit + cleanup on both paths out of a session. + + Graceful close is handled by the session-close hook, which drops the + session's items in one catalog transaction. A crash never runs that hook, + so the items are instead reclaimed the next time the catalog is opened with + write intent, which fences out every previous owner and therefore every + session that could still own one. + """ + + def forget_cached_conns() -> None: + """Drop the connections `sql_query` caches. + + A SIGKILL severs them, and reusing a dead socket surfaces as a spurious + "server closed the connection unexpectedly" rather than as a retry. + """ + for conn in c.conns.values(): + try: + conn.close() + except Exception: + pass + c.conns.clear() + + def query(sql: str) -> list[tuple]: + try: + return c.sql_query(sql) + except OperationalError: + forget_cached_conns() + raise + + def wait_for(sql: str, expected: list[tuple], what: str) -> None: + """Poll until `sql` returns `expected`.""" + deadline = time.time() + 120 + actual = None + while time.time() < deadline: + try: + actual = query(sql) + if actual == expected: + return + except OperationalError: + # environmentd is still coming back up. + pass + time.sleep(0.5) + raise UIError( + f"timed out waiting for {what}: wanted {expected}, last saw {actual}" + ) + + # Temporary items report the temporary schema sentinel '0'. + temp_item_counts = """ + SELECT + (SELECT count(*) FROM mz_tables WHERE name = 'tt' AND schema_id = '0'), + (SELECT count(*) FROM mz_views WHERE name = 'tv' AND schema_id = '0') + """ + + c.down(destroy_volumes=True) + c.up("materialized") + + # Two sessions create temporary items of the same name. Name uniqueness is + # scoped by the owning session, so both must coexist, and mz_tables and + # mz_views report every item regardless of owner. + conn_a = c.sql_connection() + conn_b = c.sql_connection() + conn_ids = {} + for label, conn in (("a", conn_a), ("b", conn_b)): + cur = conn.cursor() + cur.execute("SELECT pg_backend_pid()") + conn_ids[label] = cur.fetchall()[0][0] + cur.execute("CREATE TEMP TABLE tt (a int)") + cur.execute("CREATE TEMP VIEW tv AS SELECT * FROM tt") + + wait_for(temp_item_counts, [(2, 2)], "both sessions' temporary items to appear") + + sessions = query(f"""SELECT count(*) FROM mz_internal.mz_sessions + WHERE connection_id IN ({conn_ids["a"]}, {conn_ids["b"]})""") + assert sessions == [(2,)], f"both sessions should be in mz_sessions, saw {sessions}" + + # --- Graceful close: only the closing session's items go ------------------ + + conn_a.close() + + wait_for( + temp_item_counts, + [(1, 1)], + "session a's temporary items to be dropped and session b's to survive", + ) + wait_for( + f"""SELECT count(*) FROM mz_internal.mz_sessions + WHERE connection_id = {conn_ids["a"]}""", + [(0,)], + "session a's mz_sessions row to be retracted", + ) + + # Session b still owns and resolves its own items. + cur_b = conn_b.cursor() + cur_b.execute("INSERT INTO tt VALUES (1)") + cur_b.execute("SELECT count(*) FROM tv") + assert cur_b.fetchall() == [(1,)], "session b lost its own temporary items" + + # --- kill -9, with session b's items still live --------------------------- + + c.kill("materialized") + c.up("materialized") + forget_cached_conns() + + wait_for( + temp_item_counts, + [(0, 0)], + "the crashed session's temporary items to be reclaimed at boot", + ) + wait_for( + f"""SELECT count(*) FROM mz_internal.mz_sessions + WHERE connection_id IN ({conn_ids["a"]}, {conn_ids["b"]})""", + [(0,)], + "stale mz_sessions rows to be retracted at boot", + ) + + # mz_tables and mz_views are projections. Only mz_catalog_raw shows whether + # the durable rows themselves are gone, so a reclamation that merely stopped + # rendering the items would still be caught here. It is system-only. + ephemeral = c.sql_query( + """SELECT count(*) FROM mz_internal.mz_catalog_raw + WHERE data->>'kind' = 'Item' + AND data->'value'->>'ephemeral_owner_session' IS NOT NULL""", + port=6877, + user="mz_system", + ) + assert ephemeral == [ + (0,) + ], f"ephemeral catalog items survived the restart: {ephemeral}" + + # conn_b's socket died with the process; closing is bookkeeping only. + try: + conn_b.close() + except Exception: + pass + + def workflow_default(c: Composition) -> None: def process(name: str) -> None: if name == "default":