Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,69 @@ public void Validate_NonPositiveTimeout_Fails(string setting)
Assert.IsTrue(Validate(options).Failed);
}

private static void SetTimerSetting(GitBranchStateCacheOptions options, string setting, TimeSpan value)
{
switch (setting)
{
case "FetchTimeout":
options.FetchTimeout = value;
break;
case "DiffTimeout":
options.DiffTimeout = value;
break;
case "ProbeTimeout":
options.ProbeTimeout = value;
break;
default:
options.MaintenanceInterval = value;
break;
}
}

[TestMethod]
[DataRow("FetchTimeout")]
[DataRow("DiffTimeout")]
[DataRow("ProbeTimeout")]
[DataRow("MaintenanceInterval")]
public void Validate_TimerSettingOf49Days_Succeeds(string setting)
{
GitBranchStateCacheOptions options = Valid();
SetTimerSetting(options, setting, TimeSpan.FromDays(49));

Assert.IsTrue(Validate(options).Succeeded);
}

[TestMethod]
[DataRow("FetchTimeout")]
[DataRow("DiffTimeout")]
[DataRow("ProbeTimeout")]
[DataRow("MaintenanceInterval")]
public void Validate_TimerSettingOf50Days_Fails(string setting)
{
// Past uint.MaxValue - 1 ms, CancellationTokenSource, Task.WaitAsync and PeriodicTimer throw, so a
// value meant as "effectively no timeout" has to be refused here, naming the setting.
GitBranchStateCacheOptions options = Valid();
SetTimerSetting(options, setting, TimeSpan.FromDays(50));

ValidateOptionsResult result = Validate(options);

Assert.IsTrue(result.Failed);
Assert.Contains(setting, result.FailureMessage);
Assert.Contains(GitBranchStateCacheOptionsValidator.MaxTimerDuration.ToString(), result.FailureMessage);
}

[TestMethod]
public void MaxTimerDuration_IsTheLongestDurationTheTimersAccept()
{
TimeSpan max = GitBranchStateCacheOptionsValidator.MaxTimerDuration;

using CancellationTokenSource accepted = new(max);
using PeriodicTimer timer = new(max);
TimeSpan tooLong = max + TimeSpan.FromMilliseconds(1);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new CancellationTokenSource(tooLong));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new PeriodicTimer(tooLong));
}

[TestMethod]
public void Validate_ZeroIdleMaxAge_Succeeds()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ namespace ktsu.GitBranchStateCache.Configuration;
/// </remarks>
public sealed class GitBranchStateCacheOptionsValidator : IValidateOptions<GitBranchStateCacheOptions>
{
/// <summary>
/// The longest duration <see cref="CancellationTokenSource"/>, <see cref="Task.WaitAsync(TimeSpan)"/>
/// and <see cref="PeriodicTimer"/> accept: <c>uint.MaxValue - 1</c> milliseconds, about 49.7 days.
/// </summary>
internal static readonly TimeSpan MaxTimerDuration = TimeSpan.FromMilliseconds(uint.MaxValue - 1);

/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, GitBranchStateCacheOptions options)
{
Expand Down Expand Up @@ -115,10 +121,10 @@ private static void ValidateTimings(GitBranchStateCacheOptions options, List<str
{
RequirePositive(options.RefsTtl, "RefsTtl", failures);
RequirePositive(options.AdmissionTtl, "AdmissionTtl", failures);
RequirePositive(options.FetchTimeout, "FetchTimeout", failures);
RequirePositive(options.DiffTimeout, "DiffTimeout", failures);
RequirePositive(options.ProbeTimeout, "ProbeTimeout", failures);
RequirePositive(options.MaintenanceInterval, "MaintenanceInterval", failures);
RequirePositiveTimerDuration(options.FetchTimeout, "FetchTimeout", failures);
RequirePositiveTimerDuration(options.DiffTimeout, "DiffTimeout", failures);
RequirePositiveTimerDuration(options.ProbeTimeout, "ProbeTimeout", failures);
RequirePositiveTimerDuration(options.MaintenanceInterval, "MaintenanceInterval", failures);

// Refs older than the authorization proving they may be read would mean serving state that
// outlives the only evidence the caller was ever allowed to see it.
Expand Down Expand Up @@ -182,6 +188,25 @@ private static void RequirePositive(TimeSpan value, string setting, List<string>
}
}

/// <summary>
/// Requires a setting that is handed to a .NET timer to be positive and within
/// <see cref="MaxTimerDuration"/>.
/// </summary>
/// <remarks>
/// Without the upper bound, a value such as <c>365.00:00:00</c> meant as "effectively no timeout"
/// passed validation and then threw <see cref="ArgumentOutOfRangeException"/> at startup or on every
/// request that needed a fetch or a diff, rather than producing a message naming the setting.
/// </remarks>
private static void RequirePositiveTimerDuration(TimeSpan value, string setting, List<string> failures)
{
RequirePositive(value, setting, failures);
if (value > MaxTimerDuration)
{
failures.Add(
$"{GitBranchStateCacheOptions.SectionName}:{setting} ({value}) must not exceed {MaxTimerDuration}, the longest timeout .NET timers accept.");
}
}

/// <summary>
/// Reports whether a bound URL is absolute and addressable over HTTP.
/// </summary>
Expand Down
Loading