Войти Регистрация
Конкурентность

34. Антиспам SMS в МТС: 16 потоков и GIL

Средняя

Python · 36 строк

Раз в минуту приходит пачка входящих SMS от коротких номеров и рассыльщиков, в пик до 2 млн. score — регулярки и подсчёт цифр в тексте, чистый CPU. В одном потоке пачка обрабатывается 160 секунд на 8-ядерном сервере, а нужно уложиться в 30.

Отправитель с 1000 спам-сообщений уходит в стоп-лист: blocklist.add делает HTTP-запрос и может бросить ConnectionError. Сервис на обычном CPython 3.12. Отметьте строки с ошибками.

В работе: Антиспам-фильтр SMS-шлюза оператора.

Как проверяем Нажмите на строку с ошибкой и коротко напишите, что не так. Баг засчитан, если отмечена хотя бы одна его строка. Зачёт — найдено не меньше 60 % багов с учётом серьёзности (критичный весит 3, обычный 2, мелкий 1), а лишних отметок не больше, чем багов в коде. Пустые строки, скобки и комментарии лишними не считаем. Плюс 2 вопроса про поведение программы — на зачёт не влияют. Ответ один на подход: после него показываем разбор.
GILCPU-boundThreadPoolExecutorcheck-then-actLock

Источник: наша задача на данных МТС: фильтрация спам-рассылок по SMS. Условие пересказано своими словами.

  1. import re
  2. import threading
  3. from concurrent.futures import ThreadPoolExecutor
  4. from antispam.blocklist import blocklist
  5. SPAM_PATTERNS = [re.compile(p, re.I) for p in (
  6. r"выигр\w+ приз", r"переходи по ссылке", r"назови код из смс", r"bit\.ly/\w+",
  7. )]
  8. stats: dict[str, int] = {}
  9. stats_lock = threading.Lock()
  10. def score(text: str) -> int:
  11. s = sum(len(p.findall(text)) * 10 for p in SPAM_PATTERNS)
  12. return s + sum(ch.isdigit() for ch in text)
  13. def handle(sms: dict) -> bool:
  14. sender = sms["sender"]
  15. if sender not in stats:
  16. stats[sender] = 0
  17. spam = score(sms["text"]) >= 30
  18. if spam:
  19. stats_lock.acquire()
  20. stats[sender] += 1
  21. if stats[sender] == 1000:
  22. blocklist.add(sender)
  23. stats_lock.release()
  24. return spam
  25. def run(batch: list[dict]) -> int:
  26. with ThreadPoolExecutor(max_workers=16) as pool:
  27. return sum(pool.map(handle, batch))