83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
"""Concrete, checkable translation of the cPanel account ceiling (Chapter 03).
|
|
|
|
Single source of truth for the account's literal resource limits. Other
|
|
chapters should import BUDGET rather than re-hardcoding these numbers.
|
|
validate_budget_config() is called from create_app() so an out-of-budget
|
|
deployment fails loudly at startup instead of silently degrading under load
|
|
— this is the concrete mechanism behind Chapter 03's "flag, don't silently
|
|
accept" requirement.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from flask import Flask
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AccountBudget:
|
|
"""The hosting account's literal ceiling (Chapter 03).
|
|
|
|
iops/io_throughput are informational only here — Python config can't
|
|
enforce them directly; they constrain how Ch06/07 batch reads/writes.
|
|
"""
|
|
|
|
cpu_cores: int = 4
|
|
max_entry_processes: int = 60
|
|
memory_mb: int = 2048
|
|
iops: int = 1024
|
|
io_throughput_mb_s: int = 16
|
|
max_total_processes: int = 150
|
|
max_db_connections: int = 150
|
|
|
|
# Derived engineering targets from Chapter 03's budget table.
|
|
db_pool_size_min: int = 2
|
|
db_pool_size_max: int = 5
|
|
|
|
|
|
BUDGET = AccountBudget()
|
|
|
|
_DISALLOWED_CACHE_BACKENDS = {"redis", "rediscache", "memcached", "memcachedcache"}
|
|
|
|
|
|
def validate_budget_config(app: Flask) -> None:
|
|
"""Raise at startup if config violates a Chapter 03 rule.
|
|
|
|
Cheap checks only (string/int comparisons) since this runs on every
|
|
process boot — Passenger may recycle processes frequently (factor 9).
|
|
"""
|
|
# Validated as its own config key, not read out of
|
|
# SQLALCHEMY_ENGINE_OPTIONS — that dict is empty for SQLite (its pool
|
|
# classes reject pool_size/pool_recycle outright; see app/config.py's
|
|
# _engine_options_for), so the *intended* setting must be checked
|
|
# independently of whether the active engine actually consumes it.
|
|
pool_size = app.config.get("DB_POOL_SIZE")
|
|
if pool_size is None or not (BUDGET.db_pool_size_min <= pool_size <= BUDGET.db_pool_size_max):
|
|
raise RuntimeError(
|
|
f"DB_POOL_SIZE={pool_size} is outside the Chapter 03 budget "
|
|
f"({BUDGET.db_pool_size_min}-{BUDGET.db_pool_size_max} per process; "
|
|
f"{BUDGET.max_db_connections} total connections are shared across "
|
|
f"up to {BUDGET.max_entry_processes} entry processes)."
|
|
)
|
|
|
|
cache_type = str(app.config.get("CACHE_TYPE", "")).lower()
|
|
if any(name in cache_type for name in _DISALLOWED_CACHE_BACKENDS):
|
|
raise RuntimeError(
|
|
f"CACHE_TYPE={app.config.get('CACHE_TYPE')!r} assumes a backend "
|
|
"(Redis/Memcached) Chapter 03 says not to assume is available. "
|
|
"Use FileSystemCache or a dashboard_cache DB table (Ch06)."
|
|
)
|
|
|
|
batch_size = app.config.get("PARSE_BATCH_SIZE")
|
|
if not batch_size or batch_size <= 0:
|
|
raise RuntimeError(
|
|
"PARSE_BATCH_SIZE must be a positive integer — unbounded/whole-file "
|
|
"parsing per invocation violates the Chapter 03 memory budget."
|
|
)
|
|
|
|
max_upload_mb = app.config.get("UPLOAD_MAX_SIZE_MB")
|
|
if not max_upload_mb or max_upload_mb <= 0:
|
|
raise RuntimeError(
|
|
"UPLOAD_MAX_SIZE_MB must be a positive integer to bound disk/IOPS per upload."
|
|
)
|