Присутствие и typing
Перед началом: доставка и sync (Модуль 6). Инструменты: Redis, Go, WebSocket. Трудоёмкость: ~5 ч.
🔥 Инцидент: «Призраки онлайн»
Первая версия presence хранила статус в таблице users: вошёл — online,
вышел — offline. Через неделю аналитика: 40% пользователей «онлайн»
круглосуточно. Причина банальна: закрытый ноутбук не отправляет «выход» —
ни logout, ни крестик никто не жмёт. Вторая половина инцидента — typing:
каждое нажатие клавиши писалось в messages-подобную таблицу, за месяц
она набрала 90 миллионов «печатает» — и ни одной строки, которую стоило бы
хранить. В этом модуле присутствие и typing делаем так, как положено:
эфемерно, с TTL, вне основной БД.
📖 Теория
7.1 Presence — это TTL, а не статус
Присутствие — производное от «клиент сейчас держит соединение». Значит:
- источник истины — соединения (у нас уже есть таблица
userID → connsиз Модуля 6); - Redis-ключ
presence:{user_id}живёт с TTL (SETEX, 60 секунд) и обновляется самим фактом активности соединения: клиент шлёт ping, сервер отвечает — и заодно продлевает ключ; - смерть процесса/ноутбука = ключ истекает сам. Никаких «забыл выйти».
go// heartbeat: раз в 20 секунд на активное соединение redis.SetEx(ctx, "presence:"+userID, "1", 60*time.Second) // при закрытии ПОСЛЕДНЕГО соединения пользователя redis.Del(ctx, "presence:"+userID) presenceChanged(userID, "offline", lastSeenAt)
Тонкость с несколькими вкладками: offline шлём только когда закрылось
последнее соединение (счётчик из Модуля 6), иначе закрытие одной
вкладки «гасит» пользователя, который тут же откроет другую.
7.2 Кто видит присутствие
Приватность — продуктовое решение, фиксируем явно:
- direct-собеседники — видят всегда: это ожидание WhatsApp/Telegram;
- группы — не показываем по умолчанию: N×M событий на группу — это и нагрузка, и слежка. Альтернатива (показывать) — обсуждаемая опция, не дефолт;
- событие
presence {user_id, status, last_seen}фан-аутится только в каналы direct-диалогов пользователя: кому надо — тем и уходит.
Статус при подключении: клиент получает срез (SELECT по собеседникам)
при sync — чтобы не ждать события.
7.3 Typing — эфемерное событие с ограничителем
Typing — противоположность надёжности: потеря события — норма (клиент рисует индикатор 3 секунды и гасит). Поэтому:
- не пишется в БД, не идёт через outbox — публикуется напрямую в канал;
- rate-limit: не чаще одного события на диалог раз в ~2 секунды (клиент троттлит, сервер — проверяет; дешёвый счётчик в Redis или in-memory token bucket на соединение);
- получатели: участники диалога, кроме отправителя.
jsonc// клиент → сервер {"type": "typing", "conversation_id": "…"} // сервер → участникам (кроме автора) {"type": "typing", "conversation_id": "…", "user_id": "…"}
7.4 Деградация без Redis
Что если Redis недоступен? Принцип: деградируют украшения, не ядро. Сообщения, sync, receipts живут в Postgres и не замечают Redis. Присутствие и typing — гаснут: ошибки Redis-вызовов логируются, но не всплывают пользователю; присутствие показывается как «неизвестно». Это тот самый случай, когда «фича не работает» лучше, чем «мессенджер не работает».
💻 Практика
-
Подключите Redis-клиент (в compose он уже поднят; в конфиг —
REDIS_URL, дефолтlocalhost:6380). -
Presence:
- на каждый активный WS-коннект — heartbeat-продление ключа (совместите с ping-loop из Модуля 6: раз в 20 секунд);
- при закрытии соединения:
releaseConnуже знает, последнее ли это соединение — на последнем делайтеDEL+presenceChanged; - событие
presenceфан-аут в direct-каналы пользователя; - в sync-ответ добавьте срез присутствия собеседников.
-
Typing:
- приём
typingот клиента: проверка участия + rate-limit (не чаще 1 события / 2 с на диалог; превышение — молча дропаем); - публикация в канал диалога без записи в БД.
- приём
-
Мини-клиент для проверки (до фронтенда Модуля 8):
- откройте
wscatили тестовую страничку от имени Алисы и Боба в одном прямом диалоге; - Боб «печатает» — Алиса видит
typingв консоли; - Боб закрывает вкладку — Алиса получает
presence offline.
- откройте
-
Тесты и сценарии:
- две вкладки Боба: закрытие одной не шлёт
offline, закрытие второй — шлёт; - убийство Redis в процессе: сообщения продолжают ходить, presence не сыплет ошибками наружу (в логах — warning);
- рестарт сервера: через TTL presence самоочищается (нет вечных «онлайн»).
- две вкладки Боба: закрытие одной не шлёт
Чек-лист модуля: статус живёт TTL-ключом и обновляется heartbeat'ом; несколько вкладок не «гасят» пользователя; typing не попадает в БД и ограничен по частоте; Redis-падение не ломает сообщения.
🧩 Quiz
offline?presence-событие в нашей политике приватности?✅ Итоги модуля
- Presence — TTL-ключ Redis плюс heartbeat соединения: «вечных онлайн» не бывает по устройству.
- Мультивкладочность учтена счётчиком соединений — offline только на последнем.
- Приватность присутствия — явная политика: direct да, группы нет.
- Typing — эфемерное событие мимо outbox, с throttle на сервере.
- Деградация без Redis не трогает ядро: сообщения, история, receipts живы.
🔗 Мостик к Модулю 8
Бэкенд-ядро готово: диалоги, порядок, группы, доставка, присутствие. Дальше — клиент, и первое, что видит пользователь при входе, — список диалогов: непрочитанные, последние сообщения, живые статусы. Строим его на FSD-слоях, где серверные данные и WS-события сходятся в одной модели.