Skip to content

Commit 8c75060

Browse files
committed
Add performance benchmarks for load balancing policies
Add micro-benchmarks measuring query plan generation throughput for DCAwareRoundRobinPolicy, RackAwareRoundRobinPolicy, TokenAwarePolicy, DefaultLoadBalancingPolicy, and HostFilterPolicy. Uses pytest-benchmark for accurate timing and statistical reporting with a simulated 45-node cluster topology (5 DCs x 3 racks x 3 nodes) and 100,000 deterministic queries. Also rename tests/integration/standard/column_encryption/test_policies.py to test_encrypted_policies.py to avoid module name conflicts when running the full test suite. Run with: pytest -m benchmark tests/performance/
1 parent 153c913 commit 8c75060

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ log_level = "DEBUG"
121121
log_date_format = "%Y-%m-%d %H:%M:%S"
122122
xfail_strict = true
123123
addopts = "-rf"
124+
markers = [
125+
"benchmark: marks tests as performance benchmarks (deselect with '-m \"not benchmark\"')",
126+
]
124127

125128
[tool.setuptools_scm]
126129
version_file = "cassandra/_version.py"

tests/integration/standard/column_encryption/test_policies.py renamed to tests/integration/standard/column_encryption/test_encrypted_policies.py

File renamed without changes.
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
"""A micro-benchmark for performance of load balancing policies.
2+
3+
Measures query plan generation throughput for various load balancing policy
4+
configurations using a simulated cluster topology (5 DCs, 3 racks/DC,
5+
3 nodes/rack = 45 nodes) with 100,000 deterministic queries.
6+
7+
Usage:
8+
pytest -m benchmark tests/performance/
9+
pytest --benchmark-only tests/performance/
10+
"""
11+
12+
import uuid
13+
import struct
14+
import os
15+
from unittest.mock import Mock
16+
import pytest
17+
18+
from cassandra.policies import (
19+
DCAwareRoundRobinPolicy,
20+
RackAwareRoundRobinPolicy,
21+
TokenAwarePolicy,
22+
DefaultLoadBalancingPolicy,
23+
HostFilterPolicy,
24+
)
25+
from cassandra.pool import Host
26+
from cassandra.cluster import SimpleConvictionPolicy
27+
28+
29+
class MockEndPoint:
30+
"""Mock for Connection/EndPoint since Host expects it."""
31+
32+
__slots__ = ("address",)
33+
34+
def __init__(self, address):
35+
self.address = address
36+
37+
def __str__(self):
38+
return self.address
39+
40+
41+
class MockStatement:
42+
"""Mock statement with a routing key for token-aware routing."""
43+
44+
__slots__ = ("routing_key", "keyspace", "table")
45+
46+
def __init__(self, routing_key, keyspace="ks", table="tbl"):
47+
self.routing_key = routing_key
48+
self.keyspace = keyspace
49+
self.table = table
50+
51+
def is_lwt(self):
52+
return False
53+
54+
55+
class MockTokenMap:
56+
__slots__ = ("token_class", "get_replicas_func")
57+
58+
def __init__(self, get_replicas_func):
59+
self.token_class = Mock()
60+
self.token_class.from_key = lambda k: k
61+
self.get_replicas_func = get_replicas_func
62+
63+
def get_replicas(self, keyspace, token):
64+
return self.get_replicas_func(keyspace, token)
65+
66+
67+
class MockTablets:
68+
__slots__ = ()
69+
70+
def get_tablet_for_key(self, keyspace, table, key):
71+
return None
72+
73+
74+
class MockMetadata:
75+
__slots__ = ("_tablets", "token_map", "get_replicas_func", "hosts_by_address")
76+
77+
def __init__(self, get_replicas_func, hosts_by_address):
78+
self._tablets = MockTablets()
79+
self.token_map = MockTokenMap(get_replicas_func)
80+
self.get_replicas_func = get_replicas_func
81+
self.hosts_by_address = hosts_by_address
82+
83+
def can_support_partitioner(self):
84+
return True
85+
86+
def get_replicas(self, keyspace, key):
87+
return self.get_replicas_func(keyspace, key)
88+
89+
def get_host(self, addr):
90+
return self.hosts_by_address.get(addr)
91+
92+
93+
class MockCluster:
94+
__slots__ = ("metadata",)
95+
96+
def __init__(self, metadata):
97+
self.metadata = metadata
98+
99+
100+
# ---------------------------------------------------------------------------
101+
# Fixtures
102+
# ---------------------------------------------------------------------------
103+
104+
105+
@pytest.fixture(scope="module")
106+
def vnode_cluster():
107+
"""Build a simulated 45-node cluster: 5 DCs x 3 racks x 3 nodes.
108+
109+
Returns a dict with:
110+
hosts - list of Host objects
111+
hosts_map - {host_id: Host}
112+
replicas_map - {routing_key_bytes: [replica_host, ...]}
113+
"""
114+
if hasattr(os, "sched_setaffinity"):
115+
try:
116+
cpu = list(os.sched_getaffinity(0))[0]
117+
os.sched_setaffinity(0, {cpu})
118+
except Exception:
119+
pass
120+
121+
hosts = []
122+
hosts_map = {}
123+
replicas_map = {}
124+
125+
dcs = ["dc{}".format(i) for i in range(5)]
126+
racks = ["rack{}".format(i) for i in range(3)]
127+
nodes_per_rack = 3
128+
129+
ip_counter = 0
130+
subnet_counter = 0
131+
for dc in dcs:
132+
for rack in racks:
133+
subnet_counter += 1
134+
for node_idx in range(nodes_per_rack):
135+
ip_counter += 1
136+
address = "127.0.{}.{}".format(subnet_counter, node_idx + 1)
137+
h_id = uuid.UUID(int=ip_counter)
138+
h = Host(MockEndPoint(address), SimpleConvictionPolicy, host_id=h_id)
139+
h.set_location_info(dc, rack)
140+
hosts.append(h)
141+
hosts_map[h_id] = h
142+
143+
# Pre-calculate replica assignments for 100k routing keys.
144+
query_count = 100_000
145+
for i in range(query_count):
146+
key = struct.pack(">I", i)
147+
replicas = [hosts[(i + r) % len(hosts)] for r in range(3)]
148+
replicas_map[key] = replicas
149+
150+
return {
151+
"hosts": hosts,
152+
"hosts_map": hosts_map,
153+
"replicas_map": replicas_map,
154+
}
155+
156+
157+
@pytest.fixture(scope="module")
158+
def benchmark_queries():
159+
"""Generate 100,000 deterministic mock queries."""
160+
query_count = 100_000
161+
return [MockStatement(routing_key=struct.pack(">I", i)) for i in range(query_count)]
162+
163+
164+
def _setup_cluster_mock(hosts, replicas_map):
165+
"""Wire up a MockCluster with metadata that resolves replicas."""
166+
hosts_by_address = {}
167+
for host in hosts:
168+
addr = getattr(host, "address", None)
169+
if addr is None and getattr(host, "endpoint", None) is not None:
170+
addr = getattr(host.endpoint, "address", None)
171+
if addr is not None:
172+
hosts_by_address[addr] = host
173+
174+
get_replicas_func = lambda ks, key: replicas_map.get(key, [])
175+
metadata = MockMetadata(get_replicas_func, hosts_by_address)
176+
return MockCluster(metadata)
177+
178+
179+
def _populate_policy(policy, hosts, replicas_map):
180+
"""Create cluster mock and populate the policy with hosts."""
181+
cluster = _setup_cluster_mock(hosts, replicas_map)
182+
policy.populate(cluster, hosts)
183+
return policy
184+
185+
186+
def _run_all_query_plans(policy, queries):
187+
"""Execute make_query_plan for every query, consuming the iterator."""
188+
for q in queries:
189+
for _ in policy.make_query_plan(working_keyspace="ks", query=q):
190+
pass
191+
192+
193+
# ---------------------------------------------------------------------------
194+
# Benchmarks — each uses pytest-benchmark for accurate timing & reporting
195+
# ---------------------------------------------------------------------------
196+
197+
198+
@pytest.mark.benchmark
199+
def test_dc_aware(benchmark, vnode_cluster, benchmark_queries):
200+
"""Benchmark DCAwareRoundRobinPolicy."""
201+
policy = DCAwareRoundRobinPolicy(local_dc="dc0", used_hosts_per_remote_dc=1)
202+
_populate_policy(policy, vnode_cluster["hosts"], vnode_cluster["replicas_map"])
203+
benchmark(_run_all_query_plans, policy, benchmark_queries)
204+
205+
206+
@pytest.mark.benchmark
207+
def test_rack_aware(benchmark, vnode_cluster, benchmark_queries):
208+
"""Benchmark RackAwareRoundRobinPolicy."""
209+
policy = RackAwareRoundRobinPolicy(
210+
local_dc="dc0", local_rack="rack0", used_hosts_per_remote_dc=1
211+
)
212+
_populate_policy(policy, vnode_cluster["hosts"], vnode_cluster["replicas_map"])
213+
benchmark(_run_all_query_plans, policy, benchmark_queries)
214+
215+
216+
@pytest.mark.benchmark
217+
def test_token_aware_wrapping_dc_aware(benchmark, vnode_cluster, benchmark_queries):
218+
"""Benchmark TokenAwarePolicy wrapping DCAwareRoundRobinPolicy."""
219+
child = DCAwareRoundRobinPolicy(local_dc="dc0", used_hosts_per_remote_dc=1)
220+
policy = TokenAwarePolicy(child, shuffle_replicas=False)
221+
_populate_policy(policy, vnode_cluster["hosts"], vnode_cluster["replicas_map"])
222+
benchmark(_run_all_query_plans, policy, benchmark_queries)
223+
224+
225+
@pytest.mark.benchmark
226+
def test_token_aware_wrapping_rack_aware(benchmark, vnode_cluster, benchmark_queries):
227+
"""Benchmark TokenAwarePolicy wrapping RackAwareRoundRobinPolicy."""
228+
child = RackAwareRoundRobinPolicy(
229+
local_dc="dc0", local_rack="rack0", used_hosts_per_remote_dc=1
230+
)
231+
policy = TokenAwarePolicy(child, shuffle_replicas=False)
232+
_populate_policy(policy, vnode_cluster["hosts"], vnode_cluster["replicas_map"])
233+
benchmark(_run_all_query_plans, policy, benchmark_queries)
234+
235+
236+
@pytest.mark.benchmark
237+
def test_default_wrapping_dc_aware(benchmark, vnode_cluster, benchmark_queries):
238+
"""Benchmark DefaultLoadBalancingPolicy wrapping DCAwareRoundRobinPolicy."""
239+
child = DCAwareRoundRobinPolicy(local_dc="dc0", used_hosts_per_remote_dc=1)
240+
policy = DefaultLoadBalancingPolicy(child)
241+
_populate_policy(policy, vnode_cluster["hosts"], vnode_cluster["replicas_map"])
242+
benchmark(_run_all_query_plans, policy, benchmark_queries)
243+
244+
245+
@pytest.mark.benchmark
246+
def test_host_filter_wrapping_dc_aware(benchmark, vnode_cluster, benchmark_queries):
247+
"""Benchmark HostFilterPolicy wrapping DCAwareRoundRobinPolicy."""
248+
child = DCAwareRoundRobinPolicy(local_dc="dc0", used_hosts_per_remote_dc=1)
249+
policy = HostFilterPolicy(
250+
child_policy=child, predicate=lambda host: host.rack != "rack2"
251+
)
252+
_populate_policy(policy, vnode_cluster["hosts"], vnode_cluster["replicas_map"])
253+
benchmark(_run_all_query_plans, policy, benchmark_queries)

0 commit comments

Comments
 (0)