Files
scylladb/test/cqlpy/test_service_levels.py
Dawid Mędrek b76af2d07f cql3: Improve errors when manipulating default service level
Before this commit, any attempt to create, alter, attach, or drop
the default service level would result in a syntax error whose
error message was unclear:

```
cqlsh> attach service level default to cassandra;
SyntaxException: line 1:21 no viable alternative at input 'default'
```

The error stems from the grammar not being able to parse `default`
as a correct service level name. To fix that, we cover it manually.
This way, the grammar accepts it and we can process it in Scylla.

The reason why we'd like to cover the default service level is that
it's an actual service level that the user should reference. Getting
a syntax error is not what should happen. Hence this fix.

We validate the input and if the given role is really the default
service level, we reject the query and provide an informative error
message.

Two validation tests are provided.

Fixes scylladb/scylladb#26699

Closes scylladb/scylladb#27162
2025-11-28 15:32:37 +03:00

235 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2020-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#############################################################################
# Tests for the service levels infrastructure. Service levels can be attached
# to roles in order to apply various role-specific parameters, like timeouts.
#############################################################################
from contextlib import contextmanager, ExitStack
from .util import unique_name, new_test_table, new_user
from .rest_api import scylla_inject_error
from cassandra.protocol import InvalidRequest, ReadTimeout
from cassandra.util import Duration
import pytest
import time
# MAX_USER_SERVICE_LEVELS represents the maximal number of service levels that users can create.
# The value is documentented in `docs/features/workload-prioritization.rst`.
# It's used for regression testing of user service level limit in this and other files.
MAX_USER_SERVICE_LEVELS = 8
@contextmanager
def new_service_level(cql, timeout=None, workload_type=None, shares=None, role=None):
params = ""
if timeout or workload_type or shares:
params = "WITH "
first = True
if timeout:
if first:
first = False
else:
params += "AND "
params += f"timeout = {timeout} "
if workload_type:
if first:
first = False
else:
params += "AND "
params += f"workload_type = '{workload_type}' "
if shares:
if first:
first = False
else:
params += "AND "
params += f"shares = {shares} "
attach_to = role if role else cql.cluster.auth_provider.username
try:
sl = f"sl_{unique_name()}"
cql.execute(f"CREATE SERVICE LEVEL {sl} {params}")
cql.execute(f"ATTACH SERVICE LEVEL {sl} TO {attach_to}")
yield sl
finally:
cql.execute(f"DETACH SERVICE LEVEL FROM {attach_to}")
cql.execute(f"DROP SERVICE LEVEL IF EXISTS {sl}")
# Test that setting service level timeouts correctly sets the timeout parameter
def test_set_service_level_timeouts(scylla_only, cql):
with new_service_level(cql) as sl:
cql.execute(f"ALTER SERVICE LEVEL {sl} WITH timeout = 575ms")
res = cql.execute(f"LIST SERVICE LEVEL {sl}")
assert res.one().timeout == Duration(0, 0, 575000000)
cql.execute(f"ALTER SERVICE LEVEL {sl} WITH timeout = 2h")
res = cql.execute(f"LIST SERVICE LEVEL {sl}")
assert res.one().timeout == Duration(0, 0, 2*60*60*10**9)
cql.execute(f"ALTER SERVICE LEVEL {sl} WITH timeout = null")
res = cql.execute(f"LIST SERVICE LEVEL {sl}")
assert not res.one().timeout
# Test that incorrect service level timeout values result in an error
def test_validate_service_level_timeouts(scylla_only, cql):
with new_service_level(cql) as sl:
for incorrect in ['1ns', '-5s','writetime', '1second', '10d', '5y', '7', '0']:
print(f"Checking {incorrect}")
with pytest.raises(Exception):
cql.execute(f"ALTER SERVICE LEVEL {sl} WITH timeout = {incorrect}")
# Test that the service level is correctly attached to the user's role
def test_attached_service_level(scylla_only, cql):
with new_service_level(cql) as sl:
res_one = cql.execute(f"LIST ATTACHED SERVICE LEVEL OF {cql.cluster.auth_provider.username}").one()
assert res_one.role == cql.cluster.auth_provider.username and res_one.service_level == sl
res_one = cql.execute(f"LIST ALL ATTACHED SERVICE LEVELS").one()
assert res_one.role == cql.cluster.auth_provider.username and res_one.service_level == sl
def test_list_effective_service_level(scylla_only, cql):
sl1 = "sl1"
sl2 = "sl2"
timeout = "10s"
workload_type = "batch"
with new_user(cql, "r1") as r1:
with new_user(cql, "r2") as r2:
with new_service_level(cql, timeout=timeout, role=r1) as sl1:
with new_service_level(cql, workload_type=workload_type, role=r2) as sl2:
cql.execute(f"GRANT {r2} TO {r1}")
list_r1 = cql.execute(f"LIST EFFECTIVE SERVICE LEVEL OF {r1}")
for row in list_r1:
if row.service_level_option == "timeout":
assert row.effective_service_level == sl1
assert row.value == "10s"
if row.service_level_option == "workload_type":
assert row.effective_service_level == sl2
assert row.value == "batch"
list_r2 = cql.execute(f"LIST EFFECTIVE SERVICE LEVEL OF {r2}")
for row in list_r2:
if row.service_level_option == "timeout":
assert row.effective_service_level == sl2
assert row.value == None
if row.service_level_option == "workload_type":
assert row.effective_service_level == sl2
assert row.value == "batch"
def test_list_effective_service_level_shares(scylla_only, cql):
sl1 = "sl1"
sl2 = "sl2"
shares1 = 500
shares2 = 200
with new_user(cql, "r1") as r1:
with new_user(cql, "r2") as r2:
with new_service_level(cql, shares=shares1, role=r1) as sl1:
with new_service_level(cql, shares=shares2, role=r2) as sl2:
cql.execute(f"GRANT {r2} TO {r1}")
list_r1 = cql.execute(f"LIST EFFECTIVE SERVICE LEVEL OF {r1}")
for row in list_r1:
if row.service_level_option == "shares":
assert row.effective_service_level == sl2
assert row.value == f"{shares2}"
list_r2 = cql.execute(f"LIST EFFECTIVE SERVICE LEVEL OF {r2}")
for row in list_r2:
if row.service_level_option == "shares":
assert row.effective_service_level == sl2
assert row.value == f"{shares2}"
def test_list_effective_service_level_without_attached(scylla_only, cql):
with new_user(cql) as role:
with pytest.raises(InvalidRequest, match=f"Role {role} doesn't have assigned any service level"):
cql.execute(f"LIST EFFECTIVE SERVICE LEVEL OF {role}")
# ScyllaDB limits the number of service levels to a small number (10 including 1 default and 1 driver service level).
# This test verifies that attempting to create more service levels than that results in an InvalidRequest error
# and doesn't silently succeed.
# The test also has a regression check if a user can create exactly 8 service levels.
# In case you are adding a new internal scheduling group and this test failed, you should increase `SCHEDULING_GROUPS_COUNT`
#
# Reproduces enterprise issue #4481.
# Reproduces enterprise issue #5014.
def test_scheduling_groups_limit(scylla_only, cql):
sl_count = 100
created_count = 0
with pytest.raises(InvalidRequest, match="Can't create service level - no more scheduling groups exist"):
with ExitStack() as stack:
for i in range(sl_count):
stack.enter_context(new_service_level(cql))
created_count = created_count + 1
assert created_count > 0
assert created_count == MAX_USER_SERVICE_LEVELS # regression check
def test_default_shares_in_listings(scylla_only, cql):
with scylla_inject_error(cql, "create_service_levels_without_default_shares", one_shot=False), \
new_user(cql) as role:
with new_service_level(cql, role=role) as sl:
list_effective = cql.execute(f"LIST EFFECTIVE SERVICE LEVEL OF {role}")
shares_info = [row for row in list_effective if row.service_level_option == "shares"][0]
assert shares_info.value == "1000"
assert shares_info.effective_service_level == sl
list_sl = cql.execute(f"LIST SERVICE LEVEL {sl}").one()
assert list_sl.shares == 1000
# Verify that we cannot manipulate the default service level
# and that the messages Scylla returns are informative.
def test_manipulating_default_service_level(cql, scylla_only):
default_sl = "default"
# Service levels are case-sensitive (if used with quotation marks).
fake_default_sl = '"DeFaUlT"'
with new_user(cql) as role:
# Creation.
create_err = f"The default service level, {default_sl}, already exists and cannot be created"
with pytest.raises(InvalidRequest, match=create_err):
cql.execute(f"CREATE SERVICE LEVEL {default_sl} WITH SHARES = 100")
# Alteration.
alter_err = f"The default service level, {default_sl}, cannot be altered"
with pytest.raises(InvalidRequest, match=alter_err):
cql.execute(f"ALTER SERVICE LEVEL {default_sl} WITH SHARES = 200")
# Attachment.
attach_err = f"The default service level, {default_sl}, cannot be attached to a role. " \
"If you want to detach an attached service level, use the DETACH SERVICE " \
"LEVEL statement"
with pytest.raises(InvalidRequest, match=attach_err):
cql.execute(f"ATTACH SERVICE LEVEL {default_sl} TO {role}")
# Dropping.
drop_err = f"The default service level, {default_sl}, cannot be dropped"
with pytest.raises(InvalidRequest, match=drop_err):
cql.execute(f"DROP SERVICE LEVEL {default_sl}")
# Verify that we can manipulate service levels whose names are similar
# to the name of the default service level, but they're not the same.
# Sanity check.
def test_manipulating_fake_default_service_level(cql, scylla_only):
# Service levels are case-sensitive (if used with quotation marks).
fake_default_sl = '"DeFaUlT"'
with new_user(cql) as role:
try:
# Creation.
cql.execute(f"CREATE SERVICE LEVEL {fake_default_sl} WITH SHARES = 100")
# Alteration.
cql.execute(f"ALTER SERVICE LEVEL {fake_default_sl} WITH SHARES = 200")
# Attachment.
cql.execute(f"ATTACH SERVICE LEVEL {fake_default_sl} TO {role}")
# Dropping.
cql.execute(f"DROP SERVICE LEVEL {fake_default_sl}")
finally:
cql.execute(f"DROP SERVICE LEVEL IF EXISTS {fake_default_sl}")