fix(msgraph): bound webhook receipt dedupe cache

This commit is contained in:
Dilee 2026-05-07 17:12:39 +03:00 committed by Teknium
parent 46a6f39024
commit 2a215de9af
2 changed files with 56 additions and 2 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import logging import logging
from collections import deque
from hashlib import sha1 from hashlib import sha1
from typing import Any, Awaitable, Callable, Dict, Optional from typing import Any, Awaitable, Callable, Dict, Optional
@ -29,6 +30,7 @@ logger = logging.getLogger(__name__)
DEFAULT_HOST = "0.0.0.0" DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8646 DEFAULT_PORT = 8646
DEFAULT_WEBHOOK_PATH = "/msgraph/webhook" DEFAULT_WEBHOOK_PATH = "/msgraph/webhook"
DEFAULT_MAX_SEEN_RECEIPTS = 5000
NotificationScheduler = Callable[[Dict[str, Any], MessageEvent], Awaitable[None] | None] NotificationScheduler = Callable[[Dict[str, Any], MessageEvent], Awaitable[None] | None]
@ -55,9 +57,13 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
if str(value).strip() if str(value).strip()
] ]
self._client_state: Optional[str] = self._string_or_none(extra.get("client_state")) self._client_state: Optional[str] = self._string_or_none(extra.get("client_state"))
self._max_seen_receipts = max(
1, int(extra.get("max_seen_receipts", DEFAULT_MAX_SEEN_RECEIPTS))
)
self._runner = None self._runner = None
self._notification_scheduler: Optional[NotificationScheduler] = None self._notification_scheduler: Optional[NotificationScheduler] = None
self._seen_receipts: set[str] = set() self._seen_receipts: set[str] = set()
self._seen_receipt_order: deque[str] = deque()
self._accepted_count = 0 self._accepted_count = 0
self._duplicate_count = 0 self._duplicate_count = 0
@ -172,10 +178,10 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
continue continue
receipt_key = self._build_receipt_key(notification) receipt_key = self._build_receipt_key(notification)
if receipt_key in self._seen_receipts: if self._has_seen_receipt(receipt_key):
duplicates += 1 duplicates += 1
continue continue
self._seen_receipts.add(receipt_key) self._remember_receipt(receipt_key)
accepted += 1 accepted += 1
scheduled += 1 scheduled += 1
@ -213,6 +219,16 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
provided = self._string_or_none(notification.get("clientState")) provided = self._string_or_none(notification.get("clientState"))
return provided == expected return provided == expected
def _has_seen_receipt(self, receipt_key: str) -> bool:
return receipt_key in self._seen_receipts
def _remember_receipt(self, receipt_key: str) -> None:
self._seen_receipts.add(receipt_key)
self._seen_receipt_order.append(receipt_key)
while len(self._seen_receipt_order) > self._max_seen_receipts:
oldest = self._seen_receipt_order.popleft()
self._seen_receipts.discard(oldest)
def _build_message_event( def _build_message_event(
self, self,
notification: Dict[str, Any], notification: Dict[str, Any],

View file

@ -185,3 +185,41 @@ class TestMSGraphNotifications:
await asyncio.sleep(0.05) await asyncio.sleep(0.05)
assert len(scheduled) == 1 assert len(scheduled) == 1
@pytest.mark.anyio
async def test_seen_receipts_are_bounded(self):
adapter = _make_adapter(max_seen_receipts=2)
async def _capture(notification, event):
return None
adapter.set_notification_scheduler(_capture)
async def _post(notification_id: str):
payload = {
"value": [
{
"id": notification_id,
"subscriptionId": "sub-1",
"changeType": "updated",
"resource": "communications/onlineMeetings/meeting-3",
"clientState": "expected-client-state",
}
]
}
return await adapter._handle_notification(_FakeRequest(json_payload=payload))
first = await _post("notif-a")
second = await _post("notif-b")
third = await _post("notif-c")
assert first.status == 202
assert second.status == 202
assert third.status == 202
assert len(adapter._seen_receipts) == 2
assert list(adapter._seen_receipt_order) == ["id:notif-b", "id:notif-c"]
replay = await _post("notif-a")
replay_data = json.loads(replay.text)
assert replay_data["accepted"] == 1
assert replay_data["duplicates"] == 0