d
This commit is contained in:
@@ -40,8 +40,12 @@ class Settings(BaseSettings):
|
||||
sync_target_region: str = "overseas"
|
||||
sync_target_api_base_url: str = ""
|
||||
sync_shared_token: str = ""
|
||||
sync_batch_size: int = 200
|
||||
sync_poll_interval_seconds: int = 30
|
||||
sync_batch_size: int = 5000
|
||||
sync_poll_interval_seconds: int = 2
|
||||
sync_pipeline_process_limit: int = 5000
|
||||
sync_pull_max_pending_items: int = 0
|
||||
sync_pull_max_register_pending_items: int = 0
|
||||
sync_pull_max_downstream_pending_items: int = 0
|
||||
build_manifest_path: str = ""
|
||||
build_commit_sha: str = ""
|
||||
build_commit_ref: str = ""
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
import time
|
||||
|
||||
import psycopg2
|
||||
from psycopg2 import errors
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -18,3 +21,39 @@ def get_db():
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
_RETRYABLE_READ_ERRORS = (
|
||||
errors.DeadlockDetected,
|
||||
errors.SerializationFailure,
|
||||
errors.LockNotAvailable,
|
||||
)
|
||||
|
||||
|
||||
def is_retryable_read_error(exc: Exception) -> bool:
|
||||
return isinstance(exc, _RETRYABLE_READ_ERRORS)
|
||||
|
||||
|
||||
def is_retryable_db_error(exc: Exception) -> bool:
|
||||
return isinstance(exc, _RETRYABLE_READ_ERRORS)
|
||||
|
||||
|
||||
def db_read_retry(*, attempts: int = 3, initial_delay_seconds: float = 0.05, backoff: float = 2.0):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
delay = max(0.0, float(initial_delay_seconds or 0.0))
|
||||
for attempt in range(1, max(1, int(attempts or 1)) + 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
if not is_retryable_read_error(exc) or attempt >= max(1, int(attempts or 1)):
|
||||
raise
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
delay *= max(1.0, float(backoff or 1.0))
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
@@ -63,9 +64,35 @@ def tail_lines(relative_path: str, max_lines: int = 120) -> list[str]:
|
||||
|
||||
|
||||
def runtime_root() -> Path:
|
||||
path = Path(__file__).resolve().parents[2] / "runtime"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
env_override = str(os.getenv("DOMAIN_API_RUNTIME_ROOT", "") or "").strip()
|
||||
candidates: list[Path] = []
|
||||
if env_override:
|
||||
candidates.append(Path(env_override))
|
||||
|
||||
base_dir = Path(__file__).resolve().parents[2]
|
||||
for parent in base_dir.parents:
|
||||
if parent.name != "releases":
|
||||
continue
|
||||
# Released builds live under /opt/domaincheck/releases/<release>/domain-api.
|
||||
# Runtime state must not be written back into the immutable release tree,
|
||||
# otherwise sync-agent / detect runtime snapshots fail with permission errors.
|
||||
candidates.append(parent.parent / "runtime" / "domain-api")
|
||||
break
|
||||
|
||||
candidates.append(base_dir / "runtime")
|
||||
|
||||
last_error: OSError | None = None
|
||||
for candidate in candidates:
|
||||
try:
|
||||
candidate.mkdir(parents=True, exist_ok=True)
|
||||
return candidate
|
||||
except OSError as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
raise RuntimeError("failed to resolve runtime root")
|
||||
|
||||
|
||||
def read_runtime_json(filename: str, default: dict | list | None = None):
|
||||
@@ -164,7 +191,16 @@ def load_detect_records() -> list[dict]:
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
except (json.JSONDecodeError, UnicodeDecodeError, OSError):
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
text = raw.decode("utf-8", errors="replace")
|
||||
decoder = json.JSONDecoder()
|
||||
payload, _ = decoder.raw_decode(text)
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user