fix(guardrails/purview): raise_for_status on HTTP errors, cap scope cache, reuse executor

This commit is contained in:
Sameer Kankute 2026-04-02 11:29:19 +05:30
parent c51b8fdc54
commit 592e418012
No known key found for this signature in database
2 changed files with 14 additions and 6 deletions

View file

@ -1,6 +1,7 @@
import time
import uuid
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, Optional, Tuple
from litellm._logging import verbose_proxy_logger
from litellm.llms.custom_httpx.http_handler import (
@ -56,7 +57,9 @@ class PurviewGuardrailBase:
self._token_cache: Optional[Tuple[str, float]] = None
# Protection scope cache: user_id -> (etag, scope_response, fetched_at)
self._scope_cache: Dict[str, Tuple[str, Dict, float]] = {}
# Capped at 1000 entries (LRU eviction) to avoid unbounded growth.
self._scope_cache: MutableMapping[str, Tuple[str, Dict, float]] = OrderedDict()
self._scope_cache_maxsize = 1000
# ------------------------------------------------------------------
# OAuth2 token management
@ -80,6 +83,7 @@ class PurviewGuardrailBase:
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
response.raise_for_status()
token_data = response.json()
access_token = token_data["access_token"]
expires_in = int(token_data.get("expires_in", 3599))
@ -116,6 +120,7 @@ class PurviewGuardrailBase:
response = await self.async_handler.post(
url=url, headers=headers, json=json_body
)
response.raise_for_status()
response_json: Dict[str, Any] = response.json()
response_headers = dict(response.headers)
verbose_proxy_logger.debug("Purview Graph response: %s", response_json)
@ -157,6 +162,9 @@ class PurviewGuardrailBase:
etag = response_headers.get("etag", response_headers.get("ETag", ""))
self._scope_cache[user_id] = (etag, response_json, now)
# Evict oldest entry when cache exceeds max size.
while len(self._scope_cache) > self._scope_cache_maxsize:
self._scope_cache.popitem(last=False) # type: ignore[attr-defined]
return etag, response_json
# ------------------------------------------------------------------

View file

@ -8,6 +8,7 @@ Supports three modes:
"""
import asyncio
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
@ -75,6 +76,7 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail):
)
self._logging_only = logging_only
self.guardrail_provider = "microsoft_purview"
self._executor = ThreadPoolExecutor(max_workers=1)
verbose_proxy_logger.info(
"Initialized Microsoft Purview DLP Guardrail: %s (logging_only=%s)",
guardrail_name,
@ -229,7 +231,6 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail):
self, kwargs: dict, result: Any, call_type: str
) -> Tuple[dict, Any]:
"""Sync wrapper for async_logging_hook (follows Presidio pattern)."""
from concurrent.futures import ThreadPoolExecutor
def run_in_new_loop() -> Tuple[dict, Any]:
new_loop = asyncio.new_event_loop()
@ -246,9 +247,8 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail):
try:
_ = asyncio.get_running_loop()
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(run_in_new_loop)
return future.result()
future = self._executor.submit(run_in_new_loop)
return future.result()
except RuntimeError:
return run_in_new_loop()