feat: async background chat, performance scaling, and dead code cleanup

Async chat processing eliminates 504 timeouts - POST returns immediately,
backend processes in background, frontend polls for results with timestamps.
Scale to ~12 simultaneous chats via 8 uvicorn workers + 16-thread executor.
Parallelized MCP data collection, 2h session cache, 5min tool timeout.
Full dead code cleanup across backend, frontend, MCP, docker, and nginx.
This commit is contained in:
nogueiraguh
2026-03-06 11:06:21 -03:00
parent ef43eaa7ba
commit 2d024d3130
7 changed files with 272 additions and 265 deletions

View File

@@ -11,13 +11,10 @@ import concurrent.futures
import datetime
import json
import os
import sys
import time
import traceback
from configparser import ConfigParser
from functools import partial
from threading import Thread
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
@@ -27,7 +24,8 @@ server = Server("cis-compliance")
# session_key -> {checker, sections_collected: set, sections_analyzed: set, ...}
_sessions: dict = {}
SESSION_TTL = 1800
SESSION_TTL = 7200 # 2 hours — reduce cold starts
TOOL_TIMEOUT = 300 # 5 min max per tool call
def _session_key(config_id: str, regions: list[str] | None = None) -> str:
@@ -42,15 +40,6 @@ OCI_CONFIGS_DIR = os.environ.get("OCI_CONFIGS_DIR", "/data/oci_configs")
# Per-section data collectors: which private methods to call
# ──────────────────────────────────────────────────────────────────────
# Home-region functions (run once, needed by most sections)
HOME_BASE = [
"_CIS_Report__identity_read_compartments",
"_CIS_Report__cloud_guard_read_cloud_guard_configuration",
"_CIS_Report__identity_read_domains",
"_CIS_Report__identity_read_groups",
"_CIS_Report__identity_read_availability_domains",
]
# Section -> (home_region_collectors, regional_collectors, analysis_methods)
SECTION_DEPS = {
"iam": {
@@ -217,12 +206,20 @@ def _ensure_base(session):
if session["base_collected"]:
return
checker = session["checker"]
# Sequential: compartments + cloud_guard first, then domains, then groups
getattr(checker, "_CIS_Report__identity_read_compartments")()
getattr(checker, "_CIS_Report__cloud_guard_read_cloud_guard_configuration")()
getattr(checker, "_CIS_Report__identity_read_domains")()
getattr(checker, "_CIS_Report__identity_read_groups")()
getattr(checker, "_CIS_Report__identity_read_availability_domains")()
base_methods = [
"_CIS_Report__identity_read_compartments",
"_CIS_Report__cloud_guard_read_cloud_guard_configuration",
"_CIS_Report__identity_read_domains",
"_CIS_Report__identity_read_groups",
"_CIS_Report__identity_read_availability_domains",
]
def _run_base(fn_name):
try:
getattr(checker, fn_name)()
except Exception as e:
print(f"Warning: {fn_name} failed: {e}")
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as ex:
list(ex.map(_run_base, base_methods))
session["base_collected"] = True
@@ -236,23 +233,21 @@ def _collect_section(session, section: str):
if not deps:
return
# Home region collectors (sequential for safety)
for method_name in deps["home"]:
def _run_method(fn_name):
try:
getattr(checker, method_name)()
getattr(checker, fn_name)()
except Exception as e:
print(f"Warning: {method_name} failed: {e}")
print(f"Warning: {fn_name} failed: {e}")
# Home region collectors (parallel)
if deps["home"]:
with concurrent.futures.ThreadPoolExecutor(max_workers=len(deps["home"])) as ex:
list(ex.map(_run_method, deps["home"]))
# Regional collectors (parallel with thread pool)
if deps["regional"]:
def _run(fn_name):
try:
getattr(checker, fn_name)()
except Exception as e:
print(f"Warning: {fn_name} failed: {e}")
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex:
list(ex.map(_run, deps["regional"]))
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
list(ex.map(_run_method, deps["regional"]))
session["sections_collected"].add(section)
@@ -627,8 +622,13 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' not found"}))]
try:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, partial(handler, arguments))
result = await asyncio.wait_for(
loop.run_in_executor(None, partial(handler, arguments)),
timeout=TOOL_TIMEOUT
)
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
except asyncio.TimeoutError:
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' timed out after {TOOL_TIMEOUT}s"}))]
except Exception as e:
return [TextContent(type="text", text=json.dumps({"error": str(e), "traceback": traceback.format_exc()}))]