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
235 lines
10 KiB
Python
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}")
|