Files
crm-calls/calls/backend/Calls.Api/Recording/CallGate.cs

77 lines
3.7 KiB
C#
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.
using System.Collections.Concurrent;
namespace Calls.Api.Recording;
/// <summary>
/// Лимит одновременных звонков (MAX_CONCURRENT_CALLS). Звонок занят, пока в его комнате есть участники
/// (данные монитора из prosody), плюс короткая бронь комнаты после выдачи пропуска — человек ещё подключается.
/// В уже идущий звонок пускаем всегда. Если данных о занятости нет (prosody недоступен) — не блокируем.
/// </summary>
public sealed class CallGate(RecordingOptions opt, TimeProvider? time = null, ILogger<CallGate>? log = null)
{
public const string BusyMessage = "Зараз уже йде інший дзвінок, спробуйте пізніше.";
public static readonly TimeSpan Reservation = TimeSpan.FromMinutes(2);
public static readonly TimeSpan SnapshotMaxAge = TimeSpan.FromSeconds(30);
private readonly TimeProvider _time = time ?? TimeProvider.System;
private readonly ConcurrentDictionary<string, DateTimeOffset> _reserved = new();
private readonly Lock _lock = new();
private Dictionary<string, int> _occupied = [];
private DateTimeOffset _snapshotAt = DateTimeOffset.MinValue;
private DateTimeOffset _warnedAt = DateTimeOffset.MinValue;
/// <summary>Монитор: занятость комнат (только комнаты с участниками).</summary>
public void Update(Dictionary<string, int> occupied)
{
lock (_lock)
{
// Звонок закончился (была занята — стала пуста): бронь больше не нужна, место свободно сразу.
foreach (var (room, n) in _occupied)
if (n > 0 && !occupied.ContainsKey(room)) _reserved.TryRemove(room, out _);
_occupied = occupied;
_snapshotAt = _time.GetUtcNow();
}
}
public IReadOnlyCollection<string> ActiveRooms()
{
lock (_lock) return Active(_time.GetUtcNow());
}
private HashSet<string> Active(DateTimeOffset now)
{
var set = new HashSet<string>(_occupied.Where(kv => kv.Value > 0).Select(kv => kv.Key));
foreach (var (room, until) in _reserved)
if (until > now) set.Add(room); else _reserved.TryRemove(room, out _);
return set;
}
/// <summary>Можно ли войти в комнату (без брони) — проверка перед «Приєднатися».</summary>
public bool CanEnter(string room) => Decide(room, reserve: false);
/// <summary>Выдача пропуска: можно ли войти; если да — комната бронируется на пару минут.</summary>
public bool TryEnter(string room) => Decide(room, reserve: true);
private bool Decide(string room, bool reserve)
{
lock (_lock)
{
var now = _time.GetUtcNow();
if (now - _snapshotAt > SnapshotMaxAge)
{
if (now - _warnedAt > TimeSpan.FromMinutes(1))
{
log?.LogWarning("Немає свіжих даних про зайнятість кімнат (prosody), ліміт дзвінків не застосовується");
_warnedAt = now;
}
if (reserve) _reserved[room] = now + Reservation;
return true;
}
var active = Active(now);
var ok = active.Contains(room) || active.Count < opt.MaxConcurrentCalls;
if (ok && reserve) _reserved[room] = now + Reservation;
return ok;
}
}
}