From 6b0a627f323cb95ecbfb5812d9dfde880ac9e2ac Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 14 Nov 2024 21:27:04 +0700 Subject: [PATCH] Fix `WithSingleton` and `WithSingletonProxy` API (#520) --- .../ClusterSingletonSpecs.cs | 194 +++++++++- .../AkkaClusterHostingExtensions.cs | 340 +++++++++++++++--- .../CoreApiSpec.ApproveCluster.verified.txt | 6 + 3 files changed, 480 insertions(+), 60 deletions(-) diff --git a/src/Akka.Cluster.Hosting.Tests/ClusterSingletonSpecs.cs b/src/Akka.Cluster.Hosting.Tests/ClusterSingletonSpecs.cs index 88ebf115..8c83c549 100644 --- a/src/Akka.Cluster.Hosting.Tests/ClusterSingletonSpecs.cs +++ b/src/Akka.Cluster.Hosting.Tests/ClusterSingletonSpecs.cs @@ -3,9 +3,11 @@ using Akka.Actor; using Akka.Hosting; using FluentAssertions; +using FluentAssertions.Extensions; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; +using static FluentAssertions.FluentActions; namespace Akka.Cluster.Hosting.Tests; @@ -50,7 +52,145 @@ public async Task Should_launch_ClusterSingletonAndProxy() await host.StopAsync(); } - [Fact] + [Fact(DisplayName = "Should launch singleton manager and proxy at the appropriate path (no manager name, actor props)")] + public async Task ClusterSingletonAndProxyWithNoManagerNameTest() + { + using var host = await TestHelper.CreateHost( + builder => + { + builder.WithSingleton( + singletonName: "my-singleton", + actorProps: MySingletonActor.MyProps); + }, + new ClusterOptions + { + Roles = new[] { "my-host" } + }, Output); + + var system = host.Services.GetRequiredService(); + var registry = host.Services.GetRequiredService(); + var singletonProxy = await registry.GetAsync(); + + var address = Cluster.Get(system).SelfAddress; + var expectedSingletonPath = new RootActorPath(address) / "user" / "my-singleton" / "my-singleton"; + var singletonSelector = system.ActorSelection(expectedSingletonPath); + + await Awaiting(async () => + { + var identify = await singletonSelector.ResolveOne(3.Seconds()); + identify.Should().NotBe(ActorRefs.Nobody); + }).Should().NotThrowAsync(); + + singletonProxy.Path.ToString().Should().Be("akka://TestSys/user/my-singleton-proxy"); + + await host.StopAsync(); + } + + [Fact(DisplayName = "Should launch singleton manager and proxy at the appropriate path (no manager name, actor factory)")] + public async Task ClusterSingletonAndProxyWithNoManagerNameAndFactoryTest() + { + using var host = await TestHelper.CreateHost( + builder => + { + builder.WithSingleton( + singletonName: "my-singleton", + propsFactory: (_, _, _) => MySingletonActor.MyProps); + }, + new ClusterOptions + { + Roles = new[] { "my-host" } + }, Output); + + var system = host.Services.GetRequiredService(); + var registry = host.Services.GetRequiredService(); + var singletonProxy = await registry.GetAsync(); + + var address = Cluster.Get(system).SelfAddress; + var expectedSingletonPath = new RootActorPath(address) / "user" / "my-singleton" / "my-singleton"; + var singletonSelector = system.ActorSelection(expectedSingletonPath); + + await Awaiting(async () => + { + var identify = await singletonSelector.ResolveOne(3.Seconds()); + identify.Should().NotBe(ActorRefs.Nobody); + }).Should().NotThrowAsync(); + + singletonProxy.Path.ToString().Should().Be("akka://TestSys/user/my-singleton-proxy"); + + await host.StopAsync(); + } + + [Fact(DisplayName = "Should launch singleton manager and proxy at the appropriate path (with manager name, actor props)")] + public async Task ClusterSingletonAndProxyWithManagerNameTest() + { + using var host = await TestHelper.CreateHost( + builder => + { + builder.WithSingleton( + singletonManagerName: "my-singleton", + singletonName: "singleton", + actorProps: MySingletonActor.MyProps); + }, + new ClusterOptions + { + Roles = new[] { "my-host" } + }, Output); + + var system = host.Services.GetRequiredService(); + var registry = host.Services.GetRequiredService(); + var singletonProxy = await registry.GetAsync(); + + var address = Cluster.Get(system).SelfAddress; + var expectedSingletonPath = new RootActorPath(address) / "user" / "my-singleton" / "singleton"; + var singletonSelector = system.ActorSelection(expectedSingletonPath); + + await Awaiting(async () => + { + var identify = await singletonSelector.ResolveOne(3.Seconds()); + identify.Should().NotBe(ActorRefs.Nobody); + }).Should().NotThrowAsync(); + + singletonProxy.Path.ToString().Should().Be("akka://TestSys/user/singleton-proxy"); + + await host.StopAsync(); + } + + [Fact(DisplayName = "Should launch singleton manager and proxy at the appropriate path (with manager name, actor factory)")] + public async Task ClusterSingletonAndProxyWithManagerNameAndFactoryTest() + { + using var host = await TestHelper.CreateHost( + builder => + { + builder.WithSingleton( + singletonManagerName: "my-singleton", + singletonName: "singleton", + propsFactory: (_, _, _) => MySingletonActor.MyProps); + }, + new ClusterOptions + { + Roles = new[] { "my-host" } + }, Output); + + var system = host.Services.GetRequiredService(); + var registry = host.Services.GetRequiredService(); + var singletonProxy = await registry.GetAsync(); + + var address = Cluster.Get(system).SelfAddress; + var expectedSingletonPath = new RootActorPath(address) / "user" / "my-singleton" / "singleton"; + var singletonSelector = system.ActorSelection(expectedSingletonPath); + + await Awaiting(async () => + { + var identify = await singletonSelector.ResolveOne(3.Seconds()); + identify.Should().NotBe(ActorRefs.Nobody); + }).Should().NotThrowAsync(); + + singletonProxy.Path.ToString().Should().Be("akka://TestSys/user/singleton-proxy"); + + await host.StopAsync(); + } + + [Fact(DisplayName = "WithSingletonProxy should work with no manager name")] public async Task Should_launch_ClusterSingleton_and_Proxy_separately() { // arrange @@ -80,4 +220,56 @@ public async Task Should_launch_ClusterSingleton_and_Proxy_separately() await Task.WhenAll(singletonHost.StopAsync(), singletonProxyHost.StopAsync()); } + + [Fact(DisplayName = "WithSingletonProxy should work with manager name")] + public async Task SeparateProxyWithManagerNameTest() + { + // arrange + + var singletonOptions = new ClusterSingletonOptions() { Role = "my-host" }; + using var singletonHost = await TestHelper.CreateHost( + builder => + { + builder.WithSingleton( + singletonManagerName: "my-singleton", + singletonName: "singleton", + actorProps: MySingletonActor.MyProps, + options: singletonOptions, + createProxyToo:false); + }, + new ClusterOptions + { + Roles = new[] { "my-host" } + }, Output); + + var singletonSystem = singletonHost.Services.GetRequiredService(); + var address = Cluster.Get(singletonSystem).SelfAddress; + + using var singletonProxyHost = await TestHelper.CreateHost( + builder => + { + builder.WithSingletonProxy( + singletonManagerName: "my-singleton", + singletonName: "singleton", + options: singletonOptions); + }, + new ClusterOptions + { + Roles = new[] { "proxy" }, + SeedNodes = new []{ address.ToString() } + }, Output); + + var registry = singletonProxyHost.Services.GetRequiredService(); + var singletonProxy = await registry.GetAsync(); + + // act + + // verify round-trip to the singleton proxy and back + var respond = await singletonProxy.Ask("hit", 3.Seconds()); + + // assert + respond.Should().Be("hit"); + + await Task.WhenAll(singletonHost.StopAsync(), singletonProxyHost.StopAsync()); + } } \ No newline at end of file diff --git a/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs b/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs index ce12efcb..30c3906a 100644 --- a/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs +++ b/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs @@ -160,6 +160,53 @@ public sealed class ClusterSingletonOptions /// The interval between retries for acquiring the lease /// public TimeSpan? LeaseRetryInterval { get; set; } + + /// + /// Interval at which the proxy will try to resolve the singleton instance. + /// + public TimeSpan? SingletonIdentificationInterval { get; set; } + + /// + /// Should the singleton proxy publish a warning if no singleton actor were found after a period of time + /// + public bool? LogSingletonIdentificationFailure { get; set; } + + /// + /// The period the proxy will wait until it logs a missing singleton warning, defaults to 1 minute + /// + public TimeSpan? SingletonIdentificationFailurePeriod { get; set; } + + internal ClusterSingletonManagerSettings ToManagerSettings(string singletonName, ActorSystem system) + { + var settings = ClusterSingletonManagerSettings.Create(system); + + var retry = LeaseRetryInterval ?? system.Settings.Config.GetTimeSpan("akka.cluster.singleton.lease-retry-interval"); + var leaseSettings = LeaseImplementation is not null + ? new LeaseUsageSettings(LeaseImplementation.ConfigPath, retry) + : null; + + return new ClusterSingletonManagerSettings( + singletonName: singletonName, + role: Role ?? settings.Role, + removalMargin: settings.RemovalMargin, + handOverRetryInterval: settings.HandOverRetryInterval, + leaseSettings: leaseSettings ?? settings.LeaseSettings, + considerAppVersion: false); + } + + internal ClusterSingletonProxySettings ToProxySettings(string singletonName, ActorSystem system) + { + var settings = ClusterSingletonProxySettings.Create(system); + return new ClusterSingletonProxySettings( + singletonName: singletonName, + role: Role ?? settings.Role, + singletonIdentificationInterval: SingletonIdentificationInterval ?? settings.SingletonIdentificationInterval, + bufferSize: BufferSize ?? settings.BufferSize, + considerAppVersion: settings.ConsiderAppVersion, + logSingletonIdentificationFailure: LogSingletonIdentificationFailure ?? settings.LogSingletonIdentificationFailure, + singletonIdentificationFailurePeriod: SingletonIdentificationFailurePeriod ?? settings.SingletonIdentificationFailurePeriod); + } + } public sealed class ShardOptions @@ -1183,12 +1230,22 @@ public static AkkaConfigurationBuilder WithDistributedPubSub( /// /// - /// Creates a new to host an actor created via . + /// Creates a new to host an actor created via . /// /// /// If is set to true then this method will also create a /// that will be added to the using the key - /// . Otherwise this method will register nothing with the . + /// . Otherwise, this method will register nothing with the . + /// + /// + /// The complete singleton manager actor path name will be + /// akka://MyActorSystem/user/singletonName/singletonName + /// + /// + /// The complete singleton proxy actor path name, if created, will be + /// akka://MyActorSystem/user/singletonName-proxy + /// + /// /// /// /// The builder instance being configured. @@ -1196,7 +1253,7 @@ public static AkkaConfigurationBuilder WithDistributedPubSub( /// /// The name of this singleton instance. Will also be used in the for the /// and optionally, the created - /// by this method. + /// by this method. See summary above. /// /// /// A function that accepts the , , and @@ -1222,49 +1279,89 @@ public static AkkaConfigurationBuilder WithSingleton( Func propsFactory, ClusterSingletonOptions? options = null, bool createProxyToo = true) + => builder.WithSingleton(singletonName, singletonName, propsFactory, options, createProxyToo); + + /// + /// + /// Creates a new to host an actor created via . + /// + /// If is set to true then this method will also create a + /// that will be added to the using the key + /// . Otherwise, this method will register nothing with the . + /// + /// + /// The complete singleton manager actor path name will be + /// akka://MyActorSystem/user/singletonManagerName/singletonName + /// + /// + /// The complete singleton proxy actor path name, if created, will be + /// akka://MyActorSystem/user/singletonName-proxy + /// + /// + /// + /// + /// The builder instance being configured. + /// + /// + /// The name of the created by this method. See summary above. + /// + /// + /// The name of this singleton instance and optionally, part of the + /// name created by this method. See summary above. + /// + /// + /// A function that accepts the , , and + /// and returns the for the actor + /// + /// + /// Optional. The set of options for configuring both the and + /// optionally, the . + /// + /// + /// When set to true>, creates a that automatically points to + /// the created by this method. + /// + /// + /// The key type to use for the when is set to true. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithSingleton( + this AkkaConfigurationBuilder builder, + string singletonManagerName, + string singletonName, + Func propsFactory, + ClusterSingletonOptions? options = null, + bool createProxyToo = true) { + // make sure that default configuration is loaded, not an exhaustive check. + if (!builder.Configuration.HasValue || builder.Configuration.Value.HasPath("akka.cluster.singleton")) + { + builder.AddHocon(ClusterSingletonManager.DefaultConfig(), HoconAddMode.Append); + } + return builder.WithActors((system, registry, resolver) => { var actorProps = propsFactory(system, registry, resolver); options ??= new ClusterSingletonOptions(); - var clusterSingletonManagerSettings = - ClusterSingletonManagerSettings.Create(system).WithSingletonName(singletonName); - - if (options.LeaseImplementation is not null) - { - var retry = options.LeaseRetryInterval ?? TimeSpan.FromSeconds(5); - clusterSingletonManagerSettings = clusterSingletonManagerSettings - .WithLeaseSettings(new LeaseUsageSettings(options.LeaseImplementation.ConfigPath, retry)); - } - - var singletonProxySettings = - ClusterSingletonProxySettings.Create(system).WithSingletonName(singletonName); - - if (!string.IsNullOrEmpty(options.Role)) - { - clusterSingletonManagerSettings = clusterSingletonManagerSettings.WithRole(options.Role); - singletonProxySettings = singletonProxySettings.WithRole(options.Role); - } + var clusterSingletonManagerSettings = options.ToManagerSettings(singletonName, system); var singletonProps = options.TerminationMessage == null ? ClusterSingletonManager.Props(actorProps, clusterSingletonManagerSettings) : ClusterSingletonManager.Props(actorProps, options.TerminationMessage, clusterSingletonManagerSettings); - var singletonManagerRef = system.ActorOf(singletonProps, singletonName); + var singletonManagerRef = system.ActorOf(singletonProps, singletonManagerName); // create a proxy that can talk to the singleton we just created // and add it to the ActorRegistry if (createProxyToo) { - if (options.BufferSize != null) - { - singletonProxySettings = singletonProxySettings.WithBufferSize(options.BufferSize.Value); - } - - CreateAndRegisterSingletonProxy(singletonManagerRef.Path.Name, - $"/user/{singletonManagerRef.Path.Name}", singletonProxySettings, system, registry); + var singletonProxySettings = options.ToProxySettings(singletonName, system); + CreateAndRegisterSingletonProxy(singletonName, + $"/user/{singletonManagerName}", singletonProxySettings, system, registry); } }); } @@ -1276,7 +1373,75 @@ public static AkkaConfigurationBuilder WithSingleton( /// /// If is set to true then this method will also create a /// that will be added to the using the key - /// . Otherwise this method will register nothing with the . + /// . Otherwise, this method will register nothing with the . + /// + /// + /// The complete singleton manager actor path name will be + /// akka://MyActorSystem/user/singletonManagerName/singletonName + /// + /// + /// The complete singleton proxy actor path name, if created, will be + /// akka://MyActorSystem/user/singletonName-proxy + /// + /// + /// + /// + /// The builder instance being configured. + /// + /// + /// The name of the created by this method. See summary above. + /// + /// + /// The name of this singleton instance and optionally, part of the + /// name created by this method. See summary above. + /// + /// + /// The underlying actor type. SHOULD NOT BE CREATED USING + /// + /// + /// Optional. The set of options for configuring both the and + /// optionally, the . + /// + /// + /// When set to true>, creates a that automatically points to + /// the created by this method. + /// + /// + /// The key type to use for the when is set to true. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithSingleton( + this AkkaConfigurationBuilder builder, + string singletonManagerName, + string singletonName, + Props actorProps, + ClusterSingletonOptions? options = null, + bool createProxyToo = true) + { + return builder.WithSingleton(singletonManagerName, singletonName, (_, _, _) => actorProps, options, + createProxyToo); + } + + /// + /// + /// Creates a new to host an actor created via . + /// + /// + /// If is set to true then this method will also create a + /// that will be added to the using the key + /// . Otherwise, this method will register nothing with the . + /// + /// + /// The complete singleton manager actor path name will be + /// akka://MyActorSystem/user/singletonName/singletonName + /// + /// + /// The complete singleton proxy actor path name, if created, will be + /// akka://MyActorSystem/user/singletonName-proxy + /// + /// /// /// /// The builder instance being configured. @@ -1284,7 +1449,7 @@ public static AkkaConfigurationBuilder WithSingleton( /// /// The name of this singleton instance. Will also be used in the for the /// and optionally, the created - /// by this method. + /// by this method. See summary above. /// /// /// The underlying actor type. SHOULD NOT BE CREATED USING @@ -1310,42 +1475,101 @@ public static AkkaConfigurationBuilder WithSingleton( ClusterSingletonOptions? options = null, bool createProxyToo = true) { - return builder.WithSingleton(singletonName, (_, _, _) => actorProps, options, + return builder.WithSingleton(singletonName, singletonName, (_, _, _) => actorProps, options, createProxyToo); } private static void CreateAndRegisterSingletonProxy( - string singletonActorName, - string singletonActorPath, + string singletonName, + string singletonManagerActorPath, ClusterSingletonProxySettings singletonProxySettings, ActorSystem system, IActorRegistry registry) { - var singletonProxyProps = ClusterSingletonProxy.Props(singletonActorPath, - singletonProxySettings); - var singletonProxy = system.ActorOf(singletonProxyProps, $"{singletonActorName}-proxy"); + var singletonProxyProps = ClusterSingletonProxy.Props( + singletonManagerPath: singletonManagerActorPath, + settings: singletonProxySettings); + var singletonProxy = system.ActorOf(singletonProxyProps, $"{singletonName}-proxy"); registry.Register(singletonProxy); } /// - /// Creates a and adds it to the using the - /// given . + /// + /// Creates a and adds it to the using + /// the given . + /// + /// + /// The complete singleton proxy actor path name will be akka://MyActorSystem/user/singletonName-proxy /// /// /// The builder instance being configured. /// + /// + /// The name of the singleton manager. + /// /// - /// The name of this singleton instance. Will also be used in the for the - /// and optionally, the created - /// by this method. + /// The name of the singleton. Will also be part of the created + /// by this method. See summary above. + /// + /// + /// Optional. The set of options for configuring the . + /// + /// + /// The key type to use for the . + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithSingletonProxy( + this AkkaConfigurationBuilder builder, + string singletonManagerName, + string singletonName, + ClusterSingletonOptions? options = null) + { + // make sure that default configuration is loaded, not an exhaustive check. + if (!builder.Configuration.HasValue || builder.Configuration.Value.HasPath("akka.cluster.singleton-proxy")) + { + builder.AddHocon(ClusterSingletonManager.DefaultConfig(), HoconAddMode.Append); + } + + return builder.WithActors((system, registry) => + { + options ??= new ClusterSingletonOptions(); + var singletonProxySettings = options.ToProxySettings(singletonName, system); + + var singletonManagerPath = $"/user/{singletonManagerName}"; + + CreateAndRegisterSingletonProxy( + singletonName: singletonName, + singletonManagerActorPath: singletonManagerPath, + singletonProxySettings: singletonProxySettings, + system: system, + registry: registry); + }); + + } + + /// + /// + /// Creates a and adds it to the using + /// the given . + /// + /// + /// The complete singleton proxy actor path name will be akka://MyActorSystem/user/singletonName-proxy + /// + /// + /// The builder instance being configured. + /// + /// + /// The name of the singleton. Will also be part of the created by this method. /// /// /// Optional. The set of options for configuring the . /// /// - /// Optional. By default Akka.Hosting will assume the is hosted at - /// "/user/{singletonName}" - but if for some reason the path is different you can use this property to + /// Optional. By default, Akka.Hosting will assume the is hosted at + /// "/user/{singletonManagerName}" - but if for some reason the path is different you can use this property to /// override that value. /// /// @@ -1360,27 +1584,25 @@ public static AkkaConfigurationBuilder WithSingletonProxy( ClusterSingletonOptions? options = null, string? singletonManagerPath = null) { + // make sure that default configuration is loaded, not an exhaustive check. + if (!builder.Configuration.HasValue || builder.Configuration.Value.HasPath("akka.cluster.singleton-proxy")) + { + builder.AddHocon(ClusterSingletonManager.DefaultConfig(), HoconAddMode.Append); + } + return builder.WithActors((system, registry) => { options ??= new ClusterSingletonOptions(); - - var singletonProxySettings = - ClusterSingletonProxySettings.Create(system).WithSingletonName(singletonName); - - if (!string.IsNullOrEmpty(options.Role)) - { - singletonProxySettings = singletonProxySettings.WithRole(options.Role); - } - - if (options.BufferSize != null) - { - singletonProxySettings = singletonProxySettings.WithBufferSize(options.BufferSize.Value); - } + var singletonProxySettings = options.ToProxySettings(singletonName, system); singletonManagerPath ??= $"/user/{singletonName}"; - CreateAndRegisterSingletonProxy(singletonName, singletonManagerPath, singletonProxySettings, - system, registry); + CreateAndRegisterSingletonProxy( + singletonName: singletonName, + singletonManagerActorPath: singletonManagerPath, + singletonProxySettings: singletonProxySettings, + system: system, + registry: registry); }); } diff --git a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCluster.verified.txt b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCluster.verified.txt index 43b54bf2..968634e8 100644 --- a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCluster.verified.txt +++ b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveCluster.verified.txt @@ -34,6 +34,9 @@ namespace Akka.Cluster.Hosting public static Akka.Hosting.AkkaConfigurationBuilder WithShardedDaemonProcessProxy(this Akka.Hosting.AkkaConfigurationBuilder builder, string name, int numberOfInstances, string role) { } public static Akka.Hosting.AkkaConfigurationBuilder WithSingleton(this Akka.Hosting.AkkaConfigurationBuilder builder, string singletonName, Akka.Actor.Props actorProps, Akka.Cluster.Hosting.ClusterSingletonOptions? options = null, bool createProxyToo = true) { } public static Akka.Hosting.AkkaConfigurationBuilder WithSingleton(this Akka.Hosting.AkkaConfigurationBuilder builder, string singletonName, System.Func propsFactory, Akka.Cluster.Hosting.ClusterSingletonOptions? options = null, bool createProxyToo = true) { } + public static Akka.Hosting.AkkaConfigurationBuilder WithSingleton(this Akka.Hosting.AkkaConfigurationBuilder builder, string singletonManagerName, string singletonName, Akka.Actor.Props actorProps, Akka.Cluster.Hosting.ClusterSingletonOptions? options = null, bool createProxyToo = true) { } + public static Akka.Hosting.AkkaConfigurationBuilder WithSingleton(this Akka.Hosting.AkkaConfigurationBuilder builder, string singletonManagerName, string singletonName, System.Func propsFactory, Akka.Cluster.Hosting.ClusterSingletonOptions? options = null, bool createProxyToo = true) { } + public static Akka.Hosting.AkkaConfigurationBuilder WithSingletonProxy(this Akka.Hosting.AkkaConfigurationBuilder builder, string singletonManagerName, string singletonName, Akka.Cluster.Hosting.ClusterSingletonOptions? options = null) { } public static Akka.Hosting.AkkaConfigurationBuilder WithSingletonProxy(this Akka.Hosting.AkkaConfigurationBuilder builder, string singletonName, Akka.Cluster.Hosting.ClusterSingletonOptions? options = null, string? singletonManagerPath = null) { } } public sealed class ClusterClientDiscoveryOptions @@ -75,7 +78,10 @@ namespace Akka.Cluster.Hosting public int? BufferSize { get; set; } public Akka.Hosting.Coordination.LeaseOptionBase? LeaseImplementation { get; set; } public System.TimeSpan? LeaseRetryInterval { get; set; } + public bool? LogSingletonIdentificationFailure { get; set; } public string? Role { get; set; } + public System.TimeSpan? SingletonIdentificationFailurePeriod { get; set; } + public System.TimeSpan? SingletonIdentificationInterval { get; set; } public object? TerminationMessage { get; set; } } public class DDataOptions