77 lines
3.7 KiB
C#
77 lines
3.7 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|