using System.Diagnostics.CodeAnalysis;
using System.Threading.Channels;
namespace AIStudio.Tools.Services;
///
/// The queue of indexing runs: which data sources wait for a run, which run, and which get one more run afterward.
///
///
/// A data source holds at most one place in the queue, and at most one follow-up run while it runs.
///
/// The queue has two lanes, cf. DataSourceQueueLane. Every run in the front lane comes out before
/// any run in the back lane, and each lane hands out its runs in the order they came in. The data
/// sources in the back lane therefore take turns.
///
/// The channel carries copies of ids, each with its lane and a number. Why a run was asked for is
/// kept next to it, together with the number of the copy which counts, and taking a run out of the
/// queue takes both along. Any other copy of the same id which is still in the channel then finds
/// nothing, or another number, and is skipped. That is how a data source which was deleted or
/// repaired while it waited leaves nothing behind: no run in an outdated mode, no second run
/// circling through the queue next to the one which was queued anew, and no copy in the front lane
/// which lets a later run of the data source overtake the back lane.
///
/// Everything but the channel is guarded by one lock, so reserving, starting and completing a run
/// never see each other halfway.
///
internal sealed class DataSourceRunQueue
{
private readonly Channel channel = Channel.CreateUnboundedPrioritized(new UnboundedPrioritizedChannelOptions { Comparer = Comparer.Create(CompareCopies) });
///
/// The data sources which wait for a run, each with why it was asked for.
///
private readonly Dictionary queuedModes = new(StringComparer.OrdinalIgnoreCase);
///
/// The data sources which wait in the channel, each with the number of the copy which counts.
///
///
/// A data source whose place is reserved gets its number once it is enqueued.
///
private readonly Dictionary queuedCopyNumbers = new(StringComparer.OrdinalIgnoreCase);
///
/// The data sources whose run has started and not completed yet.
///
private readonly HashSet runningIds = new(StringComparer.OrdinalIgnoreCase);
///
/// The running data sources which get one more run once the current one completes, each with why it was asked for.
///
private readonly Dictionary pendingModes = new(StringComparer.OrdinalIgnoreCase);
private readonly Lock stateLock = new();
private long lastCopyNumber;
///
/// One copy of the id of a data source in the channel.
///
/// The id of the data source.
/// The lane the copy waits in.
/// Counts up with every copy. It keeps each lane in the order its copies came in, and tells the copy which counts from those left behind.
private sealed record QueuedCopy(string DataSourceId, DataSourceQueueLane Lane, long Number);
///
/// Reserves a place in the queue for a data source, or one follow-up run while it runs.
///
///
/// A request for a data source which holds a place or a follow-up run already merges into it.
/// The first request decides why the run happens, and any later one only confirms that it is
/// needed. A manual retry is the exception and replaces any other mode: it is the only one which
/// signs in to a mailbox again after its server refused a sign-in, cf. MailboxIndexer. Behind a
/// sync at the interval which waits already, the retry the user asked for would be lost.
///
/// The place is taken once its id is in the channel, cf. EnqueueAsync.
///
/// The id of the data source.
/// Whether a request while the data source runs leaves a follow-up run.
/// Why the run is asked for.
/// QUEUED when the caller has to enqueue the data source now, or what became of the request instead.
public DataSourceQueueRequestResult TryReserve(string dataSourceId, bool queueAfterCurrentRun, DataSourceEmbeddingRefreshMode refreshMode)
{
lock (this.stateLock)
{
if (this.runningIds.Contains(dataSourceId))
{
if (!queueAfterCurrentRun)
return DataSourceQueueRequestResult.RUNNING;
return TryAddOrMerge(this.pendingModes, dataSourceId, refreshMode)
? DataSourceQueueRequestResult.RUNNING_MARKED_PENDING
: DataSourceQueueRequestResult.RUNNING;
}
return TryAddOrMerge(this.queuedModes, dataSourceId, refreshMode)
? DataSourceQueueRequestResult.QUEUED
: DataSourceQueueRequestResult.ALREADY_QUEUED;
}
}
///
/// Puts a data source into the queue, once its place is reserved.
///
/// The id of the data source.
/// The lane the data source waits in.
/// The cancellation token.
public ValueTask EnqueueAsync(string dataSourceId, DataSourceQueueLane lane, CancellationToken token = default)
{
QueuedCopy copy;
lock (this.stateLock)
{
// Cleared since its place was reserved, e.g. because the data source was deleted:
if (!this.queuedModes.ContainsKey(dataSourceId))
return ValueTask.CompletedTask;
copy = new(dataSourceId, lane, ++this.lastCopyNumber);
this.queuedCopyNumbers[dataSourceId] = copy.Number;
}
return this.channel.Writer.WriteAsync(copy, token);
}
///
/// Waits for the next run, takes it out of the queue, and marks it as running.
///
/// The cancellation token.
/// The run.
public async ValueTask DequeueAsync(CancellationToken token)
{
while (true)
{
if (this.TryDequeue(out var runRequest))
return runRequest;
if (!await this.channel.Reader.WaitToReadAsync(token))
throw new ChannelClosedException();
}
}
///
/// Takes the next run out of the queue, when there is one, and marks it as running.
///
/// The run, when there is one.
/// True when a run was taken out of the queue.
public bool TryDequeue([NotNullWhen(true)] out DataSourceRunRequest? runRequest)
{
while (this.channel.Reader.TryRead(out var copy))
{
lock (this.stateLock)
{
// Released or cleared while it waited, or enqueued anew since, with another copy which counts instead:
if (!this.queuedCopyNumbers.TryGetValue(copy.DataSourceId, out var number) || number != copy.Number)
continue;
this.queuedCopyNumbers.Remove(copy.DataSourceId);
this.queuedModes.Remove(copy.DataSourceId, out var refreshMode);
this.runningIds.Add(copy.DataSourceId);
runRequest = new(copy.DataSourceId, refreshMode);
return true;
}
}
runRequest = null;
return false;
}
///
/// Marks a data source as running which did not come out of the queue, unless it runs or waits in the queue already.
///
///
/// For the startup check, which works through every data source on its own. Its runs have to
/// count as running as well: a request during one of them then leaves one follow-up run, instead
/// of a second run next to it.
///
/// The id of the data source.
/// True when the caller runs the data source now, and completes the run with TryComplete.
public bool TryBeginRun(string dataSourceId)
{
lock (this.stateLock)
{
if (this.queuedModes.ContainsKey(dataSourceId))
return false;
return this.runningIds.Add(dataSourceId);
}
}
///
/// Marks the run of a data source as completed, and reserves the place of its follow-up run when it has one.
///
/// The id of the data source.
/// False drops the follow-up run, e.g. because the data source is gone.
/// Why the follow-up run was asked for.
/// True when the caller has to enqueue the follow-up run now.
public bool TryComplete(string dataSourceId, bool requeuePending, out DataSourceEmbeddingRefreshMode pendingRefreshMode)
{
lock (this.stateLock)
{
this.runningIds.Remove(dataSourceId);
if (!this.pendingModes.Remove(dataSourceId, out pendingRefreshMode))
return false;
return requeuePending && TryAddOrMerge(this.queuedModes, dataSourceId, pendingRefreshMode);
}
}
///
/// Gives up the reserved place of a data source, because it could not be enqueued.
///
/// The id of the data source.
public void Release(string dataSourceId)
{
lock (this.stateLock)
{
this.queuedModes.Remove(dataSourceId);
this.queuedCopyNumbers.Remove(dataSourceId);
}
}
///
/// Forgets the place and the follow-up run of a data source, e.g. because it was deleted.
///
///
/// A run which is going on stays marked as running until it completes. Whoever calls this stops
/// that run on their own.
///
/// The id of the data source.
public void Clear(string dataSourceId)
{
lock (this.stateLock)
{
this.queuedModes.Remove(dataSourceId);
this.queuedCopyNumbers.Remove(dataSourceId);
this.pendingModes.Remove(dataSourceId);
}
}
///
/// Orders the copies in the channel: the front lane before the back lane, and each lane in the order its copies came in.
///
private static int CompareCopies(QueuedCopy x, QueuedCopy y)
{
if (x.Lane != y.Lane)
return x.Lane is DataSourceQueueLane.FRONT ? -1 : 1;
return x.Number.CompareTo(y.Number);
}
///
/// Records why a run of a data source was asked for, or merges the request into the one recorded already.
///
/// The recorded runs.
/// The id of the data source.
/// Why the run is asked for.
/// True when there was no run recorded for the data source yet.
private static bool TryAddOrMerge(Dictionary modes, string dataSourceId, DataSourceEmbeddingRefreshMode refreshMode)
{
if (modes.TryAdd(dataSourceId, refreshMode))
return true;
if (refreshMode is DataSourceEmbeddingRefreshMode.MANUAL_RETRY)
modes[dataSourceId] = refreshMode;
return false;
}
}