Files
rengine/api/app/main.py
T
Yogesh OjhaandClaude Opus 5 1e2d16eed2 feat(ops): a backup that has been restored, and pools that cannot silently outgrow the server
There was no backup path at all, and the connection arithmetic did not add up.

scripts/backup.sh dumps the four things an instance actually is: the database, the
scan media on disk, the docker volumes holding uploaded wordlists, nuclei templates,
report themes and fonts, and the settings a restore needs. SECRET_KEY travels with
it deliberately — API keys, proxy credentials and notification secrets are Fernet
encrypted with a key derived from it, so an archive without it restores a database
full of unreadable secrets. The header says so, and backups/ and *.tar are
gitignored, because an archive is a credential.

Restore is the half that is usually never tried, so it was: a 305 MB archive of this
instance was restored over it and came back at 9 scans, 59,858 subdomains, 535,000
endpoints, 29 findings and 7 targets — identical — with the migration head intact,
the API answering and an encrypted API key still decrypting. It stops the api and
both workers first so nothing writes underneath it, and asks for the database name
before it destroys anything.

The pools could ask for more connections than the server would give. The api takes
10 + 20, each celery prefork child takes 2 + 3, and beat takes another pool: at the
concurrency of 8 this box runs that is 75 against a max_connections of 100, and at
the 12 the compose file now defaults to it is 95 — five short of the ceiling, which
fails as a checkout timeout under load and reads like a slow query.

max_connections is now set from compose with the arithmetic beside it, and the api
checks it at startup: pool_demand() against max_connections less the superuser
reserve, warned once rather than discovered during an incident. The inputs moved to
shared settings because the sum spans both processes and neither could see the
other's numbers.

Worth recording: .env on this box pins CELERY_SCAN_CONCURRENCY=8, so raising the
compose default to 12 changes new installs and not this one. It also still carries
CELERY_DEFAULT_CONCURRENCY, which nothing has read since it was renamed.

Three tests: the demand counts every pool, the shipped defaults fit the shipped
server, and an oversized pool warns rather than failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dKUV96WkQQETZBMoAG98C
2026-09-10 23:16:46 +05:30

104 lines
2.8 KiB
Python

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi_pagination import add_pagination
from sqlalchemy.exc import DBAPIError
from app.api.router import router as api_router
from app.config import settings
from app.core.database import check_capacity
from app.core.db_errors import data_exception_handler
from app.core.redis_sse_bridge import RedisSSEBridge
from app.core.sanitize import RejectNulMiddleware
from app.core.throttle import GlobalRateLimitMiddleware
from app.core.validation_errors import validation_error_handler
from app.utils.helpers import create_initial_admin
from shared.logging import get_logger
logger = get_logger(__name__)
redis_sse_bridge = RedisSSEBridge(settings.redis_url)
@asynccontextmanager
async def lifespan(_app: FastAPI):
logger.info("Starting Backend...")
try:
await create_initial_admin()
await check_capacity()
await redis_sse_bridge.start()
except Exception as e:
logger.exception("Failed to initialize the application: %s", e)
raise e
yield
logger.info("Shutting down reNgine Backend...")
await redis_sse_bridge.stop()
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
lifespan=lifespan,
)
add_pagination(app)
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="reNgine 3.0 - Attack Surface Management Platform",
routes=app.routes,
)
openapi_schema["components"]["securitySchemes"] = {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Enter your JWT token from /auth/login",
}
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
if "*" in settings.CORS_ORIGINS:
msg = "CORS_ORIGINS must be an explicit allowlist (no '*') when credentials are enabled"
raise ValueError(msg)
app.add_exception_handler(DBAPIError, data_exception_handler)
app.add_exception_handler(RequestValidationError, validation_error_handler)
app.add_middleware(GlobalRateLimitMiddleware)
app.add_middleware(RejectNulMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
@app.get("/")
async def root():
logger.debug("Root endpoint accessed")
return {
"message": "Welcome to reNgine API",
"version": settings.APP_VERSION,
"docs": "/docs",
}