Build 060.28: implement Persistent Checkpoint and Startup Recovery

This commit is contained in:
2026-08-01 20:55:32 +03:00
parent 58e5a12a4d
commit 8c485e32b1
63 changed files with 12430 additions and 92 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import ipaddress
import math
import re
import time
from collections.abc import Mapping
@@ -177,28 +178,26 @@ def release_postgres_test_lock(
def count_other_test_connections(
connection: psycopg.Connection[Any],
) -> int:
"""Посчитать оставшиеся соединения стенда и пула с тестовой базой."""
"""Посчитать клиентские соединения с проверенной тестовой БД."""
database_name = _validated_postgres_test_control_database_name(connection)
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT COUNT(*)
FROM pg_stat_activity
WHERE datname = current_database()
AND application_name = %s
FROM pg_catalog.pg_stat_activity
WHERE datname = %s
AND backend_type = 'client backend'
AND application_name IS DISTINCT FROM %s
""",
(POSTGRES_TEST_APPLICATION_NAME,),
(database_name, POSTGRES_TEST_CONTROL_APPLICATION_NAME),
)
row = cursor.fetchone()
if (
not isinstance(row, tuple)
or len(row) != 1
or isinstance(row[0], bool)
or not isinstance(row[0], int)
):
raise RuntimeError("PostgreSQL returned an invalid connection count")
return row[0]
return _validated_postgres_count(
row,
error_message="PostgreSQL returned an invalid connection count",
)
def wait_for_postgres_advisory_lock_waiters(
@@ -260,6 +259,138 @@ def wait_for_postgres_advisory_lock_waiters(
time.sleep(0.01)
def wait_for_postgres_relation_lock_waiters(
connection: psycopg.Connection[Any],
*,
relation_name: str,
expected_count: int,
timeout_seconds: float = 5.0,
) -> None:
"""Дождаться точного числа ожидающих блокировку заданной таблицы."""
if not isinstance(relation_name, str) or not relation_name.strip():
raise ValueError("relation_name must be a non-empty string")
if (
isinstance(expected_count, bool)
or not isinstance(expected_count, int)
or expected_count <= 0
):
raise ValueError("expected_count must be a positive integer")
if (
isinstance(timeout_seconds, bool)
or not isinstance(timeout_seconds, (int, float))
or not math.isfinite(float(timeout_seconds))
or timeout_seconds <= 0
):
raise ValueError("timeout_seconds must be a positive finite number")
database_name = _validated_postgres_test_control_database_name(connection)
with connection.cursor() as cursor:
cursor.execute(
"SELECT pg_catalog.to_regclass(%s)::oid",
(relation_name,),
)
relation_row = cursor.fetchone()
if (
not isinstance(relation_row, tuple)
or len(relation_row) != 1
or isinstance(relation_row[0], bool)
or not isinstance(relation_row[0], int)
or relation_row[0] <= 0
):
raise RuntimeError(
"PostgreSQL did not resolve the requested test relation"
)
relation_oid = relation_row[0]
deadline = time.monotonic() + timeout_seconds
while True:
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT COUNT(*)
FROM pg_catalog.pg_locks
WHERE locktype = 'relation'
AND database = (
SELECT oid
FROM pg_catalog.pg_database
WHERE datname = %s
)
AND relation = %s
AND NOT granted
""",
(database_name, relation_oid),
)
row = cursor.fetchone()
observed_count = _validated_postgres_count(
row,
error_message=(
"PostgreSQL returned an invalid relation-lock waiter count"
),
)
if observed_count == expected_count:
return
if time.monotonic() >= deadline:
raise TimeoutError(
"PostgreSQL did not observe all relation-lock callers; "
f"expected {expected_count}, observed {observed_count}."
)
time.sleep(0.01)
def _validated_postgres_test_control_database_name(
connection: psycopg.Connection[Any],
) -> str:
"""Повторно подтвердить безопасную БД и управляющее соединение."""
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT current_database(), current_setting('application_name')
"""
)
identity = cursor.fetchone()
if (
not isinstance(identity, tuple)
or len(identity) != 2
or not isinstance(identity[0], str)
or not _SAFE_DATABASE_NAME.fullmatch(identity[0])
or identity[1] != POSTGRES_TEST_CONTROL_APPLICATION_NAME
):
raise RuntimeError(
"PostgreSQL connection is not the validated test control "
"connection."
)
return identity[0]
def _validated_postgres_count(
row: object,
*,
error_message: str,
) -> int:
"""Проверить форму и тип результата PostgreSQL COUNT(*)."""
if (
not isinstance(row, tuple)
or len(row) != 1
or isinstance(row[0], bool)
or not isinstance(row[0], int)
or row[0] < 0
):
raise RuntimeError(error_message)
return row[0]
def _validate_local_endpoint(parameters: Mapping[str, object]) -> None:
service = str(parameters.get("service", "")).strip()
host = str(parameters.get("host", "")).strip()

View File

@@ -30,12 +30,17 @@ RUNTIME_CLEANUP_TIMEOUT_SECONDS = 5.0
OWNED_TASK_NAMES = frozenset(
{
"application-shutdown",
"market-data-storage-shutdown",
"market-data-storage-startup",
"telegram-polling",
"trade-stream-market-processing",
"trade-stream-receive",
"trade-stream-runtime",
"trade-stream-runtime-recovery",
"trade-stream-scheduler",
"trade-stream-startup-recovery",
"trade-stream-state-hydration",
"trade-stream-startup",
}
)
@@ -173,7 +178,10 @@ def active_owned_task_names() -> tuple[str, ...]:
for task in asyncio.all_tasks()
if task is not current_task
and not task.done()
and task.get_name() in OWNED_TASK_NAMES
and (
task.get_name() in OWNED_TASK_NAMES
or task.get_name().startswith("persistent-application-")
)
)
)