README에 conda/systemd/Apache 설치 문서 추가
- Miniconda(ncue) 기반 설치/실행 및 systemd 예시 추가 - Apache ProxyPass 설정 및 503 트러블슈팅 절차 정리 - Flask DB pool lazy 생성으로 임포트 단계 장애(503) 완화 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
73
flask_app.py
73
flask_app.py
@@ -24,13 +24,6 @@ def env(name: str, default: str = "") -> str:
|
||||
return str(os.getenv(name, default))
|
||||
|
||||
|
||||
def must(name: str) -> str:
|
||||
v = env(name).strip()
|
||||
if not v:
|
||||
raise RuntimeError(f"Missing env: {name}")
|
||||
return v
|
||||
|
||||
|
||||
_IDENT_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||
|
||||
|
||||
@@ -51,11 +44,11 @@ def parse_email_csv(s: str) -> list[str]:
|
||||
|
||||
PORT = int(env("PORT", "8023") or "8023")
|
||||
|
||||
DB_HOST = must("DB_HOST")
|
||||
DB_HOST = env("DB_HOST", "").strip()
|
||||
DB_PORT = int(env("DB_PORT", "5432") or "5432")
|
||||
DB_NAME = must("DB_NAME")
|
||||
DB_USER = must("DB_USER")
|
||||
DB_PASSWORD = must("DB_PASSWORD")
|
||||
DB_NAME = env("DB_NAME", "").strip()
|
||||
DB_USER = env("DB_USER", "").strip()
|
||||
DB_PASSWORD = env("DB_PASSWORD", "").strip()
|
||||
|
||||
TABLE = safe_ident(env("TABLE", "ncue_user") or "ncue_user")
|
||||
CONFIG_TABLE = "ncue_app_config"
|
||||
@@ -71,31 +64,51 @@ AUTH0_GOOGLE_CONNECTION = env("AUTH0_GOOGLE_CONNECTION", "").strip()
|
||||
# Optional CORS (for static on different origin)
|
||||
CORS_ORIGINS = env("CORS_ORIGINS", "*").strip() or "*"
|
||||
|
||||
_POOL: Optional[psycopg2.pool.SimpleConnectionPool] = None
|
||||
|
||||
POOL: psycopg2.pool.SimpleConnectionPool = psycopg2.pool.SimpleConnectionPool(
|
||||
minconn=1,
|
||||
maxconn=10,
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
dbname=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
sslmode="disable",
|
||||
)
|
||||
|
||||
def db_configured() -> bool:
|
||||
return bool(DB_HOST and DB_NAME and DB_USER and DB_PASSWORD)
|
||||
|
||||
|
||||
def get_pool() -> psycopg2.pool.SimpleConnectionPool:
|
||||
"""
|
||||
Lazy DB pool creation.
|
||||
- prevents app import failure (which causes Apache 503) when DB is temporarily unavailable
|
||||
- /api/config/auth can still work purely from .env without DB
|
||||
"""
|
||||
global _POOL
|
||||
if _POOL is not None:
|
||||
return _POOL
|
||||
if not db_configured():
|
||||
raise RuntimeError("db_not_configured")
|
||||
_POOL = psycopg2.pool.SimpleConnectionPool(
|
||||
minconn=1,
|
||||
maxconn=10,
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
dbname=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
sslmode="disable",
|
||||
)
|
||||
return _POOL
|
||||
|
||||
|
||||
def db_exec(sql: str, params: Tuple[Any, ...] = ()) -> None:
|
||||
conn = POOL.getconn()
|
||||
pool = get_pool()
|
||||
conn = pool.getconn()
|
||||
try:
|
||||
conn.autocommit = True
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
finally:
|
||||
POOL.putconn(conn)
|
||||
pool.putconn(conn)
|
||||
|
||||
|
||||
def db_one(sql: str, params: Tuple[Any, ...] = ()) -> Optional[tuple]:
|
||||
conn = POOL.getconn()
|
||||
pool = get_pool()
|
||||
conn = pool.getconn()
|
||||
try:
|
||||
conn.autocommit = True
|
||||
with conn.cursor() as cur:
|
||||
@@ -103,7 +116,7 @@ def db_one(sql: str, params: Tuple[Any, ...] = ()) -> Optional[tuple]:
|
||||
row = cur.fetchone()
|
||||
return row
|
||||
finally:
|
||||
POOL.putconn(conn)
|
||||
pool.putconn(conn)
|
||||
|
||||
|
||||
def ensure_user_table() -> None:
|
||||
@@ -248,6 +261,8 @@ def static_files(filename: str) -> Response:
|
||||
@app.get("/healthz")
|
||||
def healthz() -> Response:
|
||||
try:
|
||||
if not db_configured():
|
||||
return jsonify({"ok": False, "error": "db_not_configured"}), 500
|
||||
row = db_one("select 1 as ok")
|
||||
if not row:
|
||||
return jsonify({"ok": False}), 500
|
||||
@@ -259,6 +274,8 @@ def healthz() -> Response:
|
||||
@app.post("/api/auth/sync")
|
||||
def api_auth_sync() -> Response:
|
||||
try:
|
||||
if not db_configured():
|
||||
return jsonify({"ok": False, "error": "db_not_configured"}), 500
|
||||
ensure_user_table()
|
||||
id_token = bearer_token()
|
||||
if not id_token:
|
||||
@@ -318,6 +335,8 @@ def api_auth_sync() -> Response:
|
||||
@app.post("/api/auth/logout")
|
||||
def api_auth_logout() -> Response:
|
||||
try:
|
||||
if not db_configured():
|
||||
return jsonify({"ok": False, "error": "db_not_configured"}), 500
|
||||
ensure_user_table()
|
||||
id_token = bearer_token()
|
||||
if not id_token:
|
||||
@@ -364,6 +383,8 @@ def api_config_auth_get() -> Response:
|
||||
}
|
||||
)
|
||||
|
||||
if not db_configured():
|
||||
return jsonify({"ok": False, "error": "not_set"}), 404
|
||||
ensure_config_table()
|
||||
row = db_one(f"select value, updated_at from public.{CONFIG_TABLE} where key = %s", ("auth",))
|
||||
if not row:
|
||||
@@ -384,6 +405,8 @@ def api_config_auth_get() -> Response:
|
||||
@app.post("/api/config/auth")
|
||||
def api_config_auth_post() -> Response:
|
||||
try:
|
||||
if not db_configured():
|
||||
return jsonify({"ok": False, "error": "db_not_configured"}), 500
|
||||
ensure_config_table()
|
||||
if not CONFIG_TOKEN:
|
||||
return jsonify({"ok": False, "error": "config_token_not_set"}), 403
|
||||
|
||||
Reference in New Issue
Block a user