Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/ReachabilityFacts/ReachabilityFactsStore.cs
StellaOps Bot 3b96b2e3ea
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
up
2025-11-27 23:45:09 +02:00

214 lines
6.8 KiB
C#

using System.Collections.Concurrent;
namespace StellaOps.Policy.Engine.ReachabilityFacts;
/// <summary>
/// Store interface for reachability facts persistence.
/// </summary>
public interface IReachabilityFactsStore
{
/// <summary>
/// Gets a single reachability fact by key.
/// </summary>
Task<ReachabilityFact?> GetAsync(
string tenantId,
string componentPurl,
string advisoryId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets multiple reachability facts by keys.
/// </summary>
Task<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(
IReadOnlyList<ReachabilityFactKey> keys,
CancellationToken cancellationToken = default);
/// <summary>
/// Queries reachability facts with filtering.
/// </summary>
Task<IReadOnlyList<ReachabilityFact>> QueryAsync(
ReachabilityFactsQuery query,
CancellationToken cancellationToken = default);
/// <summary>
/// Saves or updates a reachability fact.
/// </summary>
Task SaveAsync(
ReachabilityFact fact,
CancellationToken cancellationToken = default);
/// <summary>
/// Saves multiple reachability facts.
/// </summary>
Task SaveBatchAsync(
IReadOnlyList<ReachabilityFact> facts,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a reachability fact.
/// </summary>
Task DeleteAsync(
string tenantId,
string componentPurl,
string advisoryId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the count of facts for a tenant.
/// </summary>
Task<long> CountAsync(
string tenantId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// In-memory implementation of the reachability facts store for development and testing.
/// </summary>
public sealed class InMemoryReachabilityFactsStore : IReachabilityFactsStore
{
private readonly ConcurrentDictionary<ReachabilityFactKey, ReachabilityFact> _facts = new();
private readonly TimeProvider _timeProvider;
public InMemoryReachabilityFactsStore(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<ReachabilityFact?> GetAsync(
string tenantId,
string componentPurl,
string advisoryId,
CancellationToken cancellationToken = default)
{
var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId);
_facts.TryGetValue(key, out var fact);
return Task.FromResult(fact);
}
public Task<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(
IReadOnlyList<ReachabilityFactKey> keys,
CancellationToken cancellationToken = default)
{
var result = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
foreach (var key in keys)
{
if (_facts.TryGetValue(key, out var fact))
{
result[key] = fact;
}
}
return Task.FromResult<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>>(result);
}
public Task<IReadOnlyList<ReachabilityFact>> QueryAsync(
ReachabilityFactsQuery query,
CancellationToken cancellationToken = default)
{
var now = _timeProvider.GetUtcNow();
var results = _facts.Values
.Where(f => f.TenantId == query.TenantId)
.Where(f => query.ComponentPurls == null || query.ComponentPurls.Contains(f.ComponentPurl))
.Where(f => query.AdvisoryIds == null || query.AdvisoryIds.Contains(f.AdvisoryId))
.Where(f => query.States == null || query.States.Contains(f.State))
.Where(f => !query.MinConfidence.HasValue || f.Confidence >= query.MinConfidence.Value)
.Where(f => query.IncludeExpired || !f.ExpiresAt.HasValue || f.ExpiresAt > now)
.OrderByDescending(f => f.ComputedAt)
.Skip(query.Skip)
.Take(query.Limit)
.ToList();
return Task.FromResult<IReadOnlyList<ReachabilityFact>>(results);
}
public Task SaveAsync(ReachabilityFact fact, CancellationToken cancellationToken = default)
{
var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId);
_facts[key] = fact;
return Task.CompletedTask;
}
public Task SaveBatchAsync(IReadOnlyList<ReachabilityFact> facts, CancellationToken cancellationToken = default)
{
foreach (var fact in facts)
{
var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId);
_facts[key] = fact;
}
return Task.CompletedTask;
}
public Task DeleteAsync(
string tenantId,
string componentPurl,
string advisoryId,
CancellationToken cancellationToken = default)
{
var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId);
_facts.TryRemove(key, out _);
return Task.CompletedTask;
}
public Task<long> CountAsync(string tenantId, CancellationToken cancellationToken = default)
{
var count = _facts.Values.Count(f => f.TenantId == tenantId);
return Task.FromResult((long)count);
}
}
/// <summary>
/// Index definitions for MongoDB reachability_facts collection.
/// </summary>
public static class ReachabilityFactsIndexes
{
/// <summary>
/// Primary compound index for efficient lookups.
/// </summary>
public const string PrimaryIndex = "tenant_component_advisory";
/// <summary>
/// Index for querying by tenant and state.
/// </summary>
public const string TenantStateIndex = "tenant_state_computed";
/// <summary>
/// Index for TTL expiration.
/// </summary>
public const string ExpirationIndex = "expires_at_ttl";
/// <summary>
/// Gets the index definitions for creating MongoDB indexes.
/// </summary>
public static IReadOnlyList<ReachabilityIndexDefinition> GetIndexDefinitions()
{
return new[]
{
new ReachabilityIndexDefinition(
PrimaryIndex,
new[] { "tenant_id", "component_purl", "advisory_id" },
Unique: true),
new ReachabilityIndexDefinition(
TenantStateIndex,
new[] { "tenant_id", "state", "computed_at" },
Unique: false),
new ReachabilityIndexDefinition(
ExpirationIndex,
new[] { "expires_at" },
Unique: false,
ExpireAfterSeconds: 0),
};
}
}
/// <summary>
/// Index definition for MongoDB collection.
/// </summary>
public sealed record ReachabilityIndexDefinition(
string Name,
IReadOnlyList<string> Fields,
bool Unique,
int? ExpireAfterSeconds = null);