/* * 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.Collections.Concurrent; namespace ProtonVPN.Common.Core.Helpers; /// /// Runs only once at any time even if called multiple times during that [execution time + timeout interval]. /// Tasks called while a execution is running are released when execution is finished. /// Tasks called in the timeout interval wait for that interval to end before executing (and then only one is ran as expected). /// public class SingleFunctionExecutor { private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly ConcurrentQueue _queue = new(); private readonly Func _function; private readonly TimeSpan _retryTimeoutInterval; private DateTime _retryDateUtc = DateTime.MinValue; public SingleFunctionExecutor(Func function, TimeSpan? retryTimeoutInterval = null) { _function = function; _retryTimeoutInterval = retryTimeoutInterval ?? TimeSpan.Zero; } public async Task ExecuteAsync() { TaskCompletionSource tcs = new(); bool isFirst; TimeSpan? delay = null; await _semaphore.WaitAsync(); try { _queue.Enqueue(tcs); isFirst = _queue.Count == 1; DateTime utcNow = DateTime.UtcNow; if (utcNow < _retryDateUtc) { delay = _retryDateUtc - utcNow; } } finally { _semaphore.Release(); } if (delay is not null && delay.Value > TimeSpan.Zero && delay.Value.TotalMilliseconds < int.MaxValue) { await Task.Delay(delay.Value); } if (isFirst) { try { await _function(); } finally { await _semaphore.WaitAsync(); try { while (_queue.TryDequeue(out TaskCompletionSource? queuedTcs)) { queuedTcs.TrySetResult(); } _queue.Clear(); _retryDateUtc = DateTime.UtcNow + _retryTimeoutInterval; } finally { _semaphore.Release(); } } } else { await tcs.Task; } } }