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:
@@ -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()}))]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user