Added a pause between indexing steps for shared embedding servers (#1114)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-27 08:05:52 +01:00
1 parent cbc4ce5413
commit dcba62b45c
21 files changed
+627 -9

No files matched your search

@@ -68,7 +68,7 @@ public sealed class DataSourceRelocationWorkflowTests
clients.Add(DatabaseRole.INDEX_STORE, this.store.Client);
clients.Add(DatabaseRole.VECTOR_STORE, this.vectors);
var guard = new PromptInjectionGuardService(null!, this.settings, NullLogger<PromptInjectionGuardService>.Instance, NullLoggerFactory.Instance);
this.service = new DataSourceEmbeddingService(this.settings, null!, this.databases, guard, null!, NullLogger<DataSourceEmbeddingService>.Instance);
this.service = new DataSourceEmbeddingService(this.settings, null!, this.databases, guard, null!, new ThreadSafeRandom(), NullLogger<DataSourceEmbeddingService>.Instance);
this.lifetime = new CancellationTokenSource();
this.chunkIds.Clear();
this.runningTasks.Clear();
+143
View File
@@ -0,0 +1,143 @@
using AIStudio.Tools.Services.Indexing;
namespace AIStudio.Tests.Tools;
/// <summary>
/// Checks the pause between two indexing steps, which spares an embedding server that many people share.
/// </summary>
/// <remarks>
/// The pause is a gap after every step which worked on documents. It is drawn only once a step is
/// about to start, and the time which passed by then counts toward it.
/// </remarks>
[TestFixture]
public sealed class PauseBetweenStepsTests
{
private static readonly DateTimeOffset STEP_ENDED = new(2026, 10, 27, 14, 0, 0, TimeSpan.Zero);
private static readonly PauseRange THIRTY_TO_SIXTY_SECONDS = new(30, 60);
[Test]
public void BeforeTheFirstStepNothingWaits()
{
var random = new LargestRandom();
var pause = new PauseBetweenSteps().GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, random);
Assert.Multiple(() =>
{
Assert.That(pause, Is.EqualTo(PauseBeforeStep.NONE));
Assert.That(random.Draws, Is.Zero);
});
}
[Test]
public void TheNextStepWaitsForTheWholePauseRightAfterAStep()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, new LargestRandom());
Assert.That(pause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60))));
}
[Test]
public void TheUpperBoundCanBeDrawn()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED, new PauseRange(45, 45), new LargestRandom());
Assert.That(pause.Pause, Is.EqualTo(TimeSpan.FromSeconds(45)));
}
[Test]
public void TimeWhichPassedSinceTheStepCountsTowardThePause()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(25), THIRTY_TO_SIXTY_SECONDS, new LargestRandom());
Assert.That(pause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(35))));
}
[Test]
public void AStepWhichStartsLaterAnywayDoesNotWait()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(60), THIRTY_TO_SIXTY_SECONDS, new LargestRandom());
Assert.That(pause, Is.EqualTo(PauseBeforeStep.NONE));
}
[Test]
public void NothingIsDrawnWhileThereIsNoPause()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var random = new LargestRandom();
var withoutPause = pauses.GetPauseBeforeNextStep(STEP_ENDED, PauseRange.NONE, random);
var withPause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(10), THIRTY_TO_SIXTY_SECONDS, random);
Assert.Multiple(() =>
{
Assert.That(withoutPause, Is.EqualTo(PauseBeforeStep.NONE));
Assert.That(withPause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(50))));
Assert.That(random.Draws, Is.EqualTo(1));
});
}
[Test]
public void AStepWhichStoppedWhileWaitingLeavesTheRestOfThePauseToTheNextOne()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var random = new LargestRandom();
pauses.GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, random);
var nextPause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(20), new PauseRange(1, 1), random);
Assert.Multiple(() =>
{
Assert.That(nextPause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(40))));
Assert.That(random.Draws, Is.EqualTo(1));
});
}
[Test]
public void ANewStepOpensANewGap()
{
var pauses = new PauseBetweenSteps();
pauses.RecordStepEnded(STEP_ENDED);
var random = new LargestRandom();
pauses.GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, random);
var nextStepEnded = STEP_ENDED + TimeSpan.FromMinutes(6);
pauses.RecordStepEnded(nextStepEnded);
var pause = pauses.GetPauseBeforeNextStep(nextStepEnded + TimeSpan.FromSeconds(5), new PauseRange(10, 10), random);
Assert.Multiple(() =>
{
Assert.That(pause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5))));
Assert.That(random.Draws, Is.EqualTo(2));
});
}
/// <summary>
/// Always draws the largest value it may, and counts how often it was asked.
/// </summary>
private sealed class LargestRandom : Random
{
public int Draws { get; private set; }
public override int Next(int minValue, int maxValue)
{
this.Draws++;
return maxValue - 1;
}
}
}
+72
View File
@@ -0,0 +1,72 @@
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services.Indexing;
namespace AIStudio.Tests.Tools;
/// <summary>
/// Checks which bounds of the pause between two indexing steps apply, as the user set them and as configuration plugins lock them.
/// </summary>
[TestFixture]
public sealed class PauseRangeTests
{
[Test]
public void BoundsInOrderStayAsTheyAre()
{
Assert.That(PauseRange.GetEffective(10, false, 20, false), Is.EqualTo(new PauseRange(10, 20)));
}
[Test]
public void AnUpperBoundBelowTheLowerOneIsRaisedToIt()
{
Assert.That(PauseRange.GetEffective(60, false, 20, false), Is.EqualTo(new PauseRange(60, 60)));
}
[Test]
public void ALockedLowerBoundRaisesTheUpperOne()
{
Assert.That(PauseRange.GetEffective(60, true, 20, false), Is.EqualTo(new PauseRange(60, 60)));
}
[Test]
public void ALockedUpperBoundLowersTheLowerOne()
{
Assert.That(PauseRange.GetEffective(60, false, 20, true), Is.EqualTo(new PauseRange(20, 20)));
}
[Test]
public void BothBoundsLockedTheWrongWayRoundLetTheLongerPauseWin()
{
Assert.That(PauseRange.GetEffective(60, true, 20, true), Is.EqualTo(new PauseRange(60, 60)));
}
[Test]
public void BoundsOutsideTheAllowedRangeAreClamped()
{
Assert.That(PauseRange.GetEffective(-5, false, 1_000, false), Is.EqualTo(new PauseRange(0, DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS)));
}
[Test]
public void BoundsInOrderComeFromTheSettingsWithoutAskingForLocks()
{
var settings = new DataDataSourceIndexing
{
MinimumPauseBetweenStepsSeconds = 10,
MaximumPauseBetweenStepsSeconds = 20
};
Assert.That(PauseRange.FromSettings(settings), Is.EqualTo(new PauseRange(10, 20)));
}
[Test]
public void OnlyZeroForBothBoundsMeansNoPause()
{
Assert.Multiple(() =>
{
Assert.That(PauseRange.GetEffective(0, false, 0, false).IsNone, Is.True);
Assert.That(PauseRange.GetEffective(0, false, 5, false).IsNone, Is.False);
Assert.That(PauseRange.GetEffective(30, false, 0, false), Is.EqualTo(new PauseRange(30, 30)));
Assert.That(new DataDataSourceIndexing().MinimumPauseBetweenStepsSeconds, Is.Zero);
Assert.That(new DataDataSourceIndexing().MaximumPauseBetweenStepsSeconds, Is.Zero);
});
}
}
@@ -117,5 +117,5 @@ public sealed class MailDraftSyncTests : ToolRegistryTestBase
}
// Queueing a run needs none of the services which carry it out:
private DataSourceEmbeddingService CreateService() => new(this.SettingsManager, null!, null!, null!, null!, NullLogger<DataSourceEmbeddingService>.Instance);
private DataSourceEmbeddingService CreateService() => new(this.SettingsManager, null!, null!, null!, null!, null!, NullLogger<DataSourceEmbeddingService>.Instance);
}