mirror of
https://github.com/neondatabase/neon.git
synced 2026-10-27 14:06:45 +00:00
Support offloaded timelines during shard split (#9489)
Before, we didn't copy over the `index-part.json` of offloaded timelines to the new shard's location, resulting in the new shard not knowing the timeline even exists. In #9444, we copy over the manifest, but we also need to do this for `index-part.json`. As the operations to do are mostly the same between offloaded and non-offloaded timelines, we can iterate over all of them in the same loop, after the introduction of a `TimelineOrOffloadedArcRef` type to generalize over the two cases. This is analogous to the deletion code added in #8907. The added test also ensures that the sharded archival config endpoint works, something that has not yet been ensured by tests. Part of #8088
This commit is contained in:
@@ -44,7 +44,14 @@ from urllib3.util.retry import Retry
|
||||
|
||||
from fixtures import overlayfs
|
||||
from fixtures.auth_tokens import AuthKeys, TokenScope
|
||||
from fixtures.common_types import Lsn, NodeId, TenantId, TenantShardId, TimelineId
|
||||
from fixtures.common_types import (
|
||||
Lsn,
|
||||
NodeId,
|
||||
TenantId,
|
||||
TenantShardId,
|
||||
TimelineArchivalState,
|
||||
TimelineId,
|
||||
)
|
||||
from fixtures.endpoint.http import EndpointHttpClient
|
||||
from fixtures.log_helper import log
|
||||
from fixtures.metrics import Metrics, MetricsGetter, parse_metrics
|
||||
@@ -2132,6 +2139,24 @@ class NeonStorageController(MetricsGetter, LogUtils):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def timeline_archival_config(
|
||||
self,
|
||||
tenant_id: TenantId,
|
||||
timeline_id: TimelineId,
|
||||
state: TimelineArchivalState,
|
||||
):
|
||||
config = {"state": state.value}
|
||||
log.info(
|
||||
f"requesting timeline archival config {config} for tenant {tenant_id} and timeline {timeline_id}"
|
||||
)
|
||||
res = self.request(
|
||||
"PUT",
|
||||
f"{self.api}/v1/tenant/{tenant_id}/timeline/{timeline_id}/archival_config",
|
||||
json=config,
|
||||
headers=self.headers(TokenScope.ADMIN),
|
||||
)
|
||||
return res.json()
|
||||
|
||||
def configure_failpoints(self, config_strings: tuple[str, str] | list[tuple[str, str]]):
|
||||
if isinstance(config_strings, tuple):
|
||||
pairs = [config_strings]
|
||||
|
||||
@@ -142,6 +142,19 @@ class TenantConfig:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimelinesInfoAndOffloaded:
|
||||
timelines: list[dict[str, Any]]
|
||||
offloaded: list[dict[str, Any]]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, d: dict[str, Any]) -> TimelinesInfoAndOffloaded:
|
||||
return TimelinesInfoAndOffloaded(
|
||||
timelines=d["timelines"],
|
||||
offloaded=d["offloaded"],
|
||||
)
|
||||
|
||||
|
||||
class PageserverHttpClient(requests.Session, MetricsGetter):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -464,6 +477,18 @@ class PageserverHttpClient(requests.Session, MetricsGetter):
|
||||
assert isinstance(res_json, list)
|
||||
return res_json
|
||||
|
||||
def timeline_and_offloaded_list(
|
||||
self,
|
||||
tenant_id: Union[TenantId, TenantShardId],
|
||||
) -> TimelinesInfoAndOffloaded:
|
||||
res = self.get(
|
||||
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline_and_offloaded",
|
||||
)
|
||||
self.verbose_error(res)
|
||||
res_json = res.json()
|
||||
assert isinstance(res_json, dict)
|
||||
return TimelinesInfoAndOffloaded.from_json(res_json)
|
||||
|
||||
def timeline_create(
|
||||
self,
|
||||
pg_version: PgVersion,
|
||||
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from fixtures.common_types import Lsn, TenantId, TenantShardId, TimelineId
|
||||
from fixtures.common_types import Lsn, TenantId, TenantShardId, TimelineArchivalState, TimelineId
|
||||
from fixtures.compute_reconfigure import ComputeReconfigure
|
||||
from fixtures.log_helper import log
|
||||
from fixtures.neon_fixtures import (
|
||||
@@ -353,6 +353,145 @@ def test_sharding_split_compaction(neon_env_builder: NeonEnvBuilder, failpoint:
|
||||
workload.validate()
|
||||
|
||||
|
||||
def test_sharding_split_offloading(neon_env_builder: NeonEnvBuilder):
|
||||
"""
|
||||
Test that during a split, we don't miss archived and offloaded timelines.
|
||||
"""
|
||||
|
||||
TENANT_CONF = {
|
||||
# small checkpointing and compaction targets to ensure we generate many upload operations
|
||||
"checkpoint_distance": 128 * 1024,
|
||||
"compaction_threshold": 1,
|
||||
"compaction_target_size": 128 * 1024,
|
||||
# no PITR horizon, we specify the horizon when we request on-demand GC
|
||||
"pitr_interval": "3600s",
|
||||
# disable background compaction, GC and offloading. We invoke it manually when we want it to happen.
|
||||
"gc_period": "0s",
|
||||
"compaction_period": "0s",
|
||||
# Disable automatic creation of image layers, as we will create them explicitly when we want them
|
||||
"image_creation_threshold": 9999,
|
||||
"image_layer_creation_check_threshold": 0,
|
||||
"lsn_lease_length": "0s",
|
||||
}
|
||||
|
||||
neon_env_builder.storage_controller_config = {
|
||||
# Default neon_local uses a small timeout: use a longer one to tolerate longer pageserver restarts.
|
||||
"max_offline": "30s",
|
||||
"max_warming_up": "300s",
|
||||
}
|
||||
|
||||
env = neon_env_builder.init_start(initial_tenant_conf=TENANT_CONF)
|
||||
tenant_id = env.initial_tenant
|
||||
timeline_id_main = env.initial_timeline
|
||||
|
||||
# Check that we created with an unsharded TenantShardId: this is the default,
|
||||
# but check it in case we change the default in future
|
||||
assert env.storage_controller.inspect(TenantShardId(tenant_id, 0, 0)) is not None
|
||||
|
||||
workload_main = Workload(env, tenant_id, timeline_id_main, branch_name="main")
|
||||
workload_main.init()
|
||||
workload_main.write_rows(256)
|
||||
workload_main.validate()
|
||||
workload_main.stop()
|
||||
|
||||
# Create two timelines, archive one, offload the other
|
||||
timeline_id_archived = env.create_branch("archived_not_offloaded")
|
||||
timeline_id_offloaded = env.create_branch("archived_offloaded")
|
||||
|
||||
def timeline_id_set_for(list: list[dict[str, Any]]) -> set[TimelineId]:
|
||||
return set(
|
||||
map(
|
||||
lambda t: TimelineId(t["timeline_id"]),
|
||||
list,
|
||||
)
|
||||
)
|
||||
|
||||
expected_offloaded_set = {timeline_id_offloaded}
|
||||
expected_timeline_set = {timeline_id_main, timeline_id_archived}
|
||||
|
||||
with env.get_tenant_pageserver(tenant_id).http_client() as http_client:
|
||||
http_client.timeline_archival_config(
|
||||
tenant_id, timeline_id_archived, TimelineArchivalState.ARCHIVED
|
||||
)
|
||||
http_client.timeline_archival_config(
|
||||
tenant_id, timeline_id_offloaded, TimelineArchivalState.ARCHIVED
|
||||
)
|
||||
http_client.timeline_offload(tenant_id, timeline_id_offloaded)
|
||||
list = http_client.timeline_and_offloaded_list(tenant_id)
|
||||
assert timeline_id_set_for(list.offloaded) == expected_offloaded_set
|
||||
assert timeline_id_set_for(list.timelines) == expected_timeline_set
|
||||
|
||||
# Do a full image layer generation before splitting
|
||||
http_client.timeline_checkpoint(
|
||||
tenant_id, timeline_id_main, force_image_layer_creation=True, wait_until_uploaded=True
|
||||
)
|
||||
|
||||
# Split one shard into two
|
||||
shards = env.storage_controller.tenant_shard_split(tenant_id, shard_count=2)
|
||||
|
||||
# Let all shards move into their stable locations, so that during subsequent steps we
|
||||
# don't have reconciles in progress (simpler to reason about what messages we expect in logs)
|
||||
env.storage_controller.reconcile_until_idle()
|
||||
|
||||
# Check we got the shard IDs we expected
|
||||
assert env.storage_controller.inspect(TenantShardId(tenant_id, 0, 2)) is not None
|
||||
assert env.storage_controller.inspect(TenantShardId(tenant_id, 1, 2)) is not None
|
||||
|
||||
workload_main.validate()
|
||||
workload_main.stop()
|
||||
|
||||
env.storage_controller.consistency_check()
|
||||
|
||||
# Ensure each shard has the same list of timelines and offloaded timelines
|
||||
for shard in shards:
|
||||
ps = env.get_tenant_pageserver(shard)
|
||||
|
||||
list = ps.http_client().timeline_and_offloaded_list(shard)
|
||||
assert timeline_id_set_for(list.offloaded) == expected_offloaded_set
|
||||
assert timeline_id_set_for(list.timelines) == expected_timeline_set
|
||||
|
||||
ps.http_client().timeline_compact(shard, timeline_id_main)
|
||||
|
||||
# Check that we can still read all the data
|
||||
workload_main.validate()
|
||||
|
||||
# Force a restart, which requires the state to be persisted.
|
||||
env.pageserver.stop()
|
||||
env.pageserver.start()
|
||||
|
||||
# Ensure each shard has the same list of timelines and offloaded timelines
|
||||
for shard in shards:
|
||||
ps = env.get_tenant_pageserver(shard)
|
||||
|
||||
list = ps.http_client().timeline_and_offloaded_list(shard)
|
||||
assert timeline_id_set_for(list.offloaded) == expected_offloaded_set
|
||||
assert timeline_id_set_for(list.timelines) == expected_timeline_set
|
||||
|
||||
ps.http_client().timeline_compact(shard, timeline_id_main)
|
||||
|
||||
# Compaction shouldn't make anything unreadable
|
||||
workload_main.validate()
|
||||
|
||||
# Do sharded unarchival
|
||||
env.storage_controller.timeline_archival_config(
|
||||
tenant_id, timeline_id_offloaded, TimelineArchivalState.UNARCHIVED
|
||||
)
|
||||
env.storage_controller.timeline_archival_config(
|
||||
tenant_id, timeline_id_archived, TimelineArchivalState.UNARCHIVED
|
||||
)
|
||||
|
||||
for shard in shards:
|
||||
ps = env.get_tenant_pageserver(shard)
|
||||
|
||||
list = ps.http_client().timeline_and_offloaded_list(shard)
|
||||
assert timeline_id_set_for(list.offloaded) == set()
|
||||
assert timeline_id_set_for(list.timelines) == {
|
||||
timeline_id_main,
|
||||
timeline_id_archived,
|
||||
timeline_id_offloaded,
|
||||
}
|
||||
|
||||
|
||||
def test_sharding_split_smoke(
|
||||
neon_env_builder: NeonEnvBuilder,
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user