mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
541 lines
18 KiB
Python
541 lines
18 KiB
Python
import pytest
|
|
import re
|
|
from helpers.cluster import ClickHouseCluster
|
|
from helpers.client import QueryRuntimeException
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
|
|
|
node = cluster.add_instance(
|
|
"node1",
|
|
with_zookeeper=True,
|
|
macros={"replica": "r1"},
|
|
main_configs=["config/config.xml", "config/config1.xml"],
|
|
)
|
|
|
|
node2 = cluster.add_instance(
|
|
"node2",
|
|
with_zookeeper=True,
|
|
macros={"replica": "r2"},
|
|
main_configs=["config/config.xml", "config/config2.xml", "config/named_collections.xml"],
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def started_cluster():
|
|
try:
|
|
cluster.start()
|
|
yield cluster
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
def verify_warning_with_values(node, current_val, warn_val, throw_val):
|
|
"""
|
|
Parses system.warnings to find a warning message matching the expected values.
|
|
"""
|
|
warnings_result = node.query("SELECT message, message_format_string FROM system.warnings")
|
|
rows = warnings_result.strip().split('\n')
|
|
|
|
found = False
|
|
found_message = ""
|
|
|
|
for row in rows:
|
|
if not row: continue
|
|
parts = row.split('\t')
|
|
if len(parts) < 1: continue
|
|
|
|
message = parts[0]
|
|
|
|
# Regex to capture values from message:
|
|
match = re.search(
|
|
r"\((\d+)\) exceeds the warning limit of (\d+)(?:\. You will not be able to create new .* limit of (\d+) is reached)?",
|
|
message)
|
|
|
|
if match:
|
|
msg_curr = int(match.group(1))
|
|
msg_warn = int(match.group(2))
|
|
msg_throw = int(match.group(3)) if match.group(3) else None
|
|
|
|
# We allow current_val to be >= expected because some internal system objects might have been attached
|
|
if msg_curr >= current_val and msg_warn == warn_val and msg_throw == throw_val:
|
|
found = True
|
|
found_message = message
|
|
break
|
|
|
|
assert found, (
|
|
f"Warning not found or values mismatch.\n"
|
|
f"Expected: Current>={current_val}, Warn={warn_val}, Throw={throw_val}\n"
|
|
f"Last found message: {found_message}"
|
|
)
|
|
|
|
|
|
def verify_no_warning(node, message_part):
|
|
"""
|
|
Verifies that no warning containing message_part exists in system.warnings.
|
|
"""
|
|
warnings = node.query("SELECT message FROM system.warnings").strip()
|
|
if not warnings:
|
|
return
|
|
assert message_part not in warnings, f"Found unexpected warning containing '{message_part}':\n{warnings}"
|
|
|
|
|
|
def test_table_limit(started_cluster):
|
|
warn_limit = 5
|
|
throw_limit = 10
|
|
|
|
for i in range(15):
|
|
node.query(f"DROP TABLE IF EXISTS t{i} SYNC")
|
|
|
|
for i in range(warn_limit):
|
|
node.query(f"CREATE TABLE t{i} (a Int32) Engine = Log")
|
|
|
|
node.query(f"CREATE TABLE t{warn_limit} (a Int32) Engine = Log")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=warn_limit + 1,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
for i in range(warn_limit + 1, throw_limit):
|
|
node.query(f"CREATE TABLE t{i} (a Int32) Engine = Log")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=throw_limit,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
assert "TOO_MANY_TABLES" in node.query_and_get_error(
|
|
f"CREATE TABLE t{throw_limit} (a Int32) Engine = Log"
|
|
)
|
|
|
|
# Cleanup and check warning disappears
|
|
for i in range(throw_limit):
|
|
node.query(f"DROP TABLE t{i} SYNC")
|
|
|
|
verify_no_warning(node, "The number of attached tables")
|
|
|
|
|
|
def test_view_limit(started_cluster):
|
|
warn_limit = 5
|
|
throw_limit = 10
|
|
|
|
for i in range(warn_limit + 1):
|
|
node.query(f"CREATE VIEW v{i} AS SELECT 1")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=warn_limit + 1,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
for i in range(warn_limit + 1, throw_limit):
|
|
node.query(f"CREATE VIEW v{i} AS SELECT 1")
|
|
|
|
assert "TOO_MANY_TABLES" in node.query_and_get_error(
|
|
f"CREATE VIEW v{throw_limit} AS SELECT 1"
|
|
)
|
|
|
|
# Cleanup and check warning disappears
|
|
for i in range(throw_limit):
|
|
node.query(f"DROP VIEW v{i}")
|
|
|
|
verify_no_warning(node, "The number of attached views")
|
|
|
|
|
|
def test_database_limit(started_cluster):
|
|
warn_limit = 5
|
|
throw_limit = 10
|
|
created_dbs = []
|
|
|
|
try:
|
|
initial_dbs = int(node.query("SELECT value FROM system.metrics WHERE name = 'AttachedDatabase'"))
|
|
|
|
# 1. Reach Warning Limit
|
|
dbs_to_warn = max(0, warn_limit - initial_dbs + 1)
|
|
for i in range(dbs_to_warn):
|
|
name = f"db_test_{i}"
|
|
node.query(f"CREATE DATABASE {name}")
|
|
created_dbs.append(name)
|
|
|
|
current_total = int(node.query("SELECT value FROM system.metrics WHERE name = 'AttachedDatabase'"))
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=current_total,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
# 2. Reach Throw Limit (and try to exceed)
|
|
limit_hit = False
|
|
|
|
# Try creating enough databases to definitely hit the limit + safety margin
|
|
for i in range(len(created_dbs), throw_limit + 10):
|
|
name = f"db_test_{i}"
|
|
try:
|
|
node.query(f"CREATE DATABASE {name}")
|
|
created_dbs.append(name)
|
|
except QueryRuntimeException as e:
|
|
if "TOO_MANY_DATABASES" in str(e):
|
|
limit_hit = True
|
|
break
|
|
else:
|
|
raise e
|
|
|
|
assert limit_hit, f"Failed to trigger TOO_MANY_DATABASES limit. Created {len(created_dbs)} databases on top of {initial_dbs} initial."
|
|
|
|
# Cleanup and verify warning disappears
|
|
for db in created_dbs:
|
|
node.query(f"DROP DATABASE IF EXISTS {db}")
|
|
|
|
verify_no_warning(node, "The number of attached databases")
|
|
created_dbs = [] # Clear list so finally block doesn't duplicate effort
|
|
|
|
finally:
|
|
for db in created_dbs:
|
|
node.query(f"DROP DATABASE IF EXISTS {db}")
|
|
|
|
|
|
def test_dictionary_limit(started_cluster):
|
|
warn_limit = 5
|
|
throw_limit = 10
|
|
|
|
for i in range(warn_limit + 1):
|
|
node.query(f"CREATE DICTIONARY d{i} (a Int32) primary key a source(null()) layout(flat()) lifetime(1000)")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=warn_limit + 1,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
for i in range(warn_limit + 1, throw_limit):
|
|
node.query(f"CREATE DICTIONARY d{i} (a Int32) primary key a source(null()) layout(flat()) lifetime(1000)")
|
|
|
|
assert "TOO_MANY_TABLES" in node.query_and_get_error(
|
|
f"CREATE DICTIONARY d{throw_limit} (a Int32) primary key a source(null()) layout(flat()) lifetime(1000)"
|
|
)
|
|
|
|
# Cleanup and check warning disappears
|
|
for i in range(throw_limit):
|
|
node.query(f"DROP DICTIONARY d{i}")
|
|
|
|
verify_no_warning(node, "The number of attached dictionaries")
|
|
|
|
|
|
def test_named_collection_limit(started_cluster):
|
|
warn_limit = 5
|
|
throw_limit = 10
|
|
|
|
def _get_number_of_collections():
|
|
return int(node.query("SELECT value FROM system.metrics WHERE name = 'NamedCollection'"))
|
|
|
|
try:
|
|
for i in range(warn_limit):
|
|
node.query(f"CREATE NAMED COLLECTION nc_{i} AS key=1")
|
|
|
|
node.query(f"CREATE NAMED COLLECTION nc_{warn_limit} AS key=1")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=warn_limit + 1,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
for i in range(warn_limit + 1, throw_limit):
|
|
node.query(f"CREATE NAMED COLLECTION nc_{i} AS key=1")
|
|
|
|
assert _get_number_of_collections() == throw_limit
|
|
|
|
assert "TOO_MANY_NAMED_COLLECTIONS" in node.query_and_get_error(
|
|
f"CREATE NAMED COLLECTION nc_{throw_limit} AS key=1"
|
|
)
|
|
|
|
node.query("DROP NAMED COLLECTION IF EXISTS nc_1")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=throw_limit-1,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
node.query("DROP NAMED COLLECTION IF EXISTS nc_1")
|
|
|
|
verify_warning_with_values(
|
|
node,
|
|
current_val=throw_limit-1,
|
|
warn_val=warn_limit,
|
|
throw_val=throw_limit
|
|
)
|
|
|
|
# Cleanup and check warning disappears
|
|
for i in range(throw_limit + 1):
|
|
node.query(f"DROP NAMED COLLECTION IF EXISTS nc_{i}")
|
|
|
|
verify_no_warning(node, "The number of named collections")
|
|
|
|
finally:
|
|
for i in range(throw_limit + 1):
|
|
node.query(f"DROP NAMED COLLECTION IF EXISTS nc_{i}")
|
|
|
|
|
|
def test_named_collection_if_not_exists_metric(started_cluster):
|
|
"""
|
|
CREATE NAMED COLLECTION IF NOT EXISTS must not inflate the NamedCollection
|
|
metric when the collection already exists.
|
|
DROP NAMED COLLECTION IF EXISTS on a nonexistent collection must not
|
|
deflate the metric either.
|
|
https://github.com/ClickHouse/ClickHouse/issues/102507
|
|
"""
|
|
|
|
def _get_number_of_collections():
|
|
return int(node.query("SELECT value FROM system.metrics WHERE name = 'NamedCollection'"))
|
|
|
|
try:
|
|
node.query("DROP NAMED COLLECTION IF EXISTS nc_ine_test")
|
|
before = _get_number_of_collections()
|
|
|
|
node.query("CREATE NAMED COLLECTION nc_ine_test AS key = 1")
|
|
assert _get_number_of_collections() == before + 1
|
|
|
|
# IF NOT EXISTS on an already-existing collection must not change the metric
|
|
node.query("CREATE NAMED COLLECTION IF NOT EXISTS nc_ine_test AS key = 2")
|
|
assert _get_number_of_collections() == before + 1
|
|
|
|
node.query("CREATE NAMED COLLECTION IF NOT EXISTS nc_ine_test AS key = 3")
|
|
assert _get_number_of_collections() == before + 1
|
|
|
|
node.query("DROP NAMED COLLECTION nc_ine_test")
|
|
assert _get_number_of_collections() == before
|
|
|
|
# DROP IF EXISTS on an already-dropped collection must not change the metric
|
|
node.query("DROP NAMED COLLECTION IF EXISTS nc_ine_test")
|
|
assert _get_number_of_collections() == before
|
|
|
|
node.query("DROP NAMED COLLECTION IF EXISTS nc_ine_test")
|
|
assert _get_number_of_collections() == before
|
|
|
|
finally:
|
|
node.query("DROP NAMED COLLECTION IF EXISTS nc_ine_test")
|
|
|
|
|
|
def test_named_collection_metric_on_startup(started_cluster):
|
|
"""
|
|
Collections loaded from config on startup must be reflected in the
|
|
NamedCollection metric. node2 starts with 2 config-defined collections
|
|
(from_config_1, from_config_2).
|
|
https://github.com/ClickHouse/ClickHouse/issues/102507
|
|
"""
|
|
|
|
def _get_metric(n):
|
|
return int(n.query("SELECT value FROM system.metrics WHERE name = 'NamedCollection'"))
|
|
|
|
metric = _get_metric(node2)
|
|
assert metric >= 2, (
|
|
f"Expected NamedCollection metric >= 2 (from_config_1 + from_config_2), got {metric}"
|
|
)
|
|
|
|
|
|
def test_named_collection_metric_after_config_reload(started_cluster):
|
|
"""
|
|
SYSTEM RELOAD CONFIG must not break the NamedCollection metric for
|
|
SQL-created collections.
|
|
https://github.com/ClickHouse/ClickHouse/issues/102507
|
|
"""
|
|
|
|
def _get_metric(n):
|
|
return int(n.query("SELECT value FROM system.metrics WHERE name = 'NamedCollection'"))
|
|
|
|
try:
|
|
node2.query("DROP NAMED COLLECTION IF EXISTS nc_reload_test")
|
|
before = _get_metric(node2)
|
|
|
|
node2.query("CREATE NAMED COLLECTION nc_reload_test AS key = 1")
|
|
assert _get_metric(node2) == before + 1
|
|
|
|
node2.query("SYSTEM RELOAD CONFIG")
|
|
assert _get_metric(node2) == before + 1
|
|
|
|
node2.query("DROP NAMED COLLECTION nc_reload_test")
|
|
assert _get_metric(node2) == before
|
|
|
|
finally:
|
|
node2.query("DROP NAMED COLLECTION IF EXISTS nc_reload_test")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"setting_name, initial_limit, lower_limit, metric_name, error_name, kind",
|
|
[
|
|
pytest.param("max_table_num_to_throw", 10, 6, "AttachedTable", "TOO_MANY_TABLES", "table", id="table"),
|
|
pytest.param(
|
|
"max_replicated_table_num_to_throw",
|
|
5,
|
|
2,
|
|
"AttachedReplicatedTable",
|
|
"TOO_MANY_TABLES",
|
|
"replicated_table",
|
|
id="replicated-table",
|
|
),
|
|
pytest.param("max_view_num_to_throw", 10, 6, "AttachedView", "TOO_MANY_TABLES", "view", id="view"),
|
|
pytest.param(
|
|
"max_dictionary_num_to_throw",
|
|
10,
|
|
6,
|
|
"AttachedDictionary",
|
|
"TOO_MANY_TABLES",
|
|
"dictionary",
|
|
id="dictionary",
|
|
),
|
|
pytest.param("max_database_num_to_throw", 10, 6, "AttachedDatabase", "TOO_MANY_DATABASES", "database", id="database"),
|
|
pytest.param(
|
|
"max_named_collection_num_to_throw",
|
|
10,
|
|
6,
|
|
"NamedCollection",
|
|
"TOO_MANY_NAMED_COLLECTIONS",
|
|
"named_collection",
|
|
id="named-collection",
|
|
),
|
|
],
|
|
)
|
|
def test_runtime_config_reload_of_to_throw_limits(
|
|
started_cluster,
|
|
setting_name,
|
|
initial_limit,
|
|
lower_limit,
|
|
metric_name,
|
|
error_name,
|
|
kind,
|
|
):
|
|
"""Runtime reload must affect each `to_throw` limit enforcement path."""
|
|
|
|
config_path = "/etc/clickhouse-server/config.d/reload_test.xml"
|
|
max_objects = initial_limit + 2
|
|
|
|
def _get_server_setting():
|
|
return int(node.query(f"SELECT value FROM system.server_settings WHERE name = '{setting_name}'").strip())
|
|
|
|
def _get_server_setting_function():
|
|
return int(node.query(f"SELECT getServerSetting('{setting_name}')").strip())
|
|
|
|
def _get_count_for_limit():
|
|
if kind == "database":
|
|
return int(node.query(
|
|
"""
|
|
SELECT count()
|
|
FROM system.databases
|
|
WHERE name NOT IN ('_temporary_and_external_tables', 'system', 'information_schema', 'INFORMATION_SCHEMA')
|
|
"""
|
|
).strip())
|
|
return int(node.query(f"SELECT value FROM system.metrics WHERE name = '{metric_name}'").strip())
|
|
|
|
def _reload_without_override():
|
|
node.exec_in_container(["bash", "-c", f"rm -f {config_path}"])
|
|
node.query("SYSTEM RELOAD CONFIG")
|
|
|
|
def _reload_with_limit(limit):
|
|
node.replace_config(config_path, f"<clickhouse><{setting_name}>{limit}</{setting_name}></clickhouse>")
|
|
node.query("SYSTEM RELOAD CONFIG")
|
|
|
|
def _object_name(i):
|
|
return f"reload_limit_{kind}_{i}"
|
|
|
|
def _create(i):
|
|
name = _object_name(i)
|
|
if kind == "table":
|
|
node.query(f"CREATE TABLE {name} (a Int32) ENGINE = Log")
|
|
elif kind == "replicated_table":
|
|
node.query(
|
|
f"CREATE TABLE {name} (a Int32) "
|
|
f"ENGINE = ReplicatedMergeTree('/clickhouse/test_table_db_num_limit/{name}', 'r1') "
|
|
"ORDER BY a"
|
|
)
|
|
elif kind == "view":
|
|
node.query(f"CREATE VIEW {name} AS SELECT 1")
|
|
elif kind == "dictionary":
|
|
node.query(
|
|
f"CREATE DICTIONARY {name} (a Int32) PRIMARY KEY a "
|
|
"SOURCE(NULL()) LAYOUT(FLAT()) LIFETIME(1000)"
|
|
)
|
|
elif kind == "database":
|
|
node.query(f"CREATE DATABASE {name}")
|
|
elif kind == "named_collection":
|
|
node.query(f"CREATE NAMED COLLECTION {name} AS key = 1")
|
|
else:
|
|
raise AssertionError(f"Unknown limit kind: {kind}")
|
|
|
|
def _create_and_get_error(i):
|
|
name = _object_name(i)
|
|
if kind == "table":
|
|
return node.query_and_get_error(f"CREATE TABLE {name} (a Int32) ENGINE = Log")
|
|
if kind == "replicated_table":
|
|
return node.query_and_get_error(
|
|
f"CREATE TABLE {name} (a Int32) "
|
|
f"ENGINE = ReplicatedMergeTree('/clickhouse/test_table_db_num_limit/{name}', 'r1') "
|
|
"ORDER BY a"
|
|
)
|
|
if kind == "view":
|
|
return node.query_and_get_error(f"CREATE VIEW {name} AS SELECT 1")
|
|
if kind == "dictionary":
|
|
return node.query_and_get_error(
|
|
f"CREATE DICTIONARY {name} (a Int32) PRIMARY KEY a "
|
|
"SOURCE(NULL()) LAYOUT(FLAT()) LIFETIME(1000)"
|
|
)
|
|
if kind == "database":
|
|
return node.query_and_get_error(f"CREATE DATABASE {name}")
|
|
if kind == "named_collection":
|
|
return node.query_and_get_error(f"CREATE NAMED COLLECTION {name} AS key = 1")
|
|
raise AssertionError(f"Unknown limit kind: {kind}")
|
|
|
|
def _cleanup():
|
|
for i in range(max_objects):
|
|
name = _object_name(i)
|
|
if kind == "table" or kind == "replicated_table":
|
|
node.query(f"DROP TABLE IF EXISTS {name} SYNC")
|
|
elif kind == "view":
|
|
node.query(f"DROP VIEW IF EXISTS {name}")
|
|
elif kind == "dictionary":
|
|
node.query(f"DROP DICTIONARY IF EXISTS {name}")
|
|
elif kind == "database":
|
|
node.query(f"DROP DATABASE IF EXISTS {name}")
|
|
elif kind == "named_collection":
|
|
node.query(f"DROP NAMED COLLECTION IF EXISTS {name}")
|
|
|
|
try:
|
|
_cleanup()
|
|
_reload_without_override()
|
|
|
|
assert _get_server_setting() == initial_limit
|
|
assert _get_server_setting_function() == initial_limit
|
|
|
|
target_limit = max(lower_limit, _get_count_for_limit())
|
|
assert target_limit < initial_limit
|
|
|
|
i = 0
|
|
while _get_count_for_limit() < target_limit:
|
|
_create(i)
|
|
i += 1
|
|
|
|
_reload_with_limit(target_limit)
|
|
|
|
assert _get_server_setting() == target_limit
|
|
assert _get_server_setting_function() == target_limit
|
|
assert error_name in _create_and_get_error(i)
|
|
|
|
_reload_without_override()
|
|
|
|
assert _get_server_setting() == initial_limit
|
|
assert _get_server_setting_function() == initial_limit
|
|
|
|
finally:
|
|
_cleanup()
|
|
_reload_without_override()
|