lizerk f15e8aca3f feat(ldap): add LDAP sync retry queue with Celery
- Add LdapSyncQueue model for deferred LDAP operations
- Add Celery tasks: process_ldap_queue_item, process_ldap_queue
- Refactor all LDAP signals (employee, department, group, position)
  to enqueue operations on DirectoryLdapError/DirectoryServiceError
- Add _queue.py helper for enqueuing with automatic Celery dispatch
- Add Celery Beat periodic task (every 60s) for queue processing
- Add management command: ldap_queue (status/retry/cleanup)
- Add migration 0043_ldap_sync_queue
- Add 18 tests covering model, tasks, signals, and enqueue logic
- Include LDAP services refactoring (base_service, constants,
  user_login_service, user_mapper_service, user_password_service,
  phone_utils)
2026-03-19 15:07:34 +03:00

112 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Management-команда для работы с очередью LDAP retry.
Примеры:
# Показать статистику очереди
python manage.py ldap_queue status
# Повторить все pending-операции
python manage.py ldap_queue retry
# Повторить failed-операции (сбрасывает счётчик попыток)
python manage.py ldap_queue retry --failed
# Очистить completed-записи старше 7 дней
python manage.py ldap_queue cleanup --days 7
"""
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
class Command(BaseCommand):
help = "Управление очередью отложенных LDAP-операций (retry)"
def add_arguments(self, parser):
subparsers = parser.add_subparsers(dest="action")
subparsers.add_parser("status", help="Показать статистику очереди")
retry_parser = subparsers.add_parser("retry", help="Запустить повтор операций")
retry_parser.add_argument(
"--failed", action="store_true",
help="Включить failed-записи (сбросить счётчик попыток)",
)
cleanup_parser = subparsers.add_parser("cleanup", help="Очистить старые записи")
cleanup_parser.add_argument(
"--days", type=int, default=30,
help="Удалять completed/failed записи старше N дней (по умолчанию 30)",
)
def handle(self, *args, **options):
action = options.get("action")
if action == "status":
self._status()
elif action == "retry":
self._retry(include_failed=options.get("failed", False))
elif action == "cleanup":
self._cleanup(days=options["days"])
else:
self.stderr.write("Укажите действие: status, retry, cleanup")
def _status(self):
from employees.models import LdapSyncQueue
total = LdapSyncQueue.objects.count()
by_status = {}
for s in LdapSyncQueue.Status:
count = LdapSyncQueue.objects.filter(status=s.value).count()
by_status[s.label] = count
self.stdout.write(f"\nОчередь LDAP sync: {total} записей")
for label, count in by_status.items():
self.stdout.write(f" {label}: {count}")
# Последние 5 failed
failed = LdapSyncQueue.objects.filter(
status=LdapSyncQueue.Status.FAILED,
).order_by("-updated_at")[:5]
if failed:
self.stdout.write("\nПоследние ошибки:")
for item in failed:
self.stdout.write(
f" [{item.pk}] {item.operation} {item.model_name}:{item.object_pk} "
f"попыток: {item.attempts}/{item.max_attempts} — {item.last_error[:100]}"
)
def _retry(self, include_failed: bool):
from employees.models import LdapSyncQueue
from employees.tasks import process_ldap_queue_item
qs = LdapSyncQueue.objects.filter(status=LdapSyncQueue.Status.PENDING)
if include_failed:
failed_qs = LdapSyncQueue.objects.filter(status=LdapSyncQueue.Status.FAILED)
reset_count = failed_qs.count()
# Сбрасываем failed → pending
failed_qs.update(
status=LdapSyncQueue.Status.PENDING,
attempts=0,
next_retry_at=None,
)
self.stdout.write(f"Сброшено {reset_count} failed-записей → pending")
qs = LdapSyncQueue.objects.filter(status=LdapSyncQueue.Status.PENDING)
pks = list(qs.values_list("pk", flat=True))
for pk in pks:
process_ldap_queue_item.delay(pk)
self.stdout.write(self.style.SUCCESS(f"Запущено {len(pks)} задач в Celery"))
def _cleanup(self, days: int):
from employees.models import LdapSyncQueue
cutoff = timezone.now() - timedelta(days=days)
deleted, _ = LdapSyncQueue.objects.filter(
status__in=[LdapSyncQueue.Status.COMPLETED, LdapSyncQueue.Status.FAILED],
updated_at__lt=cutoff,
).delete()
self.stdout.write(self.style.SUCCESS(f"Удалено {deleted} старых записей"))