Files
git.stella-ops.org/src/StellaOps.Feedser.Storage.Mongo.Tests/AdvisoryStorePerformanceTests.cs
master b97fc7685a
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Initial commit (history squashed)
2025-10-11 23:28:35 +03:00

196 lines
8.9 KiB
C#

using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Aliases;
using StellaOps.Feedser.Storage.Mongo.Migrations;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Feedser.Storage.Mongo.Tests;
[Collection("mongo-fixture")]
public sealed class AdvisoryStorePerformanceTests : IClassFixture<MongoIntegrationFixture>
{
private const int LargeAdvisoryCount = 30;
private const int AliasesPerAdvisory = 24;
private const int ReferencesPerAdvisory = 180;
private const int AffectedPackagesPerAdvisory = 140;
private const int VersionRangesPerPackage = 4;
private const int CvssMetricsPerAdvisory = 24;
private const int ProvenanceEntriesPerAdvisory = 16;
private static readonly string LargeSummary = new('A', 128 * 1024);
private static readonly DateTimeOffset BasePublished = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset BaseRecorded = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static readonly TimeSpan TotalBudget = TimeSpan.FromSeconds(28);
private const double UpsertBudgetPerAdvisoryMs = 500;
private const double FetchBudgetPerAdvisoryMs = 200;
private const double FindBudgetPerAdvisoryMs = 200;
private readonly MongoIntegrationFixture _fixture;
private readonly ITestOutputHelper _output;
public AdvisoryStorePerformanceTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_output = output;
}
[Fact]
public async Task UpsertAndQueryLargeAdvisories_CompletesWithinBudget()
{
var databaseName = $"feedser-performance-{Guid.NewGuid():N}";
var database = _fixture.Client.GetDatabase(databaseName);
try
{
var migrationRunner = new MongoMigrationRunner(
database,
Array.Empty<IMongoMigration>(),
NullLogger<MongoMigrationRunner>.Instance,
TimeProvider.System);
var bootstrapper = new MongoBootstrapper(
database,
Options.Create(new MongoStorageOptions()),
NullLogger<MongoBootstrapper>.Instance,
migrationRunner);
await bootstrapper.InitializeAsync(CancellationToken.None);
var aliasStore = new AliasStore(database, NullLogger<AliasStore>.Instance);
var store = new AdvisoryStore(database, aliasStore, NullLogger<AdvisoryStore>.Instance, TimeProvider.System);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
// Warm up collections (indexes, serialization caches) so perf timings exclude one-time setup work.
var warmup = CreateLargeAdvisory(-1);
await store.UpsertAsync(warmup, cts.Token);
_ = await store.FindAsync(warmup.AdvisoryKey, cts.Token);
_ = await store.GetRecentAsync(1, cts.Token);
var advisories = Enumerable.Range(0, LargeAdvisoryCount)
.Select(CreateLargeAdvisory)
.ToArray();
var upsertWatch = Stopwatch.StartNew();
foreach (var advisory in advisories)
{
await store.UpsertAsync(advisory, cts.Token);
}
upsertWatch.Stop();
var upsertPerAdvisory = upsertWatch.Elapsed.TotalMilliseconds / LargeAdvisoryCount;
var fetchWatch = Stopwatch.StartNew();
var recent = await store.GetRecentAsync(LargeAdvisoryCount, cts.Token);
fetchWatch.Stop();
var fetchPerAdvisory = fetchWatch.Elapsed.TotalMilliseconds / LargeAdvisoryCount;
Assert.Equal(LargeAdvisoryCount, recent.Count);
var findWatch = Stopwatch.StartNew();
foreach (var advisory in advisories)
{
var fetched = await store.FindAsync(advisory.AdvisoryKey, cts.Token);
Assert.NotNull(fetched);
}
findWatch.Stop();
var findPerAdvisory = findWatch.Elapsed.TotalMilliseconds / LargeAdvisoryCount;
var totalElapsed = upsertWatch.Elapsed + fetchWatch.Elapsed + findWatch.Elapsed;
_output.WriteLine($"Upserted {LargeAdvisoryCount} large advisories in {upsertWatch.Elapsed} ({upsertPerAdvisory:F2} ms/doc).");
_output.WriteLine($"Fetched recent advisories in {fetchWatch.Elapsed} ({fetchPerAdvisory:F2} ms/doc).");
_output.WriteLine($"Looked up advisories individually in {findWatch.Elapsed} ({findPerAdvisory:F2} ms/doc).");
_output.WriteLine($"Total elapsed {totalElapsed}.");
Assert.True(upsertPerAdvisory <= UpsertBudgetPerAdvisoryMs, $"Upsert exceeded {UpsertBudgetPerAdvisoryMs} ms per advisory: {upsertPerAdvisory:F2} ms.");
Assert.True(fetchPerAdvisory <= FetchBudgetPerAdvisoryMs, $"GetRecent exceeded {FetchBudgetPerAdvisoryMs} ms per advisory: {fetchPerAdvisory:F2} ms.");
Assert.True(findPerAdvisory <= FindBudgetPerAdvisoryMs, $"Find exceeded {FindBudgetPerAdvisoryMs} ms per advisory: {findPerAdvisory:F2} ms.");
Assert.True(totalElapsed <= TotalBudget, $"Mongo advisory operations exceeded total budget {TotalBudget}: {totalElapsed}.");
}
finally
{
await _fixture.Client.DropDatabaseAsync(databaseName);
}
}
private static Advisory CreateLargeAdvisory(int index)
{
var baseKey = $"ADV-LARGE-{index:D4}";
var published = BasePublished.AddDays(index);
var modified = published.AddHours(6);
var aliases = Enumerable.Range(0, AliasesPerAdvisory)
.Select(i => $"ALIAS-{baseKey}-{i:D4}")
.ToArray();
var provenance = Enumerable.Range(0, ProvenanceEntriesPerAdvisory)
.Select(i => new AdvisoryProvenance(
source: i % 2 == 0 ? "nvd" : "vendor",
kind: i % 3 == 0 ? "normalized" : "enriched",
value: $"prov-{baseKey}-{i:D3}",
recordedAt: BaseRecorded.AddDays(i)))
.ToArray();
var references = Enumerable.Range(0, ReferencesPerAdvisory)
.Select(i => new AdvisoryReference(
url: $"https://vuln.example.com/{baseKey}/ref/{i:D4}",
kind: i % 2 == 0 ? "advisory" : "article",
sourceTag: $"tag-{i % 7}",
summary: $"Reference {baseKey} #{i}",
provenance: provenance[i % provenance.Length]))
.ToArray();
var affectedPackages = Enumerable.Range(0, AffectedPackagesPerAdvisory)
.Select(i => new AffectedPackage(
type: i % 3 == 0 ? AffectedPackageTypes.Rpm : AffectedPackageTypes.Deb,
identifier: $"pkg/{baseKey}/{i:D4}",
platform: i % 4 == 0 ? "linux/x86_64" : "linux/aarch64",
versionRanges: Enumerable.Range(0, VersionRangesPerPackage)
.Select(r => new AffectedVersionRange(
rangeKind: r % 2 == 0 ? "semver" : "evr",
introducedVersion: $"1.{index}.{i}.{r}",
fixedVersion: $"2.{index}.{i}.{r}",
lastAffectedVersion: $"1.{index}.{i}.{r}",
rangeExpression: $">=1.{index}.{i}.{r} <2.{index}.{i}.{r}",
provenance: provenance[(i + r) % provenance.Length]))
.ToArray(),
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[]
{
provenance[i % provenance.Length],
provenance[(i + 3) % provenance.Length],
}))
.ToArray();
var cvssMetrics = Enumerable.Range(0, CvssMetricsPerAdvisory)
.Select(i => new CvssMetric(
version: i % 2 == 0 ? "3.1" : "2.0",
vector: $"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:{(i % 3 == 0 ? "H" : "L")}",
baseScore: Math.Max(0, 9.8 - i * 0.2),
baseSeverity: i % 3 == 0 ? "critical" : "high",
provenance: provenance[i % provenance.Length]))
.ToArray();
return new Advisory(
advisoryKey: baseKey,
title: $"Large advisory {baseKey}",
summary: LargeSummary,
language: "en",
published: published,
modified: modified,
severity: "critical",
exploitKnown: index % 2 == 0,
aliases: aliases,
references: references,
affectedPackages: affectedPackages,
cvssMetrics: cvssMetrics,
provenance: provenance);
}
}