mirror of
https://github.com/neondatabase/neon.git
synced 2026-10-27 14:06:45 +00:00
Merge commit '84a2556c9' into problame/standby-horizon-leases
This commit is contained in:
@@ -1542,6 +1542,17 @@ class NeonEnv:
|
||||
|
||||
raise RuntimeError(f"Pageserver with ID {id} not found")
|
||||
|
||||
def get_safekeeper(self, id: int) -> Safekeeper:
|
||||
"""
|
||||
Look up a safekeeper by its ID.
|
||||
"""
|
||||
|
||||
for sk in self.safekeepers:
|
||||
if sk.id == id:
|
||||
return sk
|
||||
|
||||
raise RuntimeError(f"Safekeeper with ID {id} not found")
|
||||
|
||||
def get_tenant_pageserver(self, tenant_id: TenantId | TenantShardId):
|
||||
"""
|
||||
Get the NeonPageserver where this tenant shard is currently attached, according
|
||||
@@ -5403,15 +5414,24 @@ class Safekeeper(LogUtils):
|
||||
return timeline_status.commit_lsn
|
||||
|
||||
def pull_timeline(
|
||||
self, srcs: list[Safekeeper], tenant_id: TenantId, timeline_id: TimelineId
|
||||
self,
|
||||
srcs: list[Safekeeper],
|
||||
tenant_id: TenantId,
|
||||
timeline_id: TimelineId,
|
||||
mconf: MembershipConfiguration | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
pull_timeline from srcs to self.
|
||||
"""
|
||||
src_https = [f"http://localhost:{sk.port.http}" for sk in srcs]
|
||||
res = self.http_client().pull_timeline(
|
||||
{"tenant_id": str(tenant_id), "timeline_id": str(timeline_id), "http_hosts": src_https}
|
||||
)
|
||||
body: dict[str, Any] = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"timeline_id": str(timeline_id),
|
||||
"http_hosts": src_https,
|
||||
}
|
||||
if mconf is not None:
|
||||
body["mconf"] = mconf.__dict__
|
||||
res = self.http_client().pull_timeline(body)
|
||||
src_ids = [sk.id for sk in srcs]
|
||||
log.info(f"finished pulling timeline from {src_ids} to {self.id}")
|
||||
return res
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
@@ -12,7 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
# TODO(diko): pageserver spams with various errors during safekeeper migration.
|
||||
# Fix the code so it handles the migration better.
|
||||
ALLOWED_PAGESERVER_ERRORS = [
|
||||
PAGESERVER_ALLOWED_ERRORS = [
|
||||
".*Timeline .* was cancelled and cannot be used anymore.*",
|
||||
".*Timeline .* has been deleted.*",
|
||||
".*Timeline .* was not found in global map.*",
|
||||
@@ -35,7 +36,7 @@ def test_safekeeper_migration_simple(neon_env_builder: NeonEnvBuilder):
|
||||
"timeline_safekeeper_count": 1,
|
||||
}
|
||||
env = neon_env_builder.init_start()
|
||||
env.pageserver.allowed_errors.extend(ALLOWED_PAGESERVER_ERRORS)
|
||||
env.pageserver.allowed_errors.extend(PAGESERVER_ALLOWED_ERRORS)
|
||||
|
||||
ep = env.endpoints.create("main", tenant_id=env.initial_tenant)
|
||||
|
||||
@@ -136,7 +137,7 @@ def test_safekeeper_migration_common_set_failpoints(neon_env_builder: NeonEnvBui
|
||||
"timeline_safekeeper_count": 3,
|
||||
}
|
||||
env = neon_env_builder.init_start()
|
||||
env.pageserver.allowed_errors.extend(ALLOWED_PAGESERVER_ERRORS)
|
||||
env.pageserver.allowed_errors.extend(PAGESERVER_ALLOWED_ERRORS)
|
||||
|
||||
mconf = env.storage_controller.timeline_locate(env.initial_tenant, env.initial_timeline)
|
||||
assert len(mconf["sk_set"]) == 3
|
||||
@@ -196,3 +197,92 @@ def test_safekeeper_migration_common_set_failpoints(neon_env_builder: NeonEnvBui
|
||||
assert (
|
||||
f"timeline {env.initial_tenant}/{env.initial_timeline} deleted" in exc.value.response.text
|
||||
)
|
||||
|
||||
|
||||
def test_sk_generation_aware_tombstones(neon_env_builder: NeonEnvBuilder):
|
||||
"""
|
||||
Test that safekeeper respects generations:
|
||||
1. Check that migration back and forth between two safekeepers works.
|
||||
2. Check that sk refuses to execute requests with stale generation.
|
||||
"""
|
||||
neon_env_builder.num_safekeepers = 3
|
||||
neon_env_builder.storage_controller_config = {
|
||||
"timelines_onto_safekeepers": True,
|
||||
"timeline_safekeeper_count": 1,
|
||||
}
|
||||
env = neon_env_builder.init_start()
|
||||
env.pageserver.allowed_errors.extend(PAGESERVER_ALLOWED_ERRORS)
|
||||
|
||||
mconf = env.storage_controller.timeline_locate(env.initial_tenant, env.initial_timeline)
|
||||
assert mconf["new_sk_set"] is None
|
||||
assert len(mconf["sk_set"]) == 1
|
||||
cur_sk = mconf["sk_set"][0]
|
||||
|
||||
second_sk, third_sk = [sk.id for sk in env.safekeepers if sk.id != cur_sk]
|
||||
cur_gen = 1
|
||||
|
||||
# Pull the timeline manually to third_sk, so the timeline exists there with stale generation.
|
||||
# This is needed for the test later.
|
||||
env.get_safekeeper(third_sk).pull_timeline(
|
||||
[env.get_safekeeper(cur_sk)], env.initial_tenant, env.initial_timeline
|
||||
)
|
||||
|
||||
def expect_deleted(sk_id: int):
|
||||
with pytest.raises(requests.exceptions.HTTPError, match="Not Found") as exc:
|
||||
env.get_safekeeper(sk_id).http_client().timeline_status(
|
||||
env.initial_tenant, env.initial_timeline
|
||||
)
|
||||
assert exc.value.response.status_code == 404
|
||||
assert re.match(r".*timeline .* deleted.*", exc.value.response.text)
|
||||
|
||||
def get_mconf(sk_id: int):
|
||||
status = (
|
||||
env.get_safekeeper(sk_id)
|
||||
.http_client()
|
||||
.timeline_status(env.initial_tenant, env.initial_timeline)
|
||||
)
|
||||
assert status.mconf is not None
|
||||
return status.mconf
|
||||
|
||||
def migrate():
|
||||
nonlocal cur_sk, second_sk, cur_gen
|
||||
env.storage_controller.migrate_safekeepers(
|
||||
env.initial_tenant, env.initial_timeline, [second_sk]
|
||||
)
|
||||
cur_sk, second_sk = second_sk, cur_sk
|
||||
cur_gen += 2
|
||||
|
||||
# Migrate the timeline back and forth between cur_sk and second_sk.
|
||||
for _i in range(3):
|
||||
migrate()
|
||||
# Timeline should exist on cur_sk.
|
||||
assert get_mconf(cur_sk).generation == cur_gen
|
||||
# Timeline should be deleted on second_sk.
|
||||
expect_deleted(second_sk)
|
||||
|
||||
# Remember current mconf.
|
||||
mconf = get_mconf(cur_sk)
|
||||
|
||||
# Migrate the timeline one more time.
|
||||
# It increases the generation by 2.
|
||||
migrate()
|
||||
|
||||
# Check that sk refuses to execute the exclude request with the old mconf.
|
||||
with pytest.raises(requests.exceptions.HTTPError, match="Conflict") as exc:
|
||||
env.get_safekeeper(cur_sk).http_client().timeline_exclude(
|
||||
env.initial_tenant, env.initial_timeline, mconf
|
||||
)
|
||||
assert re.match(r".*refused to switch into excluding mconf.*", exc.value.response.text)
|
||||
# We shouldn't have deleted the timeline.
|
||||
assert get_mconf(cur_sk).generation == cur_gen
|
||||
|
||||
# Check that sk refuses to execute the pull_timeline request with the old mconf.
|
||||
# Note: we try to pull from third_sk, which has a timeline with stale generation.
|
||||
# Thus, we bypass some preliminary generation checks and actually test tombstones.
|
||||
with pytest.raises(requests.exceptions.HTTPError, match="Conflict") as exc:
|
||||
env.get_safekeeper(second_sk).pull_timeline(
|
||||
[env.get_safekeeper(third_sk)], env.initial_tenant, env.initial_timeline, mconf
|
||||
)
|
||||
assert re.match(r".*Timeline .* deleted.*", exc.value.response.text)
|
||||
# The timeline should remain deleted.
|
||||
expect_deleted(second_sk)
|
||||
|
||||
Reference in New Issue
Block a user