Skip to content

Commit 2c58ad3

Browse files
authored
fix: replica lag evalution in load balancer (#1122)
- fix: the primary/replica evaluation order was inverted, so the replica ban never triggered - fix: `prefer_primary` incorrectly excluded primary when all replicas were banned @stewart-glabs 🙏 - feat: show the reason why pools are unbanned (helps debugging)
1 parent a3c46ea commit 2c58ad3

6 files changed

Lines changed: 218 additions & 42 deletions

File tree

pgdog/src/admin/ban.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl Command for Ban {
5353
}
5454

5555
if self.unban {
56-
ban.unban(false);
56+
ban.unban(false, pool::lb::UnbanReason::Manual);
5757
} else {
5858
ban.ban(pool::Error::ManualBan, Duration::MAX);
5959
}

pgdog/src/backend/pool/lb/ban.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::*;
22
use parking_lot::RwLock;
3-
use std::time::Instant;
3+
use std::{fmt::Display, time::Instant};
44

55
use tracing::{error, warn};
66

@@ -11,6 +11,23 @@ pub struct Ban {
1111
pool: Pool,
1212
}
1313

14+
#[derive(Debug, Copy, Clone)]
15+
pub enum UnbanReason {
16+
AllTargetsBanned,
17+
Expired,
18+
Manual,
19+
}
20+
21+
impl Display for UnbanReason {
22+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23+
match self {
24+
Self::AllTargetsBanned => write!(f, "all targets banned"),
25+
Self::Expired => write!(f, "expired"),
26+
Self::Manual => write!(f, "manual"),
27+
}
28+
}
29+
}
30+
1431
impl Ban {
1532
/// Create new ban handler.
1633
pub(super) fn new(pool: &Pool) -> Self {
@@ -48,15 +65,25 @@ impl Ban {
4865
}
4966

5067
/// Unban the database.
51-
pub fn unban(&self, manual_check: bool) {
68+
///
69+
/// FIXME(lev): `reason` seems like it should be
70+
/// used as an operand but it's only used for logging.
71+
/// We should unify methods and provide one public interface to this.
72+
///
73+
pub fn unban(&self, manual_check: bool, reason: UnbanReason) {
5274
let mut guard = self.inner.upgradable_read();
5375
if let Some(ref ban) = guard.ban {
76+
let mut unbanned = false;
5477
if ban.error != Error::ManualBan || !manual_check {
5578
guard.with_upgraded(|guard| {
5679
guard.ban = None;
5780
});
81+
unbanned = true;
82+
}
83+
84+
if unbanned {
85+
warn!("resuming read queries: {} [{}]", reason, self.pool.addr());
5886
}
59-
warn!("resuming read queries [{}]", self.pool.addr());
6087
}
6188
}
6289

@@ -114,7 +141,11 @@ impl Ban {
114141
};
115142
drop(guard);
116143
if unbanned {
117-
warn!("resuming read queries [{}]", self.pool.addr());
144+
warn!(
145+
"resuming read queries: {} [{}]",
146+
UnbanReason::Expired,
147+
self.pool.addr()
148+
);
118149
}
119150
unbanned
120151
}
@@ -185,7 +216,7 @@ mod tests {
185216
let pool = Pool::new_test();
186217
let ban = Ban::new(&pool);
187218
ban.ban(Error::ServerError, Duration::from_secs(1));
188-
ban.unban(false);
219+
ban.unban(false, UnbanReason::Expired);
189220
assert!(!ban.banned());
190221
assert!(ban.error().is_none());
191222
}
@@ -195,7 +226,7 @@ mod tests {
195226
let pool = Pool::new_test();
196227
let ban = Ban::new(&pool);
197228
ban.ban(Error::ManualBan, Duration::from_secs(1));
198-
ban.unban(true);
229+
ban.unban(true, UnbanReason::Expired);
199230
assert!(ban.banned());
200231
assert_eq!(ban.error(), Some(Error::ManualBan));
201232
}
@@ -205,7 +236,7 @@ mod tests {
205236
let pool = Pool::new_test();
206237
let ban = Ban::new(&pool);
207238
ban.ban(Error::ServerError, Duration::from_secs(1));
208-
ban.unban(true);
239+
ban.unban(true, UnbanReason::Manual);
209240
assert!(!ban.banned());
210241
assert!(ban.error().is_none());
211242
}
@@ -303,7 +334,7 @@ mod tests {
303334

304335
for _ in 0..100 {
305336
ban.ban(Error::ServerError, Duration::from_secs(1));
306-
ban.unban(false);
337+
ban.unban(false, UnbanReason::Expired);
307338
}
308339

309340
h1.join().unwrap();
@@ -374,7 +405,7 @@ mod tests {
374405
for _ in 0..10 {
375406
assert!(ban.ban(Error::ServerError, Duration::from_secs(1)));
376407
assert!(ban.banned());
377-
ban.unban(false);
408+
ban.unban(false, UnbanReason::AllTargetsBanned);
378409
assert!(!ban.banned());
379410
}
380411
}

pgdog/src/backend/pool/lb/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod monitor;
2525
pub mod target_health;
2626

2727
use ban::Ban;
28+
pub use ban::UnbanReason;
2829
use monitor::*;
2930
pub use target_health::*;
3031

@@ -349,12 +350,16 @@ impl LoadBalancer {
349350
let primary_reads = match self.rw_split {
350351
IncludePrimary => true,
351352
IncludePrimaryIfReplicaBanned => candidates.iter().any(|target| target.ban.banned()),
352-
// Under PreferPrimary, default queries already route to the primary as writes;
353-
// a read only reaches here when it explicitly opted into replicas, so honor it
354-
// like ExcludePrimary and fall back to the primary only if there are no replicas.
355-
ExcludePrimary | PreferPrimary => !candidates
353+
// we read from the primary if we have no replicas
354+
ExcludePrimary => !candidates
356355
.iter()
357356
.any(|target| matches!(target.role(), Role::Replica | Role::Auto)),
357+
// PreferPrimary makes all queries writes. If a query lands here,
358+
// it's because of pgdog.role=replica. Let it use the primary only if
359+
// no replicas are available.
360+
PreferPrimary => !candidates.iter().any(|target| {
361+
matches!(target.role(), Role::Replica | Role::Auto) && !target.ban.banned()
362+
}),
358363
};
359364

360365
if !primary_reads {
@@ -427,7 +432,9 @@ impl LoadBalancer {
427432
}
428433
}
429434

430-
candidates.iter().for_each(|target| target.ban.unban(true));
435+
candidates
436+
.iter()
437+
.for_each(|target| target.ban.unban(true, UnbanReason::AllTargetsBanned));
431438

432439
Err(Error::AllReplicasDown)
433440
}

pgdog/src/backend/pool/lb/monitor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl Monitor {
128128
// intent.
129129
if targets.len() == unavailable {
130130
targets.iter().for_each(|target| {
131-
target.ban.unban(true);
131+
target.ban.unban(true, UnbanReason::AllTargetsBanned);
132132
});
133133
} else {
134134
for (i, reason) in ban_targets {

pgdog/src/backend/pool/lb/test.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ async fn test_replica_manual_unban() {
7979
assert!(ban.banned());
8080

8181
// Manually unban
82-
ban.unban(false);
82+
ban.unban(false, UnbanReason::Manual);
8383

8484
assert!(!ban.banned());
8585

@@ -634,6 +634,41 @@ async fn test_read_write_split_with_banned_replicas() {
634634
replicas.shutdown();
635635
}
636636

637+
#[tokio::test]
638+
async fn test_prefer_primary_with_banned_replicas_falls_back_to_primary() {
639+
let primary_config = create_test_pool_config("127.0.0.1", 5432);
640+
let primary_pool = Pool::new(&primary_config);
641+
primary_pool.launch();
642+
643+
let replica_configs = [create_test_pool_config("localhost", 5432)];
644+
645+
let replicas = LoadBalancer::new(
646+
&Some(primary_pool),
647+
&replica_configs,
648+
LoadBalancingStrategy::Random,
649+
ReadWriteSplit::PreferPrimary,
650+
);
651+
replicas.launch();
652+
653+
let replica_ban = &replicas.targets[0].ban;
654+
replica_ban.ban(Error::ServerError, Duration::from_millis(1000));
655+
656+
let request = Request::default();
657+
658+
let mut used_pool_ids = HashSet::new();
659+
for _ in 0..10 {
660+
let conn = replicas.get(&request).await.unwrap();
661+
used_pool_ids.insert(conn.pool.id());
662+
}
663+
664+
assert_eq!(used_pool_ids.len(), 1);
665+
666+
let primary_id = replicas.primary().unwrap().id();
667+
assert!(used_pool_ids.contains(&primary_id));
668+
669+
replicas.shutdown();
670+
}
671+
637672
#[tokio::test]
638673
async fn test_read_write_split_exclude_primary_with_round_robin() {
639674
let primary_config = create_test_pool_config("127.0.0.1", 5432);

0 commit comments

Comments
 (0)