Настроить человеческий фактор
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить человеческий фактор
1. Цель задачи
Реализовать механизм учёта занятости операторов (статусы online/offline, в разговоре) в системе диспетчеризации или поддержки. Интегрировать этот механизм с логикой эскалации задач, чтобы вновь поступающие или эскалированные запросы направлялись только тем операторам, которые действительно готовы их принять (свободны). Задача имитирует типичный production-сценарий, где человеческий фактор (занятость, доступность) критически влияет на SLA и нагрузку.
Ключевой результат Эскалация задач происходит только на свободных операторов, исключая ситуации, когда задача попадает к занятому коллеге.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Система с задачами и операторами (тестовая) | Разработать простой Python-фреймворк (события, задачи, операторы) или использовать уже имеющуюся (например, Pet-проект с очередями). |
| Данные о статусах операторов | Сгенерировать симулированные события: оператор залогинился/вылогинился, начал/завершил диалог. |
| Система эскалации (простая логика: при превышении времени берёт следующий тикет и назначает оператору) | Написать заново как часть задачи. |
| Инструмент для хранения статусов | Redis / SQLite / in-memory хранилище (выбор по усмотрению). |
| Дашборд или CLI для мониторинга статусов | Опционально — простой дашборд на Streamlit или вывод в консоль. |
Если нет реального инструмента — симулируем:
- Разработать эмулятор работы операторов — скрипт на Python, который случайным образом меняет статусы (online/offline/in_call) каждые 5–30 секунд.
- Эмулятор должен отправлять события в общий канал (Redis pub/sub или очередь).
- Реализовать симуляцию потока задач: каждые 10–20 секунд генерируется новая задача с приоритетом и временем жизни.
- Система эскалации должна реагировать на статусы операторов и назначать задачу только тем, кто в статусе "online" и не "in_call".
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык реализации | Python 3.11+ | Основная логика системы |
| Хранилище статусов | Redis (предпочтительно) или SQLite | Быстрое чтение/запись текущего статуса, атомарные обновления |
| Очередь сообщений / Pub/Sub | Redis pub/sub или RabbitMQ | Передача событий статусов и задач между эмулятором и эскалатором |
| Асинхронность | asyncio (в Python) | Неблокирующая обработка событий |
| Тестирование | pytest + moto (если Redis) | Юнит-тесты для модуля статусов и эскалации |
| Мониторинг (опционально) | Streamlit / Dash / просто логи | Визуализация статусов и истории назначений |
4. Этапы выполнения
Этап 1: Анализ требований и подготовка окружения (30 мин)
Действия
- Определить модели данных:
- 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.
- Выбрать способ хранения: Redis с ключами вида operator:{id}:status и operator:{id}:current_task.
- Развернуть Redis (локальный Docker docker run -d -p 6379:6379 redis:7-alpine) или использовать in-memory dict с thread-safe замками.
- Создать виртуальное окружение, установить зависимости:
redis[hiredis],pytest,pytest-asyncio,async-timeout.
Ожидаемый результат этапа Рабочий Redis (или in-memory заменитель), понимание модели данных, написан скелет проекта с requirements.txt.
Этап 2: Проектирование модуля учёта статусов (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]: - Реализовать класс
RedisStatusManager:- Использовать
hsetдля хранения полей{status, current_task}. get_free_operators— атомарныйSMEMBERSоператоров со статусомonlineи безcurrent_task.- Добавить TTL на статус (например, 5 минут для offline).
- Использовать
- Реализовать класс
InMemoryStatusManagerсasyncio.Lockдля использования без Redis. - Написать юнит-тесты для каждого метода (используя
pytestиfake_redisпри Redis).
Ожидаемый результат этапа Работающий модуль статусов, покрытый тестами, 100% зелёных.
Этап 3: Разработка эмулятора операторов и задач (1.5 часа)
Действия
- Создать эмулятор
OperatorSimulator:- Запускает N операторов (N=3–5).
- Каждый оператор — корутина, которая циклически:
- Меняет статус на
online, ждет случайное время (10–60 сек). - Переключается в
in_callна случайное время (30–120 сек). - После завершения разговора возвращается в
online. - В конце смены уходит в
offline.
- Меняет статус на
- Использовать
StatusManagerдля обновления статусов.
- Создать эмулятор задач
TaskSimulator: - Обеспечить логирование ключевых событий (смена статуса, создание задачи) через
logging.
Ожидаемый результат этапа При запуске эмулятора видно в логах смену статусов и генерацию задач; статусы корректно записываются в Redis.
Этап 4: Реализация логики эскалации с учётом статусов (1.5 часа)
Действия
- Разработать модуль
Escalator:- Подписывается на очередь задач.
- Когда задача «висит» дольше
max_wait_time(например, 30 сек), запускается эскалация.
- Реализовать функцию
assign_task(task, status_manager):- Вызвать
status_manager.get_free_operators(). - Если список пуст — записать в лог
No free operators, waitingи вернуть задачу в очередь (backoff). - Если есть свободные — выбрать одного (round-robin или наименее загруженного), установить ему
current_taskи статусin_call.
- Вызвать
- Добавить обработку таймаутов: если оператор не отвечает (не завершает задачу за разумное время) — принудительно освободить оператора и передать задачу другому.
- Написать интеграционные тесты с эмулятором: за 2 минуты работы системы убедиться, что ни одна задача не назначается оператору, который уже
in_call.
Ожидаемый результат этапа Система эскалации назначает задачи только свободным операторам; в логах нет случаев operator busy при попытке назначения.
Этап 5: Тестирование, отладка и финальное оформление (1 час)
Действия
- Запустить систему с эмулятором на 2–3 минуты, собрать статистику:
- Количество задач создано, назначено, эскалировано.
- Процент задач, назначенных с первого раза (без ожидания свободного оператора).
- Время ожидания задачи при отсутствии свободных.
- Проверить граничные случаи:
- Написать итоговую документацию: README с инструкцией запуска, краткими пояснениями архитектуры.
- (Опционально) Создать простую визуализацию в
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) описывает, как запустить систему и что ожидать.