chore(claude-do): UsageGate: Parallelitaet stufenweise drosseln statt erst bei
## Kontext: Limits sind Fenster, nicht Summen Die Runs laufen ueber das Claude-Abo. Limits greifen pro 5h-Fenster und pro 7 Tage. Nicht die Wochensumme tut weh, sondern dass ein Agent-Burst ein Fenster leerraeumt, in dem Mika selbst interaktiv arbeiten will. ## Messgrundlage (alle Transcripts unter ~/.claude/projects) Agent-Runs sind ueber die ganze Historie nur **18,4 %** des Account-Verbrauch ClaudeDo-Task: 87105f5e-c4f4-4af4-ae60-89cd2e153e3c
This commit is contained in:
@@ -142,4 +142,86 @@ public sealed class UsageSnapshotBuilderTests : IDisposable
|
||||
Assert.True(dto.IsStale);
|
||||
Assert.Equal("boom", dto.LastError);
|
||||
}
|
||||
|
||||
private async Task SetThrottleAsync(int maxParallel, int softPct, int hardPct)
|
||||
{
|
||||
using var ctx = _db.CreateContext();
|
||||
var repo = new AppSettingsRepository(ctx);
|
||||
var settings = await repo.GetAsync();
|
||||
settings.MaxParallelExecutions = maxParallel;
|
||||
settings.UsageThrottleSoftPct = softPct;
|
||||
settings.UsageThrottleHardPct = hardPct;
|
||||
await repo.UpdateAsync(settings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Throttled_slots_and_decisive_bucket_reported()
|
||||
{
|
||||
await SetThresholdsAsync(80, 90);
|
||||
await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);
|
||||
|
||||
var state = new UsageState();
|
||||
state.ReportSuccess(new UsageSnapshot(
|
||||
new UsageBucket(70, null), new UsageBucket(20, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));
|
||||
|
||||
var builder = CreateBuilder(state, new UsageGateDecision(false, null));
|
||||
|
||||
var dto = await builder.BuildAsync();
|
||||
|
||||
Assert.Equal(3, dto.ConfiguredSlots);
|
||||
Assert.Equal(1, dto.EffectiveSlots);
|
||||
Assert.Equal("five_hour", dto.ThrottleBucket);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SevenDay_decisive_bucket_reported_when_higher()
|
||||
{
|
||||
await SetThresholdsAsync(80, 90);
|
||||
await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);
|
||||
|
||||
var state = new UsageState();
|
||||
state.ReportSuccess(new UsageSnapshot(
|
||||
new UsageBucket(10, null), new UsageBucket(70, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));
|
||||
|
||||
var builder = CreateBuilder(state, new UsageGateDecision(false, null));
|
||||
|
||||
var dto = await builder.BuildAsync();
|
||||
|
||||
Assert.Equal("seven_day", dto.ThrottleBucket);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Not_throttled_reports_null_bucket_and_equal_slots()
|
||||
{
|
||||
await SetThresholdsAsync(80, 90);
|
||||
await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);
|
||||
|
||||
var state = new UsageState();
|
||||
state.ReportSuccess(new UsageSnapshot(
|
||||
new UsageBucket(10, null), new UsageBucket(20, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));
|
||||
|
||||
var builder = CreateBuilder(state, new UsageGateDecision(false, null));
|
||||
|
||||
var dto = await builder.BuildAsync();
|
||||
|
||||
Assert.Equal(3, dto.ConfiguredSlots);
|
||||
Assert.Equal(3, dto.EffectiveSlots);
|
||||
Assert.Null(dto.ThrottleBucket);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task No_snapshot_falls_back_to_configured_slots_without_throttling()
|
||||
{
|
||||
await SetThresholdsAsync(80, 90);
|
||||
await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);
|
||||
|
||||
var state = new UsageState();
|
||||
var builder = CreateBuilder(state, new UsageGateDecision(false, null));
|
||||
|
||||
var dto = await builder.BuildAsync();
|
||||
|
||||
Assert.Equal(3, dto.ConfiguredSlots);
|
||||
Assert.Equal(3, dto.EffectiveSlots);
|
||||
Assert.Null(dto.ThrottleBucket);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using ClaudeDo.Worker.Usage;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Usage;
|
||||
|
||||
public sealed class UsageThrottleTests
|
||||
{
|
||||
private const int Soft = 50;
|
||||
private const int Hard = 65;
|
||||
private const int GateFive = 80;
|
||||
private const int GateSeven = 90;
|
||||
|
||||
private static int Effective(double? five, double? seven, int configured = 3) =>
|
||||
UsageThrottle.EffectiveSlots(configured, five, seven, Soft, Hard, GateFive, GateSeven);
|
||||
|
||||
[Fact]
|
||||
public void BelowSoftThreshold_ReturnsFullConfiguredSlots()
|
||||
{
|
||||
Assert.Equal(3, Effective(30, 40));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtSoftThreshold_CapsAtTwo()
|
||||
{
|
||||
Assert.Equal(2, Effective(50, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetweenSoftAndHard_CapsAtTwo()
|
||||
{
|
||||
Assert.Equal(2, Effective(60, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtHardThreshold_CapsAtOne()
|
||||
{
|
||||
Assert.Equal(1, Effective(65, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetweenHardAndGate_CapsAtOne()
|
||||
{
|
||||
Assert.Equal(1, Effective(70, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtGateThreshold_ReturnsZero()
|
||||
{
|
||||
Assert.Equal(0, Effective(80, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AboveGateThreshold_ReturnsZero()
|
||||
{
|
||||
Assert.Equal(0, Effective(95, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiveHourAloneDecisive_SevenDayLow()
|
||||
{
|
||||
Assert.Equal(1, Effective(70, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenDayAloneDecisive_FiveHourLow()
|
||||
{
|
||||
Assert.Equal(1, Effective(5, 70));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenDayAloneAtGate_ReturnsZero()
|
||||
{
|
||||
Assert.Equal(0, Effective(10, 90));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiveHourAloneAtGate_ReturnsZero()
|
||||
{
|
||||
Assert.Equal(0, Effective(80, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfiguredSlotsSmallerThanCap_NeverIncreased()
|
||||
{
|
||||
// Soft tier caps at 2, but configured is only 1 — throttle never raises parallelism.
|
||||
Assert.Equal(1, Effective(55, 0, configured: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoUtilizationAtAll_ReturnsFullConfiguredSlots()
|
||||
{
|
||||
Assert.Equal(3, Effective(null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroSoftAndHardThresholds_NeverThrottleBelowGate()
|
||||
{
|
||||
Assert.Equal(3, UsageThrottle.EffectiveSlots(3, 79, 89, softPct: 0, hardPct: 0, gateFiveHourPct: GateFive, gateSevenDayPct: GateSeven));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroGateThresholds_NeverHardBlock()
|
||||
{
|
||||
Assert.Equal(1, UsageThrottle.EffectiveSlots(3, 99, 99, softPct: Soft, hardPct: Hard, gateFiveHourPct: 0, gateSevenDayPct: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfiguredSlotsBelowOne_ClampedToOneBeforeThrottling()
|
||||
{
|
||||
Assert.Equal(1, Effective(10, 10, configured: 0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user