Skip to content

Commit 3f134b5

Browse files
authored
fix(rds): stop CreateDBInstance corrupting state when runtime absent (#2107) (#2109)
2 parents 54f9c5b + 389a200 commit 3f134b5

3 files changed

Lines changed: 103 additions & 4 deletions

File tree

crates/fakecloud-rds/src/service/instances.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ impl RdsService {
2525
let master_user_password = optional_query_param(request, "MasterUserPassword")
2626
.unwrap_or_else(|| "Password1!".to_string());
2727
let db_name = optional_query_param(request, "DBName");
28-
let engine_version =
29-
optional_query_param(request, "EngineVersion").unwrap_or_else(|| "16.3".to_string());
28+
let engine_version = optional_query_param(request, "EngineVersion")
29+
.unwrap_or_else(|| default_engine_version(&engine).to_string());
3030
let publicly_accessible =
3131
parse_optional_bool(optional_query_param(request, "PubliclyAccessible").as_deref())?
3232
.unwrap_or(true);
@@ -94,6 +94,14 @@ impl RdsService {
9494
port,
9595
)?;
9696

97+
// Resolve the container runtime BEFORE reserving the identifier.
98+
// If it is unavailable this returns early, and reserving first
99+
// would leak the reservation into `in_progress_instance_ids`
100+
// (nothing ever cancels it), corrupting the service: every later
101+
// CreateDBInstance then fails with DBInstanceAlreadyExists while
102+
// Describe/Delete see an empty instance map. See issue #2107.
103+
let runtime = self.require_runtime()?.clone();
104+
97105
{
98106
let mut accounts = self.state.write();
99107
let state = accounts.get_or_create(&request.account_id);
@@ -117,8 +125,6 @@ impl RdsService {
117125
}
118126
}
119127

120-
let runtime = self.require_runtime()?.clone();
121-
122128
let logical_db_name = db_name
123129
.clone()
124130
.unwrap_or_else(|| default_db_name(&engine).to_string());

crates/fakecloud-rds/src/service_helpers.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,23 @@ pub fn default_port_for_engine(engine: &str) -> i32 {
20122012
}
20132013
}
20142014

2015+
/// Pick the default `EngineVersion` for `engine` when the caller omits
2016+
/// it. Must land on a version in that engine's supported list
2017+
/// (`validate_create_request`) -- a fixed postgres default like `16.3`
2018+
/// would make every version-less mysql/mariadb/oracle/... create fail
2019+
/// with "EngineVersion '16.3' is not available". See issue #2107.
2020+
pub(crate) fn default_engine_version(engine: &str) -> &'static str {
2021+
match engine {
2022+
"postgres" => "16.3",
2023+
"mysql" => "8.0",
2024+
"mariadb" => "10.11",
2025+
"oracle-ee" | "oracle-se2" | "oracle-ee-cdb" | "oracle-se2-cdb" => "19.0.0",
2026+
"sqlserver-ee" | "sqlserver-se" | "sqlserver-ex" | "sqlserver-web" => "15.00.4322.2.v1",
2027+
"db2-se" | "db2-ae" => "11.5.9.0.sb00000000.r1",
2028+
_ => "16.3",
2029+
}
2030+
}
2031+
20152032
/// Pick the built-in parameter group name AWS assigns to a new
20162033
/// instance when the caller doesn't override it. The name encodes the
20172034
/// engine family plus its major version (e.g. `default.postgres16`,

crates/fakecloud-rds/src/service_tests.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2832,3 +2832,79 @@ async fn snapshot_hook_fires_with_store() {
28322832
.expect("hook present when a store is set");
28332833
hook().await;
28342834
}
2835+
2836+
#[tokio::test]
2837+
async fn repro_issue_2107_corrupt_state_when_runtime_absent() {
2838+
// No runtime -> require_runtime() fails. Reproduce issue #2107:
2839+
// a failed create must NOT leak the in-progress reservation.
2840+
let svc = make_service();
2841+
let create = request(
2842+
"CreateDBInstance",
2843+
&[
2844+
("DBInstanceIdentifier", "corrupt-db"),
2845+
("Engine", "mysql"),
2846+
("EngineVersion", "8.0"),
2847+
("DBInstanceClass", "db.t3.micro"),
2848+
("MasterUsername", "admin"),
2849+
("MasterUserPassword", "secretpass"),
2850+
],
2851+
);
2852+
2853+
// First create fails because there is no container runtime.
2854+
let e1 = match svc.create_db_instance(&create).await {
2855+
Ok(_) => panic!("expected failure with no runtime"),
2856+
Err(e) => e,
2857+
};
2858+
assert_eq!(e1.code(), "InsufficientDBInstanceCapacity");
2859+
2860+
// Second create must ALSO be InsufficientDBInstanceCapacity, NOT
2861+
// DBInstanceAlreadyExists. A leaked reservation shows up here.
2862+
let e2 = match svc.create_db_instance(&create).await {
2863+
Ok(_) => panic!("expected failure with no runtime"),
2864+
Err(e) => e,
2865+
};
2866+
assert_ne!(
2867+
e2.code(),
2868+
"DBInstanceAlreadyExists",
2869+
"leaked in-progress reservation corrupts state"
2870+
);
2871+
2872+
let state = svc.state.read();
2873+
assert!(
2874+
state.default_ref().in_progress_instance_ids.is_empty(),
2875+
"failed create left a leaked reservation"
2876+
);
2877+
}
2878+
2879+
#[tokio::test]
2880+
async fn create_without_engine_version_uses_engine_default() {
2881+
// Issue #2107: a version-less create must default EngineVersion to a
2882+
// version in the requested engine's supported list -- not a fixed
2883+
// postgres value like "16.3", which would make every version-less
2884+
// mysql/mariadb/oracle/... create fail validation with
2885+
// "EngineVersion '16.3' is not available" before it ever reaches the
2886+
// runtime. With no runtime the create still fails, but it must fail
2887+
// at require_runtime (InsufficientDBInstanceCapacity), proving
2888+
// validation passed with the engine-appropriate default.
2889+
let svc = make_service();
2890+
let create = request(
2891+
"CreateDBInstance",
2892+
&[
2893+
("DBInstanceIdentifier", "mysql-nover"),
2894+
("Engine", "mysql"),
2895+
("DBInstanceClass", "db.t3.micro"),
2896+
("MasterUsername", "admin"),
2897+
("MasterUserPassword", "secretpass"),
2898+
],
2899+
);
2900+
let err = match svc.create_db_instance(&create).await {
2901+
Ok(_) => panic!("expected failure with no runtime"),
2902+
Err(e) => e,
2903+
};
2904+
assert_eq!(
2905+
err.code(),
2906+
"InsufficientDBInstanceCapacity",
2907+
"version-less mysql create must pass validation via the engine default, \
2908+
not fail on a hardcoded postgres version"
2909+
);
2910+
}

0 commit comments

Comments
 (0)