""" Celery задачи для модуля communications """ import logging from datetime import timedelta from celery import shared_task from django.db import transaction from django.utils import timezone logger = logging.getLogger(__name__) @shared_task def cleanup_orphaned_attachments(): """ Очистка временных вложений без привязки к сообщению. Удаляет MessageAttachment с message=null старше 1 часа. """ from communications.models import MessageAttachment cutoff_time = timezone.now() - timedelta(hours=1) orphaned = MessageAttachment.objects.filter( message__isnull=True, uploaded_at__lt=cutoff_time ) count = 0 # Candidate IDs are only hints. An upload may be claimed while this task is # running, so recheck it under the same row lock used by message edits. for attachment_id in orphaned.values_list("pk", flat=True).iterator(chunk_size=200): try: with transaction.atomic(): attachment = ( MessageAttachment.objects.select_for_update(skip_locked=True) .filter( pk=attachment_id, message__isnull=True, uploaded_at__lt=cutoff_time, ) .first() ) if attachment is None: continue files = [ (file.storage, file.name) for file in (attachment.file, attachment.thumbnail) if file ] attachment.delete() for storage, name in files: transaction.on_commit( lambda storage=storage, name=name: storage.delete(name), robust=True, ) count += 1 except Exception as exc: logger.exception( "Orphaned chat attachment deletion failed", extra={ "event": "chat.attachment_cleanup.item_failed", "attachment_id": attachment_id, "error_type": type(exc).__name__, }, ) if count: logger.info( "Orphaned chat attachment cleanup completed", extra={ "event": "chat.attachment_cleanup.completed", "attachment_count": count, }, ) return count