Skip to content

Commit 87453ba

Browse files
committed
load_balancing: hash round robin's index
When RoundRobinPolicy is used as a child policy, calling its plan method twice will cause the round robin's index to increase by 2. This could led to some nodes not receiving any requests. Let's consider the following scenario: - Round robin's index is even - User sends a query with a token equal to X - TokenAwarePolicy computes a set of replicas for this query - {A, B} - TokenAwarePolicy runs a RoundRobin child policy on that set and gets {A, B}. Round robin's index is odd - TokenAwarePolicy runs a RoundRobin child policy to generate fallback. Round robin's index is even Because the list of steps starts and ends with the same parity of the round robin index, the beginning of load balancing plan will be the same for every query with token equal to X. Hashing round robin index before rotating is a mitigation to this problem (@piodul's suggestion).
1 parent f72f9f2 commit 87453ba

File tree

3 files changed

+34
-15
lines changed

3 files changed

+34
-15
lines changed

scylla/src/transport/load_balancing/dc_aware_round_robin.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ mod tests {
119119
use super::*;
120120

121121
use crate::transport::load_balancing::tests;
122+
use std::collections::HashSet;
122123

123124
#[tokio::test]
124125
async fn test_dc_aware_round_robin_policy() {
@@ -127,23 +128,27 @@ mod tests {
127128
let local_dc = "eu".to_string();
128129
let policy = DcAwareRoundRobinPolicy::new(local_dc);
129130

130-
let plans = (0..4)
131+
let plans = (0..32)
131132
.map(|_| {
132133
tests::get_plan_and_collect_node_identifiers(
133134
&policy,
134135
&tests::EMPTY_STATEMENT,
135136
&cluster,
136137
)
137138
})
138-
.collect::<Vec<_>>();
139+
.collect::<HashSet<_>>();
139140

140141
let expected_plans = vec![
141142
vec![1, 2, 3, 4, 5],
143+
vec![1, 2, 3, 5, 4],
142144
vec![2, 3, 1, 5, 4],
145+
vec![2, 3, 1, 4, 5],
143146
vec![3, 1, 2, 4, 5],
144-
vec![1, 2, 3, 5, 4],
145-
];
147+
vec![3, 1, 2, 5, 4],
148+
]
149+
.into_iter()
150+
.collect::<HashSet<_>>();
146151

147-
assert_eq!(plans, expected_plans);
152+
assert_eq!(expected_plans, plans);
148153
}
149154
}

scylla/src/transport/load_balancing/mod.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use super::{cluster::ClusterData, node::Node};
77
use crate::routing::Token;
88

9-
use std::sync::Arc;
9+
use std::{collections::hash_map::DefaultHasher, hash::Hasher, sync::Arc};
1010

1111
mod dc_aware_round_robin;
1212
mod round_robin;
@@ -53,10 +53,21 @@ pub trait ChildLoadBalancingPolicy: LoadBalancingPolicy {
5353
) -> Box<dyn Iterator<Item = Arc<Node>> + Send + Sync>;
5454
}
5555

56-
// Does safe modulo
57-
fn compute_rotation(index: usize, count: usize) -> usize {
58-
if count != 0 {
59-
index % count
56+
// Hashing round robin's index is a mitigation to problems that occur when a
57+
// `RoundRobin::apply_child_policy()` is called twice by a parent policy.
58+
fn round_robin_index_hash(index: usize) -> u64 {
59+
let mut hasher = DefaultHasher::new();
60+
hasher.write_usize(index);
61+
62+
hasher.finish()
63+
}
64+
65+
// Does safe modulo and additionally hashes the index
66+
fn compute_rotation(round_robin_index: usize, sequence_length: usize) -> usize {
67+
if sequence_length > 1 {
68+
let hash = round_robin_index_hash(round_robin_index);
69+
70+
(hash % sequence_length as u64) as usize
6071
} else {
6172
0
6273
}

scylla/src/transport/load_balancing/round_robin.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ mod tests {
7171
use super::*;
7272

7373
use crate::transport::load_balancing::tests;
74+
use std::collections::HashSet;
7475

7576
// ConnectionKeeper (which lives in Node) requires context of Tokio runtime
7677
#[tokio::test]
@@ -79,25 +80,27 @@ mod tests {
7980

8081
let policy = RoundRobinPolicy::new();
8182

82-
let plans = (0..6)
83+
let plans = (0..16)
8384
.map(|_| {
8485
tests::get_plan_and_collect_node_identifiers(
8586
&policy,
8687
&tests::EMPTY_STATEMENT,
8788
&cluster,
8889
)
8990
})
90-
.collect::<Vec<_>>();
91+
.collect::<HashSet<_>>();
9192

93+
// Check if `plans` contains all possible round robin plans
9294
let expected_plans = vec![
9395
vec![1, 2, 3, 4, 5],
9496
vec![2, 3, 4, 5, 1],
9597
vec![3, 4, 5, 1, 2],
9698
vec![4, 5, 1, 2, 3],
9799
vec![5, 1, 2, 3, 4],
98-
vec![1, 2, 3, 4, 5],
99-
];
100+
]
101+
.into_iter()
102+
.collect::<HashSet<Vec<_>>>();
100103

101-
assert_eq!(plans, expected_plans);
104+
assert_eq!(expected_plans, plans);
102105
}
103106
}

0 commit comments

Comments
 (0)