/*
* Copyright (c) 2025 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ConnectLogs;
using ProtonVPN.ProcessCommunication.Contracts.Entities;
using Timer = System.Timers.Timer;
namespace ProtonVPN.Service.ControllerRetries;
public class ControllerRetryManager : IControllerRetryManager
{
private readonly TimeSpan _cleanupTimerInterval = TimeSpan.FromMinutes(10);
private readonly TimeSpan _expirationInterval = TimeSpan.FromHours(1);
private readonly ILogger _logger;
private readonly Timer _timer;
private readonly object _lock = new object();
private readonly ConcurrentDictionary _receivedRetryIds = [];
public ControllerRetryManager(ILogger logger)
{
_logger = logger;
_timer = new(_cleanupTimerInterval);
_timer.Elapsed += OnTimedEvent;
_timer.AutoReset = true;
_timer.Enabled = true;
}
private void OnTimedEvent(object sender, EventArgs e)
{
lock (_lock)
{
List> list = _receivedRetryIds.ToList();
foreach (KeyValuePair pair in list)
{
if (pair.Value <= DateTimeOffset.UtcNow)
{
_receivedRetryIds.TryRemove(pair.Key, out _);
}
}
}
}
public void EnforceRetryId(IRetryableEntity retryableEntity, [CallerMemberName] string sourceMemberName = "")
{
ThrowIfRetryIdIsEmpty(retryableEntity, sourceMemberName);
lock (_lock)
{
ThrowIfRetryIdWasAlreadyUsed(retryableEntity, sourceMemberName);
DateTimeOffset expirationDateUtc = DateTimeOffset.UtcNow + _expirationInterval;
_receivedRetryIds.AddOrUpdate(retryableEntity.RetryId, _ => expirationDateUtc, (_, _) => expirationDateUtc);
}
}
private void ThrowIfRetryIdIsEmpty(IRetryableEntity retryableEntity, string sourceMemberName)
{
if (retryableEntity.RetryId == Guid.Empty)
{
string message = $"{sourceMemberName} request dropped because the retry ID is empty.";
_logger.Info(message);
throw new ArgumentException(message);
}
}
private void ThrowIfRetryIdWasAlreadyUsed(IRetryableEntity retryableEntity, string sourceMemberName)
{
if (_receivedRetryIds.ContainsKey(retryableEntity.RetryId))
{
string message = $"{sourceMemberName} request dropped because the retry ID is repeated.";
_logger.Info(message);
throw new ArgumentException(message);
}
}
}