Corp/backend/notifications/consumers.py
Игорь Надеин 480840407a Добавлен механизм keepalive для WebSocket соединений
- Добавлен ping каждые 20 секунд в ChatConsumer
- Добавлен ping каждые 20 секунд в ChatListConsumer
- Добавлен ping каждые 20 секунд в NotificationConsumer
- Клиенты игнорируют ping сообщения
- Создан WEBSOCKET_KEEPALIVE_FIX.md с инструкциями для nginx
- Исправляет проблему закрытия соединений через 27 секунд
2025-11-28 13:57:59 +03:00

152 lines
5.3 KiB
Python

import asyncio
import json
import logging
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.utils import timezone
User = get_user_model()
logger = logging.getLogger(__name__)
class NotificationConsumer(AsyncWebsocketConsumer):
"""WebSocket consumer для уведомлений в реальном времени"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ping_task = None
self.ping_interval = 20 # Ping каждые 20 секунд
async def connect(self):
"""Подключение к WebSocket"""
self.user = self.scope['user']
if self.user.is_anonymous:
await self.close()
return
# Добавить в группу уведомлений пользователя
self.notification_group_name = f'notifications_{self.user.id}'
await self.channel_layer.group_add(
self.notification_group_name,
self.channel_name
)
await self.accept()
logger.info(f'NotificationConsumer connected: user={self.user.id}, group={self.notification_group_name}')
# Отправить текущее количество непрочитанных
unread_count = await self.get_unread_count()
await self.send(text_data=json.dumps({
'type': 'unread_count',
'count': unread_count
}))
# Запускаем ping цикл для keepalive
self.ping_task = asyncio.create_task(self._ping_loop())
async def disconnect(self, close_code):
"""Отключение от WebSocket"""
# Останавливаем ping цикл
if self.ping_task:
self.ping_task.cancel()
try:
await self.ping_task
except asyncio.CancelledError:
pass
if hasattr(self, 'notification_group_name'):
await self.channel_layer.group_discard(
self.notification_group_name,
self.channel_name
)
async def _ping_loop(self):
"""Отправка ping каждые N секунд для keepalive"""
try:
while True:
await asyncio.sleep(self.ping_interval)
await self.send(text_data=json.dumps({
'type': 'ping',
'timestamp': timezone.now().isoformat()
}))
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Ping loop error: {e}")
async def receive(self, text_data):
"""Получение сообщения от клиента"""
try:
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'mark_read':
notification_id = data.get('notification_id')
if notification_id:
await self.mark_notification_as_read(notification_id)
elif message_type == 'mark_all_read':
await self.mark_all_notifications_as_read()
except json.JSONDecodeError:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Invalid JSON'
}))
async def notification_new(self, event):
"""Отправить новое уведомление клиенту"""
logger.info(f'Sending new notification to user {self.user.id}: {event.get("notification", {}).get("title", "No title")}')
await self.send(text_data=json.dumps({
'type': 'new_notification',
'notification': event['notification']
}))
async def notification_count_update(self, event):
"""Обновить счетчик непрочитанных"""
await self.send(text_data=json.dumps({
'type': 'unread_count',
'count': event['count']
}))
@database_sync_to_async
def get_unread_count(self):
"""Получить количество непрочитанных уведомлений"""
from .models import Notification
return Notification.objects.filter(
recipient=self.user,
is_read=False,
is_archived=False
).count()
@database_sync_to_async
def mark_notification_as_read(self, notification_id):
"""Отметить уведомление как прочитанное"""
from .models import Notification
try:
notification = Notification.objects.get(
id=notification_id,
recipient=self.user
)
notification.mark_as_read()
return True
except Notification.DoesNotExist:
return False
@database_sync_to_async
def mark_all_notifications_as_read(self):
"""Отметить все уведомления как прочитанные"""
from .models import Notification
from django.utils import timezone
Notification.objects.filter(
recipient=self.user,
is_read=False
).update(
is_read=True,
read_at=timezone.now()
)