Files
Kavosh/app/budget.py
T
2026-08-07 21:17:17 +03:30

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."
)