feat: support lazy, queued tenant attaches (#6907)

Add off-by-default support for lazy queued tenant activation on attach.
This should be useful on bulk migrations as some tenants will be
activated faster due to operations or endpoint startup. Eventually all
tenants will get activated by reusing the same mechanism we have at
startup (`PageserverConf::concurrent_tenant_warmup`).

The difference to lazy attached tenants to startup ones is that we leave
their initial logical size calculation be triggered by WalReceiver or
consumption metrics.

Fixes: #6315

Co-authored-by: Arpad Müller <arpad-m@users.noreply.github.com>
This commit is contained in:
Joonas Koivunen
2024-02-29 13:26:29 +02:00
committed by GitHub
parent d04af08567
commit 4d426f6fbe
8 changed files with 255 additions and 73 deletions

View File

@@ -286,7 +286,11 @@ class PageserverHttpClient(requests.Session, MetricsGetter):
self.verbose_error(res)
def tenant_location_conf(
self, tenant_id: Union[TenantId, TenantShardId], location_conf=dict[str, Any], flush_ms=None
self,
tenant_id: Union[TenantId, TenantShardId],
location_conf=dict[str, Any],
flush_ms=None,
lazy: Optional[bool] = None,
):
body = location_conf.copy()
body["tenant_id"] = str(tenant_id)
@@ -295,6 +299,9 @@ class PageserverHttpClient(requests.Session, MetricsGetter):
if flush_ms is not None:
params["flush_ms"] = str(flush_ms)
if lazy is not None:
params["lazy"] = "true" if lazy else "false"
res = self.put(
f"http://localhost:{self.port}/v1/tenant/{tenant_id}/location_config",
json=body,

View File

@@ -2,6 +2,7 @@ import concurrent.futures
import math
import random
import time
from collections import defaultdict
from contextlib import closing
from pathlib import Path
from typing import Optional
@@ -14,6 +15,7 @@ from fixtures.neon_fixtures import (
Endpoint,
NeonEnv,
NeonEnvBuilder,
NeonPageserver,
PgBin,
VanillaPostgres,
wait_for_last_flush_lsn,
@@ -839,22 +841,40 @@ def test_ondemand_activation(neon_env_builder: NeonEnvBuilder):
)
# Deleting a stuck tenant should prompt it to go active
# in some cases, it has already been activated because it's behind the detach
delete_lazy_activating(delete_tenant_id, env.pageserver, expect_attaching=False)
tenant_ids.remove(delete_tenant_id)
# Check that all the stuck tenants proceed to active (apart from the one that deletes, and the one
# we detached)
wait_until(10, 1, all_active)
assert len(get_tenant_states()) == n_tenants - 2
def delete_lazy_activating(
delete_tenant_id: TenantId, pageserver: NeonPageserver, expect_attaching: bool
):
pageserver_http = pageserver.http_client()
# Deletion itself won't complete due to our failpoint: Tenant::shutdown can't complete while calculating
# logical size is paused in a failpoint. So instead we will use a log observation to check that
# on-demand activation was triggered by the tenant deletion
log_match = f".*attach{{tenant_id={delete_tenant_id} shard_id=0000 gen=[0-9a-f]+}}: Activating tenant \\(on-demand\\).*"
if expect_attaching:
assert pageserver_http.tenant_status(delete_tenant_id)["state"]["slug"] == "Attaching"
with concurrent.futures.ThreadPoolExecutor() as executor:
log.info("Starting background delete")
def activated_on_demand():
assert pageserver.log_contains(log_match) is not None
def delete_tenant():
env.pageserver.http_client().tenant_delete(delete_tenant_id)
pageserver_http.tenant_delete(delete_tenant_id)
background_delete = executor.submit(delete_tenant)
# Deletion itself won't complete due to our failpoint: Tenant::shutdown can't complete while calculating
# logical size is paused in a failpoint. So instead we will use a log observation to check that
# on-demand activation was triggered by the tenant deletion
log_match = f".*attach{{tenant_id={delete_tenant_id} shard_id=0000 gen=[0-9a-f]+}}: Activating tenant \\(on-demand\\).*"
def activated_on_demand():
assert env.pageserver.log_contains(log_match) is not None
log.info(f"Waiting for activation message '{log_match}'")
try:
wait_until(10, 1, activated_on_demand)
@@ -868,12 +888,6 @@ def test_ondemand_activation(neon_env_builder: NeonEnvBuilder):
# Poll for deletion to complete
wait_tenant_status_404(pageserver_http, tenant_id=delete_tenant_id, iterations=40)
tenant_ids.remove(delete_tenant_id)
# Check that all the stuck tenants proceed to active (apart from the one that deletes, and the one
# we detached)
wait_until(10, 1, all_active)
assert len(get_tenant_states()) == n_tenants - 2
def test_timeline_logical_size_task_priority(neon_env_builder: NeonEnvBuilder):
@@ -939,3 +953,159 @@ def test_timeline_logical_size_task_priority(neon_env_builder: NeonEnvBuilder):
client.configure_failpoints(
[("initial-size-calculation-permit-pause", "off"), ("walreceiver-after-ingest", "off")]
)
def test_eager_attach_does_not_queue_up(neon_env_builder: NeonEnvBuilder):
neon_env_builder.pageserver_config_override = "concurrent_tenant_warmup = '1'"
env = neon_env_builder.init_start()
# the supporting_second does nothing except queue behind env.initial_tenant
# for purposes of showing that eager_tenant breezes past the queue
supporting_second, _ = env.neon_cli.create_tenant()
eager_tenant, _ = env.neon_cli.create_tenant()
client = env.pageserver.http_client()
client.tenant_location_conf(
eager_tenant,
{
"mode": "Detached",
"secondary_conf": None,
"tenant_conf": {},
"generation": None,
},
)
env.pageserver.stop()
# pause at logical size calculation, also pause before walreceiver can give feedback so it will give priority to logical size calculation
env.pageserver.start(
extra_env_vars={
"FAILPOINTS": "timeline-calculate-logical-size-pause=pause;walreceiver-after-ingest=pause"
}
)
tenant_ids = [env.initial_tenant, supporting_second]
def get_tenant_states() -> dict[str, list[TenantId]]:
states = defaultdict(list)
for id in tenant_ids:
state = client.tenant_status(id)["state"]["slug"]
states[state].append(id)
return dict(states)
def one_is_active():
states = get_tenant_states()
log.info(f"{states}")
assert len(states["Active"]) == 1
wait_until(10, 1, one_is_active)
def other_is_attaching():
states = get_tenant_states()
assert len(states["Attaching"]) == 1
wait_until(10, 1, other_is_attaching)
def eager_tenant_is_active():
resp = client.tenant_status(eager_tenant)
assert resp["state"]["slug"] == "Active"
gen = env.attachment_service.attach_hook_issue(eager_tenant, env.pageserver.id)
client.tenant_location_conf(
eager_tenant,
{
"mode": "AttachedSingle",
"secondary_conf": None,
"tenant_conf": {},
"generation": gen,
},
lazy=False,
)
wait_until(10, 1, eager_tenant_is_active)
other_is_attaching()
client.configure_failpoints(
[("timeline-calculate-logical-size-pause", "off"), ("walreceiver-after-ingest", "off")]
)
@pytest.mark.parametrize("activation_method", ["endpoint", "branch", "delete"])
def test_lazy_attach_activation(neon_env_builder: NeonEnvBuilder, activation_method: str):
# env.initial_tenant will take up this permit when attaching with lazy because of a failpoint activated after restart
neon_env_builder.pageserver_config_override = "concurrent_tenant_warmup = '1'"
env = neon_env_builder.init_start()
# because this returns (also elsewhere in this file), we know that SpawnMode::Create skips the queue
lazy_tenant, _ = env.neon_cli.create_tenant()
client = env.pageserver.http_client()
client.tenant_location_conf(
lazy_tenant,
{
"mode": "Detached",
"secondary_conf": None,
"tenant_conf": {},
"generation": None,
},
)
env.pageserver.stop()
# pause at logical size calculation, also pause before walreceiver can give feedback so it will give priority to logical size calculation
env.pageserver.start(
extra_env_vars={
"FAILPOINTS": "timeline-calculate-logical-size-pause=pause;walreceiver-after-ingest=pause"
}
)
def initial_tenant_is_active():
resp = client.tenant_status(env.initial_tenant)
assert resp["state"]["slug"] == "Active"
wait_until(10, 1, initial_tenant_is_active)
# even though the initial tenant is now active, because it was startup time
# attach, it will consume the only permit because logical size calculation
# is paused.
gen = env.attachment_service.attach_hook_issue(lazy_tenant, env.pageserver.id)
client.tenant_location_conf(
lazy_tenant,
{
"mode": "AttachedSingle",
"secondary_conf": None,
"tenant_conf": {},
"generation": gen,
},
lazy=True,
)
def lazy_tenant_is_attaching():
resp = client.tenant_status(lazy_tenant)
assert resp["state"]["slug"] == "Attaching"
# paused logical size calculation of env.initial_tenant is keeping it attaching
wait_until(10, 1, lazy_tenant_is_attaching)
for _ in range(5):
lazy_tenant_is_attaching()
time.sleep(0.5)
def lazy_tenant_is_active():
resp = client.tenant_status(lazy_tenant)
assert resp["state"]["slug"] == "Active"
if activation_method == "endpoint":
with env.endpoints.create_start("main", tenant_id=lazy_tenant):
# starting up the endpoint should make it jump the queue
wait_until(10, 1, lazy_tenant_is_active)
elif activation_method == "branch":
env.neon_cli.create_timeline("second_branch", lazy_tenant)
wait_until(10, 1, lazy_tenant_is_active)
elif activation_method == "delete":
delete_lazy_activating(lazy_tenant, env.pageserver, expect_attaching=True)
else:
raise RuntimeError(activation_method)