mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 23:07:23 +00:00
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
107 lines
4.8 KiB
C#
107 lines
4.8 KiB
C#
using AIStudio.Settings.DataModel;
|
|
|
|
namespace AIStudio.Tools.Services;
|
|
|
|
/// <summary>
|
|
/// Keeps what the servers of ERI data sources answered to the check of their security requirements.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// One message asks this several times: Semantic Search checks the data sources when it is
|
|
/// offered and again before every search, and the classic RAG process checks them with every
|
|
/// message. Each check signs in, and a server which limits sign-ins or sessions would start
|
|
/// refusing. A check which is still running is joined rather than started a second time.<br/><br/>
|
|
/// The server's operator may change the requirements at any time, so an answer is kept for one
|
|
/// minute only. That covers the rounds of one answer, while a change still takes effect soon. A
|
|
/// server which did not answer is asked again sooner: it may well be back, and until then, its
|
|
/// data source is not searched anyway. A changed configuration, e.g., another server or another
|
|
/// account, asks anew right away.<br/><br/>
|
|
/// The minute holds for an answer which lets the data reach any provider as well, and that is a
|
|
/// deliberate trade-off. When an operator opened a server by mistake and corrects it, every AI
|
|
/// Studio follows within that minute rather than with its next check: until then, a chat with a
|
|
/// cloud provider may still search the server. Nothing outlasts the running app, though, so no
|
|
/// answer, however open, sticks for longer.<br/><br/>
|
|
/// Whenever a check starts, the expired ones are dropped, so that data sources which were deleted
|
|
/// or changed do not pile up.
|
|
/// </remarks>
|
|
/// <param name="fetch">Signs in to the server of a data source and asks for its requirements.</param>
|
|
/// <param name="timeProvider">Tells the time the checks are started at.</param>
|
|
internal sealed class ERISecurityCheckCache(Func<DataSourceERI_V1, Task<ERISecurityCheckResult>> fetch, TimeProvider timeProvider)
|
|
{
|
|
/// <summary>
|
|
/// How long the answer of an ERI server to the check of its security requirements is used.
|
|
/// </summary>
|
|
private static readonly TimeSpan ANSWER_LIFETIME = TimeSpan.FromMinutes(1);
|
|
|
|
/// <summary>
|
|
/// How long an ERI server which did not answer the check is not asked again, counted from the start of the check.
|
|
/// </summary>
|
|
private static readonly TimeSpan NO_ANSWER_LIFETIME = TimeSpan.FromSeconds(15);
|
|
|
|
/// <summary>
|
|
/// A check of an ERI server, which may still be running, together with the configuration it was started with.
|
|
/// </summary>
|
|
private readonly record struct SecurityCheck(DataSourceERI_V1 DataSource, Task<ERISecurityCheckResult> Result, DateTimeOffset StartedAt);
|
|
|
|
private readonly Lock checksLock = new();
|
|
private readonly Dictionary<string, SecurityCheck> checks = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>
|
|
/// What the server of an ERI data source requires, asked anew only once the last answer expired.
|
|
/// </summary>
|
|
/// <param name="source">The ERI data source.</param>
|
|
/// <returns>Whether the server answered, and its requirements when it did.</returns>
|
|
public Task<ERISecurityCheckResult> GetAsync(DataSourceERI_V1 source)
|
|
{
|
|
lock (this.checksLock)
|
|
{
|
|
var now = timeProvider.GetUtcNow();
|
|
if (this.checks.TryGetValue(source.Id, out var known) && known.DataSource.Equals(source) && IsValid(known.Result, now - known.StartedAt))
|
|
return known.Result;
|
|
|
|
this.RemoveExpired(now);
|
|
var check = new SecurityCheck(source, fetch(source), now);
|
|
this.checks[source.Id] = check;
|
|
return check.Result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// How many checks are kept right now, expired ones included.
|
|
/// </summary>
|
|
internal int Count
|
|
{
|
|
get
|
|
{
|
|
lock (this.checksLock)
|
|
return this.checks.Count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a check of an ERI server may still be used instead of asking the server again.
|
|
/// </summary>
|
|
/// <param name="result">The check, which may still be running.</param>
|
|
/// <param name="age">How long ago the check was started.</param>
|
|
/// <returns>True when the check is still running or its answer has not expired yet.</returns>
|
|
internal static bool IsValid(Task<ERISecurityCheckResult> result, TimeSpan age)
|
|
{
|
|
if (!result.IsCompleted)
|
|
return true;
|
|
|
|
if (!result.IsCompletedSuccessfully)
|
|
return false;
|
|
|
|
return age < (result.Result.IsReachable ? ANSWER_LIFETIME : NO_ANSWER_LIFETIME);
|
|
}
|
|
|
|
private void RemoveExpired(DateTimeOffset now)
|
|
{
|
|
var expiredIds = this.checks
|
|
.Where(pair => !IsValid(pair.Value.Result, now - pair.Value.StartedAt))
|
|
.Select(pair => pair.Key)
|
|
.ToList();
|
|
|
|
foreach (var id in expiredIds)
|
|
this.checks.Remove(id);
|
|
}
|
|
} |