| File: Language\IntegrationHostConfiguration.cs | Web Access |
| Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Configuration; namespace Aspire.Hosting.RemoteHost.Language; /// <summary> /// Snapshots and validates integration settings once for the server session. /// </summary> internal sealed class IntegrationHostConfiguration { internal static IntegrationHostConfiguration Default { get; } = new(new ConfigurationBuilder().Build()); private readonly Lazy<IReadOnlyList<IntegrationHostDescriptor>> _descriptors; public IntegrationHostConfiguration(IConfiguration configuration) { Enabled = configuration.GetValue<bool>(KnownConfigNames.IntegrationHostsEnabled); Bootstrap = configuration.GetValue<bool>(KnownConfigNames.IntegrationHostBootstrap); SocketPath = configuration["REMOTE_APP_HOST_SOCKET_PATH"]; Token = configuration[KnownConfigNames.RemoteAppHostToken]; RegistrationTimeout = ReadDuration(configuration, nameof(RegistrationTimeout), TimeSpan.FromMinutes(2)); DiscoveryTimeout = ReadDuration(configuration, nameof(DiscoveryTimeout), TimeSpan.FromMinutes(2)); InvocationTimeout = ReadDuration(configuration, nameof(InvocationTimeout), TimeSpan.FromMinutes(1)); // Guest callbacks can await integration calls. Lowering the integration deadline // must not also retire the guest before the integration can recover. CallbackTimeout = ReadDuration(configuration, nameof(CallbackTimeout), TimeSpan.FromMinutes(1)); ShutdownTimeout = ReadDuration(configuration, nameof(ShutdownTimeout), TimeSpan.FromSeconds(5)); OutputDrainIdleTimeout = ReadDuration(configuration, nameof(OutputDrainIdleTimeout), TimeSpan.FromSeconds(5)); StableHostPeriod = ReadDuration(configuration, nameof(StableHostPeriod), TimeSpan.FromMinutes(1)); RestartDelay = ReadDuration(configuration, nameof(RestartDelay), TimeSpan.FromSeconds(1)); MaxRestartDelay = ReadDuration(configuration, nameof(MaxRestartDelay), TimeSpan.FromSeconds(4)); MaxRestartAttempts = configuration.GetValue("IntegrationHost:MaxRestartAttempts", 3); if (MaxRestartAttempts < 0) { throw new InvalidOperationException("IntegrationHost:MaxRestartAttempts must be non-negative."); } if (RestartDelay > MaxRestartDelay) { throw new InvalidOperationException("IntegrationHost:RestartDelay must not exceed IntegrationHost:MaxRestartDelay."); } var entries = configuration.GetSection("IntegrationHosts").GetChildren() .Select(entry => (entry.Path, Language: entry["Language"], PackageName: entry["PackageName"], HostEntryPoint: entry["HostEntryPoint"])) .ToArray(); // Descriptor errors belong to hosted-service startup so readiness faults with the // same cause. Bootstrap intentionally runs before generated host modules exist. _descriptors = new(() => { if (entries.Length > 0 && !Enabled) { throw new InvalidOperationException($"Integration hosts require {KnownConfigNames.IntegrationHostsEnabled}=true."); } if (Bootstrap) { return []; } return entries.Select(entry => { if (string.IsNullOrEmpty(entry.Language) || string.IsNullOrEmpty(entry.PackageName) || string.IsNullOrEmpty(entry.HostEntryPoint)) { throw new InvalidOperationException( $"Malformed {entry.Path} entry: Language='{entry.Language}', PackageName='{entry.PackageName}', HostEntryPoint='{entry.HostEntryPoint}'. " + "All three fields are required."); } if (!File.Exists(entry.HostEntryPoint)) { throw new FileNotFoundException( $"Integration host entry point for package '{entry.PackageName}' [{entry.Language}] does not exist. Verify the integration path.", entry.HostEntryPoint); } return new IntegrationHostDescriptor { Language = entry.Language, PackageName = entry.PackageName, HostEntryPoint = entry.HostEntryPoint }; }).ToArray(); }); } public bool Enabled { get; } public bool Bootstrap { get; } public string? SocketPath { get; } public string? Token { get; } public TimeSpan RegistrationTimeout { get; } public TimeSpan DiscoveryTimeout { get; } public TimeSpan InvocationTimeout { get; } public TimeSpan CallbackTimeout { get; } public TimeSpan ShutdownTimeout { get; } public TimeSpan OutputDrainIdleTimeout { get; } public TimeSpan StableHostPeriod { get; } public TimeSpan RestartDelay { get; } public TimeSpan MaxRestartDelay { get; } public int MaxRestartAttempts { get; } public IReadOnlyList<IntegrationHostDescriptor> Descriptors => _descriptors.Value; public TimeSpan GetRestartDelay(int attempt) => TimeSpan.FromMilliseconds(Math.Min(RestartDelay.TotalMilliseconds * Math.Pow(2, attempt - 1), MaxRestartDelay.TotalMilliseconds)); private static TimeSpan ReadDuration(IConfiguration configuration, string name, TimeSpan defaultValue) { var key = $"IntegrationHost:{name}"; var value = configuration.GetValue(key, defaultValue); if (value <= TimeSpan.Zero || value.TotalMilliseconds > uint.MaxValue - 1) { throw new InvalidOperationException( $"{key} must be a positive TimeSpan no greater than {TimeSpan.FromMilliseconds(uint.MaxValue - 1)}."); } return value; } } /// <summary> /// Identifies an integration host configured by the AppHost server. /// </summary> internal sealed class IntegrationHostDescriptor { public required string Language { get; init; } public required string PackageName { get; init; } public required string HostEntryPoint { get; init; } }