test: add new guardrail tests matching documentation scenarios

Add tests for RF guardrails (min/max warn/fail, RF=0 bypass,
threshold=-1 disable, ALTER KEYSPACE) and write consistency level
guardrails to cover all scenarios described in guardrails.rst.

Test runtime (dev):
test_guardrail_replication_strategy - 6s
test_guardrail_write_consistency_level - 5s

Refs: SCYLLADB-257
This commit is contained in:
Andrzej Jackowski
2026-03-13 10:59:56 +01:00
parent 2a03c634c0
commit 4deeb7ebfc
2 changed files with 92 additions and 2 deletions

View File

@@ -191,3 +191,86 @@ def test_config_replication_strategy_warn_list_roundtrips_quotes(cql):
cql.execute("UPDATE system.config SET value = '[SimpleStrategy]' WHERE name = 'replication_strategy_warn_list'")
# reproduces #
cql.execute("UPDATE system.config SET value = '[\"SimpleStrategy\"]' WHERE name = 'replication_strategy_warn_list'")
def test_rf_zero_always_allowed(cql, this_dc):
"""Maximum RF guardrails fire correctly with high RF, but RF=0
(meaning 'do not replicate to this data center') must never trigger
any guardrail — even when both minimum and maximum thresholds are
active. Also verifies metric increments and message formats for
maximum RF guardrails (docs/cql/guardrails.rst)."""
with ExitStack() as config_modifications:
config_modifications.enter_context(config_value_context(cql, 'minimum_replication_factor_warn_threshold', '3'))
config_modifications.enter_context(config_value_context(cql, 'minimum_replication_factor_fail_threshold', '2'))
config_modifications.enter_context(config_value_context(cql, 'maximum_replication_factor_warn_threshold', '5'))
config_modifications.enter_context(config_value_context(cql, 'maximum_replication_factor_fail_threshold', '7'))
config_modifications.enter_context(config_value_context(cql, 'replication_strategy_warn_list', ''))
dc = re.escape(this_dc)
# max RF warn: RF=6 > warn=5 but < fail=7
create_ks_and_assert_warnings_and_errors(cql, ks_opts('NetworkTopologyStrategy', 6, dc=this_dc, tablets=False),
metric_name='scylla_cql_maximum_replication_factor_warn_violations',
warnings=[MAXIMUM_RF_WARN_RE.format(dc=dc, rf=6, threshold=5)])
# max RF fail: RF=8 > fail=7
create_ks_and_assert_warnings_and_errors(cql, ks_opts('NetworkTopologyStrategy', 8, dc=this_dc, tablets=False),
metric_name='scylla_cql_maximum_replication_factor_fail_violations',
failures=[MAXIMUM_RF_FAIL_RE.format(dc=dc, rf=8, threshold=7)])
# RF=0 bypasses all guardrails.
create_ks_and_assert_warnings_and_errors(cql, ks_opts('NetworkTopologyStrategy', 0, dc=this_dc, tablets=True))
def test_rf_threshold_minus_one_disables_check(cql, this_dc):
"""Setting an RF threshold to -1 disables that guardrail entirely.
Verify that with all four thresholds set to -1, any RF value (low or
high) is accepted without warnings or errors."""
with ExitStack() as config_modifications:
config_modifications.enter_context(config_value_context(cql, 'minimum_replication_factor_warn_threshold', '-1'))
config_modifications.enter_context(config_value_context(cql, 'minimum_replication_factor_fail_threshold', '-1'))
config_modifications.enter_context(config_value_context(cql, 'maximum_replication_factor_warn_threshold', '-1'))
config_modifications.enter_context(config_value_context(cql, 'maximum_replication_factor_fail_threshold', '-1'))
config_modifications.enter_context(config_value_context(cql, 'replication_strategy_warn_list', ''))
# RF=1 — would normally trigger the default minimum_replication_factor_warn_threshold=3
create_ks_and_assert_warnings_and_errors(cql, ks_opts('NetworkTopologyStrategy', 1, dc=this_dc, tablets=True))
# RF=100 — would normally trigger maximum thresholds; disable tablets
# to avoid the rack count check.
create_ks_and_assert_warnings_and_errors(cql, ks_opts('NetworkTopologyStrategy', 100, dc=this_dc, tablets=False))
def test_alter_keyspace_minimum_rf_warn(cql, this_dc):
with ExitStack() as config_modifications:
config_modifications.enter_context(config_value_context(cql, 'minimum_replication_factor_warn_threshold', '3'))
config_modifications.enter_context(config_value_context(cql, 'replication_strategy_warn_list', ''))
with new_test_keyspace(cql, ks_opts('NetworkTopologyStrategy', 3, dc=this_dc, tablets=False)) as ks:
response_future = cql.execute_async(f"ALTER KEYSPACE {ks}" + ks_opts('NetworkTopologyStrategy', 1, dc=this_dc))
response_future.result()
assert response_future.warnings is not None
def test_alter_keyspace_minimum_rf_fail(cql, this_dc):
with ExitStack() as config_modifications:
config_modifications.enter_context(config_value_context(cql, 'minimum_replication_factor_fail_threshold', '3'))
config_modifications.enter_context(config_value_context(cql, 'replication_strategy_warn_list', ''))
with new_test_keyspace(cql, ks_opts('NetworkTopologyStrategy', 3, dc=this_dc, tablets=False)) as ks:
with pytest.raises(ConfigurationException):
cql.execute(f"ALTER KEYSPACE {ks}" + ks_opts('NetworkTopologyStrategy', 1, dc=this_dc))
def test_alter_keyspace_maximum_rf_warn(cql, this_dc):
with ExitStack() as config_modifications:
config_modifications.enter_context(config_value_context(cql, 'maximum_replication_factor_warn_threshold', '2'))
config_modifications.enter_context(config_value_context(cql, 'replication_strategy_warn_list', ''))
with new_test_keyspace(cql, ks_opts('NetworkTopologyStrategy', 1, dc=this_dc, tablets=False)) as ks:
response_future = cql.execute_async(f"ALTER KEYSPACE {ks}" + ks_opts('NetworkTopologyStrategy', 3, dc=this_dc))
response_future.result()
assert response_future.warnings is not None
def test_alter_keyspace_maximum_rf_fail(cql, this_dc):
with ExitStack() as config_modifications:
config_modifications.enter_context(config_value_context(cql, 'maximum_replication_factor_fail_threshold', '2'))
config_modifications.enter_context(config_value_context(cql, 'replication_strategy_warn_list', ''))
with new_test_keyspace(cql, ks_opts('NetworkTopologyStrategy', 1, dc=this_dc, tablets=False)) as ks:
with pytest.raises(ConfigurationException):
cql.execute(f"ALTER KEYSPACE {ks}" + ks_opts('NetworkTopologyStrategy', 3, dc=this_dc))

View File

@@ -3,6 +3,7 @@
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
import pytest
import re
from contextlib import ExitStack
from cassandra import ConsistencyLevel, WriteFailure
from cassandra.protocol import InvalidRequest
@@ -49,13 +50,18 @@ def check_warned(cql, query, cl=ConsistencyLevel.ONE, config_value=None):
assert after_writes > before_writes
assert after_warned > before_warned
assert len(ret.response_future.warnings) > 0
cl_name = ConsistencyLevel.value_to_name[cl]
warning = "\n".join(ret.response_future.warnings)
assert re.search(f"{cl_name}.*write_consistency_levels_warned.*not recommended", warning)
def check_disallowed(cql, query, cl=ConsistencyLevel.ONE, config_value=None):
with config_value_context(cql, "write_consistency_levels_disallowed", config_value or ConsistencyLevel.value_to_name[cl]):
cl_name = ConsistencyLevel.value_to_name[cl]
with config_value_context(cql, "write_consistency_levels_disallowed", config_value or cl_name):
before_writes = get_metric(cql, WRITES_METRIC, cl)
before_disallowed = get_metric(cql, DISALLOWED_METRIC)
with pytest.raises(InvalidRequest, match="(?i)not allowed"):
# Verify the error mentions the guardrail name and that the CL is forbidden.
with pytest.raises(InvalidRequest, match=f"{cl_name}.*forbidden.*write_consistency_levels_disallowed"):
cql.execute(SimpleStatement(query, consistency_level=cl))
after_writes = get_metric(cql, WRITES_METRIC, cl)
@@ -158,3 +164,4 @@ def test_write_cl_multiple_disallowed_levels(cql, test_table):
check_disallowed(cql, query, cl=ConsistencyLevel.ALL, config_value=config)
check_disallowed(cql, query, cl=ConsistencyLevel.ANY, config_value=config)
check_no_warning(cql, query, cl=ConsistencyLevel.QUORUM, disallowed=config)