mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-17 02:12:56 +00:00
On-Demand Download
The code in this change was extracted from #2595 (Heikki’s on-demand download draft PR). High-Level Changes - New RemoteLayer Type - On-Demand Download As An Effect Of Page Reconstruction - Breaking Semantics For Physical Size Metrics There are several follow-up work items planned. Refer to the Epic issue on GitHub: https://github.com/neondatabase/neon/issues/2029 closes https://github.com/neondatabase/neon/pull/3013 Co-authored-by: Kirill Bulatov <kirill@neon.tech> Co-authored-by: Christian Schwarz <christian@neon.tech> New RemoteLayer Type ==================== Instead of downloading all layers during tenant attach, we create RemoteLayer instances for each of them and add them to the layer map. On-Demand Download As An Effect Of Page Reconstruction ====================================================== At the heart of pageserver is Timeline::get_reconstruct_data(). It traverses the layer map until it has collected all the data it needs to produce the page image. Most code in the code base uses it, though many layers of indirection. Before this patch, the function would use synchronous filesystem IO to load data from disk-resident layer files if the data was not cached. That is not possible with RemoteLayer, because the layer file has not been downloaded yet. So, we do the download when get_reconstruct_data gets there, i.e., “on demand”. The mechanics of how the download is done are rather involved, because of the infamous async-sync-async sandwich problem that plagues the async Rust world. We use the new PageReconstructResult type to work around this. Its introduction is the cause for a good amount of code churn in this patch. Refer to the block comment on `with_ondemand_download()` for details. Breaking Semantics For Physical Size Metrics ============================================ We rename prometheus metric pageserver_{current,resident}_physical_size to reflect what this metric actually represents with on-demand download. This intentionally BREAKS existing grafana dashboard and the cost model data pipeline. Breaking is desirable because the meaning of this metrics has changed with on-demand download. See https://docs.google.com/document/d/12AFpvKY-7FZdR5a4CaD6Ir_rI3QokdCLSPJ6upHxJBo/edit# for how we will handle this breakage. Likewise, we rename the new billing_metrics’s PhysicalSize => ResidentSize. This is not yet used anywhere, so, this is not a breaking change. There is still a field called TimelineInfo::current_physical_size. It is now the sum of the layer sizes in layer map, regardless of whether local or remote. To compute that sum, we added a new trait method PersistentLayer::file_size(). When updating the Python tests, we got rid of current_physical_size_non_incremental. An earlier commit removed it from the OpenAPI spec already, so this is not a breaking change. test_timeline_size.py has grown additional assertions on the resident_physical_size metric.
This commit is contained in:
committed by
Christian Schwarz
parent
31543c4acc
commit
7ff591ffbf
@@ -49,7 +49,7 @@ PAGESERVER_PER_TENANT_REMOTE_TIMELINE_CLIENT_METRICS: Tuple[str, ...] = (
|
||||
|
||||
PAGESERVER_PER_TENANT_METRICS: Tuple[str, ...] = (
|
||||
"pageserver_current_logical_size",
|
||||
"pageserver_current_physical_size",
|
||||
"pageserver_resident_physical_size",
|
||||
"pageserver_getpage_reconstruct_seconds_bucket",
|
||||
"pageserver_getpage_reconstruct_seconds_count",
|
||||
"pageserver_getpage_reconstruct_seconds_sum",
|
||||
|
||||
@@ -26,6 +26,7 @@ import asyncpg
|
||||
import backoff # type: ignore
|
||||
import boto3
|
||||
import jwt
|
||||
import prometheus_client
|
||||
import psycopg2
|
||||
import pytest
|
||||
import requests
|
||||
@@ -41,6 +42,7 @@ from fixtures.utils import (
|
||||
get_self_dir,
|
||||
subprocess_capture,
|
||||
)
|
||||
from prometheus_client.parser import text_string_to_metric_families
|
||||
|
||||
# Type-related stuff
|
||||
from psycopg2.extensions import connection as PgConnection
|
||||
@@ -1204,8 +1206,22 @@ class PageserverHttpClient(requests.Session):
|
||||
# there are no tests for those right now.
|
||||
return size
|
||||
|
||||
def timeline_list(self, tenant_id: TenantId) -> List[Dict[str, Any]]:
|
||||
res = self.get(f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline")
|
||||
def timeline_list(
|
||||
self,
|
||||
tenant_id: TenantId,
|
||||
include_non_incremental_logical_size: bool = False,
|
||||
include_timeline_dir_layer_file_size_sum: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
params = {}
|
||||
if include_non_incremental_logical_size:
|
||||
params["include-non-incremental-logical-size"] = "yes"
|
||||
if include_timeline_dir_layer_file_size_sum:
|
||||
params["include-timeline-dir-layer-file-size-sum"] = "yes"
|
||||
|
||||
res = self.get(
|
||||
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline", params=params
|
||||
)
|
||||
self.verbose_error(res)
|
||||
res_json = res.json()
|
||||
assert isinstance(res_json, list)
|
||||
@@ -1239,13 +1255,13 @@ class PageserverHttpClient(requests.Session):
|
||||
tenant_id: TenantId,
|
||||
timeline_id: TimelineId,
|
||||
include_non_incremental_logical_size: bool = False,
|
||||
include_non_incremental_physical_size: bool = False,
|
||||
include_timeline_dir_layer_file_size_sum: bool = False,
|
||||
) -> Dict[Any, Any]:
|
||||
params = {}
|
||||
if include_non_incremental_logical_size:
|
||||
params["include-non-incremental-logical-size"] = "yes"
|
||||
if include_non_incremental_physical_size:
|
||||
params["include-non-incremental-physical-size"] = "yes"
|
||||
if include_timeline_dir_layer_file_size_sum:
|
||||
params["include-timeline-dir-layer-file-size-sum"] = "yes"
|
||||
|
||||
res = self.get(
|
||||
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline/{timeline_id}",
|
||||
@@ -1320,11 +1336,88 @@ class PageserverHttpClient(requests.Session):
|
||||
res_json = res.json()
|
||||
assert res_json is None
|
||||
|
||||
def timeline_spawn_download_remote_layers(
|
||||
self, tenant_id: TenantId, timeline_id: TimelineId
|
||||
) -> dict[str, Any]:
|
||||
|
||||
res = self.post(
|
||||
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline/{timeline_id}/download_remote_layers",
|
||||
)
|
||||
self.verbose_error(res)
|
||||
res_json = res.json()
|
||||
assert res_json is not None
|
||||
assert isinstance(res_json, dict)
|
||||
return res_json
|
||||
|
||||
def timeline_poll_download_remote_layers_status(
|
||||
self,
|
||||
tenant_id: TenantId,
|
||||
timeline_id: TimelineId,
|
||||
spawn_response: dict[str, Any],
|
||||
poll_state=None,
|
||||
) -> None | dict[str, Any]:
|
||||
res = self.get(
|
||||
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/timeline/{timeline_id}/download_remote_layers",
|
||||
)
|
||||
self.verbose_error(res)
|
||||
res_json = res.json()
|
||||
assert res_json is not None
|
||||
assert isinstance(res_json, dict)
|
||||
|
||||
# assumption in this API client here is that nobody else spawns the task
|
||||
assert res_json["task_id"] == spawn_response["task_id"]
|
||||
|
||||
if poll_state is None or res_json["state"] == poll_state:
|
||||
return res_json
|
||||
return None
|
||||
|
||||
def timeline_download_remote_layers(
|
||||
self,
|
||||
tenant_id: TenantId,
|
||||
timeline_id: TimelineId,
|
||||
errors_ok=False,
|
||||
at_least_one_download=True,
|
||||
):
|
||||
res = self.timeline_spawn_download_remote_layers(tenant_id, timeline_id)
|
||||
while True:
|
||||
completed = self.timeline_poll_download_remote_layers_status(
|
||||
tenant_id, timeline_id, res, poll_state="Completed"
|
||||
)
|
||||
if not completed:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
if not errors_ok:
|
||||
assert completed["failed_download_count"] == 0
|
||||
if at_least_one_download:
|
||||
assert completed["successful_download_count"] > 0
|
||||
return completed
|
||||
|
||||
def get_metrics(self) -> str:
|
||||
res = self.get(f"http://localhost:{self.port}/metrics")
|
||||
self.verbose_error(res)
|
||||
return res.text
|
||||
|
||||
def get_timeline_metric(self, tenant_id: TenantId, timeline_id: TimelineId, metric_name: str):
|
||||
raw = self.get_metrics()
|
||||
family: List[prometheus_client.Metric] = list(text_string_to_metric_families(raw))
|
||||
[metric] = [m for m in family if m.name == metric_name]
|
||||
[sample] = [
|
||||
s
|
||||
for s in metric.samples
|
||||
if s.labels["tenant_id"] == str(tenant_id)
|
||||
and s.labels["timeline_id"] == str(timeline_id)
|
||||
]
|
||||
return sample.value
|
||||
|
||||
def get_metric_value(self, name: str) -> Optional[str]:
|
||||
metrics = self.get_metrics()
|
||||
relevant = [line for line in metrics.splitlines() if line.startswith(name)]
|
||||
if len(relevant) == 0:
|
||||
log.info(f'could not find metric "{name}"')
|
||||
return None
|
||||
assert len(relevant) == 1
|
||||
return relevant[0].lstrip(name).strip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageserverPort:
|
||||
@@ -1622,7 +1715,12 @@ class NeonCli(AbstractNeonCli):
|
||||
pageserver_config_override=self.env.pageserver.config_override,
|
||||
)
|
||||
|
||||
res = self.raw_cli(cmd)
|
||||
s3_env_vars = None
|
||||
if self.env.remote_storage is not None and isinstance(
|
||||
self.env.remote_storage, S3Storage
|
||||
):
|
||||
s3_env_vars = self.env.remote_storage.access_env_vars()
|
||||
res = self.raw_cli(cmd, extra_env_vars=s3_env_vars)
|
||||
res.check_returncode()
|
||||
return res
|
||||
|
||||
@@ -2996,13 +3094,55 @@ def check_restored_datadir_content(
|
||||
assert (mismatch, error) == ([], [])
|
||||
|
||||
|
||||
def assert_no_in_progress_downloads_for_tenant(
|
||||
pageserver_http_client: PageserverHttpClient,
|
||||
tenant: TenantId,
|
||||
def wait_until(number_of_iterations: int, interval: float, func):
|
||||
"""
|
||||
Wait until 'func' returns successfully, without exception. Returns the
|
||||
last return value from the function.
|
||||
"""
|
||||
last_exception = None
|
||||
for i in range(number_of_iterations):
|
||||
try:
|
||||
res = func()
|
||||
except Exception as e:
|
||||
log.info("waiting for %s iteration %s failed", func, i + 1)
|
||||
last_exception = e
|
||||
time.sleep(interval)
|
||||
continue
|
||||
return res
|
||||
raise Exception("timed out while waiting for %s" % func) from last_exception
|
||||
|
||||
|
||||
def wait_while(number_of_iterations: int, interval: float, func):
|
||||
"""
|
||||
Wait until 'func' returns false, or throws an exception.
|
||||
"""
|
||||
for i in range(number_of_iterations):
|
||||
try:
|
||||
if not func():
|
||||
return
|
||||
log.info("waiting for %s iteration %s failed", func, i + 1)
|
||||
time.sleep(interval)
|
||||
continue
|
||||
except Exception:
|
||||
return
|
||||
raise Exception("timed out while waiting for %s" % func)
|
||||
|
||||
|
||||
def assert_tenant_status(
|
||||
pageserver_http_client: PageserverHttpClient, tenant: TenantId, expected_status: str
|
||||
):
|
||||
tenant_status = pageserver_http_client.tenant_status(tenant)
|
||||
assert tenant_status["has_in_progress_downloads"] is False, tenant_status
|
||||
assert tenant_status["state"] == "Active"
|
||||
log.info(f"tenant_status: {tenant_status}")
|
||||
assert tenant_status["state"] == expected_status, tenant_status
|
||||
|
||||
|
||||
def tenant_exists(ps_http: PageserverHttpClient, tenant_id: TenantId):
|
||||
tenants = ps_http.tenant_list()
|
||||
matching = [t for t in tenants if TenantId(t["id"]) == tenant_id]
|
||||
assert len(matching) < 2
|
||||
if len(matching) == 0:
|
||||
return None
|
||||
return matching[0]
|
||||
|
||||
|
||||
def remote_consistent_lsn(
|
||||
@@ -3010,14 +3150,15 @@ def remote_consistent_lsn(
|
||||
) -> Lsn:
|
||||
detail = pageserver_http_client.timeline_detail(tenant, timeline)
|
||||
|
||||
lsn_str = detail["remote_consistent_lsn"]
|
||||
if lsn_str is None:
|
||||
if detail["remote_consistent_lsn"] is None:
|
||||
# No remote information at all. This happens right after creating
|
||||
# a timeline, before any part of it has been uploaded to remote
|
||||
# storage yet.
|
||||
return Lsn(0)
|
||||
assert isinstance(lsn_str, str)
|
||||
return Lsn(lsn_str)
|
||||
else:
|
||||
lsn_str = detail["remote_consistent_lsn"]
|
||||
assert isinstance(lsn_str, str)
|
||||
return Lsn(lsn_str)
|
||||
|
||||
|
||||
def wait_for_upload(
|
||||
@@ -3030,6 +3171,7 @@ def wait_for_upload(
|
||||
for i in range(20):
|
||||
current_lsn = remote_consistent_lsn(pageserver_http_client, tenant, timeline)
|
||||
if current_lsn >= lsn:
|
||||
log.info("wait finished")
|
||||
return
|
||||
log.info(
|
||||
"waiting for remote_consistent_lsn to reach {}, now {}, iteration {}".format(
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_broken_timeline(neon_env_builder: NeonEnvBuilder):
|
||||
|
||||
env.pageserver.allowed_errors.extend(
|
||||
[
|
||||
".*Failed to load delta layer.*",
|
||||
".*Failed to reconstruct the page.*",
|
||||
".*could not find data for key.*",
|
||||
".*is not active. Current state: Broken.*",
|
||||
".*will not become active. Current state: Broken.*",
|
||||
@@ -87,9 +87,9 @@ def test_broken_timeline(neon_env_builder: NeonEnvBuilder):
|
||||
f"As expected, compute startup failed eagerly for timeline with corrupt metadata: {err}"
|
||||
)
|
||||
|
||||
# Second timeline has no ancestors, only the metadata file and no layer files.
|
||||
# That is checked explicitly in the pageserver, and causes the tenant to be marked
|
||||
# as broken.
|
||||
# Second timeline has no ancestors, only the metadata file and no layer files locally,
|
||||
# and we don't have the remote storage enabled. It is loaded into memory, but getting
|
||||
# the basebackup from it will fail.
|
||||
with pytest.raises(
|
||||
Exception, match=f"Tenant {tenant2} will not become active. Current state: Broken"
|
||||
) as err:
|
||||
@@ -97,8 +97,9 @@ def test_broken_timeline(neon_env_builder: NeonEnvBuilder):
|
||||
log.info(f"As expected, compute startup failed for timeline with missing layers: {err}")
|
||||
|
||||
# Third timeline will also fail during basebackup, because the layer file is corrupt.
|
||||
# It will fail when we try to read (and reconstruct) a page from it, ergo the error message.
|
||||
# (We don't check layer file contents on startup, when loading the timeline)
|
||||
with pytest.raises(Exception, match="Failed to load delta layer") as err:
|
||||
with pytest.raises(Exception, match="Failed to reconstruct the page") as err:
|
||||
pg3.start()
|
||||
log.info(
|
||||
f"As expected, compute startup failed for timeline {tenant3}/{timeline3} with corrupt layers: {err}"
|
||||
|
||||
@@ -37,7 +37,7 @@ def metrics_handler(request: Request) -> Response:
|
||||
|
||||
checks = {
|
||||
"written_size": lambda value: value > 0,
|
||||
"physical_size": lambda value: value >= 0,
|
||||
"resident_size": lambda value: value >= 0,
|
||||
# >= 0 check here is to avoid race condition when we receive metrics before
|
||||
# remote_uploaded is updated
|
||||
"remote_storage_size": lambda value: value > 0 if remote_uploaded > 0 else value >= 0,
|
||||
|
||||
437
test_runner/regress/test_ondemand_download.py
Normal file
437
test_runner/regress/test_ondemand_download.py
Normal file
@@ -0,0 +1,437 @@
|
||||
# It's possible to run any regular test with the local fs remote storage via
|
||||
# env ZENITH_PAGESERVER_OVERRIDES="remote_storage={local_path='/tmp/neon_zzz/'}" poetry ......
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fixtures.log_helper import log
|
||||
from fixtures.neon_fixtures import (
|
||||
NeonEnvBuilder,
|
||||
RemoteStorageKind,
|
||||
assert_tenant_status,
|
||||
available_remote_storages,
|
||||
wait_for_last_record_lsn,
|
||||
wait_for_sk_commit_lsn_to_reach_remote_storage,
|
||||
wait_for_upload,
|
||||
wait_until,
|
||||
)
|
||||
from fixtures.types import Lsn
|
||||
from fixtures.utils import query_scalar
|
||||
|
||||
|
||||
def get_num_downloaded_layers(client, tenant_id, timeline_id):
|
||||
value = client.get_metric_value(
|
||||
f'pageserver_remote_operation_seconds_count{{file_kind="layer",op_kind="download",status="success",tenant_id="{tenant_id}",timeline_id="{timeline_id}"}}'
|
||||
)
|
||||
if value is None:
|
||||
return 0
|
||||
return int(value)
|
||||
|
||||
|
||||
#
|
||||
# If you have a large relation, check that the pageserver downloads parts of it as
|
||||
# require by queries.
|
||||
#
|
||||
@pytest.mark.parametrize("remote_storage_kind", available_remote_storages())
|
||||
def test_ondemand_download_large_rel(
|
||||
neon_env_builder: NeonEnvBuilder,
|
||||
remote_storage_kind: RemoteStorageKind,
|
||||
):
|
||||
neon_env_builder.enable_remote_storage(
|
||||
remote_storage_kind=remote_storage_kind,
|
||||
test_name="test_ondemand_download_large_rel",
|
||||
)
|
||||
|
||||
##### First start, insert secret data and upload it to the remote storage
|
||||
env = neon_env_builder.init_start()
|
||||
|
||||
# Override defaults, to create more layers
|
||||
tenant, _ = env.neon_cli.create_tenant(
|
||||
conf={
|
||||
# disable background GC
|
||||
"gc_period": "10 m",
|
||||
"gc_horizon": f"{10 * 1024 ** 3}", # 10 GB
|
||||
# small checkpoint distance to create more delta layer files
|
||||
"checkpoint_distance": f"{10 * 1024 ** 2}", # 10 MB
|
||||
"compaction_threshold": "3",
|
||||
"compaction_target_size": f"{10 * 1024 ** 2}", # 10 MB
|
||||
}
|
||||
)
|
||||
env.initial_tenant = tenant
|
||||
|
||||
pg = env.postgres.create_start("main")
|
||||
|
||||
client = env.pageserver.http_client()
|
||||
|
||||
tenant_id = pg.safe_psql("show neon.tenant_id")[0][0]
|
||||
timeline_id = pg.safe_psql("show neon.timeline_id")[0][0]
|
||||
|
||||
# We want to make sure that the data is large enough that the keyspace is partitioned.
|
||||
num_rows = 1000000
|
||||
|
||||
with pg.cursor() as cur:
|
||||
# data loading may take a while, so increase statement timeout
|
||||
cur.execute("SET statement_timeout='300s'")
|
||||
cur.execute(
|
||||
f"""CREATE TABLE tbl AS SELECT g as id, 'long string to consume some space' || g
|
||||
from generate_series(1,{num_rows}) g"""
|
||||
)
|
||||
cur.execute("CREATE INDEX ON tbl (id)")
|
||||
cur.execute("VACUUM tbl")
|
||||
|
||||
current_lsn = Lsn(query_scalar(cur, "SELECT pg_current_wal_flush_lsn()"))
|
||||
|
||||
# wait until pageserver receives that data
|
||||
wait_for_last_record_lsn(client, tenant_id, timeline_id, current_lsn)
|
||||
|
||||
# run checkpoint manually to be sure that data landed in remote storage
|
||||
client.timeline_checkpoint(tenant_id, timeline_id)
|
||||
|
||||
# wait until pageserver successfully uploaded a checkpoint to remote storage
|
||||
wait_for_upload(client, tenant_id, timeline_id, current_lsn)
|
||||
log.info("uploads have finished")
|
||||
|
||||
##### Stop the first pageserver instance, erase all its data
|
||||
pg.stop()
|
||||
env.pageserver.stop()
|
||||
|
||||
# remove all the layer files
|
||||
for layer in (Path(env.repo_dir) / "tenants").glob("*/timelines/*/*-*_*"):
|
||||
log.info(f"unlinking layer {layer}")
|
||||
layer.unlink()
|
||||
|
||||
##### Second start, restore the data and ensure it's the same
|
||||
env.pageserver.start()
|
||||
|
||||
pg.start()
|
||||
before_downloads = get_num_downloaded_layers(client, tenant_id, timeline_id)
|
||||
|
||||
# Probe in the middle of the table. There's a high chance that the beginning
|
||||
# and end of the table was stored together in the same layer files with data
|
||||
# from other tables, and with the entry that stores the size of the
|
||||
# relation, so they are likely already downloaded. But the middle of the
|
||||
# table should not have been needed by anything yet.
|
||||
with pg.cursor() as cur:
|
||||
assert query_scalar(cur, "select count(*) from tbl where id = 500000") == 1
|
||||
|
||||
after_downloads = get_num_downloaded_layers(client, tenant_id, timeline_id)
|
||||
log.info(f"layers downloaded before {before_downloads} and after {after_downloads}")
|
||||
assert after_downloads > before_downloads
|
||||
|
||||
|
||||
#
|
||||
# If you have a relation with a long history of updates,the pageserver downloads the layer
|
||||
# files containing the history as needed by timetravel queries.
|
||||
#
|
||||
@pytest.mark.parametrize("remote_storage_kind", available_remote_storages())
|
||||
def test_ondemand_download_timetravel(
|
||||
neon_env_builder: NeonEnvBuilder,
|
||||
remote_storage_kind: RemoteStorageKind,
|
||||
):
|
||||
neon_env_builder.enable_remote_storage(
|
||||
remote_storage_kind=remote_storage_kind,
|
||||
test_name="test_ondemand_download_timetravel",
|
||||
)
|
||||
|
||||
##### First start, insert data and upload it to the remote storage
|
||||
env = neon_env_builder.init_start()
|
||||
|
||||
# Override defaults, to create more layers
|
||||
tenant, _ = env.neon_cli.create_tenant(
|
||||
conf={
|
||||
# Disable background GC & compaction
|
||||
# We don't want GC, that would break the assertion about num downloads.
|
||||
# We don't want background compaction, we force a compaction every time we do explicit checkpoint.
|
||||
"gc_period": "0s",
|
||||
"compaction_period": "0s",
|
||||
# small checkpoint distance to create more delta layer files
|
||||
"checkpoint_distance": f"{1 * 1024 ** 2}", # 1 MB
|
||||
"compaction_threshold": "1",
|
||||
"image_creation_threshold": "1",
|
||||
"compaction_target_size": f"{1 * 1024 ** 2}", # 1 MB
|
||||
}
|
||||
)
|
||||
env.initial_tenant = tenant
|
||||
|
||||
pg = env.postgres.create_start("main")
|
||||
|
||||
client = env.pageserver.http_client()
|
||||
|
||||
tenant_id = pg.safe_psql("show neon.tenant_id")[0][0]
|
||||
timeline_id = pg.safe_psql("show neon.timeline_id")[0][0]
|
||||
|
||||
lsns = []
|
||||
|
||||
table_len = 10000
|
||||
with pg.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE testtab(id serial primary key, checkpoint_number int, data text);
|
||||
INSERT INTO testtab (checkpoint_number, data) SELECT 0, 'data' FROM generate_series(1, {table_len});
|
||||
"""
|
||||
)
|
||||
current_lsn = Lsn(query_scalar(cur, "SELECT pg_current_wal_flush_lsn()"))
|
||||
# wait until pageserver receives that data
|
||||
wait_for_last_record_lsn(client, tenant_id, timeline_id, current_lsn)
|
||||
# run checkpoint manually to be sure that data landed in remote storage
|
||||
client.timeline_checkpoint(tenant_id, timeline_id)
|
||||
lsns.append((0, current_lsn))
|
||||
|
||||
for checkpoint_number in range(1, 20):
|
||||
with pg.cursor() as cur:
|
||||
cur.execute(f"UPDATE testtab SET checkpoint_number = {checkpoint_number}")
|
||||
current_lsn = Lsn(query_scalar(cur, "SELECT pg_current_wal_flush_lsn()"))
|
||||
lsns.append((checkpoint_number, current_lsn))
|
||||
|
||||
# wait until pageserver receives that data
|
||||
wait_for_last_record_lsn(client, tenant_id, timeline_id, current_lsn)
|
||||
|
||||
# run checkpoint manually to be sure that data landed in remote storage
|
||||
client.timeline_checkpoint(tenant_id, timeline_id)
|
||||
|
||||
# wait until pageserver successfully uploaded a checkpoint to remote storage
|
||||
wait_for_upload(client, tenant_id, timeline_id, current_lsn)
|
||||
log.info("uploads have finished")
|
||||
|
||||
##### Stop the first pageserver instance, erase all its data
|
||||
env.postgres.stop_all()
|
||||
|
||||
wait_for_sk_commit_lsn_to_reach_remote_storage(
|
||||
tenant_id, timeline_id, env.safekeepers, env.pageserver
|
||||
)
|
||||
|
||||
def get_api_current_physical_size():
|
||||
d = client.timeline_detail(tenant_id, timeline_id)
|
||||
return d["current_physical_size"]
|
||||
|
||||
def get_resident_physical_size():
|
||||
return client.get_timeline_metric(
|
||||
tenant_id, timeline_id, "pageserver_resident_physical_size"
|
||||
)
|
||||
|
||||
filled_current_physical = get_api_current_physical_size()
|
||||
log.info(filled_current_physical)
|
||||
filled_size = get_resident_physical_size()
|
||||
log.info(filled_size)
|
||||
assert filled_current_physical == filled_size, "we don't yet do layer eviction"
|
||||
|
||||
env.pageserver.stop()
|
||||
|
||||
# remove all the layer files
|
||||
for layer in (Path(env.repo_dir) / "tenants").glob("*/timelines/*/*-*_*"):
|
||||
log.info(f"unlinking layer {layer}")
|
||||
layer.unlink()
|
||||
|
||||
##### Second start, restore the data and ensure it's the same
|
||||
env.pageserver.start()
|
||||
|
||||
wait_until(10, 0.2, lambda: assert_tenant_status(client, tenant_id, "Active"))
|
||||
|
||||
# current_physical_size reports sum of layer file sizes, regardless of local or remote
|
||||
assert filled_current_physical == get_api_current_physical_size()
|
||||
|
||||
num_layers_downloaded = [0]
|
||||
physical_size = [get_resident_physical_size()]
|
||||
for (checkpoint_number, lsn) in lsns:
|
||||
pg_old = env.postgres.create_start(
|
||||
branch_name="main", node_name=f"test_old_lsn_{checkpoint_number}", lsn=lsn
|
||||
)
|
||||
with pg_old.cursor() as cur:
|
||||
# assert query_scalar(cur, f"select count(*) from testtab where checkpoint_number={checkpoint_number}") == 100000
|
||||
assert (
|
||||
query_scalar(
|
||||
cur,
|
||||
f"select count(*) from testtab where checkpoint_number<>{checkpoint_number}",
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
query_scalar(
|
||||
cur,
|
||||
f"select count(*) from testtab where checkpoint_number={checkpoint_number}",
|
||||
)
|
||||
== table_len
|
||||
)
|
||||
|
||||
after_downloads = get_num_downloaded_layers(client, tenant_id, timeline_id)
|
||||
num_layers_downloaded.append(after_downloads)
|
||||
log.info(f"num_layers_downloaded[-1]={num_layers_downloaded[-1]}")
|
||||
|
||||
# Check that on each query, we need to download at least one more layer file. However in
|
||||
# practice, thanks to compaction and the fact that some requests need to download
|
||||
# more history, some points-in-time are covered by earlier downloads already. But
|
||||
# in broad strokes, as we query more points-in-time, more layers need to be downloaded.
|
||||
#
|
||||
# Do a fuzzy check on that, by checking that after each point-in-time, we have downloaded
|
||||
# more files than we had three iterations ago.
|
||||
log.info(f"layers downloaded after checkpoint {checkpoint_number}: {after_downloads}")
|
||||
if len(num_layers_downloaded) > 4:
|
||||
assert after_downloads > num_layers_downloaded[len(num_layers_downloaded) - 4]
|
||||
|
||||
# Likewise, assert that the physical_size metric grows as layers are downloaded
|
||||
physical_size.append(get_resident_physical_size())
|
||||
log.info(f"physical_size[-1]={physical_size[-1]}")
|
||||
if len(physical_size) > 4:
|
||||
assert physical_size[-1] > physical_size[len(physical_size) - 4]
|
||||
|
||||
# current_physical_size reports sum of layer file sizes, regardless of local or remote
|
||||
assert filled_current_physical == get_api_current_physical_size()
|
||||
|
||||
|
||||
#
|
||||
# Ensure that the `download_remote_layers` API works
|
||||
#
|
||||
@pytest.mark.parametrize("remote_storage_kind", [RemoteStorageKind.LOCAL_FS])
|
||||
def test_download_remote_layers_api(
|
||||
neon_env_builder: NeonEnvBuilder,
|
||||
remote_storage_kind: RemoteStorageKind,
|
||||
):
|
||||
neon_env_builder.enable_remote_storage(
|
||||
remote_storage_kind=remote_storage_kind,
|
||||
test_name="test_download_remote_layers_api",
|
||||
)
|
||||
|
||||
##### First start, insert data and upload it to the remote storage
|
||||
env = neon_env_builder.init_start()
|
||||
|
||||
# Override defaults, to create more layers
|
||||
tenant, _ = env.neon_cli.create_tenant(
|
||||
conf={
|
||||
# Disable background GC & compaction
|
||||
# We don't want GC, that would break the assertion about num downloads.
|
||||
# We don't want background compaction, we force a compaction every time we do explicit checkpoint.
|
||||
"gc_period": "0s",
|
||||
"compaction_period": "0s",
|
||||
# small checkpoint distance to create more delta layer files
|
||||
"checkpoint_distance": f"{1 * 1024 ** 2}", # 1 MB
|
||||
"compaction_threshold": "1",
|
||||
"image_creation_threshold": "1",
|
||||
"compaction_target_size": f"{1 * 1024 ** 2}", # 1 MB
|
||||
}
|
||||
)
|
||||
env.initial_tenant = tenant
|
||||
|
||||
pg = env.postgres.create_start("main")
|
||||
|
||||
client = env.pageserver.http_client()
|
||||
|
||||
tenant_id = pg.safe_psql("show neon.tenant_id")[0][0]
|
||||
timeline_id = pg.safe_psql("show neon.timeline_id")[0][0]
|
||||
|
||||
table_len = 10000
|
||||
with pg.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE testtab(id serial primary key, checkpoint_number int, data text);
|
||||
INSERT INTO testtab (checkpoint_number, data) SELECT 0, 'data' FROM generate_series(1, {table_len});
|
||||
"""
|
||||
)
|
||||
|
||||
env.postgres.stop_all()
|
||||
|
||||
wait_for_sk_commit_lsn_to_reach_remote_storage(
|
||||
tenant_id, timeline_id, env.safekeepers, env.pageserver
|
||||
)
|
||||
|
||||
def get_api_current_physical_size():
|
||||
d = client.timeline_detail(tenant_id, timeline_id)
|
||||
return d["current_physical_size"]
|
||||
|
||||
def get_resident_physical_size():
|
||||
return client.get_timeline_metric(
|
||||
tenant_id, timeline_id, "pageserver_resident_physical_size"
|
||||
)
|
||||
|
||||
filled_current_physical = get_api_current_physical_size()
|
||||
log.info(filled_current_physical)
|
||||
filled_size = get_resident_physical_size()
|
||||
log.info(filled_size)
|
||||
assert filled_current_physical == filled_size, "we don't yet do layer eviction"
|
||||
|
||||
env.pageserver.stop()
|
||||
|
||||
# remove all the layer files
|
||||
# XXX only delete some of the layer files, to show that it really just downloads all the layers
|
||||
for layer in (Path(env.repo_dir) / "tenants").glob("*/timelines/*/*-*_*"):
|
||||
log.info(f"unlinking layer {layer}")
|
||||
layer.unlink()
|
||||
|
||||
# Shut down safekeepers before starting the pageserver.
|
||||
# If we don't, the tenant's walreceiver handler will trigger the
|
||||
# the logical size computation task, and that downloads layes,
|
||||
# which makes our assertions on size fail.
|
||||
for sk in env.safekeepers:
|
||||
sk.stop(immediate=True)
|
||||
|
||||
##### Second start, restore the data and ensure it's the same
|
||||
env.pageserver.start(extra_env_vars={"FAILPOINTS": "remote-storage-download-pre-rename=return"})
|
||||
env.pageserver.allowed_errors.extend(
|
||||
[
|
||||
f".*download_all_remote_layers.*{tenant_id}.*{timeline_id}.*layer download failed.*remote-storage-download-pre-rename failpoint",
|
||||
f".*initial size calculation.*{tenant_id}.*{timeline_id}.*Failed to calculate logical size",
|
||||
]
|
||||
)
|
||||
|
||||
wait_until(10, 0.2, lambda: assert_tenant_status(client, tenant_id, "Active"))
|
||||
|
||||
###### Phase 1: exercise download error code path
|
||||
assert (
|
||||
filled_current_physical == get_api_current_physical_size()
|
||||
), "current_physical_size is sum of loaded layer sizes, independent of whether local or remote"
|
||||
post_unlink_size = get_resident_physical_size()
|
||||
log.info(post_unlink_size)
|
||||
assert (
|
||||
post_unlink_size < filled_size
|
||||
), "we just deleted layers and didn't cause anything to re-download them yet"
|
||||
assert filled_size - post_unlink_size > 5 * (
|
||||
1024**2
|
||||
), "we may be downloading some layers as part of tenant activation"
|
||||
|
||||
# issue downloads that we know will fail
|
||||
info = client.timeline_download_remote_layers(
|
||||
tenant_id, timeline_id, errors_ok=True, at_least_one_download=False
|
||||
)
|
||||
log.info(f"info={info}")
|
||||
assert info["state"] == "Completed"
|
||||
assert info["total_layer_count"] > 0
|
||||
assert info["successful_download_count"] == 0
|
||||
assert (
|
||||
info["failed_download_count"] > 0
|
||||
) # can't assert == total_layer_count because attach + tenant status downloads some layers
|
||||
assert (
|
||||
info["total_layer_count"]
|
||||
== info["successful_download_count"] + info["failed_download_count"]
|
||||
)
|
||||
assert get_api_current_physical_size() == filled_current_physical
|
||||
assert (
|
||||
get_resident_physical_size() == post_unlink_size
|
||||
), "didn't download anything new due to failpoint"
|
||||
# would be nice to assert that the layers in the layer map are still RemoteLayer
|
||||
|
||||
##### Retry, this time without failpoints
|
||||
client.configure_failpoints(("remote-storage-download-pre-rename", "off"))
|
||||
info = client.timeline_download_remote_layers(tenant_id, timeline_id, errors_ok=False)
|
||||
log.info(f"info={info}")
|
||||
|
||||
assert info["state"] == "Completed"
|
||||
assert info["total_layer_count"] > 0
|
||||
assert info["successful_download_count"] > 0
|
||||
assert info["failed_download_count"] == 0
|
||||
assert (
|
||||
info["total_layer_count"]
|
||||
== info["successful_download_count"] + info["failed_download_count"]
|
||||
)
|
||||
|
||||
refilled_size = get_resident_physical_size()
|
||||
log.info(refilled_size)
|
||||
|
||||
assert filled_size == refilled_size, "we redownloaded all the layers"
|
||||
assert get_api_current_physical_size() == filled_current_physical
|
||||
|
||||
for sk in env.safekeepers:
|
||||
sk.start()
|
||||
|
||||
# ensure that all the data is back
|
||||
pg_old = env.postgres.create_start(branch_name="main")
|
||||
with pg_old.cursor() as cur:
|
||||
assert query_scalar(cur, "select count(*) from testtab") == table_len
|
||||
@@ -14,7 +14,6 @@ from fixtures.neon_fixtures import (
|
||||
NeonEnvBuilder,
|
||||
PageserverApiException,
|
||||
RemoteStorageKind,
|
||||
assert_no_in_progress_downloads_for_tenant,
|
||||
available_remote_storages,
|
||||
wait_for_last_flush_lsn,
|
||||
wait_for_last_record_lsn,
|
||||
@@ -62,9 +61,9 @@ def test_remote_storage_backup_and_restore(
|
||||
neon_env_builder.pageserver_config_override = "test_remote_failures=1"
|
||||
|
||||
data_id = 1
|
||||
data_secret = "very secret secret"
|
||||
data = "just some data"
|
||||
|
||||
##### First start, insert secret data and upload it to the remote storage
|
||||
##### First start, insert data and upload it to the remote storage
|
||||
env = neon_env_builder.init_start()
|
||||
|
||||
# FIXME: Is this expected?
|
||||
@@ -97,8 +96,8 @@ def test_remote_storage_backup_and_restore(
|
||||
with pg.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE t{checkpoint_number}(id int primary key, secret text);
|
||||
INSERT INTO t{checkpoint_number} VALUES ({data_id}, '{data_secret}|{checkpoint_number}');
|
||||
CREATE TABLE t{checkpoint_number}(id int primary key, data text);
|
||||
INSERT INTO t{checkpoint_number} VALUES ({data_id}, '{data}|{checkpoint_number}');
|
||||
"""
|
||||
)
|
||||
current_lsn = Lsn(query_scalar(cur, "SELECT pg_current_wal_flush_lsn()"))
|
||||
@@ -133,36 +132,53 @@ def test_remote_storage_backup_and_restore(
|
||||
##### Second start, restore the data and ensure it's the same
|
||||
env.pageserver.start()
|
||||
|
||||
# Introduce failpoint in download
|
||||
pageserver_http.configure_failpoints(("remote-storage-download-pre-rename", "return"))
|
||||
|
||||
# Introduce failpoint in list remote timelines code path to make tenant_attach fail.
|
||||
# This is before the failures injected by test_remote_failures, so it's a permanent error.
|
||||
pageserver_http.configure_failpoints(("storage-sync-list-remote-timelines", "return"))
|
||||
env.pageserver.allowed_errors.append(
|
||||
".*error attaching tenant: storage-sync-list-remote-timelines",
|
||||
)
|
||||
# Attach it. This HTTP request will succeed and launch a
|
||||
# background task to load the tenant. In that background task,
|
||||
# listing the remote timelines will fail because of the failpoint,
|
||||
# and the tenant will be marked as Broken.
|
||||
client.tenant_attach(tenant_id)
|
||||
|
||||
# is there a better way to assert that failpoint triggered?
|
||||
wait_until_tenant_state(pageserver_http, tenant_id, "Broken", 15)
|
||||
|
||||
# assert cannot attach timeline that is scheduled for download
|
||||
# FIXME implement layer download retries
|
||||
# Ensure that even though the tenant is broken, we can't attach it again.
|
||||
with pytest.raises(Exception, match=f"tenant {tenant_id} already exists, state: Broken"):
|
||||
client.tenant_attach(tenant_id)
|
||||
|
||||
tenant_status = client.tenant_status(tenant_id)
|
||||
log.info("Tenant status with active failpoint: %s", tenant_status)
|
||||
# FIXME implement layer download retries
|
||||
# assert tenant_status["has_in_progress_downloads"] is True
|
||||
|
||||
# trigger temporary download files removal
|
||||
# Restart again, this implicitly clears the failpoint.
|
||||
# test_remote_failures=1 remains active, though, as it's in the pageserver config.
|
||||
# This means that any of the remote client operations after restart will exercise the
|
||||
# retry code path.
|
||||
#
|
||||
# The initiated attach operation should survive the restart, and continue from where it was.
|
||||
env.pageserver.stop()
|
||||
layer_download_failed_regex = (
|
||||
r"download.*[0-9A-F]+-[0-9A-F]+.*open a download stream for layer.*simulated failure"
|
||||
)
|
||||
assert not env.pageserver.log_contains(
|
||||
layer_download_failed_regex
|
||||
), "we shouldn't have tried any layer downloads yet since list remote timelines has a failpoint"
|
||||
env.pageserver.start()
|
||||
|
||||
# ensure that an initiated attach operation survives pageserver restart
|
||||
# Ensure that the pageserver remembers that the tenant was attaching, by
|
||||
# trying to attach it again. It should fail.
|
||||
with pytest.raises(Exception, match=f"tenant {tenant_id} already exists, state:"):
|
||||
client.tenant_attach(tenant_id)
|
||||
log.info("waiting for timeline redownload")
|
||||
log.info("waiting for tenant to become active. this should be quick with on-demand download")
|
||||
|
||||
def tenant_active():
|
||||
all_states = client.tenant_list()
|
||||
[tenant] = [t for t in all_states if TenantId(t["id"]) == tenant_id]
|
||||
assert tenant["state"] == "Active"
|
||||
|
||||
wait_until(
|
||||
number_of_iterations=20,
|
||||
number_of_iterations=5,
|
||||
interval=1,
|
||||
func=lambda: assert_no_in_progress_downloads_for_tenant(client, tenant_id),
|
||||
func=tenant_active,
|
||||
)
|
||||
|
||||
detail = client.timeline_detail(tenant_id, timeline_id)
|
||||
@@ -171,14 +187,18 @@ def test_remote_storage_backup_and_restore(
|
||||
Lsn(detail["last_record_lsn"]) >= current_lsn
|
||||
), "current db Lsn should should not be less than the one stored on remote storage"
|
||||
|
||||
log.info("select some data, this will cause layers to be downloaded")
|
||||
pg = env.postgres.create_start("main")
|
||||
with pg.cursor() as cur:
|
||||
for checkpoint_number in checkpoint_numbers:
|
||||
assert (
|
||||
query_scalar(cur, f"SELECT secret FROM t{checkpoint_number} WHERE id = {data_id};")
|
||||
== f"{data_secret}|{checkpoint_number}"
|
||||
query_scalar(cur, f"SELECT data FROM t{checkpoint_number} WHERE id = {data_id};")
|
||||
== f"{data}|{checkpoint_number}"
|
||||
)
|
||||
|
||||
log.info("ensure that we neede to retry downloads due to test_remote_failures=1")
|
||||
assert env.pageserver.log_contains(layer_download_failed_regex)
|
||||
|
||||
|
||||
# Exercises the upload queue retry code paths.
|
||||
# - Use failpoints to cause all storage ops to fail
|
||||
@@ -338,7 +358,6 @@ def test_remote_storage_upload_queue_retries(
|
||||
def tenant_active():
|
||||
all_states = client.tenant_list()
|
||||
[tenant] = [t for t in all_states if TenantId(t["id"]) == tenant_id]
|
||||
assert tenant["has_in_progress_downloads"] is False
|
||||
assert tenant["state"] == "Active"
|
||||
|
||||
wait_until(30, 1, tenant_active)
|
||||
|
||||
@@ -13,12 +13,15 @@ from fixtures.neon_fixtures import (
|
||||
PageserverHttpClient,
|
||||
PortDistributor,
|
||||
Postgres,
|
||||
assert_no_in_progress_downloads_for_tenant,
|
||||
assert_tenant_status,
|
||||
tenant_exists,
|
||||
wait_for_last_record_lsn,
|
||||
wait_for_upload,
|
||||
wait_until,
|
||||
wait_while,
|
||||
)
|
||||
from fixtures.types import Lsn, TenantId, TimelineId
|
||||
from fixtures.utils import query_scalar, start_in_background, subprocess_capture, wait_until
|
||||
from fixtures.utils import query_scalar, start_in_background, subprocess_capture
|
||||
|
||||
|
||||
def assert_abs_margin_ratio(a: float, b: float, margin_ratio: float):
|
||||
@@ -406,17 +409,13 @@ def test_tenant_relocation(
|
||||
# call to attach timeline to new pageserver
|
||||
new_pageserver_http.tenant_attach(tenant_id)
|
||||
|
||||
# check that it shows that download is in progress
|
||||
# wait for tenant to finish attaching
|
||||
tenant_status = new_pageserver_http.tenant_status(tenant_id=tenant_id)
|
||||
assert tenant_status.get("has_in_progress_downloads"), tenant_status
|
||||
|
||||
# wait until tenant is downloaded
|
||||
assert tenant_status["state"] in ["Attaching", "Active"]
|
||||
wait_until(
|
||||
number_of_iterations=10,
|
||||
interval=1,
|
||||
func=lambda: assert_no_in_progress_downloads_for_tenant(
|
||||
new_pageserver_http, tenant_id
|
||||
),
|
||||
func=lambda: assert_tenant_status(new_pageserver_http, tenant_id, "Active"),
|
||||
)
|
||||
|
||||
check_timeline_attached(
|
||||
@@ -459,9 +458,15 @@ def test_tenant_relocation(
|
||||
|
||||
# detach tenant from old pageserver before we check
|
||||
# that all the data is there to be sure that old pageserver
|
||||
# is no longer involved, and if it is, we will see the errors
|
||||
# is no longer involved, and if it is, we will see the error
|
||||
pageserver_http.tenant_detach(tenant_id)
|
||||
|
||||
# Wait a little, so that the detach operation has time to finish.
|
||||
wait_while(
|
||||
number_of_iterations=100,
|
||||
interval=1,
|
||||
func=lambda: tenant_exists(pageserver_http, tenant_id),
|
||||
)
|
||||
post_migration_check(pg_main, 500500, old_local_path_main)
|
||||
post_migration_check(pg_second, 1001000, old_local_path_second)
|
||||
|
||||
|
||||
@@ -20,44 +20,48 @@ def test_tenant_tasks(neon_env_builder: NeonEnvBuilder):
|
||||
matching = [t for t in all_states if TenantId(t["id"]) == tenant]
|
||||
return get_only_element(matching)["state"]
|
||||
|
||||
def get_metric_value(name):
|
||||
metrics = client.get_metrics()
|
||||
relevant = [line for line in metrics.splitlines() if line.startswith(name)]
|
||||
if len(relevant) == 0:
|
||||
return 0
|
||||
line = get_only_element(relevant)
|
||||
value = line.lstrip(name).strip()
|
||||
return int(value)
|
||||
|
||||
def delete_all_timelines(tenant: TenantId):
|
||||
timelines = [TimelineId(t["timeline_id"]) for t in client.timeline_list(tenant)]
|
||||
for t in timelines:
|
||||
client.timeline_delete(tenant, t)
|
||||
|
||||
def assert_active(tenant):
|
||||
assert get_state(tenant) == "Active"
|
||||
|
||||
# Create tenant, start compute
|
||||
tenant, _ = env.neon_cli.create_tenant()
|
||||
env.neon_cli.create_timeline(name, tenant_id=tenant)
|
||||
pg = env.postgres.create_start(name, tenant_id=tenant)
|
||||
assert (
|
||||
get_state(tenant) == "Active"
|
||||
), "Pageserver should activate a tenant and start background jobs if timelines are loaded"
|
||||
|
||||
# Stop compute
|
||||
pg.stop()
|
||||
|
||||
# Delete all timelines on all tenants
|
||||
# Delete all timelines on all tenants.
|
||||
#
|
||||
# FIXME: we used to check that the background jobs are stopped when all timelines
|
||||
# are removed, but we don't stop them anymore. Not sure if this test still makes sense
|
||||
# or we should just remove it.
|
||||
for tenant_info in client.tenant_list():
|
||||
tenant_id = TenantId(tenant_info["id"])
|
||||
delete_all_timelines(tenant_id)
|
||||
wait_until(10, 0.2, lambda: assert_active(tenant_id))
|
||||
|
||||
# Assert that all tasks finish quickly after tenant is detached
|
||||
assert get_metric_value('pageserver_tenant_task_events{event="start"}') > 0
|
||||
task_starts = client.get_metric_value('pageserver_tenant_task_events{event="start"}')
|
||||
assert task_starts is not None
|
||||
assert int(task_starts) > 0
|
||||
client.tenant_detach(tenant)
|
||||
client.tenant_detach(env.initial_tenant)
|
||||
|
||||
def assert_tasks_finish():
|
||||
tasks_started = get_metric_value('pageserver_tenant_task_events{event="start"}')
|
||||
tasks_ended = get_metric_value('pageserver_tenant_task_events{event="stop"}')
|
||||
tasks_panicked = get_metric_value('pageserver_tenant_task_events{event="panic"}')
|
||||
tasks_started = client.get_metric_value('pageserver_tenant_task_events{event="start"}')
|
||||
tasks_ended = client.get_metric_value('pageserver_tenant_task_events{event="stop"}')
|
||||
tasks_panicked = client.get_metric_value('pageserver_tenant_task_events{event="panic"}')
|
||||
log.info(f"started {tasks_started}, ended {tasks_ended}, panicked {tasks_panicked}")
|
||||
assert tasks_started == tasks_ended
|
||||
assert tasks_panicked == 0
|
||||
assert tasks_panicked is None or int(tasks_panicked) == 0
|
||||
|
||||
wait_until(10, 0.2, assert_tasks_finish)
|
||||
|
||||
@@ -21,7 +21,7 @@ from fixtures.neon_fixtures import (
|
||||
NeonEnvBuilder,
|
||||
Postgres,
|
||||
RemoteStorageKind,
|
||||
assert_no_in_progress_downloads_for_tenant,
|
||||
assert_tenant_status,
|
||||
available_remote_storages,
|
||||
wait_for_last_record_lsn,
|
||||
wait_for_sk_commit_lsn_to_reach_remote_storage,
|
||||
@@ -179,14 +179,6 @@ def test_tenants_attached_after_download(
|
||||
tenant_id, timeline_id, env.safekeepers, env.pageserver
|
||||
)
|
||||
|
||||
detail_before = client.timeline_detail(
|
||||
tenant_id, timeline_id, include_non_incremental_physical_size=True
|
||||
)
|
||||
assert (
|
||||
detail_before["current_physical_size_non_incremental"]
|
||||
== detail_before["current_physical_size"]
|
||||
)
|
||||
|
||||
env.pageserver.stop()
|
||||
|
||||
timeline_dir = Path(env.repo_dir) / "tenants" / str(tenant_id) / "timelines" / str(timeline_id)
|
||||
@@ -200,13 +192,16 @@ def test_tenants_attached_after_download(
|
||||
assert local_layer_deleted, f"Found no local layer files to delete in directory {timeline_dir}"
|
||||
|
||||
##### Start the pageserver, forcing it to download the layer file and load the timeline into memory
|
||||
# FIXME: just starting the pageserver no longer downloads the
|
||||
# layer files. Do we want to force download, or maybe run some
|
||||
# queries, or is it enough that it starts up without layer files?
|
||||
env.pageserver.start()
|
||||
client = env.pageserver.http_client()
|
||||
|
||||
wait_until(
|
||||
number_of_iterations=5,
|
||||
interval=1,
|
||||
func=lambda: assert_no_in_progress_downloads_for_tenant(client, tenant_id),
|
||||
func=lambda: assert_tenant_status(client, tenant_id, "Active"),
|
||||
)
|
||||
|
||||
restored_timelines = client.timeline_list(tenant_id)
|
||||
@@ -218,12 +213,6 @@ def test_tenants_attached_after_download(
|
||||
timeline_id
|
||||
), f"Tenant {tenant_id} should have its old timeline {timeline_id} restored from the remote storage"
|
||||
|
||||
# Check that the physical size matches after re-downloading
|
||||
detail_after = client.timeline_detail(
|
||||
tenant_id, timeline_id, include_non_incremental_physical_size=True
|
||||
)
|
||||
assert detail_before["current_physical_size"] == detail_after["current_physical_size"]
|
||||
|
||||
# Check that we had to retry the downloads
|
||||
assert env.pageserver.log_contains(".*download .* succeeded after 1 retries.*")
|
||||
|
||||
@@ -297,7 +286,7 @@ def test_tenant_upgrades_index_json_from_v0(
|
||||
wait_until(
|
||||
number_of_iterations=5,
|
||||
interval=1,
|
||||
func=lambda: assert_no_in_progress_downloads_for_tenant(pageserver_http, tenant_id),
|
||||
func=lambda: assert_tenant_status(pageserver_http, tenant_id, "Active"),
|
||||
)
|
||||
|
||||
pg = env.postgres.create_start("main")
|
||||
@@ -404,7 +393,7 @@ def test_tenant_ignores_backup_file(
|
||||
wait_until(
|
||||
number_of_iterations=5,
|
||||
interval=1,
|
||||
func=lambda: assert_no_in_progress_downloads_for_tenant(pageserver_http, tenant_id),
|
||||
func=lambda: assert_tenant_status(pageserver_http, tenant_id, "Active"),
|
||||
)
|
||||
|
||||
pg = env.postgres.create_start("main")
|
||||
@@ -484,14 +473,15 @@ def test_tenant_redownloads_truncated_file_on_startup(
|
||||
index_part = local_fs_index_part(env, tenant_id, timeline_id)
|
||||
assert index_part["layer_metadata"][path.name]["file_size"] == expected_size
|
||||
|
||||
##### Start the pageserver, forcing it to download the layer file and load the timeline into memory
|
||||
## Start the pageserver. It will notice that the file size doesn't match, and
|
||||
## rename away the local file. It will be re-downloaded when it's needed.
|
||||
env.pageserver.start()
|
||||
client = env.pageserver.http_client()
|
||||
|
||||
wait_until(
|
||||
number_of_iterations=5,
|
||||
interval=1,
|
||||
func=lambda: assert_no_in_progress_downloads_for_tenant(client, tenant_id),
|
||||
func=lambda: assert_tenant_status(client, tenant_id, "Active"),
|
||||
)
|
||||
|
||||
restored_timelines = client.timeline_list(tenant_id)
|
||||
@@ -503,6 +493,10 @@ def test_tenant_redownloads_truncated_file_on_startup(
|
||||
timeline_id
|
||||
), f"Tenant {tenant_id} should have its old timeline {timeline_id} restored from the remote storage"
|
||||
|
||||
# Request non-incremental logical size. Calculating it needs the layer file that
|
||||
# we corrupted, forcing it to be redownloaded.
|
||||
client.timeline_detail(tenant_id, timeline_id, include_non_incremental_logical_size=True)
|
||||
|
||||
assert os.stat(path).st_size == expected_size, "truncated layer should had been re-downloaded"
|
||||
|
||||
# the remote side of local_layer_truncated
|
||||
|
||||
@@ -20,10 +20,12 @@ from fixtures.neon_fixtures import (
|
||||
PortDistributor,
|
||||
Postgres,
|
||||
VanillaPostgres,
|
||||
assert_tenant_status,
|
||||
wait_for_last_flush_lsn,
|
||||
wait_until,
|
||||
)
|
||||
from fixtures.types import TenantId, TimelineId
|
||||
from fixtures.utils import get_timeline_dir_size, wait_until
|
||||
from fixtures.utils import get_timeline_dir_size
|
||||
|
||||
|
||||
def test_timeline_size(neon_simple_env: NeonEnv):
|
||||
@@ -320,7 +322,17 @@ def test_timeline_physical_size_init(neon_simple_env: NeonEnv):
|
||||
env.pageserver.stop()
|
||||
env.pageserver.start()
|
||||
|
||||
assert_physical_size(env, env.initial_tenant, new_timeline_id)
|
||||
# Wait for the tenant to be loaded
|
||||
client = env.pageserver.http_client()
|
||||
wait_until(
|
||||
number_of_iterations=5,
|
||||
interval=1,
|
||||
func=lambda: assert_tenant_status(client, env.initial_tenant, "Active"),
|
||||
)
|
||||
|
||||
assert_physical_size_invariants(
|
||||
get_physical_size_values(env, env.initial_tenant, new_timeline_id)
|
||||
)
|
||||
|
||||
|
||||
def test_timeline_physical_size_post_checkpoint(neon_simple_env: NeonEnv):
|
||||
@@ -341,7 +353,9 @@ def test_timeline_physical_size_post_checkpoint(neon_simple_env: NeonEnv):
|
||||
wait_for_last_flush_lsn(env, pg, env.initial_tenant, new_timeline_id)
|
||||
pageserver_http.timeline_checkpoint(env.initial_tenant, new_timeline_id)
|
||||
|
||||
assert_physical_size(env, env.initial_tenant, new_timeline_id)
|
||||
assert_physical_size_invariants(
|
||||
get_physical_size_values(env, env.initial_tenant, new_timeline_id)
|
||||
)
|
||||
|
||||
|
||||
def test_timeline_physical_size_post_compaction(neon_env_builder: NeonEnvBuilder):
|
||||
@@ -376,7 +390,9 @@ def test_timeline_physical_size_post_compaction(neon_env_builder: NeonEnvBuilder
|
||||
pageserver_http.timeline_checkpoint(env.initial_tenant, new_timeline_id)
|
||||
pageserver_http.timeline_compact(env.initial_tenant, new_timeline_id)
|
||||
|
||||
assert_physical_size(env, env.initial_tenant, new_timeline_id)
|
||||
assert_physical_size_invariants(
|
||||
get_physical_size_values(env, env.initial_tenant, new_timeline_id)
|
||||
)
|
||||
|
||||
|
||||
def test_timeline_physical_size_post_gc(neon_env_builder: NeonEnvBuilder):
|
||||
@@ -415,7 +431,9 @@ def test_timeline_physical_size_post_gc(neon_env_builder: NeonEnvBuilder):
|
||||
pageserver_http.timeline_checkpoint(env.initial_tenant, new_timeline_id)
|
||||
pageserver_http.timeline_gc(env.initial_tenant, new_timeline_id, gc_horizon=None)
|
||||
|
||||
assert_physical_size(env, env.initial_tenant, new_timeline_id)
|
||||
assert_physical_size_invariants(
|
||||
get_physical_size_values(env, env.initial_tenant, new_timeline_id)
|
||||
)
|
||||
|
||||
|
||||
# The timeline logical and physical sizes are also exposed as prometheus metrics.
|
||||
@@ -448,7 +466,7 @@ def test_timeline_size_metrics(
|
||||
# get the metrics and parse the metric for the current timeline's physical size
|
||||
metrics = env.pageserver.http_client().get_metrics()
|
||||
matches = re.search(
|
||||
f'^pageserver_current_physical_size{{tenant_id="{env.initial_tenant}",timeline_id="{new_timeline_id}"}} (\\S+)$',
|
||||
f'^pageserver_resident_physical_size{{tenant_id="{env.initial_tenant}",timeline_id="{new_timeline_id}"}} (\\S+)$',
|
||||
metrics,
|
||||
re.MULTILINE,
|
||||
)
|
||||
@@ -507,11 +525,12 @@ def test_tenant_physical_size(neon_simple_env: NeonEnv):
|
||||
|
||||
tenant, timeline = env.neon_cli.create_tenant()
|
||||
|
||||
def get_timeline_physical_size(timeline: TimelineId):
|
||||
res = client.timeline_detail(tenant, timeline, include_non_incremental_physical_size=True)
|
||||
return res["current_physical_size_non_incremental"]
|
||||
def get_timeline_resident_physical_size(timeline: TimelineId):
|
||||
sizes = get_physical_size_values(env, tenant, timeline)
|
||||
assert_physical_size_invariants(sizes)
|
||||
return sizes.prometheus_resident_physical
|
||||
|
||||
timeline_total_size = get_timeline_physical_size(timeline)
|
||||
timeline_total_resident_physical_size = get_timeline_resident_physical_size(timeline)
|
||||
for i in range(10):
|
||||
n_rows = random.randint(100, 1000)
|
||||
|
||||
@@ -528,22 +547,54 @@ def test_tenant_physical_size(neon_simple_env: NeonEnv):
|
||||
wait_for_last_flush_lsn(env, pg, tenant, timeline)
|
||||
pageserver_http.timeline_checkpoint(tenant, timeline)
|
||||
|
||||
timeline_total_size += get_timeline_physical_size(timeline)
|
||||
timeline_total_resident_physical_size += get_timeline_resident_physical_size(timeline)
|
||||
|
||||
pg.stop()
|
||||
|
||||
tenant_physical_size = int(client.tenant_status(tenant_id=tenant)["current_physical_size"])
|
||||
assert tenant_physical_size == timeline_total_size
|
||||
# ensure that tenant_status current_physical size reports sum of timeline current_physical_size
|
||||
tenant_current_physical_size = int(
|
||||
client.tenant_status(tenant_id=tenant)["current_physical_size"]
|
||||
)
|
||||
assert tenant_current_physical_size == sum(
|
||||
[tl["current_physical_size"] for tl in client.timeline_list(tenant_id=tenant)]
|
||||
)
|
||||
# since we don't do layer eviction, current_physical_size is identical to resident physical size
|
||||
assert timeline_total_resident_physical_size == tenant_current_physical_size
|
||||
|
||||
|
||||
def assert_physical_size(env: NeonEnv, tenant_id: TenantId, timeline_id: TimelineId):
|
||||
"""Check the current physical size returned from timeline API
|
||||
matches the total physical size of the timeline on disk"""
|
||||
class TimelinePhysicalSizeValues:
|
||||
api_current_physical: int
|
||||
prometheus_resident_physical: int
|
||||
python_timelinedir_layerfiles_physical: int
|
||||
|
||||
|
||||
def get_physical_size_values(
|
||||
env: NeonEnv, tenant_id: TenantId, timeline_id: TimelineId
|
||||
) -> TimelinePhysicalSizeValues:
|
||||
res = TimelinePhysicalSizeValues()
|
||||
|
||||
client = env.pageserver.http_client()
|
||||
res = client.timeline_detail(tenant_id, timeline_id, include_non_incremental_physical_size=True)
|
||||
|
||||
res.prometheus_resident_physical = client.get_timeline_metric(
|
||||
tenant_id, timeline_id, "pageserver_resident_physical_size"
|
||||
)
|
||||
|
||||
detail = client.timeline_detail(
|
||||
tenant_id, timeline_id, include_timeline_dir_layer_file_size_sum=True
|
||||
)
|
||||
res.api_current_physical = detail["current_physical_size"]
|
||||
|
||||
timeline_path = env.timeline_dir(tenant_id, timeline_id)
|
||||
assert res["current_physical_size"] == res["current_physical_size_non_incremental"]
|
||||
assert res["current_physical_size"] == get_timeline_dir_size(timeline_path)
|
||||
res.python_timelinedir_layerfiles_physical = get_timeline_dir_size(timeline_path)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def assert_physical_size_invariants(sizes: TimelinePhysicalSizeValues):
|
||||
# resident phyiscal size is defined as
|
||||
assert sizes.python_timelinedir_layerfiles_physical == sizes.prometheus_resident_physical
|
||||
# we don't do layer eviction, so, all layers are resident
|
||||
assert sizes.api_current_physical == sizes.prometheus_resident_physical
|
||||
|
||||
|
||||
# Timeline logical size initialization is an asynchronous background task that runs once,
|
||||
|
||||
@@ -585,17 +585,23 @@ def test_s3_wal_replay(neon_env_builder: NeonEnvBuilder, remote_storage_kind: Re
|
||||
if elapsed > wait_lsn_timeout:
|
||||
raise RuntimeError("Timed out waiting for WAL redo")
|
||||
|
||||
pageserver_lsn = Lsn(
|
||||
env.pageserver.http_client().timeline_detail(tenant_id, timeline_id)["last_record_lsn"]
|
||||
)
|
||||
lag = last_lsn - pageserver_lsn
|
||||
tenant_status = ps_cli.tenant_status(tenant_id)
|
||||
if tenant_status["state"] == "Loading":
|
||||
log.debug(f"Tenant {tenant_id} is still loading, retrying")
|
||||
else:
|
||||
pageserver_lsn = Lsn(
|
||||
env.pageserver.http_client().timeline_detail(tenant_id, timeline_id)[
|
||||
"last_record_lsn"
|
||||
]
|
||||
)
|
||||
lag = last_lsn - pageserver_lsn
|
||||
|
||||
if time.time() > last_debug_print + 10 or lag <= 0:
|
||||
last_debug_print = time.time()
|
||||
log.info(f"Pageserver last_record_lsn={pageserver_lsn}; lag is {lag / 1024}kb")
|
||||
if time.time() > last_debug_print + 10 or lag <= 0:
|
||||
last_debug_print = time.time()
|
||||
log.info(f"Pageserver last_record_lsn={pageserver_lsn}; lag is {lag / 1024}kb")
|
||||
|
||||
if lag <= 0:
|
||||
break
|
||||
if lag <= 0:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user