using System.Collections.Concurrent; using Deal.Grpc.Sources; using Deal.Telegram.Core; using Deal.Telegram.Sessions; using Deal.Telegram.Telegram; namespace Deal.Telegram.Dialogs; /// /// Backfill диалога /// public sealed class BackfillService { /// /// Сколько последних сообщений читает backfill. /// public const int MessagesLimit = 10; /// /// Нижняя граница анти-бан-паузы между сообщениями. /// public const double MinPerMessageDelaySeconds = 1.5; /// /// Верхняя граница анти-бан-паузы между сообщениями. /// public const double MaxPerMessageDelaySeconds = 3.0; /// /// Нижняя граница паузы между диалогами одного тенанта. /// public const double MinPerDialogDelaySeconds = 3.0; /// /// Верхняя граница паузы между диалогами одного тенанта. /// public const double MaxPerDialogDelaySeconds = 6.0; private readonly SessionFarm _sessionFarm; private readonly ICoreIngressClient _ingress; private readonly IBackfillPacer _pacer; private readonly ILogger _logger; private readonly ConcurrentDictionary<(string TenantId, string DialogId), byte> _running = new(); private readonly ConcurrentDictionary _tenantGates = new(StringComparer.Ordinal); // Момент завершения последнего backfill тенанта (пауза 3–6 с между диалогами). private readonly Dictionary _tenantLastFinishUtc = new(StringComparer.Ordinal); private readonly object _lastFinishGate = new(); /// /// Создаёт службу backfill'а. /// /// Пул сессий тенантов (read-операции диалогов). /// Канал в ядро (PushSource). /// Анти-бан-паузы (реальный — случайные, тесты — фейк). /// Логгер. public BackfillService( SessionFarm sessionFarm, ICoreIngressClient ingress, IBackfillPacer pacer, ILogger logger) { _sessionFarm = sessionFarm; _ingress = ingress; _pacer = pacer; _logger = logger; } /// /// Перечитывает последние сообщения диалога в ядро /// /// Id тенанта. /// Подписанный id диалога. /// True — «Перечитать» по кнопке (семантика флага — в ядре, см. класс). /// Отмена операции. /// Сколько сообщений отправлено в ядро (0 — диалог уже перечитывается/нет текстов). public async Task ExecuteAsync( string tenantId, string dialogId, bool force, CancellationToken cancellationToken) { var key = (tenantId, dialogId); if (!_running.TryAdd(key, 0)) { _logger.LogInformation("Аудит: backfill {TenantId} {DialogId} → пропущен (уже перечитывается)", tenantId, dialogId); return 0; } try { SemaphoreSlim gate = _tenantGates.GetOrAdd(tenantId, static _ => new SemaphoreSlim(1, 1)); await gate.WaitAsync(cancellationToken).ConfigureAwait(false); try { await EnsureDialogSpacingAsync(tenantId, cancellationToken).ConfigureAwait(false); int processed = await BackfillDialogAsync(tenantId, dialogId, cancellationToken).ConfigureAwait(false); lock (_lastFinishGate) { _tenantLastFinishUtc[tenantId] = DateTime.UtcNow; } _logger.LogInformation( "Аудит: backfill {TenantId} {DialogId} → {Processed} сообщений в ядро (force {Force})", tenantId, dialogId, processed, force); return processed; } finally { gate.Release(); } } finally { _running.TryRemove(key, out _); } } private async Task EnsureDialogSpacingAsync(string tenantId, CancellationToken cancellationToken) { DateTime? lastFinishUtc; lock (_lastFinishGate) { lastFinishUtc = _tenantLastFinishUtc.TryGetValue(tenantId, out DateTime value) ? value : null; } if (lastFinishUtc is null) { return; } double elapsedSeconds = (DateTime.UtcNow - lastFinishUtc.Value).TotalSeconds; if (elapsedSeconds >= MaxPerDialogDelaySeconds) { return; } double minSeconds = Math.Max(0, MinPerDialogDelaySeconds - elapsedSeconds); double maxSeconds = MaxPerDialogDelaySeconds - elapsedSeconds; if (maxSeconds < minSeconds) { maxSeconds = minSeconds; } await _pacer.WaitAsync(minSeconds, maxSeconds, cancellationToken).ConfigureAwait(false); } // Читает и отправляет сообщения одного диалога (паузы между сообщениями, read-ack). // tenantId: Id тенанта. // dialogId: Подписанный id диалога. // cancellationToken: Отмена операции. // Возвращает: Сколько сообщений отправлено в ядро. private async Task BackfillDialogAsync( string tenantId, string dialogId, CancellationToken cancellationToken) { IReadOnlyList messages = await _sessionFarm .GetMessagesAsync(tenantId, dialogId, MessagesLimit, cancellationToken) .ConfigureAwait(false); int processed = 0; foreach (TelegramMessage message in messages.Reverse()) { PushSourceReply reply = await _ingress .PushSourceAsync(tenantId, DialogProtoMapper.ToSourceRequest(message), cancellationToken) .ConfigureAwait(false); if (reply.Accepted && !reply.Duplicate) { processed++; } await _pacer.WaitAsync(MinPerMessageDelaySeconds, MaxPerMessageDelaySeconds, cancellationToken).ConfigureAwait(false); } await _sessionFarm.MarkReadAsync(tenantId, dialogId, cancellationToken).ConfigureAwait(false); return processed; } }