Настроить человеческий фактор

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить человеческий фактор

1. Цель задачи

Реализовать механизм учёта занятости операторов (статусы online/offline, в разговоре) в системе диспетчеризации или поддержки. Интегрировать этот механизм с логикой эскалации задач, чтобы вновь поступающие или эскалированные запросы направлялись только тем операторам, которые действительно готовы их принять (свободны). Задача имитирует типичный production-сценарий, где человеческий фактор (занятость, доступность) критически влияет на SLA и нагрузку.

Ключевой результат Эскалация задач происходит только на свободных операторов, исключая ситуации, когда задача попадает к занятому коллеге.


2. Исходные данные

Что нужноОткуда взять
Система с задачами и операторами (тестовая)Разработать простой Python-фреймворк (события, задачи, операторы) или использовать уже имеющуюся (например, Pet-проект с очередями).
Данные о статусах операторовСгенерировать симулированные события: оператор залогинился/вылогинился, начал/завершил диалог.
Система эскалации (простая логика: при превышении времени берёт следующий тикет и назначает оператору)Написать заново как часть задачи.
Инструмент для хранения статусовRedis / SQLite / in-memory хранилище (выбор по усмотрению).
Дашборд или CLI для мониторинга статусовОпционально — простой дашборд на Streamlit или вывод в консоль.

Если нет реального инструмента — симулируем:

  1. Разработать эмулятор работы операторов — скрипт на Python, который случайным образом меняет статусы (online/offline/in_call) каждые 5–30 секунд.
  2. Эмулятор должен отправлять события в общий канал (Redis pub/sub или очередь).
  3. Реализовать симуляцию потока задач: каждые 10–20 секунд генерируется новая задача с приоритетом и временем жизни.
  4. Система эскалации должна реагировать на статусы операторов и назначать задачу только тем, кто в статусе "online" и не "in_call".

3. Технологический стек

КомпонентИнструментыНазначение
Язык реализацииPython 3.11+Основная логика системы
Хранилище статусовRedis (предпочтительно) или SQLiteБыстрое чтение/запись текущего статуса, атомарные обновления
Очередь сообщений / Pub/SubRedis pub/sub или RabbitMQПередача событий статусов и задач между эмулятором и эскалатором
Асинхронностьasyncio (в Python)Неблокирующая обработка событий
Тестированиеpytest + moto (если Redis)Юнит-тесты для модуля статусов и эскалации
Мониторинг (опционально)Streamlit / Dash / просто логиВизуализация статусов и истории назначений

4. Этапы выполнения

Этап 1: Анализ требований и подготовка окружения (30 мин)

Действия

  1. Определить модели данных:
    • Operator: id, name, status (online/offline/in_call), last_seen, current_task_id (nullable).
    • Task: id, priority, created_at, assigned_to, status (new/assigned/completed/escalated).
    • EscalationPolicy: max_wait_time, max_retries, fallback_handler.
  2. Выбрать способ хранения: Redis с ключами вида operator:{id}:status и operator:{id}:current_task.
  3. Развернуть Redis (локальный Docker docker run -d -p 6379:6379 redis:7-alpine) или использовать in-memory dict с thread-safe замками.
  4. Создать виртуальное окружение, установить зависимости: redis[hiredis], pytest, pytest-asyncio, async-timeout.

Ожидаемый результат этапа Рабочий Redis (или in-memory заменитель), понимание модели данных, написан скелет проекта с requirements.txt.

Этап 2: Проектирование модуля учёта статусов (1 час)

Действия

  1. Спроектировать интерфейс StatusManager (абстрактный класс):
    class StatusManager(ABC):
        @abstractmethod
        async def set_status(self, operator_id: int, status: str, task_id: int = None):
        @abstractmethod
        async def get_status(self, operator_id: int) -> str:
        @abstractmethod
        async def get_free_operators(self) -> list[int]:
    
  2. Реализовать класс RedisStatusManager:
    • Использовать hset для хранения полей {status, current_task}.
    • get_free_operators — атомарный SMEMBERS операторов со статусом online и без current_task.
    • Добавить TTL на статус (например, 5 минут для offline).
  3. Реализовать класс InMemoryStatusManager с asyncio.Lock для использования без Redis.
  4. Написать юнит-тесты для каждого метода (используя pytest и fake_redis при Redis).

Ожидаемый результат этапа Работающий модуль статусов, покрытый тестами, 100% зелёных.

Этап 3: Разработка эмулятора операторов и задач (1.5 часа)

Действия

  1. Создать эмулятор OperatorSimulator:
    • Запускает N операторов (N=3–5).
    • Каждый оператор — корутина, которая циклически:
      • Меняет статус на online, ждет случайное время (10–60 сек).
      • Переключается в in_call на случайное время (30–120 сек).
      • После завершения разговора возвращается в online.
      • В конце смены уходит в offline.
    • Использовать StatusManager для обновления статусов.
  2. Создать эмулятор задач TaskSimulator:
    • С интервалом 10–30 сек генерирует новый Task.
    • Кладёт задачу в очередь (Redis list или asyncio.Queue).
  3. Обеспечить логирование ключевых событий (смена статуса, создание задачи) через logging.

Ожидаемый результат этапа При запуске эмулятора видно в логах смену статусов и генерацию задач; статусы корректно записываются в Redis.

Этап 4: Реализация логики эскалации с учётом статусов (1.5 часа)

Действия

  1. Разработать модуль Escalator:
    • Подписывается на очередь задач.
    • Когда задача «висит» дольше max_wait_time (например, 30 сек), запускается эскалация.
  2. Реализовать функцию assign_task(task, status_manager):
    • Вызвать status_manager.get_free_operators().
    • Если список пуст — записать в лог No free operators, waiting и вернуть задачу в очередь (backoff).
    • Если есть свободные — выбрать одного (round-robin или наименее загруженного), установить ему current_task и статус in_call.
  3. Добавить обработку таймаутов: если оператор не отвечает (не завершает задачу за разумное время) — принудительно освободить оператора и передать задачу другому.
  4. Написать интеграционные тесты с эмулятором: за 2 минуты работы системы убедиться, что ни одна задача не назначается оператору, который уже in_call.

Ожидаемый результат этапа Система эскалации назначает задачи только свободным операторам; в логах нет случаев operator busy при попытке назначения.

Этап 5: Тестирование, отладка и финальное оформление (1 час)

Действия

  1. Запустить систему с эмулятором на 2–3 минуты, собрать статистику:
    • Количество задач создано, назначено, эскалировано.
    • Процент задач, назначенных с первого раза (без ожидания свободного оператора).
    • Время ожидания задачи при отсутствии свободных.
  2. Проверить граничные случаи:
    • Все операторы offline — задачи должны ждать.
    • Внезапный выход оператора из in_call (сбой) — освобождение задачи по таймауту.
    • Одновременное назначение одной задачи двум операторам (race condition) — использовать Redis WATCH или блокировки.
  3. Написать итоговую документацию: README с инструкцией запуска, краткими пояснениями архитектуры.
  4. (Опционально) Создать простую визуализацию в Streamlit с выводом списка операторов и их статусов.

Ожидаемый результат этапа Система стабильно работает, тесты проходят, документация готова.


5. Критерии приемки (Definition of Done)

  • Реализован модуль StatusManager (Redis или in-memory) с методами set_status, get_status, get_free_operators.
  • Эмулятор операторов корректно меняет статусы (online/offline/in_call) и событие каждого изменения логируется.
  • Система эскалации при получении задачи вызывает get_free_operators() и назначает только тем, кто в списке.
  • Тесты (минимум 5 юнит-тестов и 1 интеграционный) проходят без ошибок.
  • Назначение задачи не приводит к состоянию гонки (один оператор получает одну задачу).
  • При отсутствии свободных операторов задача остаётся в очереди и повторно проверяется через заданный интервал.
  • Вся система запускается одной командой (например, python main.py или docker-compose up).
  • В репозитории имеется README с описанием архитектуры и инструкцией по запуску.

6. Ожидаемый результат

Основной артефакт — папка с проектом, содержащая:

  • status_manager.py / redis_status_manager.py — реализация модуля статусов.
  • operator_simulator.py — эмулятор операторов.
  • task_simulator.py — эмулятор задач.
  • escalator.py — логика эскалации с учётом статусов.
  • main.py — точка входа, запускающая все компоненты асинхронно.
  • tests/ — папка с тестами (pytest).
  • requirements.txt — список зависимостей.
  • README.md — документация.

Дополнительный результат (опционально): дашборд Streamlit dashboard.py с отображением статусов операторов в реальном времени.

Финальное поведение: при запуске python main.py в консоли видно логи с назначением задач только свободным операторам; статистика показывает, что задачи не назначаются занятым.


7. Возможные сложности и их решение

СложностьРешение
Состояние гонки при одновременном обновлении статуса и назначении задачиИспользовать Redis WATCH/MULTI/EXEC или блокировку на уровне приложения (asyncio.Lock). Для простоты — атомарная операция setnx для захвата задачи.
Эмулятор «засыпает» из-за блокирующих вызововВсе сетевые/ожидательные операции делать асинхронными (asyncio + aioredis или redis.asyncio).
Система не может подключиться к RedisРеализовать fallback на InMemoryStatusManager и выводить предупреждение.
Тесты зависают из-за бесконечного цикла эмуляцииВ тестах использовать фиксированные сценарии (например, один оператор с предсказуемыми статусами) и pytest-timeout.
Нужно демонстрировать без внешнего RedisИспользовать InMemoryStatusManager как стандартный, но убедиться, что код легко переключается через конфигурацию.

8. Бюджет времени (оценка)

ЭтапВремя
1. Анализ и подготовка окружения30 мин
2. Проектирование и реализация модуля статусов1 ч
3. Разработка эмулятора1 ч 30 мин
4. Логика эскалации1 ч 30 мин
5. Тестирование и оформление1 ч
Итого5 ч 30 мин

Примечание: Если Redis не установлен локально, добавить 15 минут на Docker. Для первого выполнения задачи можно увеличить оценку до 7 часов.


9. Связанные вопросы из базы знаний

ВопросТема
42Управление очередями задач (Task Queues)
118Шаблоны распределения нагрузки (Load Balancing)
205Проектирование систем с человеческим участием (Human-in-the-loop)
310Стратегии эскалации инцидентов (Escalation Policies)
487Консистентность данных в распределённых системах (Consistency models)
523Мониторинг состояния сервисов (Health checks, heartbeats)
671Атомарные операции в Redis (Transactions, Lua scripting)
739Тестирование асинхронного кода (pytest-asyncio)
840Паттерн «Пул ресурсов» (Resource Pool)
899Отказоустойчивость очередей (Dead letter queues)

10. Чек-лист самопроверки

  • У меня есть StatusManager, который хранит статусы и возвращает список свободных операторов.
  • Эмулятор меняет статусы случайным образом, но не нарушает логику (in_call возможен только из online).
  • Система эскалации перед назначением получает свободных операторов и не назначает занятому.
  • Все асинхронные корутины работают корректно и не блокируют event loop.
  • Я написал хотя бы один тест, который проверяет, что задача не назначается оператору в статусе in_call.
  • Документация (README) описывает, как запустить систему и что ожидать.