Categories
Implementation Specs (157)
- specs/AnomalyDetection/TASK1.1.md
- specs/AnomalyDetection/TASK1.2.md
- specs/AnomalyDetection/TASK1.3.md
- specs/AnomalyDetection/TASK1.4.md
- specs/AnomalyDetection/TASK2.1.md
- specs/AnomalyDetection/TASK2.2.md
- specs/AnomalyDetection/TASK3.1.md
- specs/AnomalyDetection/TASK3.2.md
- specs/AnomalyDetection/TASK3.3.md
- specs/AnomalyDetection/TASK4.1.md
- specs/AnomalyDetection/TASK4.2.md
- specs/AnomalyDetection/TASK4.3.md
- specs/AnomalyDetection/TASK4.4.md
- specs/AnomalyDetection/TASK5.1.md
- specs/AnomalyDetection/TASK5.2.md
- specs/AnomalyDetection/TASK5.3.md
- specs/AnomalyDetection/TASK6.1.md
- specs/AnomalyDetection/TASK6.2.md
- specs/AnomalyDetection/TASK6.3.md
- specs/AnomalyDetection/TASKS.md
- specs/ApiServicePostgresTestStability/TASK.md
- specs/BackendDatabaseConsolidation/TASK.md
- specs/BackendDatabaseConsolidation/TASK1.1.md
- specs/BackendDatabaseConsolidation/TASK1.2.md
- specs/BackendDatabaseConsolidation/TASK1.3.md
- specs/BackendDatabaseConsolidation/TASK2.1.md
- specs/BackendDatabaseConsolidation/TASK2.2.md
- specs/BackendDatabaseConsolidation/TASK2.3.md
- specs/BackendDatabaseConsolidation/TASK3.1.md
- specs/BackendDatabaseConsolidation/TASK3.2.md
- specs/BackendDatabaseConsolidation/TASK4.1.md
- specs/BackendDatabaseConsolidation/TASK4.2.md
- specs/BackendDatabaseConsolidation/TASK4.3.md
- specs/BackendDatabaseConsolidation/TASK5.1.md
- specs/BackendDatabaseConsolidation/TASK5.2.md
- specs/BrinkLaborGetShiftsStaging/TASK.md
- specs/BrinkPosOrdersFirstEtlJob/TASK.md
- specs/BrinkPosOrdersFirstEtlJob/TASK0.md
- specs/BrinkPosOrdersFirstEtlJob/TASK1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.md
- specs/BrinkPosOrdersFirstEtlJob/TASK7.md
- specs/BrinkPosOrdersFirstEtlJob/TASK8.md
- specs/BrinkPosOrdersFirstEtlJob/TASK9.md
- specs/BrinkSettings2ToStaging/TASK.MD
- specs/CuratedToS3Stage/TASK.md
- specs/CuratedToS3Stage/TASK0.md
- specs/CuratedToS3Stage/TASK1.md
- specs/CuratedToS3Stage/TASK2.md
- specs/CuratedToS3Stage/TASK3.md
- specs/CuratedToS3Stage/TASK4.md
- specs/CustomerCredentialCustomHeaders/TASK.md
- specs/CustomerCredentialCustomHeaders/TASK1.1.md
- specs/CustomerCredentialCustomHeaders/TASK1.2.md
- specs/CustomerCredentialCustomHeaders/TASK2.1.md
- specs/CustomerCredentialCustomHeaders/TASK2.2.md
- specs/CustomerCredentialCustomHeaders/TASK3.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK.md
- specs/CustomerCredentialTriggeredProvisioning/TASK1.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK1.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK2.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK2.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK3.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK3.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK4.1.md
- specs/EtlSourceAdapterFramework/TASK.md
- specs/EtlSourceAdapterFramework/TASK0.md
- specs/EtlSourceAdapterFramework/TASK1.md
- specs/EtlSourceAdapterFramework/TASK2.md
- specs/EtlSourceAdapterFramework/TASK3.md
- specs/EtlSourceAdapterFramework/TASK4.md
- specs/EtlSourceAdapterFramework/TASK5.md
- specs/EtlSourceAdapterFramework/TASK6.md
- specs/KdsCdcDebeziumIntegration/TASK.md
- specs/KdsCdcDebeziumIntegration/TASK1.md
- specs/KdsCdcDebeziumIntegration/TASK2.md
- specs/KdsCdcDebeziumIntegration/TASK3.md
- specs/KdsCdcDebeziumIntegration/TASK4.md
- specs/KdsCdcDebeziumIntegration/TASK5.md
- specs/KdsCdcDurableRelationRouting/TASK.md
- specs/KdsCdcDurableRelationRouting/TASK1.md
- specs/KdsCdcDurableRelationRouting/TASK2.md
- specs/KdsCdcDurableRelationRouting/TASK3.md
- specs/KdsCdcDurableRelationRouting/TASK4.md
- specs/KdsCdcDurableRelationRouting/TASK5.md
- specs/KdsCdcIngestion/TASK.md
- specs/KdsCdcIngestion/TASK1.md
- specs/KdsCdcIngestion/TASK2.md
- specs/KdsCdcIngestion/TASK3.md
- specs/KdsCdcIngestion/TASK4.md
- specs/KdsCdcIngestion/TASK5.md
- specs/KdsCdcIngestion/TASK6.md
- specs/KdsCdcIngestion/TASK7.md
- specs/KdsCdcIngestion/TASK8.md
- specs/KdsCdcIngestion/TASK9.md
- specs/KdsCdcReplayExecution/TASK.md
- specs/KdsCdcReplayExecution/TASK1.md
- specs/KdsCdcReplayExecution/TASK2.md
- specs/KdsCdcReplayExecution/TASK3.md
- specs/KdsCdcReplayExecution/TASK4.md
- specs/KdsMonitorServingContractMigration/TASK.md
- specs/KdsMonitorServingContractMigration/TASK1.1.md
- specs/KdsMonitorServingContractMigration/TASK1.2.md
- specs/KdsMonitorServingContractMigration/TASK2.1.md
- specs/KdsMonitorServingContractMigration/TASK2.2.md
- specs/KdsMonitorServingContractMigration/TASK3.1.md
- specs/KdsMonitorServingContractMigration/TASK3.2.md
- specs/RedshiftFoundation/TASK.md
- specs/RedshiftFoundation/TASK1.1.md
- specs/RedshiftFoundation/TASK1.2.md
- specs/RedshiftFoundation/TASK1.3.md
- specs/RedshiftFoundation/TASK1.4.md
- specs/RedshiftFoundation/TASK2.1.md
- specs/RedshiftFoundation/TASK2.2.md
- specs/RedshiftFoundation/TASK3.1.md
- specs/RedshiftFoundation/TASK3.2.md
- specs/RedshiftFoundation/TASK3.3.md
- specs/RedshiftSyntheticDataFeed/TASK.md
- specs/RedshiftSyntheticDataFeed/TASK1.1.md
- specs/RedshiftSyntheticDataFeed/TASK1.2.md
- specs/RedshiftSyntheticDataFeed/TASK1.3.md
- specs/RedshiftSyntheticDataFeed/TASK2.1.md
- specs/RedshiftSyntheticDataFeed/TASK2.2.md
- specs/RedshiftSyntheticDataFeed/TASK3.1.md
- specs/RedshiftSyntheticDataFeed/TASK3.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK1.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK1.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.3.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK3.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK3.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK4.1.md
ApiService PostgreSQL Test Stability - Implementation Tasks
Overview
Investigation and remediation plan for unstable execution of Kds.Ai.ApiService.Tests caused by PostgreSQL connection pool exhaustion during per-test schema bootstrap.
This task plan is derived from analysis of the current test infrastructure in:
backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Infrastructure/PostgresDatabase.csbackend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Infrastructure/TestsBase.csbackend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Infrastructure/AppFactory.cs
It is also grounded in the current TUnit guidance for ASP.NET integration tests and parallel execution:
https://tunit.dev/docs/introhttps://tunit.dev/docs/guides/performance
It is also derived from repeated local executions of:
dotnet test --project "backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj"
Observed failure signature:
Npgsql.NpgsqlException: The connection pool has been exhausted, either raise 'Max Pool Size' (currently 20) or 'Timeout' (currently 15 seconds) in your connection string.- failure location:
Kds.Ai.ApiService.Tests.Infrastructure.TestsBase.InitializeAsync(...) - failure phase:
Before(Test)hook, before individual test logic runs
The current test harness uses one shared PostgreSQL testcontainer per test session, creates a fresh schema per test, and performs schema creation plus seed data setup in parallel across many tests. The shared connection string explicitly caps MaxPoolSize at 20, which is too small for the current concurrency level and bootstrap pattern.
Source Documents
- repository analysis performed during this session
- failing local test output from repeated
Kds.Ai.ApiService.Testsruns - current TUnit integration test harness for API service
Spec Name
The canonical implementation spec should be referred to as ApiService PostgreSQL Test Stability.
Fixed Decisions
- This is a test infrastructure problem, not an application runtime bug.
- The goal is stable repeated execution of the full
Kds.Ai.ApiService.Testsproject. - The fix must target the test harness and its PostgreSQL usage pattern, not production connection settings.
- The final state must support repeated green runs of the full project without relying on lucky scheduling.
- The solution should prefer deterministic test infrastructure changes over simply retrying failed runs.
Scope
This task includes:
- documenting the exact failure mode and reproduction path,
- identifying why per-test schema bootstrap exhausts the shared Npgsql pool,
- reviewing TUnit parallelism and the current per-test setup pattern,
- choosing and implementing a stability fix in the API test harness,
- validating the fix by re-running the full API test project multiple times.
In short, this slice ends when Kds.Ai.ApiService.Tests can run reliably as a full suite without intermittent Npgsql pool exhaustion during Before(Test) initialization.
Out of Scope
The following items are explicitly excluded from this task:
- application feature changes,
- warehouse or Redshift runtime behavior,
- changes to production PostgreSQL connection strings,
- unrelated test projects unless they need the same harness correction,
- masking the issue by documenting retries as the expected workflow.
Problem Statement
What Fails
The full API integration test project is unstable:
- some runs succeed completely,
- other runs fail before test bodies execute,
- failures happen in
TestsBase.InitializeAsync(...)while opening PostgreSQL connections.
Current Harness Behavior
PostgresDatabaseprovisions one shared PostgreSQL container per test session.PostgresDatabase.ConnectionStringforcesMaxPoolSize = 20.- every test gets its own schema via
TestsBase.InitializeAsync(...). InitializeAsync(...)opens a rawNpgsqlConnection, creates a schema, creates EF tables, and seeds data.DisposeAsync()later opens another raw connection and drops the schema.- TUnit runs tests in parallel by default.
Why It Is Unstable
The current design combines:
- aggressive per-test setup,
- shared container and shared connection pool,
- explicit low pool limit,
- parallel test execution.
That means many tests compete for the same Npgsql pool at the exact same phase, and once enough tests are initializing simultaneously, connection acquisition starts timing out.
Observable Evidence
- repeated failures show the same exception family and same hook location,
- failing tests vary between runs,
- the failures are distributed across unrelated endpoint test classes,
- stack traces point to pool rent/open rather than SQL command failures,
- some full runs have succeeded earlier in the same workspace, which confirms flakiness rather than deterministic functional failure.
Reproduction
Primary reproduction command:
dotnet test --project "backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj"Typical failing message:
Npgsql.NpgsqlException: The connection pool has been exhausted, either raise 'Max Pool Size' (currently 20) or 'Timeout' (currently 15 seconds) in your connection string.Typical stack anchor:
Kds.Ai.ApiService.Tests.Infrastructure.TestsBase.InitializeAsync(CancellationToken cancellationToken)Latest validation run in this workspace:
- command:
dotnet test --project "backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj" - result:
92total,52passed,40failed, duration about49s - all observed failures still originate from
Before(Test)inTestsBase.InitializeAsync(...)
Technical Analysis
Shared Pool Bottleneck
PostgresDatabase.ConnectionString returns the container connection string with:
var builder = new NpgsqlConnectionStringBuilder(Container.GetConnectionString())
{
MaxPoolSize = 20,
};This hard cap is low relative to:
- 92 tests in the project,
- parallel
Before(Test)execution, - additional connections opened by EF
DbContextand WebApplicationFactory startup.
Hot Path In Every Test
TestsBase.InitializeAsync(...) does all of the following per test:
- open raw Npgsql connection,
- create schema,
- construct EF
KdsAiDbContext, - create all tables,
- seed base rows,
- initialize app factory lazily for later test use.
The teardown path opens another raw connection to drop the schema.
Failure Characteristics
Because the exception happens in Before(Test), the impacted tests are not semantically broken. They are simply unable to acquire a connection in time.
This is why failing test names vary from run to run and do not cluster around one business area.
TUnit-Specific Interpretation
According to the TUnit performance guidance, parallel execution should be explicitly limited when tests share constrained infrastructure such as a database. The current test project does not define any ParallelLimiter, so the engine is free to schedule more bootstrap work than the shared PostgreSQL container and connection pool can sustain.
The same guidance also matters for choosing the correct primitive:
ParallelLimiteris the right tool for capping concurrent integration tests against a shared database,ParallelGroupis not sufficient because tests inside a group still run concurrently,NotInParallelwould over-correct by serializing too much of the suite.
That makes this primarily a missing concurrency-governor problem, amplified by an artificially low MaxPoolSize.
Recommended Solution
Decision
The recommended implementation direction is Option D: combine reduced TUnit parallelism with removal of the test-only low pool cap.
This should be treated as the primary fix for the API integration harness.
Why This Is The Best Fit
- It directly addresses the root cause: too many concurrent
Before(Test)database bootstraps. - It follows the TUnit performance guidance for shared database resources.
- It keeps per-test schema isolation intact.
- It avoids the worst tradeoff of fully serial execution.
- It avoids relying on a bigger pool alone, which would only move the failure threshold.
Concrete Changes To Make
- Add a TUnit
ParallelLimiterforKds.Ai.ApiService.Tests.
Recommended shape:
using TUnit.Core;
using TUnit.Core.Interfaces;
[assembly: ParallelLimiter<ApiServicePostgresParallelLimit>]
public sealed class ApiServicePostgresParallelLimit : IParallelLimit
{
public int Limit => 6;
}Rationale for starting at 6:
- each test performs schema bootstrap in
Before(Test), - each bootstrap can consume more than one connection when raw
NpgsqlConnection, EF Core table creation, and app startup overlap, 6is low enough to stop thundering-herd pressure but still preserves useful parallelism.
This value should be treated as the starting point, not a magic constant. If one validation pass still shows pressure, reduce to 4 or 5 before considering any larger redesign.
- Remove the explicit
MaxPoolSize = 20override fromInfrastructure/PostgresDatabase.cs.
Recommended change:
- stop forcing a test-only pool of
20, - use the provider default pool settings from
Container.GetConnectionString()unless a later measurement shows a clear reason to tune them.
Rationale:
- the current cap is much lower than the effective workload generated by the suite,
- it is not representative of the intended integration-test capacity,
- keeping the hard cap while only adding a limiter leaves unnecessarily little headroom for EF Core and host startup.
- Keep the existing per-test schema model.
Do not replace schema isolation as part of the immediate fix. The schema-per-test pattern is not the core bug; the unbounded concurrency against that pattern is.
- Do not use
[NotInParallel]as the main project-wide fix.
That would likely stabilize the suite, but at a disproportionate runtime cost. TUnit's limiter is the more appropriate control.
Optional Hardening After Stabilization
These are secondary improvements, not the primary fix:
- move
TestsBasetoward TUnit'sWebApplicationTestlifecycle model (SetupAsync,ConfigureTestConfiguration) so per-test setup is expressed in the framework's intended extension points, - reduce bootstrap cost by consolidating repeated seeding logic,
- if runtime is still too high, consider a more advanced pattern such as pre-created template state or transaction-based isolation for tests that do not need schema-level independence.
Task Checklist
Phase 1: Diagnosis Lock-In
- [x] Confirm the minimal, reproducible failure mode for full
Kds.Ai.ApiService.Tests - [x] Capture the exact concurrency and connection-pool bottleneck in the current harness
- [x] Decide whether the primary fix is pool-size increase, reduced parallelism, bootstrap redesign, or a combination
Phase 2: Harness Stabilization
- [ ] Update
PostgresDatabase/TestsBaseinfrastructure to remove the pool exhaustion failure mode - [ ] Ensure the chosen fix is deterministic and does not rely on manual retries
- [ ] Keep test isolation guarantees intact while improving stability
Phase 3: Verification
- [ ] Run the full
Kds.Ai.ApiService.Testsproject repeatedly - [ ] Verify that the suite becomes stable across repeated runs
- [ ] Document the final reasoning and any remaining tradeoffs
Candidate Fix Directions
Option A: Increase Pool Size
Raise MaxPoolSize in test-only infrastructure.
Pros:
- smallest code change,
- likely addresses immediate bottleneck.
Cons:
- may only move the threshold,
- does not reduce bootstrap pressure.
Option B: Reduce Test Parallelism For This Project Or Base Class
Constrain concurrency for tests using shared per-test schema bootstrap.
Pros:
- directly addresses contention,
- deterministic.
Cons:
- increases execution time.
Option C: Reduce Connection Churn During Setup
Refactor initialization so fewer connections are opened during Before(Test) and After(Test).
Pros:
- better long-term fix,
- lowers infrastructure pressure.
Cons:
- more intrusive change.
Option D: Combine A + B Or A + C
Most likely final direction if one isolated change is insufficient.
Recommended Selection
Choose Option D, specifically:
- use B as the controlling fix by adding a TUnit
ParallelLimiter, - combine it with a small A correction by removing the artificial
MaxPoolSize = 20bottleneck, - keep C as a follow-up optimization only if stabilization succeeds but execution time remains unattractive.
This gives the suite a deterministic concurrency ceiling and removes the most obvious self-inflicted pool restriction without forcing a larger harness rewrite in the same change.
Success Criteria
dotnet test --project "backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj"completes green repeatedly- no
Npgsqlpool exhaustion errors occur inTestsBase.InitializeAsync(...) - test isolation by schema remains intact
- the solution is documented as a test harness stabilization, not a production setting change