Files
scylladb/test/cluster/test_bti_index.py
Avi Kivity 0ae22a09d4 LICENSE: Update to version 1.1
Updated terms of non-commercial use (must be a never-customer).
2026-04-12 19:46:33 +03:00

182 lines
8.4 KiB
Python

#
# Copyright (C) 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
#
import pytest
import asyncio
import random
import os
import glob
import json
import logging
from typing import Any
from test.pylib.internal_types import ServerInfo
from test.pylib.manager_client import ManagerClient
from test.pylib.rest_client import ScyllaMetrics
# main logger
logger = logging.getLogger(__name__)
async def get_metrics(manager: ManagerClient, servers: list[ServerInfo]) -> list[ScyllaMetrics]:
return await asyncio.gather(*[manager.metrics.query(s.ip_addr) for s in servers])
def get_io_read_ops(metrics: list[ScyllaMetrics]) -> int:
return int(sum([m.get("scylla_io_queue_total_read_ops") for m in metrics]))
async def live_update_config(manager: ManagerClient, servers: list[ServerInfo], key: str, value: Any):
cql, hosts = await manager.get_ready_cql(servers)
await asyncio.gather(*[manager.server_update_config(s.server_id, key, value) for s in servers])
stmt = cql.prepare("UPDATE system.config SET value=? WHERE name=?")
await asyncio.gather(*[cql.run_async(stmt, [value, key], host=host) for host in hosts])
async def get_sstable_files_for_server(data_dir, ks, cf):
base_pattern = os.path.join(data_dir, "data", f"{ks}", f"{cf}-????????????????????????????????", "*.*")
sstables = glob.glob(base_pattern)
return sstables
@pytest.mark.asyncio
@pytest.mark.skip_mode(mode='release', reason='error injections are not supported in release mode')
async def test_bti_index_enable(manager: ManagerClient) -> None:
cassandra_logger = logging.getLogger('cassandra')
cassandra_logger.setLevel(logging.INFO)
n_servers = 1
ks_name = "ks"
cf_name = "t"
# We run with `--smp=1` because the test uses CQL tracing.
#
# Trace events are written to trace tables asynchronously w.r.t.
# the traced statements, and AFAIU there's currently no way
# (other than polling) to wait for them to appear.
#
# The Python driver has a polling mechanism for traces,
# but it is only reliable if the entire statement runs on a single shard.
# (Because it only waits for the coordinator, and not for replicas, to write their events).
# So let's just make this a single-shard test, we aren't testing
# any multi-shard mechanisms here anyway.
servers = await manager.servers_add(n_servers, cmdline=['--smp=1'], config = {
'error_injections_at_startup': [
{
'name': 'suppress_features',
'value': 'MS_SSTABLE_FORMAT',
}
],
'column_index_size_in_kb': 1,
})
cql = manager.get_cql()
workdirs = await asyncio.gather(*[manager.server_get_workdir(s.server_id) for s in servers])
logger.info("Step 1: Creating keyspace and table")
await cql.run_async(
f"CREATE KEYSPACE {ks_name} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': {n_servers}}};"
)
await cql.run_async(f"CREATE TABLE {ks_name}.{cf_name} (pk int, ck int, v blob, primary key (pk, ck));")
logger.info("Step 2: Populating the table")
insert = cql.prepare(f"INSERT INTO {ks_name}.{cf_name} (pk, ck, v) VALUES (?, ?, ?);")
n_pks = 3;
n_cks = 32;
pks = [i for i in range(n_pks)]
cks = [i for i in range(n_cks)]
vs = [random.randbytes(1024) for _ in range(n_cks)]
for pk in pks:
for ck, v in zip(cks, vs):
await cql.run_async(insert, (pk, ck, v))
await asyncio.gather(*[manager.api.keyspace_flush(s.ip_addr, ks_name, cf_name) for s in servers])
async def test_files_presence(bti_should_exist: bool, big_should_exist: bool):
sstable_sets = await asyncio.gather(*[get_sstable_files_for_server(wd, ks_name, cf_name) for wd in workdirs])
for sstable_set in sstable_sets:
partitions = [x for x in sstable_set if x.endswith('Partitions.db')]
rows = [x for x in sstable_set if x.endswith('Rows.db')]
index = [x for x in sstable_set if x.endswith('Index.db')]
summary = [x for x in sstable_set if x.endswith('Summary.db')]
if bti_should_exist:
assert len(partitions) > 0
assert len(rows) > 0
else:
assert len(partitions) == 0
assert len(rows) == 0
if big_should_exist:
assert len(index) > 0
assert len(summary) > 0
else:
assert len(index) == 0
assert len(summary) == 0
select_without_cache = cql.prepare(f"SELECT pk, ck, v FROM {ks_name}.{cf_name} WHERE pk=? and ck=? BYPASS CACHE;")
select_with_cache = cql.prepare(f"SELECT pk, ck, v FROM {ks_name}.{cf_name} WHERE pk=? and ck=?;")
chosen_pk = pks[len(pks) // 2]
chosen_ck_idx = len(cks) // 2
chosen_ck = cks[chosen_ck_idx]
chosen_v = vs[chosen_ck_idx]
async def test_bti_usage_during_reads(should_use_bti: bool, use_cache: bool):
select = select_with_cache if use_cache else select_without_cache
metrics_before = await get_metrics(manager, servers)
select_result = cql.execute(select, (chosen_pk, chosen_ck), trace=True)
metrics_after = await get_metrics(manager, servers)
row = select_result.one()
assert row.pk == chosen_pk
assert row.ck == chosen_ck
assert row.v == chosen_v
trace = select_result.get_query_trace()
seen_index = False
seen_partitions = False
seen_rows = False
for event in trace.events:
logger.info(f"Trace event: {event.description}")
seen_partitions = seen_partitions or "Partitions.db" in event.description
seen_rows = seen_rows or "Rows.db" in event.description
seen_index = seen_index or "Index.db" in event.description
if not use_cache:
if should_use_bti:
assert not seen_index, "Index.db was used despite BTI preference"
assert seen_partitions, "Partitions.db was not used despite BTI preference"
assert seen_rows, "Rows.db was not used despite BTI preference"
else:
assert seen_index, "Index.db was not used despite BIG preference"
assert not seen_partitions, "Partitions.db was used despite BIG preference"
assert not seen_rows, "Rows.db was used despite BIG preference"
# Test that BYPASS CACHE does force disk reads.
io_read_ops = get_io_read_ops(metrics_after) - get_io_read_ops(metrics_before)
if should_use_bti:
# At least one read for Partitions.db, Rows.db, Data.db
assert io_read_ops >= 3
else:
# At least one read in Index.db (main index), Index.db (promoted index), Data.db
assert io_read_ops >= 3
logger.info("Step 3: Checking for BTI files (should not exist, because cluster feature is suppressed)")
await test_files_presence(bti_should_exist=False, big_should_exist=True)
await test_bti_usage_during_reads(should_use_bti=False, use_cache=False)
logger.info("Step 4: Updating config to 'me', unsuppressing the cluster feature")
await live_update_config(manager, servers, 'sstable_format', 'me')
await asyncio.gather(*[manager.server_update_config(s.server_id, "error_injections_at_startup", []) for s in servers])
manager.driver_close()
await manager.rolling_restart(servers)
await manager.driver_connect()
cql, hosts = await manager.get_ready_cql(servers)
logger.info("Step 5: Checking for BTI files (should not exist, because BTI is not enabled in the config)")
await asyncio.gather(*[manager.api.keyspace_upgrade_sstables(s.ip_addr, ks_name) for s in servers])
await test_files_presence(bti_should_exist=False, big_should_exist=True)
await test_bti_usage_during_reads(should_use_bti=False, use_cache=False)
logger.info("Step 6: Updating write config to 'ms'")
await live_update_config(manager, servers, 'sstable_format', 'ms')
await asyncio.gather(*[manager.api.keyspace_upgrade_sstables(s.ip_addr, ks_name) for s in servers])
logger.info("Step 7: Checking for BTI files (should exist)")
await test_files_presence(bti_should_exist=True, big_should_exist=False)
# Test that BYPASS CACHE does its thing.
for _ in range(3):
await test_bti_usage_during_reads(should_use_bti=True, use_cache=False)
await test_bti_usage_during_reads(should_use_bti=True, use_cache=True)
manager.driver_close()