Close 12 audit-driven platform-compliance gaps on a single branch. - D4 dispatch: app/integrations/dispatch_client.py participant `legacyhub`, emits LegacyhubDocumentIndexed + AssetDerivativeReady after the indexing commit (idempotent uuid5), http_inbox route (reindex/tombstone) with audit-based dedupe; docs/dispatch-contract.md. Celery+Redis stays intra-module. - D2 SSO: app/integrations/identity.py validates X-TeamHub-* + role/scope mapper; security.py adds trusted-header enforcement (AUTH_REQUIRE_IDENTITY) and a scope check on /search; docker-compose.teamhub.yml (external teamhub_net + internal db net, api not host-published); RUNBOOK network/firewall section. - Asset standard: SearchHit/Citation carry asset_id/owner_module; buckets renamed teamhub-legacyhub-* (+quarantine/tmp/exports); purge-by-asset_id with legal-hold guard (app/indexing/projection.py); OCR-markdown derivative event. - audit_log model + Alembic 0003 + record_audit on writes (same transaction). - Secret masking: app/common/json_logger.py recursive mask wired into structlog (+ensure_ascii=False); event payloads redacted before persistence. - Service X-API-Key mandatory on ingest endpoints (defence-in-depth). - Port: host API 8000->8050 (collision with SalesHUB/MailHUB resolved), container still listens on 8000. - Config: no plaintext secret defaults; fail-loud in non-dev (no value leak). - Docs drift: README PG 5440, layered-auth note, 5173 removed from CORS; ingest/folder gated by ENABLE_FOLDER_INGEST (410 by default). - ADRs: layers mapping, shared-core extraction, UI locale (RU-first). Tests: 78 passing (ruff, compileall, pytest, tsc, vite build, compose config). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.1 KiB
Python
72 lines
2.1 KiB
Python
"""Locust load profile for the LegacyHUB hybrid search API.
|
|
|
|
Run:
|
|
|
|
pip install locust
|
|
locust -f scripts/locustfile_search.py \
|
|
--host http://localhost:8050 \
|
|
--users 50 --spawn-rate 5 --run-time 5m
|
|
|
|
Or headless with HTML report:
|
|
|
|
locust -f scripts/locustfile_search.py --host http://localhost:8050 \
|
|
--headless --users 100 --spawn-rate 10 --run-time 10m \
|
|
--html load_search.html
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
|
|
from locust import HttpUser, between, task
|
|
|
|
QUERIES = [
|
|
"ГОСТ 21.501-93 рабочие чертежи",
|
|
"класс бетона B25",
|
|
"регламент технического обслуживания",
|
|
"контроль качества сварных соединений",
|
|
"схема электропитания корпус 3",
|
|
"журнал ремонтов узлов",
|
|
"правила производства земляных работ",
|
|
"акты приемки скрытых работ",
|
|
"fundament concrete grade",
|
|
"maintenance schedule appendix",
|
|
]
|
|
|
|
MODES = ["hybrid", "hybrid", "hybrid", "lexical", "semantic"]
|
|
|
|
|
|
class SearchUser(HttpUser):
|
|
wait_time = between(0.5, 2.5)
|
|
api_prefix = "/api/v1"
|
|
|
|
@task(8)
|
|
def hybrid_search(self):
|
|
body = {
|
|
"query": random.choice(QUERIES),
|
|
"limit": random.choice([5, 10, 20]),
|
|
"filters": {
|
|
"document_id": None,
|
|
"source_path": None,
|
|
"block_type": None,
|
|
"min_ocr_confidence": None,
|
|
},
|
|
"search_mode": random.choice(MODES),
|
|
}
|
|
with self.client.post(
|
|
f"{self.api_prefix}/search",
|
|
json=body,
|
|
name="POST /search",
|
|
catch_response=True,
|
|
) as res:
|
|
if res.status_code != 200:
|
|
res.failure(f"HTTP {res.status_code}: {res.text[:120]}")
|
|
return
|
|
data = res.json()
|
|
if not data.get("results"):
|
|
res.failure("empty results")
|
|
|
|
@task(1)
|
|
def health(self):
|
|
self.client.get(f"{self.api_prefix}/health", name="GET /health")
|