Skip to content

Commit 87da4e0

Browse files
authored
feat: maintenance mode per DB (#1014)
fix #975 Allow the maintenance mode to be enabled/disabled on a per-database basis.
1 parent beabd7d commit 87da4e0

8 files changed

Lines changed: 592 additions & 108 deletions

File tree

integration/rust/tests/integration/maintenance_mode.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ async fn test_maintenance_mode_parsing() {
120120
let result = admin.simple_query("MAINTENANCE INVALID").await;
121121
assert!(result.is_err());
122122

123-
let result = admin.simple_query("MAINTENANCE ON EXTRA").await;
123+
let result = admin.simple_query("MAINTENANCE ON DB_NAME EXTRA").await;
124124
assert!(result.is_err());
125125
}
126126

@@ -156,6 +156,67 @@ async fn test_maintenance_mode_concurrent_operations() {
156156
tokio::try_join!(admin_task, client_task).unwrap();
157157
}
158158

159+
#[tokio::test]
160+
#[serial]
161+
async fn test_maintenance_mode_single_database() {
162+
let admin = admin_tokio().await;
163+
let failover = connection_failover().await;
164+
let mut pools = connections_sqlx().await;
165+
let pgdog = pools.remove(0); // "pgdog" database
166+
167+
// Clean slate.
168+
admin.simple_query("MAINTENANCE OFF").await.unwrap();
169+
admin
170+
.simple_query("MAINTENANCE OFF failover")
171+
.await
172+
.unwrap();
173+
174+
// Both databases work normally.
175+
pgdog.execute("SELECT 1").await.unwrap();
176+
failover.execute("SELECT 1").await.unwrap();
177+
178+
// Put only the `failover` database into maintenance mode.
179+
admin.simple_query("MAINTENANCE ON failover").await.unwrap();
180+
181+
// Queries to `pgdog` must keep working, unaffected by `failover`.
182+
tokio::time::timeout(Duration::from_secs(1), pgdog.execute("SELECT 2"))
183+
.await
184+
.expect("pgdog query should not block while only failover is in maintenance")
185+
.unwrap();
186+
187+
// Meanwhile, a query to `failover` should block until maintenance is lifted.
188+
let failover_blocked = failover.clone();
189+
let blocked = tokio::spawn(async move {
190+
failover_blocked.execute("SELECT 2").await.unwrap();
191+
});
192+
193+
sleep(Duration::from_millis(100)).await;
194+
assert!(
195+
!blocked.is_finished(),
196+
"failover query should be blocked during maintenance"
197+
);
198+
199+
// `pgdog` still works while the `failover` query is blocked.
200+
tokio::time::timeout(Duration::from_secs(1), pgdog.execute("SELECT 3"))
201+
.await
202+
.expect("pgdog query should not block")
203+
.unwrap();
204+
205+
// Lift maintenance on `failover`; the blocked query should now complete.
206+
admin
207+
.simple_query("MAINTENANCE OFF failover")
208+
.await
209+
.unwrap();
210+
211+
tokio::time::timeout(Duration::from_secs(5), blocked)
212+
.await
213+
.expect("failover query should complete after maintenance is lifted")
214+
.unwrap();
215+
216+
pgdog.close().await;
217+
failover.close().await;
218+
}
219+
159220
#[tokio::test]
160221
#[serial]
161222
async fn test_maintenance_mode_transaction_behavior() {
Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,55 @@
11
//! Turn maintenance mode on/off.
2+
//!
3+
//! Maintenance mode is special: it's completely independent from the config
4+
//! and will hold true during config changes, e.g. when some databases disappear
5+
//! and new ones are added.
6+
//!
7+
//! This is useful when changing the sharding config online, for example.
8+
//!
29
310
use crate::backend::maintenance_mode;
411

512
use super::prelude::*;
613

7-
/// Turn maintenance mode on/off.
14+
/// Turn maintenance mode on/off, optionally for a single database.
815
#[derive(Default)]
916
pub struct MaintenanceMode {
1017
enable: bool,
18+
database: Option<String>,
1119
}
1220

1321
#[async_trait]
1422
impl Command for MaintenanceMode {
1523
fn parse(sql: &str) -> Result<Self, Error> {
1624
let parts = sql.split(" ").collect::<Vec<_>>();
1725

18-
match parts[..] {
19-
["maintenance", "on"] => Ok(Self { enable: true }),
20-
["maintenance", "off"] => Ok(Self { enable: false }),
21-
_ => Err(Error::Syntax),
22-
}
26+
let (enable, database) = match parts[..] {
27+
["maintenance", "on"] => (true, None),
28+
["maintenance", "off"] => (false, None),
29+
["maintenance", "on", database] => (true, Some(database.to_string())),
30+
["maintenance", "off", database] => (false, Some(database.to_string())),
31+
_ => return Err(Error::Syntax),
32+
};
33+
34+
Ok(Self { enable, database })
2335
}
2436

2537
async fn execute(&self) -> Result<Vec<Message>, Error> {
38+
let database = self.database.as_deref();
2639
if self.enable {
27-
maintenance_mode::start();
40+
maintenance_mode::start(database);
2841
} else {
29-
maintenance_mode::stop();
42+
maintenance_mode::stop(database);
3043
}
3144

3245
Ok(vec![])
3346
}
3447

3548
fn name(&self) -> String {
36-
format!("MAINTENANCE {}", if self.enable { "ON" } else { "OFF" })
49+
let state = if self.enable { "ON" } else { "OFF" };
50+
match &self.database {
51+
Some(database) => format!("MAINTENANCE {} {}", state, database),
52+
None => format!("MAINTENANCE {}", state),
53+
}
3754
}
3855
}

0 commit comments

Comments
 (0)