Initial commit (history squashed)
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
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
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user