Кратко
При ANONYMIZATION_ENABLED=true память прокси (onec_mcp_toolkit_proxy) монотонно растёт по аптайму и не возвращается. На больших результатах запросов рост скачкообразный. При жёстком лимите памяти контейнера процесс уходит в OOM, in-memory MCP-сессия теряется → разрыв клиента (Claude Code / Cursor требует Reload Window). Причина — безлимитная карта токенов анонимайзера, которая живёт per-channel всё время процесса и никогда не вытесняет старые записи.
Корневая причина (по коду main)
-
onec_mcp_toolkit_proxy/anonymizer/registry.py — AnonymizerRegistry._registry: Dict[str, Anonymizer]: один Anonymizer создаётся на канал и живёт всё время процесса, из реестра не удаляется (кроме явного clear(), который автоматически не вызывается).
-
onec_mcp_toolkit_proxy/anonymizer/token_mapper.py — TokenMapper.tokenize() добавляет записи в _real_to_token / _token_to_real и никогда не вытесняет. clear() есть, но в обычном потоке не зовётся.
Итог: каждое уникальное анонимизированное значение ([ORG-00001], [PER-00002], …) оседает в двух словарях навсегда. Один большой результат execute_query вносит тысячи уникальных значений за раз.
Воспроизведение / замеры
- Долгий аптайм: RSS рос с ~50 МБ (свежий старт) до ~3.2 ГБ за 12 дней, выбирая ОЗУ + swap небольшого VPS.
- Точечный замер: один
execute_query с большим массивом поднял RSS прокси с 47 → 425 МБ и память не вернулась (через 2 минуты простоя всё ещё 425 МБ; на холостом long-poll медленно росла дальше).
- При лимите контейнера 768 МБ — подтверждённый
dmesg: Memory cgroup out of memory: Killed process (python) anon-rss ~780000 kB.
- Часть всплеска — ленивая загрузка модели spaCy
ru_core_news_md (резидентный «пол», неустранимо при анонимизации). Но безграничный рост даёт именно карта токенов.
Второй, отдельный баг — формат токена
token_mapper.py:
TOKEN_RE = re.compile(r"\[([A-Z]+)-(\d{5})\]", re.IGNORECASE)
...
token = f"[{category}-{count:05d}]"
count монотонный и не сбрасывается. После 99999 уникальных значений в категории tokenize() выдаёт [ORG-100000] (6 цифр), но TOKEN_RE матчит ровно \d{5} → detokenize() / detokenize_for_query() такой токен не находят и оставляют как есть. Деанонимизация молча ломается. Достаточно сменить шаблон на \d{5,}.
Предлагаемый фикс
-
Ограничить карту токенов. Любой из вариантов:
- порог размера с вытеснением (LRU / FIFO), env-настраиваемый (напр.
ANONYMIZATION_TOKENMAP_MAX);
- TTL на записи (с очисткой после деанонимизации / по таймауту);
- вызывать
TokenMapper.clear() по жизненному циклу запроса/сессии, где это безопасно для деанонимизации.
Счётчики категорий при вытеснении сбрасывать нельзя (иначе коллизии номеров среди выживших токенов).
-
Регулярка: \d{5} → \d{5,}.
Временный обход (на нашей стороне)
Overlay поверх :latest: в tokenize() добавлено FIFO-вытеснение при превышении ANONYMIZATION_TOKENMAP_MAX (default 50000) + регулярка \d{5,}. Проверено: 300 000 уникальных вставок → карта остаётся 50 000, RSS +~17 МБ (не растёт); 5- и 6-значные токены деанонимизируются. Готовы прислать PR, если интересно.
Окружение
- образ
roctup/1c-mcp-toolkit-proxy:latest, режим прокси (long-poll), ANONYMIZATION_ENABLED=true
- spaCy 3.8.0 +
ru_core_news_md, toon-format 0.9.0b1
Кратко
При
ANONYMIZATION_ENABLED=trueпамять прокси (onec_mcp_toolkit_proxy) монотонно растёт по аптайму и не возвращается. На больших результатах запросов рост скачкообразный. При жёстком лимите памяти контейнера процесс уходит в OOM, in-memory MCP-сессия теряется → разрыв клиента (Claude Code / Cursor требует Reload Window). Причина — безлимитная карта токенов анонимайзера, которая живёт per-channel всё время процесса и никогда не вытесняет старые записи.Корневая причина (по коду
main)onec_mcp_toolkit_proxy/anonymizer/registry.py—AnonymizerRegistry._registry: Dict[str, Anonymizer]: одинAnonymizerсоздаётся на канал и живёт всё время процесса, из реестра не удаляется (кроме явногоclear(), который автоматически не вызывается).onec_mcp_toolkit_proxy/anonymizer/token_mapper.py—TokenMapper.tokenize()добавляет записи в_real_to_token/_token_to_realи никогда не вытесняет.clear()есть, но в обычном потоке не зовётся.Итог: каждое уникальное анонимизированное значение (
[ORG-00001],[PER-00002], …) оседает в двух словарях навсегда. Один большой результатexecute_queryвносит тысячи уникальных значений за раз.Воспроизведение / замеры
execute_queryс большим массивом поднял RSS прокси с 47 → 425 МБ и память не вернулась (через 2 минуты простоя всё ещё 425 МБ; на холостом long-poll медленно росла дальше).dmesg:Memory cgroup out of memory: Killed process (python) anon-rss ~780000 kB.ru_core_news_md(резидентный «пол», неустранимо при анонимизации). Но безграничный рост даёт именно карта токенов.Второй, отдельный баг — формат токена
token_mapper.py:countмонотонный и не сбрасывается. После 99999 уникальных значений в категорииtokenize()выдаёт[ORG-100000](6 цифр), ноTOKEN_REматчит ровно\d{5}→detokenize()/detokenize_for_query()такой токен не находят и оставляют как есть. Деанонимизация молча ломается. Достаточно сменить шаблон на\d{5,}.Предлагаемый фикс
Ограничить карту токенов. Любой из вариантов:
ANONYMIZATION_TOKENMAP_MAX);TokenMapper.clear()по жизненному циклу запроса/сессии, где это безопасно для деанонимизации.Счётчики категорий при вытеснении сбрасывать нельзя (иначе коллизии номеров среди выживших токенов).
Регулярка:
\d{5}→\d{5,}.Временный обход (на нашей стороне)
Overlay поверх
:latest: вtokenize()добавлено FIFO-вытеснение при превышенииANONYMIZATION_TOKENMAP_MAX(default 50000) + регулярка\d{5,}. Проверено: 300 000 уникальных вставок → карта остаётся 50 000, RSS +~17 МБ (не растёт); 5- и 6-значные токены деанонимизируются. Готовы прислать PR, если интересно.Окружение
roctup/1c-mcp-toolkit-proxy:latest, режим прокси (long-poll),ANONYMIZATION_ENABLED=trueru_core_news_md, toon-format 0.9.0b1