mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-03 19:42:55 +00:00
## Problem The test creates an endpoint and deletes its tenant. The compute cannot stop gracefully because it tries to write a checkpoint shutdown record into the WAL, but the timeline had been already deleted from safekeepers. - Relates to https://github.com/neondatabase/neon/pull/11712 ## Summary of changes Stop the compute before deleting a tenant
469 lines
17 KiB
Python
469 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from threading import Thread
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from fixtures.common_types import Lsn, TenantId, TimelineId
|
|
from fixtures.log_helper import log
|
|
from fixtures.neon_fixtures import (
|
|
NeonEnvBuilder,
|
|
PgBin,
|
|
wait_for_last_flush_lsn,
|
|
)
|
|
from fixtures.pageserver.http import PageserverApiException
|
|
from fixtures.pageserver.utils import (
|
|
assert_prefix_empty,
|
|
assert_prefix_not_empty,
|
|
many_small_layers_tenant_config,
|
|
wait_for_upload,
|
|
)
|
|
from fixtures.remote_storage import RemoteStorageKind, s3_storage
|
|
from fixtures.utils import run_pg_bench_small, wait_until
|
|
from fixtures.workload import Workload
|
|
from requests.exceptions import ReadTimeout
|
|
from werkzeug.wrappers.response import Response
|
|
|
|
if TYPE_CHECKING:
|
|
from werkzeug.wrappers.request import Request
|
|
|
|
|
|
def error_tolerant_delete(ps_http, tenant_id):
|
|
"""
|
|
For tests that inject 500 errors, we must retry repeatedly when issuing deletions
|
|
"""
|
|
while True:
|
|
try:
|
|
ps_http.tenant_delete(tenant_id=tenant_id)
|
|
except PageserverApiException as e:
|
|
if e.status_code == 500:
|
|
# This test uses failure injection, which can produce 500s as the pageserver expects
|
|
# the object store to always be available, and the ListObjects during deletion is generally
|
|
# an infallible operation. This can show up as a clear simulated error, or as a general
|
|
# error during delete_objects()
|
|
assert (
|
|
"simulated failure of remote operation" in e.message
|
|
or "failed to delete" in e.message
|
|
)
|
|
else:
|
|
raise
|
|
else:
|
|
# Success, drop out
|
|
break
|
|
|
|
|
|
def test_tenant_delete_smoke(
|
|
neon_env_builder: NeonEnvBuilder,
|
|
pg_bin: PgBin,
|
|
):
|
|
neon_env_builder.pageserver_config_override = "test_remote_failures=1"
|
|
|
|
remote_storage_kind = s3_storage()
|
|
neon_env_builder.enable_pageserver_remote_storage(remote_storage_kind)
|
|
|
|
env = neon_env_builder.init_start()
|
|
env.pageserver.allowed_errors.extend(
|
|
[
|
|
# The deletion queue will complain when it encounters simulated S3 errors
|
|
".*deletion executor: DeleteObjects request failed.*",
|
|
# lucky race with stopping from flushing a layer we fail to schedule any uploads
|
|
".*layer flush task.+: could not flush frozen layer: update_metadata_file",
|
|
]
|
|
)
|
|
|
|
ps_http = env.pageserver.http_client()
|
|
|
|
# first try to delete non existing tenant
|
|
tenant_id = TenantId.generate()
|
|
env.pageserver.allowed_errors.extend(
|
|
[".*NotFound.*", ".*simulated failure.*", ".*failed to delete .+ objects.*"]
|
|
)
|
|
|
|
# Check that deleting a non-existent tenant gives the expected result: this is a loop because we
|
|
# may need to retry on some remote storage errors injected by the test harness
|
|
error_tolerant_delete(ps_http, tenant_id)
|
|
|
|
env.create_tenant(
|
|
tenant_id=tenant_id,
|
|
conf=many_small_layers_tenant_config(),
|
|
)
|
|
|
|
# Default tenant and the one we created
|
|
assert ps_http.get_metric_value("pageserver_tenant_manager_slots", {"mode": "attached"}) == 2
|
|
|
|
# create two timelines one being the parent of another
|
|
parent = None
|
|
for timeline in ["first", "second"]:
|
|
timeline_id = env.create_branch(timeline, ancestor_branch_name=parent, tenant_id=tenant_id)
|
|
with env.endpoints.create_start(timeline, tenant_id=tenant_id) as endpoint:
|
|
run_pg_bench_small(pg_bin, endpoint.connstr())
|
|
wait_for_last_flush_lsn(env, endpoint, tenant=tenant_id, timeline=timeline_id)
|
|
|
|
assert_prefix_not_empty(
|
|
neon_env_builder.pageserver_remote_storage,
|
|
prefix="/".join(
|
|
(
|
|
"tenants",
|
|
str(tenant_id),
|
|
)
|
|
),
|
|
)
|
|
|
|
parent = timeline
|
|
|
|
# Upload a heatmap so that we exercise deletion of that too
|
|
ps_http.tenant_heatmap_upload(tenant_id)
|
|
|
|
assert ps_http.get_metric_value("pageserver_tenant_manager_slots", {"mode": "attached"}) == 2
|
|
error_tolerant_delete(ps_http, tenant_id)
|
|
assert ps_http.get_metric_value("pageserver_tenant_manager_slots", {"mode": "attached"}) == 1
|
|
|
|
tenant_path = env.pageserver.tenant_dir(tenant_id)
|
|
assert not tenant_path.exists()
|
|
|
|
assert_prefix_empty(
|
|
neon_env_builder.pageserver_remote_storage,
|
|
prefix="/".join(
|
|
(
|
|
"tenants",
|
|
str(tenant_id),
|
|
)
|
|
),
|
|
)
|
|
|
|
# Deletion updates the tenant count: the one default tenant remains
|
|
assert ps_http.get_metric_value("pageserver_tenant_manager_slots", {"mode": "attached"}) == 1
|
|
assert ps_http.get_metric_value("pageserver_tenant_manager_slots", {"mode": "inprogress"}) == 0
|
|
|
|
env.pageserver.stop()
|
|
|
|
|
|
def test_long_timeline_create_cancelled_by_tenant_delete(neon_env_builder: NeonEnvBuilder):
|
|
"""Reproduction of 2023-11-23 stuck tenants investigation"""
|
|
|
|
# do not use default tenant/timeline creation because it would output the failpoint log message too early
|
|
env = neon_env_builder.init_configs()
|
|
env.start()
|
|
pageserver_http = env.pageserver.http_client()
|
|
|
|
env.pageserver.allowed_errors.extend(
|
|
[
|
|
# the response hit_pausable_failpoint_and_later_fail
|
|
f".*Error processing HTTP request: InternalServerError\\(new timeline {env.initial_tenant}/{env.initial_timeline} has invalid disk_consistent_lsn",
|
|
]
|
|
)
|
|
|
|
env.pageserver.tenant_create(env.initial_tenant)
|
|
|
|
failpoint = "flush-layer-cancel-after-writing-layer-out-pausable"
|
|
pageserver_http.configure_failpoints((failpoint, "pause"))
|
|
|
|
def hit_pausable_failpoint_and_later_fail():
|
|
with pytest.raises(PageserverApiException, match="NotFound: tenant"):
|
|
pageserver_http.timeline_create(
|
|
env.pg_version, env.initial_tenant, env.initial_timeline
|
|
)
|
|
|
|
def start_deletion():
|
|
pageserver_http.tenant_delete(env.initial_tenant)
|
|
|
|
def has_hit_failpoint():
|
|
assert env.pageserver.log_contains(f"at failpoint {failpoint}") is not None
|
|
|
|
def deletion_has_started_waiting_for_timelines():
|
|
assert env.pageserver.log_contains("Waiting for timelines...") is not None
|
|
|
|
def tenant_is_deleted():
|
|
try:
|
|
pageserver_http.tenant_status(env.initial_tenant)
|
|
except PageserverApiException as e:
|
|
assert e.status_code == 404
|
|
else:
|
|
raise RuntimeError("tenant was still accessible")
|
|
|
|
creation = Thread(target=hit_pausable_failpoint_and_later_fail)
|
|
creation.start()
|
|
|
|
deletion = None
|
|
|
|
try:
|
|
wait_until(has_hit_failpoint)
|
|
|
|
# it should start ok, sync up with the stuck creation, then hang waiting for the timeline
|
|
# to shut down.
|
|
deletion = Thread(target=start_deletion)
|
|
deletion.start()
|
|
|
|
wait_until(deletion_has_started_waiting_for_timelines)
|
|
|
|
pageserver_http.configure_failpoints((failpoint, "off"))
|
|
|
|
creation.join()
|
|
deletion.join()
|
|
|
|
wait_until(tenant_is_deleted)
|
|
finally:
|
|
creation.join()
|
|
if deletion is not None:
|
|
deletion.join()
|
|
|
|
env.pageserver.stop()
|
|
|
|
|
|
def test_tenant_delete_races_timeline_creation(neon_env_builder: NeonEnvBuilder):
|
|
"""
|
|
Validate that timeline creation executed in parallel with deletion works correctly.
|
|
|
|
This is a reproducer for https://github.com/neondatabase/neon/issues/6255
|
|
"""
|
|
# The remote storage kind doesn't really matter but we use it for iterations calculation below
|
|
# (and there is no way to reconstruct the used remote storage kind)
|
|
remote_storage_kind = RemoteStorageKind.MOCK_S3
|
|
neon_env_builder.enable_pageserver_remote_storage(remote_storage_kind)
|
|
env = neon_env_builder.init_start(initial_tenant_conf=many_small_layers_tenant_config())
|
|
ps_http = env.pageserver.http_client()
|
|
tenant_id = env.initial_tenant
|
|
|
|
# When timeline creation is cancelled by tenant deletion, it is during Tenant::shutdown(), and
|
|
# acting on a shutdown tenant generates a 503 response (if caller retried they would later) get
|
|
# a 404 after the tenant is fully deleted.
|
|
CANCELLED_ERROR = (
|
|
".*POST.*Cancelled request finished successfully status=503 Service Unavailable"
|
|
)
|
|
|
|
# This can occur sometimes.
|
|
CONFLICT_MESSAGE = ".*Precondition failed: Invalid state Stopping. Expected Active or Broken.*"
|
|
|
|
env.pageserver.allowed_errors.extend(
|
|
[
|
|
# lucky race with stopping from flushing a layer we fail to schedule any uploads
|
|
".*layer flush task.+: could not flush frozen layer: update_metadata_file",
|
|
# We need the http connection close for successful reproduction
|
|
".*POST.*/timeline.* request was dropped before completing",
|
|
# Timeline creation runs into this error
|
|
CANCELLED_ERROR,
|
|
# Timeline deletion can run into this error during deletion
|
|
CONFLICT_MESSAGE,
|
|
".*tenant_delete_handler.*still waiting, taking longer than expected.*",
|
|
]
|
|
)
|
|
|
|
BEFORE_INITDB_UPLOAD_FAILPOINT = "before-initdb-upload"
|
|
DELETE_BEFORE_CLEANUP_FAILPOINT = "tenant-delete-before-cleanup-remaining-fs-traces-pausable"
|
|
|
|
# Wait just before the initdb upload
|
|
ps_http.configure_failpoints((BEFORE_INITDB_UPLOAD_FAILPOINT, "pause"))
|
|
|
|
def timeline_create():
|
|
ps_http.timeline_create(env.pg_version, tenant_id, TimelineId.generate(), timeout=1)
|
|
raise RuntimeError("creation succeeded even though it shouldn't")
|
|
|
|
def tenant_delete():
|
|
def tenant_delete_inner():
|
|
ps_http.tenant_delete(tenant_id)
|
|
|
|
wait_until(tenant_delete_inner)
|
|
|
|
# We will spawn background threads for timeline creation and tenant deletion. They will both
|
|
# get blocked on our failpoint.
|
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
create_fut = executor.submit(timeline_create)
|
|
|
|
def hit_initdb_upload_failpoint():
|
|
env.pageserver.assert_log_contains(f"at failpoint {BEFORE_INITDB_UPLOAD_FAILPOINT}")
|
|
|
|
wait_until(hit_initdb_upload_failpoint)
|
|
|
|
def creation_connection_timed_out():
|
|
env.pageserver.assert_log_contains(
|
|
"POST.*/timeline.* request was dropped before completing"
|
|
)
|
|
|
|
# Wait so that we hit the timeout and the connection is dropped
|
|
# (But timeline creation still continues)
|
|
wait_until(creation_connection_timed_out)
|
|
|
|
with pytest.raises(ReadTimeout):
|
|
# Our creation failed from the client's point of view.
|
|
create_fut.result()
|
|
|
|
ps_http.configure_failpoints((DELETE_BEFORE_CLEANUP_FAILPOINT, "pause"))
|
|
|
|
delete_fut = executor.submit(tenant_delete)
|
|
|
|
def deletion_arrived():
|
|
env.pageserver.assert_log_contains(
|
|
f"cfg failpoint: {DELETE_BEFORE_CLEANUP_FAILPOINT} pause"
|
|
)
|
|
|
|
wait_until(deletion_arrived)
|
|
|
|
ps_http.configure_failpoints((DELETE_BEFORE_CLEANUP_FAILPOINT, "off"))
|
|
|
|
# Disable the failpoint and wait for deletion to finish
|
|
ps_http.configure_failpoints((BEFORE_INITDB_UPLOAD_FAILPOINT, "off"))
|
|
|
|
delete_fut.result()
|
|
|
|
# Physical deletion should have happened
|
|
assert_prefix_empty(
|
|
neon_env_builder.pageserver_remote_storage,
|
|
prefix="/".join(
|
|
(
|
|
"tenants",
|
|
str(tenant_id),
|
|
)
|
|
),
|
|
)
|
|
|
|
# Ensure that creation cancelled and deletion didn't end up in broken state or encountered the leftover temp file
|
|
env.pageserver.assert_log_contains(CANCELLED_ERROR)
|
|
assert not env.pageserver.log_contains(
|
|
".*ERROR.*delete_tenant.*Timelines directory is not empty after all timelines deletion"
|
|
)
|
|
|
|
# Zero tenants remain (we deleted the default tenant)
|
|
assert ps_http.get_metric_value("pageserver_tenant_manager_slots", {"mode": "attached"}) == 0
|
|
|
|
# We deleted our only tenant, and the scrubber fails if it detects nothing
|
|
neon_env_builder.disable_scrub_on_exit()
|
|
|
|
env.pageserver.stop()
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
remote_storage_kind = RemoteStorageKind.MOCK_S3
|
|
neon_env_builder.enable_pageserver_remote_storage(remote_storage_kind)
|
|
env = neon_env_builder.init_start(initial_tenant_conf=many_small_layers_tenant_config())
|
|
|
|
ps_http = env.pageserver.http_client()
|
|
# create a tenant separate from the main tenant so that we have one remaining
|
|
# after we deleted it, as the scrubber treats empty buckets as an error.
|
|
(tenant_id, timeline_id) = env.create_tenant()
|
|
|
|
with env.endpoints.create_start("main", tenant_id=tenant_id) as endpoint:
|
|
run_pg_bench_small(pg_bin, endpoint.connstr())
|
|
last_flush_lsn = Lsn(endpoint.safe_psql("SELECT pg_current_wal_flush_lsn()")[0][0])
|
|
ps_http.timeline_checkpoint(tenant_id, timeline_id)
|
|
wait_for_upload(ps_http, tenant_id, timeline_id, last_flush_lsn)
|
|
env.stop()
|
|
|
|
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)
|
|
env.stop()
|
|
|
|
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
|
|
|
|
|
|
def test_tenant_delete_stale_shards(neon_env_builder: NeonEnvBuilder, pg_bin: PgBin):
|
|
"""
|
|
Deleting a tenant should also delete any stale (pre-split) shards from remote storage.
|
|
"""
|
|
remote_storage_kind = s3_storage()
|
|
neon_env_builder.enable_pageserver_remote_storage(remote_storage_kind)
|
|
|
|
env = neon_env_builder.init_start()
|
|
|
|
# Create an unsharded tenant.
|
|
tenant_id, timeline_id = env.create_tenant()
|
|
|
|
# Write some data.
|
|
workload = Workload(env, tenant_id, timeline_id, branch_name="main")
|
|
workload.init()
|
|
workload.write_rows(256)
|
|
workload.validate()
|
|
workload.stop()
|
|
|
|
assert_prefix_not_empty(
|
|
neon_env_builder.pageserver_remote_storage,
|
|
prefix="/".join(("tenants", str(tenant_id))),
|
|
)
|
|
|
|
# Upload a heatmap as well.
|
|
env.pageserver.http_client().tenant_heatmap_upload(tenant_id)
|
|
|
|
# Split off a few shards, in two rounds.
|
|
env.storage_controller.tenant_shard_split(tenant_id, shard_count=4)
|
|
env.storage_controller.tenant_shard_split(tenant_id, shard_count=16)
|
|
|
|
# Delete the tenant. This should also delete data for the unsharded and count=4 parents.
|
|
env.storage_controller.pageserver_api().tenant_delete(tenant_id=tenant_id)
|
|
|
|
assert_prefix_empty(
|
|
neon_env_builder.pageserver_remote_storage,
|
|
prefix="/".join(("tenants", str(tenant_id))),
|
|
delimiter="", # match partial prefixes, i.e. all shards
|
|
)
|
|
|
|
dirs = list(env.pageserver.tenant_dir(None).glob(f"{tenant_id}*"))
|
|
assert dirs == [], f"found tenant directories: {dirs}"
|
|
|
|
# The initial tenant created by the test harness should still be there.
|
|
# Only the tenant we deleted should be removed.
|
|
assert_prefix_not_empty(
|
|
neon_env_builder.pageserver_remote_storage,
|
|
prefix="/".join(("tenants", str(env.initial_tenant))),
|
|
)
|
|
dirs = list(env.pageserver.tenant_dir(None).glob(f"{env.initial_tenant}*"))
|
|
assert dirs != [], "missing initial tenant directory"
|
|
|
|
env.stop()
|