Optimize activity registry lookup

Add a latest-version descriptor index per tenant registry so ActivityRegistry.Find(type) can avoid scanning every registered descriptor. Keep the index in sync when descriptors are added, removed, refreshed, or cleared.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2026-05-27 09:09:17 +02:00
parent 5245599131
commit 9554385498
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
3 changed files with 185 additions and 36 deletions

View file

@ -30,6 +30,11 @@ public class TenantRegistryData
/// </remarks>
public ConcurrentDictionary<(string Type, int Version), ActivityDescriptor> ActivityDescriptors { get; } = new();
/// <summary>
/// Cached latest-version activity descriptors by type for fast lookups.
/// </summary>
internal ConcurrentDictionary<string, ActivityDescriptor> LatestActivityDescriptors { get; } = new();
/// <summary>
/// Secondary index of activity descriptors grouped by their provider type.
/// </summary>

View file

@ -24,7 +24,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
{
var registry = GetOrCreateRegistry(descriptor.TenantId);
var providerDescriptors = GetOrCreateProviderDescriptors(registry, providerType);
Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
Add(descriptor, registry, providerDescriptors);
}
/// <inheritdoc />
@ -34,7 +34,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var providerDescriptors))
{
providerDescriptors.Remove(descriptor);
registry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out _);
RemoveDescriptor(registry, descriptor);
}
}
@ -82,18 +82,14 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
// Get highest version from current tenant's registry
if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
{
var tenantDescriptor = tenantRegistry.ActivityDescriptors.Values
.Where(x => x.TypeName == type)
.MaxBy(x => x.Version);
if (tenantDescriptor != null)
if (tenantRegistry.LatestActivityDescriptors.TryGetValue(type, out var tenantDescriptor))
return tenantDescriptor;
}
// Fall back to agnostic registry only if no tenant-specific descriptor exists
return _agnosticRegistry.ActivityDescriptors.Values
.Where(x => x.TypeName == type)
.MaxBy(x => x.Version);
return _agnosticRegistry.LatestActivityDescriptors.TryGetValue(type, out var agnosticDescriptor)
? agnosticDescriptor
: null;
}
/// <inheritdoc />
@ -151,7 +147,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
{
var registry = GetOrCreateRegistry(descriptor.TenantId);
var providerDescriptors = GetOrCreateProviderDescriptors(registry, GetType());
Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
Add(descriptor, registry, providerDescriptors);
}
/// <inheritdoc />
@ -166,7 +162,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken);
var registry = GetOrCreateRegistry(activityDescriptor.TenantId);
Add(activityDescriptor, registry.ActivityDescriptors, _manualActivityDescriptors);
Add(activityDescriptor, registry, _manualActivityDescriptors);
_manualActivityDescriptors.Add(activityDescriptor);
}
@ -208,7 +204,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
{
foreach (var oldDescriptor in oldDescriptors.ToList())
{
registry.ActivityDescriptors.TryRemove((oldDescriptor.TypeName, oldDescriptor.Version), out _);
RemoveDescriptor(registry, oldDescriptor);
}
}
@ -216,7 +212,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
var providerDescriptors = new List<ActivityDescriptor>();
foreach (var descriptor in group)
{
Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
Add(descriptor, registry, providerDescriptors);
}
// Update the provider's descriptor list in this registry
@ -224,7 +220,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
}
}
private void Add(ActivityDescriptor? descriptor, ConcurrentDictionary<(string Type, int Version), ActivityDescriptor> activityDescriptors, ICollection<ActivityDescriptor> providerDescriptors)
private void Add(ActivityDescriptor? descriptor, TenantRegistryData registry, ICollection<ActivityDescriptor> providerDescriptors)
{
if (descriptor is null)
{
@ -235,8 +231,11 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
foreach (var modifier in modifiers)
modifier.Modify(descriptor);
var activityDescriptors = registry.ActivityDescriptors;
var descriptorKey = (descriptor.TypeName, descriptor.Version);
// If the descriptor already exists, replace it. But log a warning.
if (activityDescriptors.TryGetValue((descriptor.TypeName, descriptor.Version), out var existingDescriptor))
if (activityDescriptors.TryGetValue(descriptorKey, out var existingDescriptor))
{
// Remove the existing descriptor from the providerDescriptors collection.
providerDescriptors.Remove(existingDescriptor);
@ -245,7 +244,8 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
logger.LogWarning("Activity descriptor {ActivityType} v{ActivityVersion} was already registered for tenant {TenantId}. Replacing with new descriptor", descriptor.TypeName, descriptor.Version, descriptor.TenantId);
}
activityDescriptors[(descriptor.TypeName, descriptor.Version)] = descriptor;
activityDescriptors[descriptorKey] = descriptor;
UpdateLatestDescriptor(registry, descriptor);
providerDescriptors.Add(descriptor);
}
@ -254,6 +254,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
{
_tenantRegistries.Clear();
_agnosticRegistry.ActivityDescriptors.Clear();
_agnosticRegistry.LatestActivityDescriptors.Clear();
_agnosticRegistry.ProvidedActivityDescriptors.Clear();
}
@ -267,7 +268,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
&& tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var descriptors))
{
foreach (var descriptor in descriptors.ToList())
tenantRegistry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out _);
RemoveDescriptor(tenantRegistry, descriptor);
tenantRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
}
@ -276,7 +277,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
if (_agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnosticDescriptors))
{
foreach (var descriptor in agnosticDescriptors.ToList())
_agnosticRegistry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out _);
RemoveDescriptor(_agnosticRegistry, descriptor);
_agnosticRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
}
@ -305,6 +306,35 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
return registry.ProvidedActivityDescriptors.GetOrAdd(providerType, _ => new List<ActivityDescriptor>());
}
private static void UpdateLatestDescriptor(TenantRegistryData registry, ActivityDescriptor descriptor)
{
registry.LatestActivityDescriptors.AddOrUpdate(
descriptor.TypeName,
descriptor,
(_, latestDescriptor) => descriptor.Version >= latestDescriptor.Version ? descriptor : latestDescriptor);
}
private static void RemoveDescriptor(TenantRegistryData registry, ActivityDescriptor descriptor)
{
if (!registry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out var removedDescriptor))
return;
if (registry.LatestActivityDescriptors.TryGetValue(removedDescriptor.TypeName, out var latestDescriptor) && latestDescriptor.Version == removedDescriptor.Version)
RecomputeLatestDescriptor(registry, removedDescriptor.TypeName);
}
private static void RecomputeLatestDescriptor(TenantRegistryData registry, string typeName)
{
var latestDescriptor = registry.ActivityDescriptors.Values
.Where(x => x.TypeName == typeName)
.MaxBy(x => x.Version);
if (latestDescriptor == null)
registry.LatestActivityDescriptors.TryRemove(typeName, out _);
else
registry.LatestActivityDescriptors[typeName] = latestDescriptor;
}
/// <summary>
/// Normalizes tenant ID for grouping purposes.
/// Converts null to "*" so that both null and "*" descriptors are grouped together,

View file

@ -22,7 +22,7 @@ public class ActivityRegistryTests
var activityDescriber = Substitute.For<IActivityDescriber>();
_logger = Substitute.For<ILogger<ActivityRegistry>>();
_registry = new(activityDescriber, [], tenantAccessor, _logger);
// Set default tenant for all tests
tenantAccessor.TenantId.Returns(CurrentTenant);
}
@ -114,6 +114,88 @@ public class ActivityRegistryTests
Assert.Null(result);
}
[Fact]
public void Find_ReturnsNextLatestVersion_WhenLatestDescriptorRemoved()
{
// Arrange
var v1 = CreateDescriptor(TestActivityType, 1, CurrentTenant);
var v2 = CreateDescriptor(TestActivityType, 2, CurrentTenant);
var v3 = CreateDescriptor(TestActivityType, 3, CurrentTenant);
RegisterDescriptors(v1, v2, v3);
// Act
_registry.Remove(typeof(ActivityRegistry), v3);
var result = _registry.Find(TestActivityType);
// Assert
AssertDescriptor(result, CurrentTenant, 2);
}
[Fact]
public void Find_KeepsLatestVersion_WhenNonLatestDescriptorRemoved()
{
// Arrange
var v1 = CreateDescriptor(TestActivityType, 1, CurrentTenant);
var v2 = CreateDescriptor(TestActivityType, 2, CurrentTenant);
var v3 = CreateDescriptor(TestActivityType, 3, CurrentTenant);
RegisterDescriptors(v1, v2, v3);
// Act
_registry.Remove(typeof(ActivityRegistry), v1);
var result = _registry.Find(TestActivityType);
// Assert
AssertDescriptor(result, CurrentTenant, 3);
}
[Fact]
public void Find_ReturnsNull_WhenProviderWithLatestDescriptorClearedAndNoDescriptorsRemain()
{
// Arrange
var descriptor = CreateDescriptor(TestActivityType, 1, CurrentTenant);
_registry.Add(typeof(Provider1), descriptor);
// Act
_registry.ClearProvider(typeof(Provider1));
var result = _registry.Find(TestActivityType);
// Assert
Assert.Null(result);
}
[Fact]
public void Find_RecomputesLatestVersion_WhenProviderWithLatestDescriptorCleared()
{
// Arrange
var provider1V1 = CreateDescriptor(TestActivityType, 1, CurrentTenant);
var provider1V3 = CreateDescriptor(TestActivityType, 3, CurrentTenant);
var provider2V2 = CreateDescriptor(TestActivityType, 2, CurrentTenant);
_registry.Add(typeof(Provider1), provider1V1);
_registry.Add(typeof(Provider1), provider1V3);
_registry.Add(typeof(Provider2), provider2V2);
// Act
_registry.ClearProvider(typeof(Provider1));
var result = _registry.Find(TestActivityType);
// Assert
AssertDescriptor(result, CurrentTenant, 2);
}
[Fact]
public void Find_ReturnsNull_WhenRegistryCleared()
{
// Arrange
RegisterDescriptors(CreateDescriptor(TestActivityType, 2, CurrentTenant));
// Act
_registry.Clear();
var result = _registry.Find(TestActivityType);
// Assert
Assert.Null(result);
}
[Fact]
public void Find_IgnoresOtherTenantDescriptors_OnlyReturnsCurrentTenantOrAgnostic()
{
@ -147,7 +229,7 @@ public class ActivityRegistryTests
Description = "Test Activity 1",
IsBrowsable = true
};
var descriptor2 = new ActivityDescriptor
{
TypeName = "TestActivity2",
@ -157,18 +239,18 @@ public class ActivityRegistryTests
Description = "Test Activity 2",
IsBrowsable = true
};
mockProvider.GetDescriptorsAsync(Arg.Any<CancellationToken>())
.Returns(new ValueTask<IEnumerable<ActivityDescriptor>>([descriptor1, descriptor2]));
var providers = new[] { mockProvider };
// Act - First refresh
await _registry.RefreshDescriptorsAsync(providers);
// Act - Second refresh (simulates the intentional repopulation in DefaultRegistriesPopulator)
await _registry.RefreshDescriptorsAsync(providers);
// Assert - Verify no warning logs were made
_logger.DidNotReceive().Log(
LogLevel.Warning,
@ -176,14 +258,14 @@ public class ActivityRegistryTests
Arg.Is<object>(v => v.ToString()!.Contains("was already registered")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
// Verify descriptors are still registered
var allDescriptors = _registry.ListAll().ToList();
Assert.Equal(2, allDescriptors.Count);
Assert.Contains(allDescriptors, d => d.TypeName == "TestActivity1");
Assert.Contains(allDescriptors, d => d.TypeName == "TestActivity2");
}
[Fact]
public async Task RefreshDescriptorsAsync_PreservesManualDescriptors()
{
@ -198,9 +280,9 @@ public class ActivityRegistryTests
Description = "Manually registered activity",
IsBrowsable = true
};
_registry.Register(manualDescriptor);
// Create a provider descriptor
var mockProvider = Substitute.For<IActivityProvider>();
var providerDescriptor = new ActivityDescriptor
@ -212,21 +294,21 @@ public class ActivityRegistryTests
Description = "Provider activity",
IsBrowsable = true
};
mockProvider.GetDescriptorsAsync(Arg.Any<CancellationToken>())
.Returns(new ValueTask<IEnumerable<ActivityDescriptor>>([providerDescriptor]));
var providers = new[] { mockProvider };
// Act - Refresh with provider
await _registry.RefreshDescriptorsAsync(providers);
// Assert - Both manual and provider descriptors should be present
var allDescriptors = _registry.ListAll().ToList();
Assert.Equal(2, allDescriptors.Count);
Assert.Contains(allDescriptors, d => d.TypeName == "ManualActivity");
Assert.Contains(allDescriptors, d => d.TypeName == "ProviderActivity");
// Verify no warnings about manual descriptor being replaced
_logger.DidNotReceive().Log(
LogLevel.Warning,
@ -235,7 +317,7 @@ public class ActivityRegistryTests
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task RefreshDescriptorsAsync_LogsWarning_WhenDifferentProvidersRegisterSameActivity()
{
@ -277,7 +359,32 @@ public class ActivityRegistryTests
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task RefreshDescriptorsAsync_RecomputesLatestDescriptor_WhenProviderDropsLatestVersion()
{
// Arrange
var provider = new MutableProvider(
[
CreateDescriptor(TestActivityType, 1, CurrentTenant),
CreateDescriptor(TestActivityType, 3, CurrentTenant)
]);
await _registry.RefreshDescriptorsAsync(provider);
provider.Descriptors =
[
CreateDescriptor(TestActivityType, 1, CurrentTenant)
];
// Act
await _registry.RefreshDescriptorsAsync(provider);
var result = _registry.Find(TestActivityType);
// Assert
AssertDescriptor(result, CurrentTenant, 1);
}
private ActivityDescriptor CreateDescriptor(string typeName, int version, string? tenantId) =>
new()
{
@ -308,5 +415,12 @@ public class ActivityRegistryTests
{
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default) => new(descriptors);
}
private sealed class MutableProvider(IEnumerable<ActivityDescriptor> descriptors) : IActivityProvider
{
public IEnumerable<ActivityDescriptor> Descriptors { get; set; } = descriptors;
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default) => new(Descriptors);
}
}