up
Some checks failed
Build Test Deploy / build-test (push) Has been cancelled
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
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Build Test Deploy / build-test (push) Has been cancelled
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
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -1,38 +1,38 @@
|
||||
using System;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
internal sealed class AliasDocument
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
|
||||
[BsonElement("advisoryKey")]
|
||||
public string AdvisoryKey { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("scheme")]
|
||||
public string Scheme { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("value")]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("updatedAt")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
internal static class AliasDocumentExtensions
|
||||
{
|
||||
public static AliasRecord ToRecord(this AliasDocument document)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
var updatedAt = DateTime.SpecifyKind(document.UpdatedAt, DateTimeKind.Utc);
|
||||
return new AliasRecord(
|
||||
document.AdvisoryKey,
|
||||
document.Scheme,
|
||||
document.Value,
|
||||
new DateTimeOffset(updatedAt));
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
internal sealed class AliasDocument
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
|
||||
[BsonElement("advisoryKey")]
|
||||
public string AdvisoryKey { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("scheme")]
|
||||
public string Scheme { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("value")]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("updatedAt")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
internal static class AliasDocumentExtensions
|
||||
{
|
||||
public static AliasRecord ToRecord(this AliasDocument document)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
var updatedAt = DateTime.SpecifyKind(document.UpdatedAt, DateTimeKind.Utc);
|
||||
return new AliasRecord(
|
||||
document.AdvisoryKey,
|
||||
document.Scheme,
|
||||
document.Value,
|
||||
new DateTimeOffset(updatedAt));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
public sealed class AliasStore : IAliasStore
|
||||
{
|
||||
private readonly IMongoCollection<AliasDocument> _collection;
|
||||
private readonly ILogger<AliasStore> _logger;
|
||||
|
||||
public AliasStore(IMongoDatabase database, ILogger<AliasStore> logger)
|
||||
{
|
||||
_collection = (database ?? throw new ArgumentNullException(nameof(database)))
|
||||
.GetCollection<AliasDocument>(MongoStorageDefaults.Collections.Alias);
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<AliasUpsertResult> ReplaceAsync(
|
||||
string advisoryKey,
|
||||
IEnumerable<AliasEntry> aliases,
|
||||
DateTimeOffset updatedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(advisoryKey);
|
||||
|
||||
var aliasList = Normalize(aliases).ToArray();
|
||||
var deleteFilter = Builders<AliasDocument>.Filter.Eq(x => x.AdvisoryKey, advisoryKey);
|
||||
await _collection.DeleteManyAsync(deleteFilter, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (aliasList.Length > 0)
|
||||
{
|
||||
var documents = new List<AliasDocument>(aliasList.Length);
|
||||
var updatedAtUtc = updatedAt.ToUniversalTime().UtcDateTime;
|
||||
foreach (var alias in aliasList)
|
||||
{
|
||||
documents.Add(new AliasDocument
|
||||
{
|
||||
Id = ObjectId.GenerateNewId(),
|
||||
AdvisoryKey = advisoryKey,
|
||||
Scheme = alias.Scheme,
|
||||
Value = alias.Value,
|
||||
UpdatedAt = updatedAtUtc,
|
||||
});
|
||||
}
|
||||
|
||||
if (documents.Count > 0)
|
||||
{
|
||||
await _collection.InsertManyAsync(
|
||||
documents,
|
||||
new InsertManyOptions { IsOrdered = false },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (aliasList.Length == 0)
|
||||
{
|
||||
return new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>());
|
||||
}
|
||||
|
||||
var collisions = new List<AliasCollision>();
|
||||
foreach (var alias in aliasList)
|
||||
{
|
||||
var filter = Builders<AliasDocument>.Filter.Eq(x => x.Scheme, alias.Scheme)
|
||||
& Builders<AliasDocument>.Filter.Eq(x => x.Value, alias.Value);
|
||||
|
||||
using var cursor = await _collection.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var advisoryKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
foreach (var document in cursor.Current)
|
||||
{
|
||||
advisoryKeys.Add(document.AdvisoryKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (advisoryKeys.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var collision = new AliasCollision(alias.Scheme, alias.Value, advisoryKeys.ToArray());
|
||||
collisions.Add(collision);
|
||||
AliasStoreMetrics.RecordCollision(alias.Scheme, advisoryKeys.Count);
|
||||
_logger.LogWarning(
|
||||
"Alias collision detected for {Scheme}:{Value}; advisories: {Advisories}",
|
||||
alias.Scheme,
|
||||
alias.Value,
|
||||
string.Join(", ", advisoryKeys));
|
||||
}
|
||||
|
||||
return new AliasUpsertResult(advisoryKey, collisions);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scheme);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||
|
||||
var normalizedScheme = NormalizeScheme(scheme);
|
||||
var normalizedValue = value.Trim();
|
||||
var filter = Builders<AliasDocument>.Filter.Eq(x => x.Scheme, normalizedScheme)
|
||||
& Builders<AliasDocument>.Filter.Eq(x => x.Value, normalizedValue);
|
||||
|
||||
var documents = await _collection.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return documents.Select(static d => d.ToRecord()).ToArray();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(advisoryKey);
|
||||
var filter = Builders<AliasDocument>.Filter.Eq(x => x.AdvisoryKey, advisoryKey);
|
||||
var documents = await _collection.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return documents.Select(static d => d.ToRecord()).ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AliasEntry> Normalize(IEnumerable<AliasEntry> aliases)
|
||||
{
|
||||
if (aliases is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
if (alias is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var scheme = NormalizeScheme(alias.Scheme);
|
||||
var value = alias.Value?.Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = $"{scheme}\u0001{value}";
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new AliasEntry(scheme, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeScheme(string scheme)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(scheme)
|
||||
? AliasStoreConstants.UnscopedScheme
|
||||
: scheme.Trim().ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
public sealed class AliasStore : IAliasStore
|
||||
{
|
||||
private readonly IMongoCollection<AliasDocument> _collection;
|
||||
private readonly ILogger<AliasStore> _logger;
|
||||
|
||||
public AliasStore(IMongoDatabase database, ILogger<AliasStore> logger)
|
||||
{
|
||||
_collection = (database ?? throw new ArgumentNullException(nameof(database)))
|
||||
.GetCollection<AliasDocument>(MongoStorageDefaults.Collections.Alias);
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<AliasUpsertResult> ReplaceAsync(
|
||||
string advisoryKey,
|
||||
IEnumerable<AliasEntry> aliases,
|
||||
DateTimeOffset updatedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(advisoryKey);
|
||||
|
||||
var aliasList = Normalize(aliases).ToArray();
|
||||
var deleteFilter = Builders<AliasDocument>.Filter.Eq(x => x.AdvisoryKey, advisoryKey);
|
||||
await _collection.DeleteManyAsync(deleteFilter, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (aliasList.Length > 0)
|
||||
{
|
||||
var documents = new List<AliasDocument>(aliasList.Length);
|
||||
var updatedAtUtc = updatedAt.ToUniversalTime().UtcDateTime;
|
||||
foreach (var alias in aliasList)
|
||||
{
|
||||
documents.Add(new AliasDocument
|
||||
{
|
||||
Id = ObjectId.GenerateNewId(),
|
||||
AdvisoryKey = advisoryKey,
|
||||
Scheme = alias.Scheme,
|
||||
Value = alias.Value,
|
||||
UpdatedAt = updatedAtUtc,
|
||||
});
|
||||
}
|
||||
|
||||
if (documents.Count > 0)
|
||||
{
|
||||
await _collection.InsertManyAsync(
|
||||
documents,
|
||||
new InsertManyOptions { IsOrdered = false },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (aliasList.Length == 0)
|
||||
{
|
||||
return new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>());
|
||||
}
|
||||
|
||||
var collisions = new List<AliasCollision>();
|
||||
foreach (var alias in aliasList)
|
||||
{
|
||||
var filter = Builders<AliasDocument>.Filter.Eq(x => x.Scheme, alias.Scheme)
|
||||
& Builders<AliasDocument>.Filter.Eq(x => x.Value, alias.Value);
|
||||
|
||||
using var cursor = await _collection.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var advisoryKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
foreach (var document in cursor.Current)
|
||||
{
|
||||
advisoryKeys.Add(document.AdvisoryKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (advisoryKeys.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var collision = new AliasCollision(alias.Scheme, alias.Value, advisoryKeys.ToArray());
|
||||
collisions.Add(collision);
|
||||
AliasStoreMetrics.RecordCollision(alias.Scheme, advisoryKeys.Count);
|
||||
_logger.LogWarning(
|
||||
"Alias collision detected for {Scheme}:{Value}; advisories: {Advisories}",
|
||||
alias.Scheme,
|
||||
alias.Value,
|
||||
string.Join(", ", advisoryKeys));
|
||||
}
|
||||
|
||||
return new AliasUpsertResult(advisoryKey, collisions);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scheme);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||
|
||||
var normalizedScheme = NormalizeScheme(scheme);
|
||||
var normalizedValue = value.Trim();
|
||||
var filter = Builders<AliasDocument>.Filter.Eq(x => x.Scheme, normalizedScheme)
|
||||
& Builders<AliasDocument>.Filter.Eq(x => x.Value, normalizedValue);
|
||||
|
||||
var documents = await _collection.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return documents.Select(static d => d.ToRecord()).ToArray();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(advisoryKey);
|
||||
var filter = Builders<AliasDocument>.Filter.Eq(x => x.AdvisoryKey, advisoryKey);
|
||||
var documents = await _collection.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return documents.Select(static d => d.ToRecord()).ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AliasEntry> Normalize(IEnumerable<AliasEntry> aliases)
|
||||
{
|
||||
if (aliases is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
if (alias is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var scheme = NormalizeScheme(alias.Scheme);
|
||||
var value = alias.Value?.Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = $"{scheme}\u0001{value}";
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new AliasEntry(scheme, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeScheme(string scheme)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(scheme)
|
||||
? AliasStoreConstants.UnscopedScheme
|
||||
: scheme.Trim().ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
public static class AliasStoreConstants
|
||||
{
|
||||
public const string PrimaryScheme = "PRIMARY";
|
||||
public const string UnscopedScheme = "UNSCOPED";
|
||||
}
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
public static class AliasStoreConstants
|
||||
{
|
||||
public const string PrimaryScheme = "PRIMARY";
|
||||
public const string UnscopedScheme = "UNSCOPED";
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
internal static class AliasStoreMetrics
|
||||
{
|
||||
private static readonly Meter Meter = new("StellaOps.Feedser.Merge");
|
||||
|
||||
internal static readonly Counter<long> AliasCollisionCounter = Meter.CreateCounter<long>(
|
||||
"feedser.merge.alias_conflict",
|
||||
unit: "count",
|
||||
description: "Number of alias collisions detected when the same alias maps to multiple advisories.");
|
||||
|
||||
public static void RecordCollision(string scheme, int advisoryCount)
|
||||
{
|
||||
AliasCollisionCounter.Add(
|
||||
1,
|
||||
new KeyValuePair<string, object?>("scheme", scheme),
|
||||
new KeyValuePair<string, object?>("advisory_count", advisoryCount));
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
internal static class AliasStoreMetrics
|
||||
{
|
||||
private static readonly Meter Meter = new("StellaOps.Feedser.Merge");
|
||||
|
||||
internal static readonly Counter<long> AliasCollisionCounter = Meter.CreateCounter<long>(
|
||||
"feedser.merge.alias_conflict",
|
||||
unit: "count",
|
||||
description: "Number of alias collisions detected when the same alias maps to multiple advisories.");
|
||||
|
||||
public static void RecordCollision(string scheme, int advisoryCount)
|
||||
{
|
||||
AliasCollisionCounter.Add(
|
||||
1,
|
||||
new KeyValuePair<string, object?>("scheme", scheme),
|
||||
new KeyValuePair<string, object?>("advisory_count", advisoryCount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
public interface IAliasStore
|
||||
{
|
||||
Task<AliasUpsertResult> ReplaceAsync(
|
||||
string advisoryKey,
|
||||
IEnumerable<AliasEntry> aliases,
|
||||
DateTimeOffset updatedAt,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record AliasEntry(string Scheme, string Value);
|
||||
|
||||
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value, DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record AliasCollision(string Scheme, string Value, IReadOnlyList<string> AdvisoryKeys);
|
||||
|
||||
public sealed record AliasUpsertResult(string AdvisoryKey, IReadOnlyList<AliasCollision> Collisions);
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Feedser.Storage.Mongo.Aliases;
|
||||
|
||||
public interface IAliasStore
|
||||
{
|
||||
Task<AliasUpsertResult> ReplaceAsync(
|
||||
string advisoryKey,
|
||||
IEnumerable<AliasEntry> aliases,
|
||||
DateTimeOffset updatedAt,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record AliasEntry(string Scheme, string Value);
|
||||
|
||||
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value, DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record AliasCollision(string Scheme, string Value, IReadOnlyList<string> AdvisoryKeys);
|
||||
|
||||
public sealed record AliasUpsertResult(string AdvisoryKey, IReadOnlyList<AliasCollision> Collisions);
|
||||
|
||||
Reference in New Issue
Block a user