Skip to content
Merged
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
16 changes: 4 additions & 12 deletions src/Aspire.Hosting/Dcp/DcpKubernetesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ internal sealed class DcpKubernetesClient : k8s.Kubernetes

public DcpKubernetesClient(KubernetesClientConfiguration config, params DelegatingHandler[] handlers) : base(config, handlers)
{
// KubernetesService supplies the operation deadlines. The KubernetesClient NuGet package's
// separate HttpClientTimeout timer defaults to 100 seconds and would otherwise truncate
// configured budgets and surface as an unrelated cancellation.
HttpClientTimeout = Timeout.InfiniteTimeSpan;
}

/// <summary>
Expand Down Expand Up @@ -115,10 +119,6 @@ public async Task<HttpOperationResponse<Stream>> ReadSubResourceAsStreamAsync(
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
ArgumentException.ThrowIfNullOrWhiteSpace(subResource, nameof(subResource));

using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(HttpClientTimeout);
cancellationToken = cts.Token;

string url;
if (string.IsNullOrEmpty(namespaceParameter))
{
Expand Down Expand Up @@ -155,10 +155,6 @@ public async Task<HttpOperationResponse<Stream>> ReadSubResourceAsStreamAsync(
/// </summary>
public async Task<ApiServerExecution> GetExecutionDocumentAsync(CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(HttpClientTimeout);
cancellationToken = cts.Token;

var httpRequest = new HttpRequestMessage
{
Method = HttpMethod.Get,
Expand All @@ -181,10 +177,6 @@ public async Task<ApiServerExecution> PatchExecutionDocumentAsync(
ApiServerExecution apiServerExecution,
CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(HttpClientTimeout);
cancellationToken = cts.Token;

var httpRequest = new HttpRequestMessage
{
Method = HttpMethod.Patch,
Expand Down
51 changes: 51 additions & 0 deletions src/Aspire.Hosting/Dcp/DcpOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ internal sealed class DcpOptions

public int KubernetesConfigReadRetryIntervalMilliseconds { get; set; } = 100;

/// <summary>
/// Additional time added to <see cref="KubernetesApiTimeout"/> before the first successful API operation.
/// The combined budget covers client setup and the API operation.
/// Defaults to 20 seconds and is configured through <c>DcpPublisher:KubernetesInitializationAdditionalTimeout</c>.
/// Must be non-negative, and the combined budget must not exceed ten minutes.
/// </summary>
public TimeSpan KubernetesInitializationAdditionalTimeout { get; set; } = TimeSpan.FromSeconds(20);

/// <summary>
/// The base API operation budget, extended by <see cref="KubernetesInitializationAdditionalTimeout"/> until the first successful API operation.
/// Applies to connection establishment rather than stream lifetime for watches and logs.
/// Defaults to 40 seconds and is configured through <c>DcpPublisher:KubernetesApiTimeout</c>.
/// Valid values range from one second to ten minutes.
/// </summary>
public TimeSpan KubernetesApiTimeout { get; set; } = TimeSpan.FromSeconds(40);

/// <summary>
/// The total budget for creating a DCP object, including client setup, retries, and reconciliation after ambiguous failures.
/// Defaults to two minutes and is configured through <c>DcpPublisher:KubernetesCreateRecoveryTimeout</c>.
/// Valid values range from one second to ten minutes.
/// </summary>
public TimeSpan KubernetesCreateRecoveryTimeout { get; set; } = TimeSpan.FromMinutes(2);

/// <summary>
/// The duration to wait for the container runtime to become healthy before aborting startup.
/// </summary>
Expand Down Expand Up @@ -175,8 +198,33 @@ public ValidateOptionsResult Validate(string? name, DcpOptions options)
builder.AddError("The proxyless endpoint port range start must be less than or equal to the range end.", nameof(options.ProxylessEndpointPortRangeStart));
}

var apiTimeoutValid = ValidateKubernetesTimeout(builder, options.KubernetesApiTimeout, nameof(options.KubernetesApiTimeout), "API");
ValidateKubernetesTimeout(builder, options.KubernetesCreateRecoveryTimeout, nameof(options.KubernetesCreateRecoveryTimeout), "create recovery");

if (options.KubernetesInitializationAdditionalTimeout < TimeSpan.Zero)
{
builder.AddError("The Kubernetes additional initialization timeout must be non-negative.", nameof(options.KubernetesInitializationAdditionalTimeout));
}
// Compare the remaining headroom rather than adding potentially overflowing configuration values.
else if (apiTimeoutValid &&
options.KubernetesInitializationAdditionalTimeout > TimeSpan.FromMinutes(10) - options.KubernetesApiTimeout)
Comment thread
danegsta marked this conversation as resolved.
{
builder.AddError("The combined Kubernetes API and additional initialization timeouts must not exceed ten minutes.", nameof(options.KubernetesInitializationAdditionalTimeout));
}

return builder.Build();
}

private static bool ValidateKubernetesTimeout(ValidateOptionsResultBuilder builder, TimeSpan timeout, string propertyName, string description)
{
if (timeout < TimeSpan.FromSeconds(1) || timeout > TimeSpan.FromMinutes(10))
{
builder.AddError($"The Kubernetes {description} timeout must be between one second and ten minutes.", propertyName);
return false;
}

return true;
}
}

internal class ConfigureDefaultDcpOptions(
Expand Down Expand Up @@ -330,6 +378,9 @@ public void Configure(DcpOptions options)

options.KubernetesConfigReadRetryCount = dcpPublisherConfiguration.GetValue(nameof(options.KubernetesConfigReadRetryCount), options.KubernetesConfigReadRetryCount);
options.KubernetesConfigReadRetryIntervalMilliseconds = dcpPublisherConfiguration.GetValue(nameof(options.KubernetesConfigReadRetryIntervalMilliseconds), options.KubernetesConfigReadRetryIntervalMilliseconds);
options.KubernetesInitializationAdditionalTimeout = dcpPublisherConfiguration.GetValue(nameof(options.KubernetesInitializationAdditionalTimeout), options.KubernetesInitializationAdditionalTimeout);
options.KubernetesApiTimeout = dcpPublisherConfiguration.GetValue(nameof(options.KubernetesApiTimeout), options.KubernetesApiTimeout);
options.KubernetesCreateRecoveryTimeout = dcpPublisherConfiguration.GetValue(nameof(options.KubernetesCreateRecoveryTimeout), options.KubernetesCreateRecoveryTimeout);

if (!string.IsNullOrEmpty(dcpPublisherConfiguration[nameof(options.ResourceNameSuffix)]))
{
Expand Down
Loading
Loading