Harden remaining runtime transport lifecycles
This commit is contained in:
@@ -19,4 +19,29 @@ public sealed partial class EgressPolicyTests
|
||||
Assert.True(recordingPolicy.EnsureAllowedCalled);
|
||||
Assert.NotNull(client);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EgressHttpClientFactory_Create_UsingFallbackFactory_ReturnsIsolatedClients()
|
||||
{
|
||||
var recordingPolicy = new RecordingPolicy();
|
||||
var request = new EgressRequest("Component", new Uri("https://allowed.internal"), "mirror-sync");
|
||||
|
||||
using var first = EgressHttpClientFactory.Create(
|
||||
recordingPolicy,
|
||||
request,
|
||||
client =>
|
||||
{
|
||||
client.BaseAddress = new Uri("https://first.internal/");
|
||||
client.DefaultRequestHeaders.Add("X-Test", "one");
|
||||
});
|
||||
using var second = EgressHttpClientFactory.Create(recordingPolicy, request);
|
||||
|
||||
Assert.True(recordingPolicy.EnsureAllowedCalled);
|
||||
Assert.Equal(new Uri("https://first.internal/"), first.BaseAddress);
|
||||
Assert.Null(second.BaseAddress);
|
||||
Assert.True(first.DefaultRequestHeaders.Contains("X-Test"));
|
||||
Assert.False(second.DefaultRequestHeaders.Contains("X-Test"));
|
||||
Assert.NotEmpty(second.DefaultRequestHeaders.UserAgent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace StellaOps.AirGap.Policy;
|
||||
|
||||
@@ -8,6 +10,13 @@ namespace StellaOps.AirGap.Policy;
|
||||
/// </summary>
|
||||
public static class EgressHttpClientFactory
|
||||
{
|
||||
private static readonly SocketsHttpHandler SharedHandler = new()
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HttpClient"/> after validating the supplied egress request against the policy.
|
||||
/// </summary>
|
||||
@@ -19,11 +28,7 @@ public static class EgressHttpClientFactory
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(egressPolicy);
|
||||
|
||||
egressPolicy.EnsureAllowed(request);
|
||||
|
||||
var client = new HttpClient();
|
||||
configure?.Invoke(client);
|
||||
return client;
|
||||
return Create(egressPolicy, request, CreateFallbackClient, configure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,4 +95,11 @@ public static class EgressHttpClientFactory
|
||||
Func<HttpClient> clientFactory,
|
||||
Action<HttpClient>? configure = null)
|
||||
=> Create(egressPolicy, new EgressRequest(component, destination, intent), clientFactory, configure);
|
||||
|
||||
private static HttpClient CreateFallbackClient()
|
||||
{
|
||||
var client = new HttpClient(SharedHandler, disposeHandler: false);
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOpsAirGapPolicy", "1.0"));
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0030-M | DONE | Revalidated 2026-01-06; new findings recorded in audit report. |
|
||||
| AUDIT-0030-T | DONE | Revalidated 2026-01-06; test coverage tracked in AUDIT-0033. |
|
||||
| AUDIT-0030-A | TODO | Replace direct new HttpClient usage in EgressHttpClientFactory. |
|
||||
| AUDIT-0030-A | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: `EgressHttpClientFactory` now uses a shared-handler fallback instead of raw default `new HttpClient()` allocation. |
|
||||
| REMED-05 | DONE | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.md. |
|
||||
| REMED-06 | DONE | SOLID review notes updated for SPRINT_20260130_002. |
|
||||
|
||||
@@ -362,10 +362,7 @@ public sealed class GenericOciConnector : IRegistryConnectorCapability, IDisposa
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_registryUrl + "/")
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_registryUrl + "/"));
|
||||
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
@@ -375,9 +372,6 @@ public sealed class GenericOciConnector : IRegistryConnectorCapability, IDisposa
|
||||
new AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -375,10 +375,7 @@ public sealed class HarborConnector : IRegistryConnectorCapability, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_harborUrl + "/")
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_harborUrl + "/"));
|
||||
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
@@ -388,9 +385,6 @@ public sealed class HarborConnector : IRegistryConnectorCapability, IDisposable
|
||||
new AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -523,10 +523,7 @@ public sealed class JfrogArtifactoryConnector : IRegistryConnectorCapability, ID
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_artifactoryUrl + "/")
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_artifactoryUrl + "/"));
|
||||
|
||||
// Set authorization header based on available auth
|
||||
if (!string.IsNullOrEmpty(_accessToken))
|
||||
@@ -546,9 +543,6 @@ public sealed class JfrogArtifactoryConnector : IRegistryConnectorCapability, ID
|
||||
new AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -417,10 +417,7 @@ public sealed class QuayConnector : IRegistryConnectorCapability, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_quayUrl + "/")
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_quayUrl + "/"));
|
||||
|
||||
// Set authorization header
|
||||
if (!string.IsNullOrEmpty(_oauth2Token))
|
||||
@@ -436,9 +433,6 @@ public sealed class QuayConnector : IRegistryConnectorCapability, IDisposable
|
||||
new AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -350,16 +350,11 @@ public sealed class AzureDevOpsConnector : IScmConnectorCapability, IDisposable
|
||||
|
||||
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($":{pat}"));
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl)
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(baseUrl));
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Basic", authValue);
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
@@ -285,16 +285,11 @@ public sealed class GitHubConnector : IScmConnectorCapability, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl)
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(baseUrl));
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", token);
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
_httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
|
||||
|
||||
return _httpClient;
|
||||
|
||||
@@ -283,15 +283,10 @@ public sealed class GitLabConnector : IScmConnectorCapability, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl)
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(baseUrl));
|
||||
_httpClient.DefaultRequestHeaders.Add("PRIVATE-TOKEN", token);
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
@@ -292,16 +292,11 @@ public sealed class GiteaConnector : IScmConnectorCapability, IDisposable
|
||||
_baseUrl = baseUrlProp.GetString()!.TrimEnd('/');
|
||||
var apiUrl = _baseUrl + "/api/v1/";
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(apiUrl)
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(apiUrl));
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("token", token);
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ public sealed class AwsAppConfigConnector : ISettingsStoreConnectorCapability, I
|
||||
|
||||
// Use the AppConfig Data API for configuration retrieval
|
||||
var dataEndpoint = $"https://appconfigdata.{_region}.amazonaws.com";
|
||||
using var dataClient = new HttpClient { BaseAddress = new Uri(dataEndpoint) };
|
||||
using var dataClient = ConnectorHttpClients.CreateClient(new Uri(dataEndpoint));
|
||||
|
||||
// Start a configuration session
|
||||
var sessionRequest = CreateAppConfigDataRequest(
|
||||
@@ -561,14 +561,8 @@ public sealed class AwsAppConfigConnector : ISettingsStoreConnectorCapability, I
|
||||
|
||||
var endpoint = $"https://appconfig.{_region}.amazonaws.com";
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(endpoint + "/"),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(endpoint + "/"));
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateAppConfigRequest(HttpMethod method, string path, string? payload)
|
||||
|
||||
@@ -480,14 +480,8 @@ public sealed class AwsParameterStoreConnector : ISettingsStoreConnectorCapabili
|
||||
|
||||
var endpoint = $"https://ssm.{_region}.amazonaws.com";
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(endpoint + "/"),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(endpoint + "/"));
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateSsmRequest(string action, string payload)
|
||||
|
||||
@@ -528,16 +528,11 @@ public sealed class AzureAppConfigConnector : ISettingsStoreConnectorCapability,
|
||||
_label = labelProp.GetString();
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_endpoint + "/"),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_endpoint + "/"));
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/vnd.microsoft.appconfig.kv+json"));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
@@ -462,20 +462,14 @@ public sealed class ConsulKvConnector : ISettingsStoreConnectorCapability, IDisp
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_consulAddress + "/"),
|
||||
Timeout = TimeSpan.FromMinutes(6) // Allow for blocking queries
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_consulAddress + "/"));
|
||||
_httpClient.Timeout = TimeSpan.FromMinutes(6); // Allow for blocking queries
|
||||
|
||||
if (!string.IsNullOrEmpty(_token))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Consul-Token", _token);
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -473,11 +473,8 @@ public sealed class EtcdConnector : ISettingsStoreConnectorCapability, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_etcdAddress + "/"),
|
||||
Timeout = TimeSpan.FromMinutes(6)
|
||||
};
|
||||
_httpClient = ConnectorHttpClients.CreateClient(new Uri(_etcdAddress + "/"));
|
||||
_httpClient.Timeout = TimeSpan.FromMinutes(6);
|
||||
|
||||
// Authenticate if credentials provided
|
||||
if (!string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_password))
|
||||
@@ -485,9 +482,6 @@ public sealed class EtcdConnector : ISettingsStoreConnectorCapability, IDisposab
|
||||
await AuthenticateAsync(ct);
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("StellaOps", "1.0"));
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.ReleaseOrchestrator.IntegrationHub.Tests")]
|
||||
@@ -5,5 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: legacy vault/registry connectors now use shared-handler compatibility HTTP clients instead of raw temporary `HttpClient` allocation. |
|
||||
| SPRINT_20260406_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: SCM, settings-store, and remaining registry connectors now source their per-connector `HttpClient` instances from `ConnectorHttpClients.CreateClient(...)` so they reuse the shared pooled handler without leaking auth headers across integrations. |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/StellaOps.ReleaseOrchestrator.IntegrationHub.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
|
||||
|
||||
namespace StellaOps.ReleaseOrchestrator.IntegrationHub.Tests;
|
||||
|
||||
public sealed class ConnectorHttpClientsTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateClient_AssignsBaseAddressAndSharedUserAgent()
|
||||
{
|
||||
using var client = ConnectorHttpClients.CreateClient(new Uri("https://connector.example/api/"));
|
||||
|
||||
Assert.Equal(new Uri("https://connector.example/api/"), client.BaseAddress);
|
||||
Assert.Contains(client.DefaultRequestHeaders.UserAgent, agent => agent.Product?.Name == "StellaOps");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateClient_ReturnsIsolatedClientInstances()
|
||||
{
|
||||
using var first = ConnectorHttpClients.CreateClient();
|
||||
using var second = ConnectorHttpClients.CreateClient();
|
||||
|
||||
first.DefaultRequestHeaders.Add("X-Test-Header", "one");
|
||||
|
||||
Assert.NotSame(first, second);
|
||||
Assert.True(first.DefaultRequestHeaders.Contains("X-Test-Header"));
|
||||
Assert.False(second.DefaultRequestHeaders.Contains("X-Test-Header"));
|
||||
Assert.True(second.DefaultRequestHeaders.UserAgent.Any());
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| SPRINT_20260406_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added `ConnectorHttpClients` regression coverage for isolated per-connector clients on the shared pooled handler path. |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
23
src/Workflow/AGENTS.md
Normal file
23
src/Workflow/AGENTS.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# AGENTS - Workflow Module
|
||||
|
||||
## Working Directory
|
||||
- `src/Workflow/**` (workflow abstractions, engine, storage backends, signaling, and tests).
|
||||
|
||||
## Required Reading
|
||||
- `docs/README.md`
|
||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/modules/release-orchestrator/modules/workflow-engine.md`
|
||||
|
||||
## Engineering Rules
|
||||
- Preserve deterministic workflow state transitions, event ordering, and projection behavior across storage backends.
|
||||
- Keep PostgreSQL, signaling, and engine changes compatible at the contract level unless the sprint explicitly scopes a cross-backend divergence.
|
||||
- Runtime storage code must follow the shared transport attribution rules: stable client identity, pooled connections, and no anonymous long-lived transports.
|
||||
- Avoid cross-module edits unless the active sprint explicitly allows them.
|
||||
|
||||
## Testing & Verification
|
||||
- Workflow backend/storage changes must run the targeted backend test project, not just the broader module solution.
|
||||
- Prefer focused integration coverage for persistence, signaling, wake-outbox, and projection flows when touching storage or transport behavior.
|
||||
|
||||
## Sprint Discipline
|
||||
- Track task state in the active sprint file and record blockers or contract changes in `Decisions & Risks`.
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Workflow.DataStore.PostgreSQL.Tests")]
|
||||
@@ -7,6 +7,11 @@ public sealed class PostgresWorkflowBackendOptions
|
||||
public const string SectionName = $"{WorkflowBackendOptions.SectionName}:Postgres";
|
||||
|
||||
public string ConnectionStringName { get; set; } = "WorkflowPostgres";
|
||||
public string? ApplicationName { get; set; }
|
||||
public bool Pooling { get; set; } = true;
|
||||
public int MinPoolSize { get; set; } = 1;
|
||||
public int MaxPoolSize { get; set; } = 100;
|
||||
public int? ConnectionIdleLifetimeSeconds { get; set; } = 300;
|
||||
public string SchemaName { get; set; } = "srd_wfklw";
|
||||
public string RuntimeStatesTableName { get; set; } = "wf_runtime_states";
|
||||
public string HostedJobLocksTableName { get; set; } = "wf_host_locks";
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class PostgresWorkflowDatabase(
|
||||
PostgresWorkflowMutationSessionAccessor sessionAccessor,
|
||||
IWorkflowMutationScopeAccessor scopeAccessor)
|
||||
{
|
||||
private const string DefaultApplicationName = "stellaops-workflow";
|
||||
private readonly PostgresWorkflowBackendOptions postgres = options.Value;
|
||||
|
||||
internal PostgresWorkflowBackendOptions Options => postgres;
|
||||
@@ -86,14 +87,36 @@ public sealed class PostgresWorkflowDatabase(
|
||||
}
|
||||
|
||||
internal async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var connection = new NpgsqlConnection(BuildConnectionString());
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
return connection;
|
||||
}
|
||||
|
||||
internal string BuildConnectionString()
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString(postgres.ConnectionStringName)
|
||||
?? throw new InvalidOperationException(
|
||||
$"PostgreSQL workflow backend requires connection string '{postgres.ConnectionStringName}'.");
|
||||
|
||||
var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
return connection;
|
||||
var normalizedMinPoolSize = Math.Max(postgres.MinPoolSize, 0);
|
||||
var normalizedMaxPoolSize = Math.Max(postgres.MaxPoolSize, normalizedMinPoolSize);
|
||||
var builder = new NpgsqlConnectionStringBuilder(connectionString)
|
||||
{
|
||||
ApplicationName = string.IsNullOrWhiteSpace(postgres.ApplicationName)
|
||||
? DefaultApplicationName
|
||||
: postgres.ApplicationName.Trim(),
|
||||
Pooling = postgres.Pooling,
|
||||
MinPoolSize = normalizedMinPoolSize,
|
||||
MaxPoolSize = normalizedMaxPoolSize,
|
||||
};
|
||||
|
||||
if (postgres.ConnectionIdleLifetimeSeconds.HasValue && postgres.ConnectionIdleLifetimeSeconds.Value > 0)
|
||||
{
|
||||
builder.ConnectionIdleLifetime = postgres.ConnectionIdleLifetimeSeconds.Value;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using StellaOps.Workflow.DataStore.PostgreSQL;
|
||||
using StellaOps.Workflow.Engine.Services;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Npgsql;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace StellaOps.Workflow.DataStore.PostgreSQL.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class PostgresWorkflowDatabaseTests
|
||||
{
|
||||
[Test]
|
||||
public void BuildConnectionString_WhenApplicationNameIsOmitted_UsesWorkflowDefaults()
|
||||
{
|
||||
var options = new PostgresWorkflowBackendOptions
|
||||
{
|
||||
ConnectionStringName = "WorkflowPostgres",
|
||||
MinPoolSize = 2,
|
||||
MaxPoolSize = 9,
|
||||
ConnectionIdleLifetimeSeconds = 123,
|
||||
};
|
||||
|
||||
var connectionString = CreateDatabase(options).BuildConnectionString();
|
||||
var builder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
|
||||
builder.ApplicationName.Should().Be("stellaops-workflow");
|
||||
builder.Pooling.Should().BeTrue();
|
||||
builder.MinPoolSize.Should().Be(2);
|
||||
builder.MaxPoolSize.Should().Be(9);
|
||||
builder.ConnectionIdleLifetime.Should().Be(123);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildConnectionString_WhenExplicitSettingsAreProvided_AppliesNormalizedValues()
|
||||
{
|
||||
var options = new PostgresWorkflowBackendOptions
|
||||
{
|
||||
ConnectionStringName = "WorkflowPostgres",
|
||||
ApplicationName = "workflow-projection-tests",
|
||||
Pooling = false,
|
||||
MinPoolSize = 8,
|
||||
MaxPoolSize = 3,
|
||||
};
|
||||
|
||||
var connectionString = CreateDatabase(options).BuildConnectionString();
|
||||
var builder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
|
||||
builder.ApplicationName.Should().Be("workflow-projection-tests");
|
||||
builder.Pooling.Should().BeFalse();
|
||||
builder.MinPoolSize.Should().Be(8);
|
||||
builder.MaxPoolSize.Should().Be(8);
|
||||
}
|
||||
|
||||
private static PostgresWorkflowDatabase CreateDatabase(PostgresWorkflowBackendOptions options)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"ConnectionStrings:{options.ConnectionStringName}"] =
|
||||
"Host=localhost;Database=workflow;Username=workflow;Password=workflow",
|
||||
})
|
||||
.Build();
|
||||
|
||||
return new PostgresWorkflowDatabase(
|
||||
configuration,
|
||||
Options.Create(options),
|
||||
new PostgresWorkflowMutationSessionAccessor(),
|
||||
new WorkflowMutationScopeAccessor());
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ public sealed class RuntimePostgresConstructionConventionTests
|
||||
"src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresConnectivityCheck.cs",
|
||||
"src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresMigrationStatusCheck.cs",
|
||||
"src/Platform/StellaOps.Platform.WebService/Services/PlatformMigrationAdminService.cs",
|
||||
"src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs",
|
||||
"src/__Libraries/StellaOps.Doctor.Plugins.Database/Checks/DatabaseCheckBase.cs",
|
||||
"src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs",
|
||||
"src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs",
|
||||
@@ -36,10 +35,23 @@ public sealed class RuntimePostgresConstructionConventionTests
|
||||
"src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs",
|
||||
"src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs",
|
||||
"src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GenericOciConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/AzureDevOpsConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs",
|
||||
"src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs",
|
||||
|
||||
Reference in New Issue
Block a user