using System.Diagnostics.CodeAnalysis; using AIStudio.Provider; namespace AIStudio.Tools.Services.Indexing; /// /// One way through a data source, from its first step to the step which gets through it. /// /// /// A large data source is indexed in steps, each of them a run of its own, cf. RunShare. What one /// step learns about documents which failed has to reach the next one, or every step reads them /// again: that costs time, with a cloud provider money as well, and the list of failures would lose /// whatever the steps before found. A pass keeps it for as long as the steps go on. /// /// The embedding service keeps a pass only while the last step of its data source left more to do. /// Any other end of a run ends the pass, and so does a reset of the stored index. A manual retry /// begins a new pass, as does a run for other embedding settings. /// /// Documents which failed for a reason of their own are none of its business: the index remembers /// them for good, cf. PermanentIndexingFailure. Only the steps of one data source use a pass, one /// after the other, so it needs no lock. /// /// The embedding settings the pass works for. internal sealed class IndexingPass(DataSourceRunSignature runSignature) { /// /// How often a document is tried during one pass at most, whether it changed in between or not. /// /// /// A file which changes faster than it can be read would otherwise be read in every step, e.g. /// the log file of another program. /// private const int MAX_ATTEMPTS = 2; /// /// A document which failed during this pass. /// /// The fingerprint of the document when it failed the last time. /// How often it failed during this pass. /// Why it failed the last time, as the list of failures shows it. private sealed record FailedDocument(string Fingerprint, int Attempts, DataSourceEmbeddingFailure Failure); /// /// The documents which failed during this pass for a reason which may pass, by their keys. /// private readonly Dictionary failedDocuments = new(StringComparer.OrdinalIgnoreCase); // // Which kinds of provider failure the user was already told about in this pass. A rejected API // key is the same problem for every one of a few thousand documents, and one message is what it // takes to send the user to the settings. Kept for the whole pass, so a recurring failure such // as a rate limit does not show up anew with every step. // private readonly HashSet reportedFailureReasons = []; public DataSourceRunSignature RunSignature => runSignature; /// /// How many documents failed during this pass and are not tried again for now. /// public int FailedDocumentCount => this.failedDocuments.Count; /// /// The pass a run works in: the one the last step of its data source left, or a new one. /// /// The pass the last step of the data source left, or null when there is none. /// Why the run was asked for. /// The embedding settings the run works for. /// The pass of the run. public static IndexingPass ContinueOrBegin(IndexingPass? current, DataSourceEmbeddingRefreshMode refreshMode, DataSourceRunSignature runSignature) { // // A manual retry is the user asking for every failed document to be tried again, so it // never carries on with what a pass knows. Neither does a run for other embedding settings: // what failed with the old ones may well work with the new. // if (current is not null && refreshMode is not DataSourceEmbeddingRefreshMode.MANUAL_RETRY && current.RunSignature == runSignature) return current; return new(runSignature); } /// /// Records that a document failed for a reason which may pass. /// /// The key of the document. /// The fingerprint of the document as it was read. /// Why it failed, as the list of failures shows it. public void RecordFailure(string documentKey, string fingerprint, DataSourceEmbeddingFailure failure) { var attempts = this.failedDocuments.TryGetValue(documentKey, out var earlierFailure) ? earlierFailure.Attempts + 1 : 1; this.failedDocuments[documentKey] = new(fingerprint, attempts, failure); } /// /// Forgets that a document failed, e.g. because it was indexed after all. /// /// The key of the document. public void Forget(string documentKey) => this.failedDocuments.Remove(documentKey); /// /// Whether a document failed during this pass and is not tried again before the next one. /// /// /// A document which changed since it failed gets another try, as long as it did not fail /// MAX_ATTEMPTS times during this pass already. /// /// The key of the document. /// The fingerprint of the document as it is now. /// Why it failed the last time, when it is not tried again. /// True when the document is left alone. public bool TryGetEarlierFailure(string documentKey, string fingerprint, [NotNullWhen(true)] out DataSourceEmbeddingFailure? failure) { failure = null; if (!this.failedDocuments.TryGetValue(documentKey, out var failedDocument)) return false; if (failedDocument.Attempts < MAX_ATTEMPTS && !string.Equals(failedDocument.Fingerprint, fingerprint, StringComparison.Ordinal)) return false; failure = failedDocument.Failure; return true; } /// /// Marks a kind of provider failure as reported to the user. /// /// The kind of provider failure. /// True when the user was not told about it during this pass yet. public bool TryMarkReported(ProviderRequestFailureReason reason) => this.reportedFailureReasons.Add(reason); }