/*
* Copyright (c) 2023 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.Threading;
using System.Threading.Tasks;
using ProtonVPN.Common.Legacy.Threading.TaskQueueing;
namespace ProtonVPN.Common.Legacy.Threading;
///
/// Queues at most two tasks: one running and one pending. Next queued task will either
/// join pending or running task or cancel them to be scheduled.
///
/// Type of action performed by the tasks
/// Type of the result of queued tasks
public class CoalescingTaskQueue
{
///
/// The callback delegate called during each call to to make
/// decision on joining or cancelling pending and running task.
///
/// The new action being scheduled.
/// The pending or running action.
/// True if action is running, false if pending
///
public delegate CoalesceDecision CoalesceDecisionDelegate(TArg newArg, TArg arg, bool running);
private readonly CoalesceDecisionDelegate _decisionCallback;
private volatile CoalescingTaskQueueData _work = CoalescingTaskQueueData.Empty;
///
/// Creates the .
///
///
/// The callback delegate called during each call to to make
/// decision on joining or cancelling pending and running task. The callback delegate might be called
/// multiple times during one call to .
///
public CoalescingTaskQueue(CoalesceDecisionDelegate decisionCallback)
{
_decisionCallback = decisionCallback;
}
///
/// Schedules new task. The coalesce decision callback will be called for pending and running tasks
/// before returning from this method.
///
/// The task to schedule.
/// The action to schedule. Will be passed as an argument to the coalesce decision callback.
/// The result returned by the task.
public Task Enqueue(Func> action, TArg arg)
{
CoalescingTaskQueueData work;
CoalescingTaskQueueData newWork;
CoalescingTaskQueueData previousWork;
bool cancelRunningTask;
QueuedTask newTask = null;
do
{
work = _work;
cancelRunningTask = false;
newTask?.Dispose();
if (work.PendingTask != null)
{
// Makes decision on pending task
var decision = _decisionCallback(arg, work.PendingTask.Arg, false);
if (decision == CoalesceDecision.Join)
{
return _work.PendingTask.Task;
}
}
if (work.RunningTask != null && !work.RunningTask.CancellationRequested)
{
// Makes decision on running task
var decision = _decisionCallback(arg, work.RunningTask.Arg, true);
if (decision == CoalesceDecision.Join)
{
return _work.RunningTask.Task;
}
// Doesn't cancel running task until successfully scheduling new task, will do that later.
cancelRunningTask = decision == CoalesceDecision.Cancel;
}
newTask = new QueuedTask(action, arg);
if (work.RunningTask == null)
{
// No task is running. Schedule new running task, will start it later.
newWork = new CoalescingTaskQueueData(
null,
newTask);
}
else
{
// A task is running. Schedule new pending task.
newWork = new CoalescingTaskQueueData(
newTask,
work.RunningTask);
}
previousWork = Interlocked.CompareExchange(ref _work, newWork, work);
} while (previousWork != work);
// Scheduling succeeded
// Cancel previous pending task if any
var pendingTask = work.PendingTask;
if (pendingTask != null)
{
pendingTask.Cancel(false);
pendingTask.Dispose();
}
// Cancel running task if requested by decision callback
if (cancelRunningTask)
{
work.RunningTask.Cancel(true);
}
// Start new task if scheduled as running
if (newWork.RunningTask == newTask)
{
Run(newTask);
}
return newTask.Task;
}
///
/// Cancels both pending and running tasks if any.
///
public void Cancel()
{
CoalescingTaskQueueData work;
CoalescingTaskQueueData previousWork;
do
{
work = _work;
var newWork = new CoalescingTaskQueueData(
null,
work.RunningTask);
previousWork = Interlocked.CompareExchange(ref _work, newWork, work);
} while (previousWork != work);
work.PendingTask?.Cancel(false);
work.PendingTask?.Dispose();
work.RunningTask?.Cancel(true);
}
private void Run(QueuedTask queuedTask)
{
Task.Run(async () =>
{
await queuedTask.Run();
RunPendingTask();
});
}
///
/// Starts pending task if any.
///
private void RunPendingTask()
{
CoalescingTaskQueueData work;
CoalescingTaskQueueData newWork;
CoalescingTaskQueueData previousWork;
do
{
work = _work;
newWork = new CoalescingTaskQueueData(
null,
work.PendingTask);
previousWork = Interlocked.CompareExchange(ref _work, newWork, work);
} while (previousWork != work);
work.RunningTask?.Dispose();
if (newWork.RunningTask != null)
{
Run(newWork.RunningTask);
}
}
}
public enum CoalesceDecision
{
None,
Cancel,
Join
}