Add safekeeper scrubber test (#8785)

The test is very rudimentary, it only checks that before and after
tenant deletion, we can run `scan_metadata` for the safekeeper node
kind. Also, we don't actually expect any uploaded data, for that we
don't have enough WAL (needs to create at least one S3-uploaded file,
the scrubber doesn't recognize partial files yet).

The `scan_metadata` scrubber subcommand is extended to support either
specifying a database connection string, which was previously the only
way, and required a database to be present, or specifying the timeline
information manually via json. This is ideal for testing scenarios
because in those, the number of timelines is usually limited,
but it is involved to spin up a database just to write the timeline
information.
This commit is contained in:
Arpad Müller
2024-08-31 01:12:25 +02:00
committed by GitHub
parent 05caaab850
commit 3ec785f30d
4 changed files with 213 additions and 73 deletions
+41 -5
View File
@@ -4625,12 +4625,20 @@ class Safekeeper(LogUtils):
wait_until(20, 0.5, paused)
# TODO: Replace with `StrEnum` when we upgrade to python 3.11
class NodeKind(str, Enum):
PAGESERVER = "pageserver"
SAFEKEEPER = "safekeeper"
class StorageScrubber:
def __init__(self, env: NeonEnv, log_dir: Path):
self.env = env
self.log_dir = log_dir
def scrubber_cli(self, args: list[str], timeout) -> str:
def scrubber_cli(
self, args: list[str], timeout, extra_env: Optional[Dict[str, str]] = None
) -> str:
assert isinstance(self.env.pageserver_remote_storage, S3Storage)
s3_storage = self.env.pageserver_remote_storage
@@ -4645,6 +4653,9 @@ class StorageScrubber:
if s3_storage.endpoint is not None:
env.update({"AWS_ENDPOINT_URL": s3_storage.endpoint})
if extra_env is not None:
env.update(extra_env)
base_args = [
str(self.env.neon_binpath / "storage_scrubber"),
f"--controller-api={self.env.storage_controller.api_root()}",
@@ -4672,18 +4683,43 @@ class StorageScrubber:
assert stdout is not None
return stdout
def scan_metadata(self, post_to_storage_controller: bool = False) -> Tuple[bool, Any]:
def scan_metadata_safekeeper(
self,
timeline_lsns: List[Dict[str, Any]],
cloud_admin_api_url: str,
cloud_admin_api_token: str,
) -> Tuple[bool, Any]:
extra_env = {
"CLOUD_ADMIN_API_URL": cloud_admin_api_url,
"CLOUD_ADMIN_API_TOKEN": cloud_admin_api_token,
}
return self.scan_metadata(
node_kind=NodeKind.SAFEKEEPER, timeline_lsns=timeline_lsns, extra_env=extra_env
)
def scan_metadata(
self,
post_to_storage_controller: bool = False,
node_kind: NodeKind = NodeKind.PAGESERVER,
timeline_lsns: Optional[List[Dict[str, Any]]] = None,
extra_env: Optional[Dict[str, str]] = None,
) -> Tuple[bool, Any]:
"""
Returns the health status and the metadata summary.
"""
args = ["scan-metadata", "--node-kind", "pageserver", "--json"]
args = ["scan-metadata", "--node-kind", node_kind.value, "--json"]
if post_to_storage_controller:
args.append("--post")
stdout = self.scrubber_cli(args, timeout=30)
if timeline_lsns is not None:
args.append("--timeline-lsns")
args.append(json.dumps(timeline_lsns))
stdout = self.scrubber_cli(args, timeout=30, extra_env=extra_env)
try:
summary = json.loads(stdout)
healthy = not summary["with_errors"] and not summary["with_warnings"]
# summary does not contain "with_warnings" if node_kind is the safekeeper
no_warnings = "with_warnings" not in summary or not summary["with_warnings"]
healthy = not summary["with_errors"] and no_warnings
return healthy, summary
except:
log.error("Failed to decode JSON output from `scan-metadata`. Dumping stdout:")
+51 -1
View File
@@ -1,7 +1,9 @@
import json
from threading import Thread
import pytest
from fixtures.common_types import Lsn, TenantId, TimelineId
from fixtures.log_helper import log
from fixtures.neon_fixtures import (
NeonEnvBuilder,
PgBin,
@@ -17,6 +19,8 @@ from fixtures.pageserver.utils import (
from fixtures.remote_storage import RemoteStorageKind, s3_storage
from fixtures.utils import run_pg_bench_small, wait_until
from requests.exceptions import ReadTimeout
from werkzeug.wrappers.request import Request
from werkzeug.wrappers.response import Response
def error_tolerant_delete(ps_http, tenant_id):
@@ -322,7 +326,7 @@ def test_tenant_delete_races_timeline_creation(neon_env_builder: NeonEnvBuilder)
env.pageserver.stop()
def test_tenant_delete_scrubber(pg_bin: PgBin, neon_env_builder: NeonEnvBuilder):
def test_tenant_delete_scrubber(pg_bin: PgBin, make_httpserver, neon_env_builder: NeonEnvBuilder):
"""
Validate that creating and then deleting the tenant both survives the scrubber,
and that one can run the scrubber without problems.
@@ -347,6 +351,45 @@ def test_tenant_delete_scrubber(pg_bin: PgBin, neon_env_builder: NeonEnvBuilder)
healthy, _ = env.storage_scrubber.scan_metadata()
assert healthy
timeline_lsns = {
"tenant_id": f"{tenant_id}",
"timeline_id": f"{timeline_id}",
"timeline_start_lsn": f"{last_flush_lsn}",
"backup_lsn": f"{last_flush_lsn}",
}
cloud_admin_url = f"http://{make_httpserver.host}:{make_httpserver.port}/"
cloud_admin_token = ""
def get_branches(request: Request):
# Compare definition with `BranchData` struct
dummy_data = {
"id": "test-branch-id",
"created_at": "", # TODO
"updated_at": "", # TODO
"name": "testbranchname",
"project_id": "test-project-id",
"timeline_id": f"{timeline_id}",
"default": False,
"deleted": False,
"logical_size": 42000,
"physical_size": 42000,
"written_size": 42000,
}
# This test does all its own compute configuration (by passing explicit pageserver ID to Workload functions),
# so we send controller notifications to /dev/null to prevent it fighting the test for control of the compute.
log.info(f"got get_branches request: {request.json}")
return Response(json.dumps(dummy_data), content_type="application/json", status=200)
make_httpserver.expect_request("/branches", method="GET").respond_with_handler(get_branches)
healthy, _ = env.storage_scrubber.scan_metadata_safekeeper(
timeline_lsns=[timeline_lsns],
cloud_admin_api_url=cloud_admin_url,
cloud_admin_api_token=cloud_admin_token,
)
assert healthy
env.start()
ps_http = env.pageserver.http_client()
ps_http.tenant_delete(tenant_id)
@@ -354,3 +397,10 @@ def test_tenant_delete_scrubber(pg_bin: PgBin, neon_env_builder: NeonEnvBuilder)
healthy, _ = env.storage_scrubber.scan_metadata()
assert healthy
healthy, _ = env.storage_scrubber.scan_metadata_safekeeper(
timeline_lsns=[timeline_lsns],
cloud_admin_api_url=cloud_admin_url,
cloud_admin_api_token=cloud_admin_token,
)
assert healthy