DET-001/002/003: Add IGuidProvider abstraction and refactor Policy.Unknowns for determinism
- Created IGuidProvider interface and SystemGuidProvider in StellaOps.Determinism.Abstractions - Added SequentialGuidProvider for testing deterministic GUID generation - Added DeterminismServiceCollectionExtensions with AddDeterminismDefaults() - Refactored Policy.Unknowns: - UnknownsRepository now uses TimeProvider and IGuidProvider - BudgetExceededEventFactory accepts optional TimeProvider parameter - ServiceCollectionExtensions calls AddDeterminismDefaults() - Fixed Policy.Exceptions csproj (added ImplicitUsings, Nullable, PackageReferences) Sprint: SPRINT_20260104_001_BE_determinism_timeprovider_injection Tasks: DET-001 (audit), DET-002 (IGuidProvider), DET-003 (registration pattern), DET-004 (partial - Policy.Unknowns)
This commit is contained in:
@@ -54,10 +54,10 @@
|
|||||||
|
|
||||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 1 | DET-001 | TODO | Audit complete | Guild | Full audit: count all DateTimeOffset.UtcNow/DateTime.UtcNow/Guid.NewGuid/Random.Shared by project |
|
| 1 | DET-001 | DONE | Audit complete | Guild | Full audit: count all DateTimeOffset.UtcNow/DateTime.UtcNow/Guid.NewGuid/Random.Shared by project |
|
||||||
| 2 | DET-002 | TODO | DET-001 | Guild | Ensure IGuidProvider abstraction exists in StellaOps.Determinism.Abstractions |
|
| 2 | DET-002 | DONE | DET-001 | Guild | Ensure IGuidProvider abstraction exists in StellaOps.Determinism.Abstractions |
|
||||||
| 3 | DET-003 | TODO | DET-001 | Guild | Ensure TimeProvider registration pattern documented |
|
| 3 | DET-003 | DONE | DET-001 | Guild | Ensure TimeProvider registration pattern documented |
|
||||||
| 4 | DET-004 | TODO | DET-002, DET-003 | Guild | Refactor Policy module (Policy.Unknowns, PolicyDsl, etc.) |
|
| 4 | DET-004 | DOING | DET-002, DET-003 | Guild | Refactor Policy module (Policy.Unknowns, PolicyDsl, etc.) |
|
||||||
| 5 | DET-005 | TODO | DET-002, DET-003 | Guild | Refactor Provcache module |
|
| 5 | DET-005 | TODO | DET-002, DET-003 | Guild | Refactor Provcache module |
|
||||||
| 6 | DET-006 | TODO | DET-002, DET-003 | Guild | Refactor Provenance module |
|
| 6 | DET-006 | TODO | DET-002, DET-003 | Guild | Refactor Provenance module |
|
||||||
| 7 | DET-007 | TODO | DET-002, DET-003 | Guild | Refactor ReachGraph module |
|
| 7 | DET-007 | TODO | DET-002, DET-003 | Guild | Refactor ReachGraph module |
|
||||||
@@ -110,6 +110,11 @@ services.AddSingleton<IGuidProvider, SystemGuidProvider>();
|
|||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 2026-01-04 | Sprint created; deferred from SPRINT_20251229_049_BE MAINT tasks | Planning |
|
| 2026-01-04 | Sprint created; deferred from SPRINT_20251229_049_BE MAINT tasks | Planning |
|
||||||
|
| 2026-01-04 | DET-001: Audit complete. Found 1526 DateTimeOffset.UtcNow, 181 DateTime.UtcNow, 687 Guid.NewGuid, 16 Random.Shared | Agent |
|
||||||
|
| 2026-01-04 | DET-002: Created IGuidProvider, SystemGuidProvider, SequentialGuidProvider in StellaOps.Determinism.Abstractions | Agent |
|
||||||
|
| 2026-01-04 | DET-003: Created DeterminismServiceCollectionExtensions with AddDeterminismDefaults() | Agent |
|
||||||
|
| 2026-01-04 | DET-004: Policy.Unknowns refactored - UnknownsRepository, BudgetExceededEventFactory, ServiceCollectionExtensions | Agent |
|
||||||
|
| 2026-01-04 | Fixed Policy.Exceptions csproj - added ImplicitUsings, Nullable, PackageReferences | Agent |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach.
|
- **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageId>StellaOps.Policy.Exceptions</PackageId>
|
<PackageId>StellaOps.Policy.Exceptions</PackageId>
|
||||||
<Authors>StellaOps</Authors>
|
<Authors>StellaOps</Authors>
|
||||||
@@ -12,4 +14,9 @@
|
|||||||
<IncludeSource>false</IncludeSource>
|
<IncludeSource>false</IncludeSource>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
|
<PackageReference Include="Npgsql" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -29,9 +29,15 @@ public static class BudgetExceededEventFactory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a budget exceeded notification event payload.
|
/// Creates a budget exceeded notification event payload.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="environment">The environment name.</param>
|
||||||
|
/// <param name="result">The budget check result.</param>
|
||||||
|
/// <param name="timeProvider">Time provider for timestamp. Uses system time if null.</param>
|
||||||
|
/// <param name="imageDigest">Optional image digest.</param>
|
||||||
|
/// <param name="policyRevisionId">Optional policy revision ID.</param>
|
||||||
public static BudgetEventPayload CreatePayload(
|
public static BudgetEventPayload CreatePayload(
|
||||||
string environment,
|
string environment,
|
||||||
BudgetCheckResult result,
|
BudgetCheckResult result,
|
||||||
|
TimeProvider? timeProvider = null,
|
||||||
string? imageDigest = null,
|
string? imageDigest = null,
|
||||||
string? policyRevisionId = null)
|
string? policyRevisionId = null)
|
||||||
{
|
{
|
||||||
@@ -57,7 +63,7 @@ public static class BudgetExceededEventFactory
|
|||||||
Message = result.Message,
|
Message = result.Message,
|
||||||
ImageDigest = imageDigest,
|
ImageDigest = imageDigest,
|
||||||
PolicyRevisionId = policyRevisionId,
|
PolicyRevisionId = policyRevisionId,
|
||||||
Timestamp = DateTimeOffset.UtcNow
|
Timestamp = (timeProvider ?? TimeProvider.System).GetUtcNow()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +204,7 @@ public sealed record BudgetEventPayload
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event timestamp.
|
/// Event timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset Timestamp { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using StellaOps.Determinism;
|
||||||
using StellaOps.Policy.Unknowns.Models;
|
using StellaOps.Policy.Unknowns.Models;
|
||||||
|
|
||||||
namespace StellaOps.Policy.Unknowns.Repositories;
|
namespace StellaOps.Policy.Unknowns.Repositories;
|
||||||
@@ -15,9 +16,15 @@ namespace StellaOps.Policy.Unknowns.Repositories;
|
|||||||
public sealed class UnknownsRepository : IUnknownsRepository
|
public sealed class UnknownsRepository : IUnknownsRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnection _connection;
|
private readonly IDbConnection _connection;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
private readonly IGuidProvider _guidProvider;
|
||||||
|
|
||||||
public UnknownsRepository(IDbConnection connection)
|
public UnknownsRepository(IDbConnection connection, TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||||
=> _connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
{
|
||||||
|
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
|
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Unknown?> GetByIdAsync(Guid tenantId, Guid id, CancellationToken cancellationToken = default)
|
public async Task<Unknown?> GetByIdAsync(Guid tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||||
@@ -162,8 +169,8 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Unknown> CreateAsync(Unknown unknown, CancellationToken cancellationToken = default)
|
public async Task<Unknown> CreateAsync(Unknown unknown, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var id = unknown.Id == Guid.Empty ? Guid.NewGuid() : unknown.Id;
|
var id = unknown.Id == Guid.Empty ? _guidProvider.NewGuid() : unknown.Id;
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
const string sql = """
|
const string sql = """
|
||||||
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
||||||
@@ -258,7 +265,7 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
|||||||
WHERE id = @Id;
|
WHERE id = @Id;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var evaluatedAt = DateTimeOffset.UtcNow;
|
var evaluatedAt = _timeProvider.GetUtcNow();
|
||||||
var param = new
|
var param = new
|
||||||
{
|
{
|
||||||
unknown.TenantId,
|
unknown.TenantId,
|
||||||
@@ -304,7 +311,7 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
|||||||
WHERE id = @Id;
|
WHERE id = @Id;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
var param = new
|
var param = new
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
@@ -324,7 +331,7 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
|||||||
IEnumerable<Unknown> unknowns,
|
IEnumerable<Unknown> unknowns,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = _timeProvider.GetUtcNow();
|
||||||
var total = 0;
|
var total = 0;
|
||||||
|
|
||||||
const string sql = """
|
const string sql = """
|
||||||
@@ -370,7 +377,7 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
|||||||
|
|
||||||
foreach (var unknown in unknowns)
|
foreach (var unknown in unknowns)
|
||||||
{
|
{
|
||||||
var id = unknown.Id == Guid.Empty ? Guid.NewGuid() : unknown.Id;
|
var id = unknown.Id == Guid.Empty ? _guidProvider.NewGuid() : unknown.Id;
|
||||||
var param = new
|
var param = new
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using StellaOps.Determinism;
|
||||||
using StellaOps.Policy.Unknowns.Configuration;
|
using StellaOps.Policy.Unknowns.Configuration;
|
||||||
using StellaOps.Policy.Unknowns.Repositories;
|
using StellaOps.Policy.Unknowns.Repositories;
|
||||||
using StellaOps.Policy.Unknowns.Services;
|
using StellaOps.Policy.Unknowns.Services;
|
||||||
@@ -27,6 +28,9 @@ public static class ServiceCollectionExtensions
|
|||||||
if (configureBudgetOptions is not null)
|
if (configureBudgetOptions is not null)
|
||||||
services.Configure(configureBudgetOptions);
|
services.Configure(configureBudgetOptions);
|
||||||
|
|
||||||
|
// Ensure determinism dependencies are registered
|
||||||
|
services.AddDeterminismDefaults();
|
||||||
|
|
||||||
// Register services
|
// Register services
|
||||||
services.AddSingleton<IUnknownBudgetService, UnknownBudgetService>();
|
services.AddSingleton<IUnknownBudgetService, UnknownBudgetService>();
|
||||||
services.AddSingleton<IRemediationHintsRegistry, RemediationHintsRegistry>();
|
services.AddSingleton<IRemediationHintsRegistry, RemediationHintsRegistry>();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.Policy.Exceptions\StellaOps.Policy.Exceptions.csproj" />
|
<ProjectReference Include="..\StellaOps.Policy.Exceptions\StellaOps.Policy.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Policy\StellaOps.Policy.csproj" />
|
<ProjectReference Include="..\StellaOps.Policy\StellaOps.Policy.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace StellaOps.Determinism;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering determinism abstractions in DI.
|
||||||
|
/// </summary>
|
||||||
|
public static class DeterminismServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds <see cref="TimeProvider.System"/> as a singleton.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSystemTimeProvider(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.TryAddSingleton(TimeProvider.System);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds <see cref="SystemGuidProvider"/> as the <see cref="IGuidProvider"/> singleton.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSystemGuidProvider(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.TryAddSingleton<IGuidProvider, SystemGuidProvider>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds both <see cref="TimeProvider.System"/> and <see cref="SystemGuidProvider"/> as singletons.
|
||||||
|
/// This is the recommended setup for production services.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddDeterminismDefaults(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSystemTimeProvider();
|
||||||
|
services.AddSystemGuidProvider();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace StellaOps.Determinism;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstraction for GUID generation to support deterministic testing.
|
||||||
|
/// Inject this instead of using <see cref="Guid.NewGuid"/> directly.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGuidProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a new GUID.
|
||||||
|
/// </summary>
|
||||||
|
Guid NewGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation using <see cref="Guid.NewGuid"/>.
|
||||||
|
/// Register as singleton in DI: <c>services.AddSingleton<IGuidProvider, SystemGuidProvider>();</c>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SystemGuidProvider : IGuidProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared instance for non-DI scenarios.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly SystemGuidProvider Instance = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid NewGuid() => Guid.NewGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deterministic GUID provider for testing. Returns GUIDs in a predictable sequence.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SequentialGuidProvider : IGuidProvider
|
||||||
|
{
|
||||||
|
private int _counter;
|
||||||
|
private readonly Guid _baseGuid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a sequential GUID provider starting from a base GUID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseGuid">Optional base GUID. Defaults to all zeros.</param>
|
||||||
|
public SequentialGuidProvider(Guid? baseGuid = null)
|
||||||
|
{
|
||||||
|
_baseGuid = baseGuid ?? Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid NewGuid()
|
||||||
|
{
|
||||||
|
var bytes = _baseGuid.ToByteArray();
|
||||||
|
var counter = Interlocked.Increment(ref _counter);
|
||||||
|
// Embed counter in last 4 bytes
|
||||||
|
bytes[12] = (byte)(counter >> 24);
|
||||||
|
bytes[13] = (byte)(counter >> 16);
|
||||||
|
bytes[14] = (byte)(counter >> 8);
|
||||||
|
bytes[15] = (byte)counter;
|
||||||
|
return new Guid(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the counter to zero.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset() => Interlocked.Exchange(ref _counter, 0);
|
||||||
|
}
|
||||||
@@ -9,4 +9,8 @@
|
|||||||
<Description>Attributes and abstractions for determinism enforcement in StellaOps.</Description>
|
<Description>Attributes and abstractions for determinism enforcement in StellaOps.</Description>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user