Skip to content

Commit 49563f3

Browse files
rkargMsftdmorganMsftReubenBond
authored
Silo Metadata and Placement Filtering (#9271)
* Initial implementation Still requires tests * Remove dead silos from metadata cache * Allow overwriting of metadata values * Adding Silo Metadata tests * Tests for Grain Placement Filter * Tests for silo metadata placement filters * Adding test category * Testing loading silo metadata from config * Required and Preferred match filtering unit tests * Testing multiple metadata keys in attribute * Ordering support for filters * Moving reusable types to Core projects Orleans.Core.Abstractions: - PlacementFilterAttribute - PlacementFilterStrategy Orelans.Core: - IPlacementFilterDirector - PlacementFilterExtensions * Correct comment Co-authored-by: dmorganMsft <[email protected]> * Correcting documentation * Allowing chaining extension method * #nullable enable for new files * Review suggestions * Fixups * Add PlacementFilterContext * rename file --------- Co-authored-by: dmorganMsft <[email protected]> Co-authored-by: Reuben Bond <[email protected]>
1 parent 6905fa9 commit 49563f3

File tree

39 files changed

+1962
-25
lines changed

39 files changed

+1962
-25
lines changed

src/Orleans.Core.Abstractions/Manifest/GrainProperties.cs

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public static class WellKnownGrainTypeProperties
6868
/// </summary>
6969
public const string PlacementStrategy = "placement-strategy";
7070

71+
/// <summary>
72+
/// The name of the placement strategy for grains of this type.
73+
/// </summary>
74+
public const string PlacementFilter = "placement-filter";
75+
7176
/// <summary>
7277
/// The directory policy for grains of this type.
7378
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Orleans.Metadata;
4+
using Orleans.Runtime;
5+
6+
#nullable enable
7+
namespace Orleans.Placement;
8+
9+
/// <summary>
10+
/// Base for all placement filter marker attributes.
11+
/// </summary>
12+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
13+
public abstract class PlacementFilterAttribute : Attribute, IGrainPropertiesProviderAttribute
14+
{
15+
/// <summary>
16+
/// Gets the placement filter strategy.
17+
/// </summary>
18+
public PlacementFilterStrategy PlacementFilterStrategy { get; private set; }
19+
20+
protected PlacementFilterAttribute(PlacementFilterStrategy placement)
21+
{
22+
ArgumentNullException.ThrowIfNull(placement);
23+
PlacementFilterStrategy = placement;
24+
}
25+
26+
/// <inheritdoc />
27+
public virtual void Populate(IServiceProvider services, Type grainClass, GrainType grainType, Dictionary<string, string> properties)
28+
=> PlacementFilterStrategy?.PopulateGrainProperties(services, grainClass, grainType, properties);
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using Orleans.Metadata;
5+
using Orleans.Runtime;
6+
7+
#nullable enable
8+
namespace Orleans.Placement;
9+
10+
/// <summary>
11+
/// Represents a strategy for filtering silos which a grain can be placed on.
12+
/// </summary>
13+
public abstract class PlacementFilterStrategy
14+
{
15+
public int Order { get; private set; }
16+
17+
protected PlacementFilterStrategy(int order)
18+
{
19+
Order = order;
20+
}
21+
22+
/// <summary>
23+
/// Initializes an instance of this type using the provided grain properties.
24+
/// </summary>
25+
/// <param name="properties">
26+
/// The grain properties.
27+
/// </param>
28+
public void Initialize(GrainProperties properties)
29+
{
30+
var orderProperty = GetPlacementFilterGrainProperty("order", properties);
31+
if (!int.TryParse(orderProperty, out var parsedOrder))
32+
{
33+
throw new ArgumentException("Invalid order property value.");
34+
}
35+
36+
Order = parsedOrder;
37+
38+
AdditionalInitialize(properties);
39+
}
40+
41+
public virtual void AdditionalInitialize(GrainProperties properties)
42+
{
43+
}
44+
45+
/// <summary>
46+
/// Populates grain properties to specify the preferred placement strategy.
47+
/// </summary>
48+
/// <param name="services">The service provider.</param>
49+
/// <param name="grainClass">The grain class.</param>
50+
/// <param name="grainType">The grain type.</param>
51+
/// <param name="properties">The grain properties which will be populated by this method call.</param>
52+
public void PopulateGrainProperties(IServiceProvider services, Type grainClass, GrainType grainType, Dictionary<string, string> properties)
53+
{
54+
var typeName = GetType().Name;
55+
if (properties.TryGetValue(WellKnownGrainTypeProperties.PlacementFilter, out var existingValue))
56+
{
57+
properties[WellKnownGrainTypeProperties.PlacementFilter] = $"{existingValue},{typeName}";
58+
}
59+
else
60+
{
61+
properties[WellKnownGrainTypeProperties.PlacementFilter] = typeName;
62+
}
63+
64+
properties[$"{WellKnownGrainTypeProperties.PlacementFilter}.{typeName}.order"] = Order.ToString(CultureInfo.InvariantCulture);
65+
66+
foreach (var additionalGrainProperty in GetAdditionalGrainProperties(services, grainClass, grainType, properties))
67+
{
68+
properties[$"{WellKnownGrainTypeProperties.PlacementFilter}.{typeName}.{additionalGrainProperty.Key}"] = additionalGrainProperty.Value;
69+
}
70+
}
71+
72+
protected string? GetPlacementFilterGrainProperty(string key, GrainProperties properties)
73+
{
74+
var typeName = GetType().Name;
75+
return properties.Properties.TryGetValue($"{WellKnownGrainTypeProperties.PlacementFilter}.{typeName}.{key}", out var value) ? value : null;
76+
}
77+
78+
protected virtual IEnumerable<KeyValuePair<string, string>> GetAdditionalGrainProperties(IServiceProvider services, Type grainClass, GrainType grainType, IReadOnlyDictionary<string, string> existingProperties)
79+
=> Array.Empty<KeyValuePair<string, string>>();
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Collections.Generic;
2+
using Orleans.Runtime;
3+
using Orleans.Runtime.Placement;
4+
5+
#nullable enable
6+
namespace Orleans.Placement;
7+
8+
public interface IPlacementFilterDirector
9+
{
10+
IEnumerable<SiloAddress> Filter(PlacementFilterStrategy filterStrategy, PlacementFilterContext context, IEnumerable<SiloAddress> silos);
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using Orleans.Runtime;
2+
3+
#nullable enable
4+
namespace Orleans.Placement;
5+
6+
public readonly record struct PlacementFilterContext(GrainType GrainType, GrainInterfaceType InterfaceType, ushort InterfaceVersion);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
#nullable enable
4+
namespace Orleans.Placement;
5+
6+
public static class PlacementFilterExtensions
7+
{
8+
/// <summary>
9+
/// Configures a <typeparamref name="TFilter"/> for filtering candidate grain placements.
10+
/// </summary>
11+
/// <typeparam name="TFilter">The placement filter.</typeparam>
12+
/// <typeparam name="TDirector">The placement filter director.</typeparam>
13+
/// <param name="services">The service collection.</param>
14+
/// <param name="strategyLifetime">The lifetime of the placement strategy.</param>
15+
/// <returns>The service collection.</returns>
16+
public static IServiceCollection AddPlacementFilter<TFilter, TDirector>(this IServiceCollection services, ServiceLifetime strategyLifetime)
17+
where TFilter : PlacementFilterStrategy
18+
where TDirector : class, IPlacementFilterDirector
19+
{
20+
services.Add(ServiceDescriptor.DescribeKeyed(typeof(PlacementFilterStrategy), typeof(TFilter).Name, typeof(TFilter), strategyLifetime));
21+
services.AddKeyedSingleton<IPlacementFilterDirector, TDirector>(typeof(TFilter));
22+
23+
return services;
24+
}
25+
26+
}

src/Orleans.Core/Runtime/Constants.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal static class Constants
1212
public static readonly GrainType DirectoryCacheValidatorType = SystemTargetGrainId.CreateGrainType("dir.cache-validator");
1313
public static readonly GrainType ClientDirectoryType = SystemTargetGrainId.CreateGrainType("dir.client");
1414
public static readonly GrainType SiloControlType = SystemTargetGrainId.CreateGrainType("silo-control");
15+
public static readonly GrainType SiloMetadataType = SystemTargetGrainId.CreateGrainType("silo-metadata");
1516
public static readonly GrainType CatalogType = SystemTargetGrainId.CreateGrainType("catalog");
1617
public static readonly GrainType MembershipServiceType = SystemTargetGrainId.CreateGrainType("clustering");
1718
public static readonly GrainType SystemMembershipTableType = SystemTargetGrainId.CreateGrainType("clustering.dev");
@@ -27,8 +28,8 @@ internal static class Constants
2728
public static readonly GrainType ActivationMigratorType = SystemTargetGrainId.CreateGrainType("migrator");
2829
public static readonly GrainType ActivationRepartitionerType = SystemTargetGrainId.CreateGrainType("repartitioner");
2930
public static readonly GrainType ActivationRebalancerMonitorType = SystemTargetGrainId.CreateGrainType("rebalancer-monitor");
30-
public static readonly GrainType GrainDirectoryPartition = SystemTargetGrainId.CreateGrainType("dir.grain.part");
31-
public static readonly GrainType GrainDirectory = SystemTargetGrainId.CreateGrainType("dir.grain");
31+
public static readonly GrainType GrainDirectoryPartitionType = SystemTargetGrainId.CreateGrainType("dir.grain.part");
32+
public static readonly GrainType GrainDirectoryType = SystemTargetGrainId.CreateGrainType("dir.grain");
3233

3334
public static readonly GrainId SiloDirectConnectionId = GrainId.Create(
3435
GrainType.Create(GrainTypePrefix.SystemPrefix + "silo"),
@@ -41,6 +42,7 @@ internal static class Constants
4142
{DirectoryServiceType, "DirectoryService"},
4243
{DirectoryCacheValidatorType, "DirectoryCacheValidator"},
4344
{SiloControlType, "SiloControl"},
45+
{SiloMetadataType, "SiloMetadata"},
4446
{ClientDirectoryType, "ClientDirectory"},
4547
{CatalogType,"Catalog"},
4648
{MembershipServiceType,"MembershipService"},
@@ -57,7 +59,7 @@ internal static class Constants
5759
{ActivationMigratorType, "ActivationMigrator"},
5860
{ActivationRepartitionerType, "ActivationRepartitioner"},
5961
{ActivationRebalancerMonitorType, "ActivationRebalancerMonitor"},
60-
{GrainDirectory, "GrainDirectory"},
62+
{GrainDirectoryType, "GrainDirectory"},
6163
}.ToFrozenDictionary();
6264

6365
public static string SystemTargetName(GrainType id) => SingletonSystemTargetNames.TryGetValue(id, out var name) ? name : id.ToString();

src/Orleans.Runtime/GrainDirectory/DistributedGrainDirectory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public DistributedGrainDirectory(
8484
ILocalSiloDetails localSiloDetails,
8585
ILoggerFactory loggerFactory,
8686
IServiceProvider serviceProvider,
87-
IInternalGrainFactory grainFactory) : base(Constants.GrainDirectory, localSiloDetails.SiloAddress, loggerFactory)
87+
IInternalGrainFactory grainFactory) : base(Constants.GrainDirectoryType, localSiloDetails.SiloAddress, loggerFactory)
8888
{
8989
_serviceProvider = serviceProvider;
9090
_membershipService = membershipService;

src/Orleans.Runtime/GrainDirectory/GrainDirectoryPartition.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal sealed partial class GrainDirectoryPartition(
2929
IInternalGrainFactory grainFactory)
3030
: SystemTarget(CreateGrainId(localSiloDetails.SiloAddress, partitionIndex), localSiloDetails.SiloAddress, loggerFactory), IGrainDirectoryPartition, IGrainDirectoryTestHooks
3131
{
32-
internal static SystemTargetGrainId CreateGrainId(SiloAddress siloAddress, int partitionIndex) => SystemTargetGrainId.Create(Constants.GrainDirectoryPartition, siloAddress, partitionIndex.ToString(CultureInfo.InvariantCulture));
32+
internal static SystemTargetGrainId CreateGrainId(SiloAddress siloAddress, int partitionIndex) => SystemTargetGrainId.Create(Constants.GrainDirectoryPartitionType, siloAddress, partitionIndex.ToString(CultureInfo.InvariantCulture));
3333
private readonly Dictionary<GrainId, GrainAddress> _directory = [];
3434
private readonly int _partitionIndex = partitionIndex;
3535
private readonly DistributedGrainDirectory _owner = owner;
@@ -665,7 +665,7 @@ private async IAsyncEnumerable<List<GrainAddress>> GetRegisteredActivations(Dire
665665
async Task<List<GrainAddress>> GetRegisteredActivationsFromClusterMember(MembershipVersion version, RingRange range, SiloAddress siloAddress, bool isValidation)
666666
{
667667
var stopwatch = ValueStopwatch.StartNew();
668-
var client = _grainFactory.GetSystemTarget<IGrainDirectoryClient>(Constants.GrainDirectory, siloAddress);
668+
var client = _grainFactory.GetSystemTarget<IGrainDirectoryClient>(Constants.GrainDirectoryType, siloAddress);
669669
var result = await InvokeOnClusterMember(
670670
siloAddress,
671671
async () =>

src/Orleans.Runtime/Hosting/DefaultSiloServices.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@
4343
using Orleans.Serialization.Internal;
4444
using Orleans.Core;
4545
using Orleans.Placement.Repartitioning;
46-
using Orleans.GrainDirectory;
47-
using Orleans.Runtime.Hosting;
46+
using Orleans.Runtime.Placement.Filtering;
4847

4948
namespace Orleans.Hosting
5049
{
@@ -206,6 +205,10 @@ internal static void AddDefaultServices(ISiloBuilder builder)
206205
// Configure the default placement strategy.
207206
services.TryAddSingleton<PlacementStrategy, RandomPlacement>();
208207

208+
// Placement filters
209+
services.AddSingleton<PlacementFilterStrategyResolver>();
210+
services.AddSingleton<PlacementFilterDirectorResolver>();
211+
209212
// Placement directors
210213
services.AddPlacementDirector<RandomPlacement, RandomPlacementDirector>();
211214
services.AddPlacementDirector<PreferLocalPlacement, PreferLocalPlacementDirector>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#nullable enable
2+
3+
namespace Orleans.Runtime.MembershipService.SiloMetadata;
4+
5+
public interface ISiloMetadataCache
6+
{
7+
SiloMetadata GetSiloMetadata(SiloAddress siloAddress);
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Threading.Tasks;
2+
3+
#nullable enable
4+
namespace Orleans.Runtime.MembershipService.SiloMetadata;
5+
6+
internal interface ISiloMetadataClient
7+
{
8+
Task<SiloMetadata> GetSiloMetadata(SiloAddress siloAddress);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Threading.Tasks;
2+
3+
#nullable enable
4+
namespace Orleans.Runtime.MembershipService.SiloMetadata;
5+
6+
[Alias("Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget")]
7+
internal interface ISiloMetadataSystemTarget : ISystemTarget
8+
{
9+
[Alias("GetSiloMetadata")]
10+
Task<SiloMetadata> GetSiloMetadata();
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Logging;
8+
9+
#nullable enable
10+
namespace Orleans.Runtime.MembershipService.SiloMetadata;
11+
12+
internal class SiloMetadataCache(
13+
ISiloMetadataClient siloMetadataClient,
14+
MembershipTableManager membershipTableManager,
15+
ILogger<SiloMetadataCache> logger)
16+
: ISiloMetadataCache, ILifecycleParticipant<ISiloLifecycle>, IDisposable
17+
{
18+
private readonly ConcurrentDictionary<SiloAddress, SiloMetadata> _metadata = new();
19+
private readonly CancellationTokenSource _cts = new();
20+
21+
void ILifecycleParticipant<ISiloLifecycle>.Participate(ISiloLifecycle lifecycle)
22+
{
23+
Task? task = null;
24+
Task OnStart(CancellationToken _)
25+
{
26+
task = Task.Run(() => this.ProcessMembershipUpdates(_cts.Token));
27+
return Task.CompletedTask;
28+
}
29+
30+
async Task OnStop(CancellationToken ct)
31+
{
32+
await _cts.CancelAsync().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
33+
if (task is not null)
34+
{
35+
await task.WaitAsync(ct).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
36+
}
37+
}
38+
39+
lifecycle.Subscribe(
40+
nameof(ClusterMembershipService),
41+
ServiceLifecycleStage.RuntimeServices,
42+
OnStart,
43+
OnStop);
44+
}
45+
46+
private async Task ProcessMembershipUpdates(CancellationToken ct)
47+
{
48+
try
49+
{
50+
if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug("Starting to process membership updates.");
51+
await foreach (var update in membershipTableManager.MembershipTableUpdates.WithCancellation(ct))
52+
{
53+
// Add entries for members that aren't already in the cache
54+
foreach (var membershipEntry in update.Entries.Where(e => e.Value.Status is SiloStatus.Active or SiloStatus.Joining))
55+
{
56+
if (!_metadata.ContainsKey(membershipEntry.Key))
57+
{
58+
try
59+
{
60+
var metadata = await siloMetadataClient.GetSiloMetadata(membershipEntry.Key).WaitAsync(ct);
61+
_metadata.TryAdd(membershipEntry.Key, metadata);
62+
}
63+
catch(Exception exception)
64+
{
65+
logger.LogError(exception, "Error fetching metadata for silo {Silo}", membershipEntry.Key);
66+
}
67+
}
68+
}
69+
70+
// Remove entries for members that are now dead
71+
foreach (var membershipEntry in update.Entries.Where(e => e.Value.Status == SiloStatus.Dead))
72+
{
73+
_metadata.TryRemove(membershipEntry.Key, out _);
74+
}
75+
76+
// Remove entries for members that are no longer in the table
77+
foreach (var silo in _metadata.Keys.ToList())
78+
{
79+
if (!update.Entries.ContainsKey(silo))
80+
{
81+
_metadata.TryRemove(silo, out _);
82+
}
83+
}
84+
}
85+
}
86+
catch (OperationCanceledException) when (ct.IsCancellationRequested)
87+
{
88+
// Ignore and continue shutting down.
89+
}
90+
catch (Exception exception)
91+
{
92+
logger.LogError(exception, "Error processing membership updates");
93+
}
94+
finally
95+
{
96+
if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug("Stopping membership update processor");
97+
}
98+
}
99+
100+
public SiloMetadata GetSiloMetadata(SiloAddress siloAddress) => _metadata.GetValueOrDefault(siloAddress) ?? SiloMetadata.Empty;
101+
102+
public void SetMetadata(SiloAddress siloAddress, SiloMetadata metadata) => _metadata.TryAdd(siloAddress, metadata);
103+
104+
public void Dispose() => _cts.Cancel();
105+
}

0 commit comments

Comments
 (0)