From 20f8b25ca3a4137fd41edefcb547b0375264ee5b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:48:14 -0700 Subject: [PATCH 01/10] fix(testing): isolate reminder event continuations (#10899) --- .../Diagnostics/ReminderEventsTests.cs | 28 ++++++ .../ReminderDiagnosticObserver.cs | 98 +++++++++++++++---- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs b/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs index a967bab00d..432490ea4e 100644 --- a/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs +++ b/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs @@ -29,6 +29,34 @@ public void EmitRegistered_EmitsGrainIdAndReminderName() Assert.Same(siloAddress, registered.SiloAddress); } + [TestSuite("BVT")] + [TestProvider("None")] + [Fact, TestCategory("BVT")] + public void ReminderDiagnosticObserver_ServiceStartedWait_DoesNotRunContinuationsInline() + { + using var observer = ReminderDiagnosticObserver.Create(); + using var continuationRan = new ManualResetEventSlim(); + var siloAddress = SiloAddress.New(new IPEndPoint(IPAddress.Loopback, 14010), 11); + var waitTask = observer.WaitForReminderServiceStartedAsync(TestContext.Current.CancellationToken, siloAddress); + var emitterThread = Environment.CurrentManagedThreadId; + var continuationThread = 0; + + _ = waitTask.ContinueWith( + _ => + { + continuationThread = Environment.CurrentManagedThreadId; + continuationRan.Set(); + }, + TestContext.Current.CancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + + ReminderEvents.EmitReminderServiceStarted(siloAddress); + + Assert.True(continuationRan.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)); + Assert.NotEqual(emitterThread, continuationThread); + } + [TestSuite("BVT")] [TestProvider("None")] [Fact, TestCategory("BVT")] diff --git a/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs b/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs index 4a40a4a229..67f6aa2b92 100644 --- a/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs +++ b/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs @@ -1,9 +1,9 @@ #nullable enable using System.Diagnostics.CodeAnalysis; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Reactive.Threading.Tasks; using Orleans; using Orleans.Internal; using Orleans.Runtime; @@ -149,10 +149,10 @@ private void StoreEvent(ReminderEvents.ReminderEvent value) /// public Task WaitForReminderTickAsync(GrainId grainId, CancellationToken cancellationToken, string? reminderName = null) { - return _events - .OfType() - .FirstAsync(e => MatchesReminder(e, grainId, reminderName)) - .ToTask(cancellationToken); + return WaitForEventAsync( + _events.OfType(), + e => MatchesReminder(e, grainId, reminderName), + cancellationToken); } /// @@ -206,10 +206,10 @@ public async Task WaitForTickConditionAsync(GrainId grainId, Func public Task WaitForReminderRegisteredAsync(GrainId grainId, string reminderName, CancellationToken cancellationToken) { - return _events - .OfType() - .FirstAsync(e => MatchesReminder(e, grainId, reminderName)) - .ToTask(cancellationToken); + return WaitForEventAsync( + _events.OfType(), + e => MatchesReminder(e, grainId, reminderName), + cancellationToken); } /// @@ -217,10 +217,10 @@ public async Task WaitForTickConditionAsync(GrainId grainId, Func public Task WaitForReminderServiceStartedAsync(CancellationToken cancellationToken, SiloAddress? siloAddress = null) { - return _serviceEvents - .OfType() - .FirstAsync(e => siloAddress is null || Equals(e.SiloAddress, siloAddress)) - .ToTask(cancellationToken); + return WaitForEventAsync( + _serviceEvents.OfType(), + e => siloAddress is null || Equals(e.SiloAddress, siloAddress), + cancellationToken); } /// @@ -228,10 +228,10 @@ public async Task WaitForTickConditionAsync(GrainId grainId, Func public Task WaitForReminderUnregisteredAsync(GrainId grainId, string reminderName, CancellationToken cancellationToken) { - return _events - .OfType() - .FirstAsync(e => MatchesReminder(e, grainId, reminderName)) - .ToTask(cancellationToken); + return WaitForEventAsync( + _events.OfType(), + e => MatchesReminder(e, grainId, reminderName), + cancellationToken); } /// @@ -330,6 +330,70 @@ public Task WaitForGlobalQuiescenceAsync( return waiter.TaskSource.Task; } + private static Task WaitForEventAsync( + IObservable events, + Func predicate, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var subscription = new SingleAssignmentDisposable(); + var cancellationRegistration = cancellationToken.Register( + static state => + { + var (source, eventSubscription, token) = + ((TaskCompletionSource Source, SingleAssignmentDisposable Subscription, CancellationToken Token))state!; + eventSubscription.Dispose(); + source.TrySetCanceled(token); + }, + (completion, subscription, cancellationToken)); + _ = completion.Task.ContinueWith( + static (_, state) => ((CancellationTokenRegistration)state!).Dispose(), + cancellationRegistration, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + + subscription.Disposable = events.Subscribe( + value => + { + bool matches; + try + { + matches = predicate(value); + } + catch (Exception exception) + { + subscription.Dispose(); + completion.TrySetException(exception); + return; + } + + if (!matches) + { + return; + } + + subscription.Dispose(); + completion.TrySetResult(value); + }, + error => + { + subscription.Dispose(); + completion.TrySetException(error); + }, + () => + { + subscription.Dispose(); + completion.TrySetException(new InvalidOperationException("The diagnostic event stream completed before a matching event was observed.")); + }); + return completion.Task; + } + private static bool MatchesReminder(ReminderEvents.ReminderEvent evt, GrainId grainId, string? reminderName) { return evt.GrainId == grainId From 526d5139acae08fd34a4f66495e029b8eb79decc Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:48:36 -0700 Subject: [PATCH 02/10] test(reminders): add lifecycle conformance scenarios (#10896) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Orleans.Reminders.TestKit/README.md | 54 +- .../ReminderServiceLifecycleTestRunner.cs | 763 ++++++++++++++++++ .../Orleans.Reminders.csproj | 2 +- src/Orleans.Runtime/Orleans.Runtime.csproj | 1 + .../Orleans.AWS.Tests.csproj | 1 + .../Reminder/DynamoDBRemindersTableTests.cs | 61 ++ .../ReminderServiceLifecycleTests_AdoNet.cs | 110 +++ .../ReminderTests_AdoNet_SqlServer.cs | 16 +- .../Reminder/ReminderTests_AzureTable.cs | 14 + .../ReminderTests_Cosmos.cs | 14 + .../Orleans.Redis.Tests.csproj | 1 + .../Reminders/RedisReminderTableTests.cs | 63 +- .../FirestoreRemindersTests.cs | 65 +- .../Orleans.Reminders.Firestore.Tests.csproj | 1 + ...eminderServiceLifecycleConformanceTests.cs | 407 ++++++++++ .../Diagnostics/ReminderEventsTests.cs | 41 + .../ReminderServiceLifecycleTestsBase.cs | 80 ++ .../TimerTests/ReminderTests_TableGrain.cs | 13 + .../Orleans.Testing.Reminders.csproj | 5 + .../ReminderDiagnosticObserver.cs | 169 +++- .../ReminderServiceLifecycleHarness.cs | 208 +++++ .../ReminderTestClock.cs | 8 + 22 files changed, 2079 insertions(+), 18 deletions(-) create mode 100644 src/Orleans.Reminders.TestKit/ReminderServiceLifecycleTestRunner.cs create mode 100644 test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderServiceLifecycleTests_AdoNet.cs create mode 100644 test/Orleans.Reminders.TestKit.Tests/ReminderServiceLifecycleConformanceTests.cs create mode 100644 test/Orleans.Reminders.Tests/TimerTests/ReminderServiceLifecycleTestsBase.cs create mode 100644 test/Orleans.Testing.Reminders/ReminderServiceLifecycleHarness.cs diff --git a/src/Orleans.Reminders.TestKit/README.md b/src/Orleans.Reminders.TestKit/README.md index 7df327d4bb..2f7465ef9e 100644 --- a/src/Orleans.Reminders.TestKit/README.md +++ b/src/Orleans.Reminders.TestKit/README.md @@ -12,6 +12,8 @@ cardinality setup and cleanup, bounded retries, diagnostics, and failure message | --- | --- | | `ReminderTableTestRunner` | Direct conformance facts for every documented table guarantee. | | `ReminderServiceTestRunner` | Cluster-level registration, replacement, lookup, enumeration, and removal conformance. | +| `ReminderServiceLifecycleTestRunner` | Deterministic startup, ownership, exact-due, reconciliation, churn, and cleanup-isolation conformance. | +| `IReminderServiceLifecycleHarness` | Adapter contract for one cluster, one reminder clock, diagnostics, and explicit topology barriers. | | `ReminderTableModelBasedTestRunner` | Generated sequential conformance against the same full contract. | | `IdealizedReminderTable` | Deterministic, strongly consistent reference implementation and fault-injection oracle. | | `ReminderTableTestFixture` | In-process cluster fixture which deploys and resolves a provider. | @@ -173,6 +175,54 @@ public sealed class MyReminderServiceTests } ``` +### Run lifecycle and churn conformance + +`ReminderServiceLifecycleTestRunner` adds the shared service-level contract. The same eight scenarios run for every +service provider: startup readiness, single registration ownership, in-place schedule update, removal quiescence, +exact-due recovery, stale-owner registration reconciliation, one-silo join/leave transfer, and cleanup isolation. + +Use `ReminderTestClock` as the sole time driver and `ReminderDiagnosticObserver` as the lifecycle/tick source. The +`ReminderServiceLifecycleHarness` adapter for `InProcessTestCluster` supplies explicit membership and reminder-range +reconciliation barriers: + +```csharp +var clock = builder.AddReminderTestClock(); +var cluster = builder.Build(); +await cluster.DeployAsync(); + +var options = cluster.Silos[0].ServiceProvider + .GetRequiredService>().Value; +var harness = new ReminderServiceLifecycleHarness( + cluster, + clock, + clock.DiagnosticObserver, + options.ReminderLoadingWindow); + +public sealed class MyLifecycleTests : ReminderServiceLifecycleTestRunner +{ + public MyLifecycleTests(IReminderServiceLifecycleHarness harness) + : base(harness, "MyProvider", seed: 42) + { + } + + [Fact] + public override Task ReminderService_OneSiloJoinLeaveTransfersOwnership() + => base.ReminderService_OneSiloJoinLeaveTransfersOwnership(); +} +``` + +Do not replace harness barriers with delays, retry loops, longer timeouts, or provider-specific skips. Scenario cleanup +uses its own bounded token, removes only deterministic scenario rows using their current ETags, advances one explicit +refresh for owner quiescence, and verifies their absence; it never clears unrelated provider rows or replaces the +original scenario failure. Ownership assertions count local reminder instance identities, including duplicate +instances on one silo, rather than counting distinct silo addresses. + +The built-in in-memory, Azure Table, Cosmos DB, ADO.NET SQL Server, PostgreSQL, MySQL, Redis, DynamoDB, and Firestore +providers all expose these same inherited facts. Their adapters contain only backend precondition/setup and provider +registration. External-service availability can skip fixture construction, but no provider disables individual +lifecycle guarantees. Add a documented capability boundary here before omitting a future provider which cannot host +the Orleans reminder service or participate in in-process silo churn. + ## Deterministic oracle and cluster testing `IdealizedReminderTable` supplies a strongly consistent reference implementation for TestKit self-tests and @@ -200,7 +250,9 @@ The oracle exposes: - `FreezeReads` for stale-read convergence scenarios; and - lifecycle cancellation and invariant checks. -The TestKit cluster integration suite uses these controls to cover exact-due delivery, exact-due storage recovery, +`ReminderTestClock` creates its lifecycle observer before the cluster is built, allowing startup conformance to await +one `ReminderServiceStarted` event for every silo before liveness and range-reconciliation barriers. The TestKit +cluster integration suite uses these controls to cover exact-due delivery, exact-due storage recovery, due times beyond the platform timer limit, stale-refresh suppression after unregister, and single-owner delivery in a multi-silo cluster. diff --git a/src/Orleans.Reminders.TestKit/ReminderServiceLifecycleTestRunner.cs b/src/Orleans.Reminders.TestKit/ReminderServiceLifecycleTestRunner.cs new file mode 100644 index 0000000000..06e7737c55 --- /dev/null +++ b/src/Orleans.Reminders.TestKit/ReminderServiceLifecycleTestRunner.cs @@ -0,0 +1,763 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Orleans.Runtime; + +namespace Orleans.Reminders.TestKit; + +/// +/// Supplies deterministic clock, diagnostics, and topology controls to +/// . +/// +public interface IReminderServiceLifecycleHarness +{ + /// Gets the deployed cluster's grain factory. + IGrainFactory GrainFactory { get; } + + /// Gets the provider table used by the reminder service. + IReminderTable ReminderTable { get; } + + /// Gets the deterministic reminder clock time. + DateTimeOffset UtcNow { get; } + + /// Gets the configured reminder loading window. + TimeSpan ReminderLoadingWindow { get; } + + /// Gets the configured reminder-table refresh period. + TimeSpan ReminderRefreshPeriod { get; } + + /// Gets the currently active silos. + IReadOnlyList ActiveSilos { get; } + + /// Waits until every active reminder service is ready. + Task WaitForStartupReadinessAsync(CancellationToken cancellationToken); + + /// Advances the one reminder clock driver. + Task AdvanceAsync(TimeSpan amount, CancellationToken cancellationToken); + + /// Refreshes every active reminder service without advancing time. + Task RefreshAsync(CancellationToken cancellationToken); + + /// Waits for exactly local owners. + Task WaitForOwnerCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken); + + /// Gets the current local owners. + IReadOnlyList GetOwners(GrainId grainId, string reminderName); + + /// Returns whether a silo's current ring range owns a grain identity. + bool IsOwner(SiloAddress siloAddress, GrainId grainId); + + /// Waits until the current owner has armed its persisted schedule. + Task WaitForScheduleAsync(GrainId grainId, string reminderName, CancellationToken cancellationToken); + + /// Gets the number of local reminder instances started for a reminder. + int GetLocalStartCount(GrainId grainId, string reminderName); + + /// Gets the number of local reminder instances stopped for a reminder. + int GetLocalStopCount(GrainId grainId, string reminderName); + + /// Gets the number of local schedule changes for a reminder. + int GetScheduleChangeCount(GrainId grainId, string reminderName); + + /// Waits for the local schedule-change count. + Task WaitForScheduleChangeCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken); + + /// Waits for exactly the requested completed tick count. + Task WaitForTickCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken); + + /// Gets the current completed tick count. + int GetTickCount(GrainId grainId, string reminderName); + + /// Starts one silo and waits for its reminder service to become ready. + Task JoinOneSiloAsync(CancellationToken cancellationToken); + + /// Stops the specified silo. + Task LeaveSiloAsync(SiloAddress siloAddress, CancellationToken cancellationToken); + + /// Waits for membership and reminder-range reconciliation on all active silos. + Task WaitForTopologyReconciliationAsync(CancellationToken cancellationToken); +} + +/// +/// Shared deterministic conformance scenarios for reminder service lifecycle, ownership, and churn. +/// +/// +/// Provider suites supply only an . The runner owns identities, +/// schedules, topology transitions, exact assertions, and cleanup. Each scenario cleans only the reminders which +/// it created and verifies their absence, so unrelated rows and concurrently running provider suites are isolated. +/// +public abstract class ReminderServiceLifecycleTestRunner +{ + private static readonly TimeSpan Period = TimeSpan.FromMinutes(2); + private readonly IReminderServiceLifecycleHarness _harness; + private readonly int _seed; + private int _grainCounter; + + /// Initializes the runner. + protected ReminderServiceLifecycleTestRunner( + IReminderServiceLifecycleHarness harness, + string providerName, + int seed = 0) + { + _harness = harness ?? throw new ArgumentNullException(nameof(harness)); + ArgumentException.ThrowIfNullOrWhiteSpace(providerName); + ProviderName = providerName; + _seed = seed; + } + + /// Gets the provider name used in diagnostics. + protected string ProviderName { get; } + + /// Guarantee: every active silo reports reminder-service readiness before operations begin. + public virtual Task ReminderService_StartupReadiness() + => RunReminderService_StartupReadiness(CancellationToken.None); + + /// Runs the startup-readiness scenario. + public async Task RunReminderService_StartupReadiness(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_StartupReadiness); + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + await _harness.WaitForStartupReadinessAsync(cancellationToken); + if (_harness.ActiveSilos.Count == 0) + { + Fail(Guarantee, "startup") + .WithExpected("at least one ready initial silo") + .WithObserved($"activeSilos=[{string.Join(", ", _harness.ActiveSilos)}]") + .Throw(); + } + }, + _ => Task.CompletedTask); + } + + /// Guarantee: a registration has one owner and one exact delivery. + public virtual Task ReminderService_RegistrationHasSingleOwner() + => RunReminderService_RegistrationHasSingleOwner(CancellationToken.None); + + /// Runs the registration-ownership scenario. + public async Task RunReminderService_RegistrationHasSingleOwner(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_RegistrationHasSingleOwner); + const string Name = "registration-owner"; + var grain = CreateGrain(Guarantee); + var due = TimeSpan.FromSeconds(3); + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + var expectedStart = _harness.UtcNow.UtcDateTime + due; + await grain.RegisterOrUpdateAsync(Name, due, Period).WaitAsync(cancellationToken); + await WaitForOwnersAfterRefreshAsync([(grain, Name)], cancellationToken); + if (_harness.GetOwners(grain.GetGrainId(), Name).Count != 1) + { + OwnershipFailure(Guarantee, grain.GetGrainId(), Name, 1).Throw(); + } + + await _harness.WaitForScheduleAsync(grain.GetGrainId(), Name, cancellationToken); + await AssertPersistedAsync(Guarantee, grain.GetGrainId(), Name, expectedStart, Period, cancellationToken); + var tick = _harness.WaitForTickCountAsync(grain.GetGrainId(), Name, 1, cancellationToken); + await _harness.AdvanceAsync(expectedStart - _harness.UtcNow.UtcDateTime, cancellationToken); + await tick; + AssertCounts(Guarantee, grain, Name, owners: null, ticks: 1); + }, + cleanupToken => CleanupAsync(Guarantee, grain, Name, cleanupToken)); + } + + /// Guarantee: updating a reminder changes its schedule without restarting its local owner. + public virtual Task ReminderService_UpdateDoesNotRestartLocalOwner() + => RunReminderService_UpdateDoesNotRestartLocalOwner(CancellationToken.None); + + /// Runs the in-place update scenario. + public async Task RunReminderService_UpdateDoesNotRestartLocalOwner(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_UpdateDoesNotRestartLocalOwner); + const string Name = "in-place-update"; + var grain = CreateGrain(Guarantee); + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + await grain.RegisterOrUpdateAsync(Name, TimeSpan.FromSeconds(3), Period).WaitAsync(cancellationToken); + await WaitForOwnersAfterRefreshAsync([(grain, Name)], cancellationToken); + await _harness.WaitForScheduleAsync(grain.GetGrainId(), Name, cancellationToken); + var original = await ReadRequiredAsync(Guarantee, grain.GetGrainId(), Name, cancellationToken); + var owners = _harness.GetOwners(grain.GetGrainId(), Name).ToArray(); + var starts = _harness.GetLocalStartCount(grain.GetGrainId(), Name); + var stops = _harness.GetLocalStopCount(grain.GetGrainId(), Name); + var scheduleChanges = _harness.GetScheduleChangeCount(grain.GetGrainId(), Name); + var due = TimeSpan.FromSeconds(4); + var expectedStart = _harness.UtcNow.UtcDateTime + due; + + var changed = _harness.WaitForScheduleChangeCountAsync( + grain.GetGrainId(), + Name, + scheduleChanges + 1, + cancellationToken); + await grain.RegisterOrUpdateAsync(Name, due, Period + TimeSpan.FromMinutes(1)).WaitAsync(cancellationToken); + await _harness.RefreshAsync(cancellationToken); + await changed; + await _harness.WaitForScheduleAsync(grain.GetGrainId(), Name, cancellationToken); + var updated = await ReadRequiredAsync(Guarantee, grain.GetGrainId(), Name, cancellationToken); + + if (!owners.SequenceEqual(_harness.GetOwners(grain.GetGrainId(), Name)) + || starts != _harness.GetLocalStartCount(grain.GetGrainId(), Name) + || stops != _harness.GetLocalStopCount(grain.GetGrainId(), Name) + || string.Equals(original.ETag, updated.ETag, StringComparison.Ordinal) + || updated.StartAt != expectedStart + || updated.Period != Period + TimeSpan.FromMinutes(1)) + { + Fail(Guarantee, "RegisterOrUpdateReminder") + .WithIdentity(grain.GetGrainId(), Name) + .WithExpected($"same single owner, starts={starts}, stops={stops}, rotated ETag, StartAt={expectedStart:O}") + .WithObserved( + $"owners=[{string.Join(", ", _harness.GetOwners(grain.GetGrainId(), Name))}], " + + $"starts={_harness.GetLocalStartCount(grain.GetGrainId(), Name)}, " + + $"stops={_harness.GetLocalStopCount(grain.GetGrainId(), Name)}, row={Describe(updated)}") + .WithETags(updated.ETag, original.ETag) + .Throw(); + } + }, + cleanupToken => CleanupAsync(Guarantee, grain, Name, cleanupToken)); + } + + /// Guarantee: removal reaches quiescence and cannot deliver a later occurrence. + public virtual Task ReminderService_RemovalReachesQuiescence() + => RunReminderService_RemovalReachesQuiescence(CancellationToken.None); + + /// Runs the removal/quiescence scenario. + public async Task RunReminderService_RemovalReachesQuiescence(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_RemovalReachesQuiescence); + const string Name = "removal-quiescence"; + var grain = CreateGrain(Guarantee); + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + await grain.RegisterOrUpdateAsync(Name, TimeSpan.FromSeconds(3), Period).WaitAsync(cancellationToken); + await WaitForOwnersAfterRefreshAsync([(grain, Name)], cancellationToken); + await _harness.WaitForScheduleAsync(grain.GetGrainId(), Name, cancellationToken); + + if (!await grain.UnregisterAsync(Name).WaitAsync(cancellationToken)) + { + Fail(Guarantee, "UnregisterReminder") + .WithIdentity(grain.GetGrainId(), Name) + .WithExpected("successful removal") + .WithObserved("removal returned false") + .Throw(); + } + + var quiescence = _harness.WaitForOwnerCountAsync(grain.GetGrainId(), Name, 0, cancellationToken); + await _harness.RefreshAsync(cancellationToken); + await quiescence; + await AssertAbsentAsync(Guarantee, grain.GetGrainId(), Name, cancellationToken); + await _harness.AdvanceAsync(Period, cancellationToken); + AssertCounts(Guarantee, grain, Name, owners: 0, ticks: 0); + }, + cleanupToken => CleanupAsync(Guarantee, grain, Name, cleanupToken)); + } + + /// Guarantee: a persisted reminder entering the loading window fires at its exact due time. + public virtual Task ReminderService_ExactDueRecovery() + => RunReminderService_ExactDueRecovery(CancellationToken.None); + + /// Runs the exact-due recovery scenario. + public async Task RunReminderService_ExactDueRecovery(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_ExactDueRecovery); + const string Name = "exact-due-recovery"; + var grain = CreateGrain(Guarantee); + var due = _harness.ReminderLoadingWindow + + _harness.ReminderRefreshPeriod + + _harness.ReminderRefreshPeriod; + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + var expectedStart = _harness.UtcNow.UtcDateTime + due; + await grain.RegisterOrUpdateAsync(Name, due, Period).WaitAsync(cancellationToken); + var boundaryStep = TimeSpan.FromTicks(1); + var preWindowAdvance = due - _harness.ReminderLoadingWindow - boundaryStep; + await _harness.AdvanceAsync(preWindowAdvance, cancellationToken); + if (_harness.GetOwners(grain.GetGrainId(), Name).Count != 0) + { + Fail(Guarantee, "before loading window") + .WithIdentity(grain.GetGrainId(), Name) + .WithExpected("no local owner immediately before entering the loading window") + .WithObserved($"owners=[{string.Join(", ", _harness.GetOwners(grain.GetGrainId(), Name))}]") + .Throw(); + } + + var owner = _harness.WaitForOwnerCountAsync(grain.GetGrainId(), Name, 1, cancellationToken); + await _harness.AdvanceAsync(boundaryStep, cancellationToken); + await _harness.RefreshAsync(cancellationToken); + await owner; + await _harness.WaitForTopologyReconciliationAsync(cancellationToken); + await _harness.WaitForOwnerCountAsync(grain.GetGrainId(), Name, 1, cancellationToken); + await _harness.WaitForScheduleAsync(grain.GetGrainId(), Name, cancellationToken); + var tick = _harness.WaitForTickCountAsync(grain.GetGrainId(), Name, 1, cancellationToken); + await _harness.AdvanceAsync(expectedStart - _harness.UtcNow.UtcDateTime, cancellationToken); + await tick; + await AssertPersistedAsync(Guarantee, grain.GetGrainId(), Name, expectedStart, Period, cancellationToken); + AssertCounts(Guarantee, grain, Name, owners: null, ticks: 1); + }, + cleanupToken => CleanupAsync(Guarantee, grain, Name, cleanupToken)); + } + + /// Guarantee: registration and ownership reconcile to one owner after a silo joins. + public virtual Task ReminderService_StaleOwnerRegistrationReconciles() + => RunReminderService_StaleOwnerRegistrationReconciles(CancellationToken.None); + + /// Runs stale-owner registration reconciliation. + public async Task RunReminderService_StaleOwnerRegistrationReconciles(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_StaleOwnerRegistrationReconciles); + var reminders = CreateGrains(Guarantee, 16); + var phase = "join"; + SiloAddress? joined = null; + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + try + { + joined = await _harness.JoinOneSiloAsync(cancellationToken); + phase = "registration"; + await Task.WhenAll(reminders.Select(item => + item.Grain.RegisterOrUpdateAsync(item.Name, TimeSpan.FromSeconds(3), Period) + .WaitAsync(cancellationToken))); + + phase = "topology reconciliation"; + await _harness.WaitForTopologyReconciliationAsync(cancellationToken); + phase = "owner reconciliation"; + var ownerWaits = reminders + .Select(item => _harness.WaitForOwnerCountAsync( + item.Grain.GetGrainId(), + item.Name, + 1, + cancellationToken)) + .ToArray(); + await _harness.RefreshAsync(cancellationToken); + await Task.WhenAll(ownerWaits); + await _harness.WaitForTopologyReconciliationAsync(cancellationToken); + await Task.WhenAll(reminders.Select(item => + _harness.WaitForOwnerCountAsync( + item.Grain.GetGrainId(), + item.Name, + 1, + cancellationToken))); + foreach (var (grain, name) in reminders) + { + phase = $"schedule reconciliation for {grain.GetGrainId()}/{name}"; + await _harness.WaitForScheduleAsync(grain.GetGrainId(), name, cancellationToken); + if (_harness.GetOwners(grain.GetGrainId(), name).Count != 1) + { + OwnershipFailure(Guarantee, grain.GetGrainId(), name, 1).Throw(); + } + } + } + catch (OperationCanceledException exception) when (cancellationToken.IsCancellationRequested) + { + Fail(Guarantee, phase) + .WithExpected("all registrations reconcile to one current owner with an armed schedule") + .WithObserved(string.Join( + "; ", + reminders.Select(item => + $"{item.Grain.GetGrainId()}/{item.Name}=" + + $"[{string.Join(", ", _harness.GetOwners(item.Grain.GetGrainId(), item.Name))}]"))) + .Throw(exception); + } + }, + async cleanupToken => + { + if (joined is not null && _harness.ActiveSilos.Contains(joined)) + { + await _harness.LeaveSiloAsync(joined, cleanupToken); + await _harness.WaitForTopologyReconciliationAsync(cleanupToken); + } + + await CleanupAsync(Guarantee, reminders, cleanupToken); + }); + } + + /// Guarantee: one-silo join/leave transfers ownership without duplicates or missed delivery. + public virtual Task ReminderService_OneSiloJoinLeaveTransfersOwnership() + => RunReminderService_OneSiloJoinLeaveTransfersOwnership(CancellationToken.None); + + /// Runs the one-silo join/leave ownership-transfer scenario. + public async Task RunReminderService_OneSiloJoinLeaveTransfersOwnership(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_OneSiloJoinLeaveTransfersOwnership); + var reminders = new List<(IReminderServiceTestGrain Grain, string Name)>(); + SiloAddress? joined = null; + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + joined = await _harness.JoinOneSiloAsync(cancellationToken); + await _harness.WaitForTopologyReconciliationAsync(cancellationToken); + var grain = CreateGrainOwnedBy(joined, Guarantee); + const string Name = "join-leave-owner"; + reminders.Add((grain, Name)); + var due = TimeSpan.FromSeconds(3); + var firstTickTime = _harness.UtcNow.UtcDateTime + due; + + await grain.RegisterOrUpdateAsync(Name, due, Period).WaitAsync(cancellationToken); + await WaitForOwnersAfterRefreshAsync(reminders, cancellationToken); + var joinedOwner = _harness.GetOwners(grain.GetGrainId(), Name); + if (joinedOwner.Count != 1 || !joinedOwner[0].Equals(joined)) + { + OwnershipFailure(Guarantee, grain.GetGrainId(), Name, 1) + .WithExpected($"the joined silo {joined} owns the selected reminder") + .WithObserved($"owners=[{string.Join(", ", joinedOwner)}]") + .Throw(); + } + + await _harness.LeaveSiloAsync(joined, cancellationToken); + joined = null; + await _harness.WaitForTopologyReconciliationAsync(cancellationToken); + await _harness.WaitForOwnerCountAsync( + grain.GetGrainId(), + Name, + 1, + cancellationToken); + await _harness.WaitForTopologyReconciliationAsync(cancellationToken); + await _harness.WaitForOwnerCountAsync( + grain.GetGrainId(), + Name, + 1, + cancellationToken); + await _harness.WaitForScheduleAsync(grain.GetGrainId(), Name, cancellationToken); + var tick = _harness.WaitForTickCountAsync( + grain.GetGrainId(), + Name, + 1, + cancellationToken); + await _harness.AdvanceAsync(firstTickTime - _harness.UtcNow.UtcDateTime, cancellationToken); + await tick; + AssertCounts( + Guarantee, + grain, + Name, + owners: null, + ticks: 1); + }, + async cleanupToken => + { + if (joined is not null && _harness.ActiveSilos.Contains(joined)) + { + await _harness.LeaveSiloAsync(joined, cleanupToken); + await _harness.WaitForTopologyReconciliationAsync(cleanupToken); + } + + await CleanupAsync(Guarantee, reminders, cleanupToken); + }); + } + + /// Guarantee: scenario cleanup removes only rows owned by that scenario. + public virtual Task ReminderService_CleanupIsIsolated() + => RunReminderService_CleanupIsIsolated(CancellationToken.None); + + /// Runs cleanup isolation. + public async Task RunReminderService_CleanupIsIsolated(CancellationToken cancellationToken) + { + const string Guarantee = nameof(ReminderService_CleanupIsIsolated); + var sentinel = CreateGrain($"{Guarantee}/sentinel"); + var subject = CreateGrain($"{Guarantee}/subject"); + const string SentinelName = "unrelated-sentinel"; + const string SubjectName = "scenario-owned"; + await ExecuteWithCleanupAsync( + Guarantee, + cancellationToken, + async () => + { + await Task.WhenAll( + sentinel.RegisterOrUpdateAsync(SentinelName, TimeSpan.FromMinutes(1), Period).WaitAsync(cancellationToken), + subject.RegisterOrUpdateAsync(SubjectName, TimeSpan.FromMinutes(1), Period).WaitAsync(cancellationToken)); + await CleanupAsync(Guarantee, subject, SubjectName, cancellationToken); + var sentinelRow = await _harness.ReminderTable.ReadRow(sentinel.GetGrainId(), SentinelName).WaitAsync(cancellationToken); + if (sentinelRow is null) + { + Fail(Guarantee, "cleanup") + .WithIdentity(sentinel.GetGrainId(), SentinelName) + .WithExpected("unrelated sentinel remains registered") + .WithObserved("sentinel row was removed") + .Throw(); + } + }, + async cleanupToken => + { + await CleanupAsync(Guarantee, sentinel, SentinelName, cleanupToken); + await CleanupAsync(Guarantee, subject, SubjectName, cleanupToken); + }); + } + + private List<(IReminderServiceTestGrain Grain, string Name)> CreateGrains(string guarantee, int count) + => Enumerable.Range(0, count) + .Select(index => (CreateGrain($"{guarantee}/{index}"), $"churn-{index.ToString("D2", CultureInfo.InvariantCulture)}")) + .ToList(); + + private IReminderServiceTestGrain CreateGrain(string label) + { + var ordinal = Interlocked.Increment(ref _grainCounter); + var key = ReminderTestData.CreateGuid(_seed, $"{ProviderName}/{label}/{ordinal}"); + return _harness.GrainFactory.GetGrain(key); + } + + private IReminderServiceTestGrain CreateGrainOwnedBy(SiloAddress owner, string label) + { + var ordinal = Interlocked.Increment(ref _grainCounter); + for (var candidate = 0; candidate < ushort.MaxValue; candidate++) + { + var key = ReminderTestData.CreateGuid( + _seed, + $"{ProviderName}/{label}/{ordinal}/{candidate.ToString(CultureInfo.InvariantCulture)}"); + var grain = _harness.GrainFactory.GetGrain(key); + if (_harness.IsOwner(owner, grain.GetGrainId())) + { + return grain; + } + } + + Fail(label, "identity selection") + .WithExpected($"a deterministic grain identity owned by {owner}") + .WithObserved("no owned identity in 65,535 deterministic candidates") + .Throw(); + return null!; + } + + private async Task ExecuteWithCleanupAsync( + string guarantee, + CancellationToken scenarioCancellationToken, + Func scenario, + Func cleanup) + { + ExceptionDispatchInfo? scenarioFailure = null; + try + { + await _harness.WaitForStartupReadinessAsync(scenarioCancellationToken); + await scenario(); + } + catch (Exception exception) + { + scenarioFailure = ExceptionDispatchInfo.Capture(exception); + } + + Exception? cleanupFailure = null; + using (var cleanupCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30))) + { + try + { + await cleanup(cleanupCancellation.Token); + } + catch (Exception exception) + { + cleanupFailure = exception; + } + } + + if (scenarioFailure is not null) + { + if (cleanupFailure is not null) + { + scenarioFailure.SourceException.Data["ReminderServiceLifecycleCleanupFailure"] = cleanupFailure.ToString(); + } + + scenarioFailure.Throw(); + } + + if (cleanupFailure is not null) + { + Fail(guarantee, "scenario cleanup") + .WithExpected("all scenario-owned reminders absent and all temporary topology restored") + .WithObserved($"{cleanupFailure.GetType().FullName}: {cleanupFailure.Message}") + .Throw(cleanupFailure); + } + } + + private async Task WaitForOwnersAfterRefreshAsync( + IReadOnlyList<(IReminderServiceTestGrain Grain, string Name)> reminders, + CancellationToken cancellationToken) + { + var ownerWaits = reminders + .Select(item => _harness.WaitForOwnerCountAsync( + item.Grain.GetGrainId(), + item.Name, + 1, + cancellationToken)) + .ToArray(); + await _harness.RefreshAsync(cancellationToken); + await Task.WhenAll(ownerWaits); + } + + private async Task CleanupAsync( + string guarantee, + IReadOnlyList<(IReminderServiceTestGrain Grain, string Name)> reminders, + CancellationToken cancellationToken) + { + await Task.WhenAll(reminders.Select(item => + RemovePersistedRowAsync(guarantee, item.Grain.GetGrainId(), item.Name, cancellationToken))); + var quiescence = reminders + .Select(item => _harness.WaitForOwnerCountAsync( + item.Grain.GetGrainId(), + item.Name, + 0, + cancellationToken)) + .ToArray(); + await _harness.RefreshAsync(cancellationToken); + await Task.WhenAll(quiescence); + await Task.WhenAll(reminders.Select(item => + AssertAbsentAsync(guarantee, item.Grain.GetGrainId(), item.Name, cancellationToken))); + } + + private Task CleanupAsync( + string guarantee, + IReminderServiceTestGrain grain, + string name, + CancellationToken cancellationToken) + => CleanupAsync(guarantee, [(grain, name)], cancellationToken); + + private async Task RemovePersistedRowAsync( + string guarantee, + GrainId grainId, + string name, + CancellationToken cancellationToken) + { + var row = await _harness.ReminderTable.ReadRow(grainId, name).WaitAsync(cancellationToken); + if (row is null) + { + return; + } + + if (!await _harness.ReminderTable.RemoveRow(grainId, name, row.ETag!).WaitAsync(cancellationToken)) + { + Fail(guarantee, "scenario cleanup remove") + .WithIdentity(grainId, name) + .WithExpected("scenario-owned row removed using its current ETag") + .WithObserved(Describe(row)) + .WithETags(row.ETag, supplied: row.ETag) + .Throw(); + } + } + + private async Task AssertPersistedAsync( + string guarantee, + GrainId grainId, + string name, + DateTime expectedStart, + TimeSpan expectedPeriod, + CancellationToken cancellationToken) + { + var row = await ReadRequiredAsync(guarantee, grainId, name, cancellationToken); + if (row.StartAt != expectedStart || row.Period != expectedPeriod || string.IsNullOrEmpty(row.ETag)) + { + Fail(guarantee, "ReadRow") + .WithIdentity(grainId, name) + .WithExpected($"StartAt={expectedStart:O}, Period={expectedPeriod}, non-empty ETag") + .WithObserved(Describe(row)) + .WithSchedule(row.StartAt, row.Period) + .WithETags(row.ETag) + .Throw(); + } + } + + private async Task ReadRequiredAsync( + string guarantee, + GrainId grainId, + string name, + CancellationToken cancellationToken) + { + var row = await _harness.ReminderTable.ReadRow(grainId, name).WaitAsync(cancellationToken); + if (row is null) + { + Fail(guarantee, "ReadRow") + .WithIdentity(grainId, name) + .WithExpected("one persisted row") + .WithObserved("") + .Throw(); + } + + return row!; + } + + private async Task AssertAbsentAsync( + string guarantee, + GrainId grainId, + string name, + CancellationToken cancellationToken) + { + var row = await _harness.ReminderTable.ReadRow(grainId, name).WaitAsync(cancellationToken); + if (row is not null) + { + Fail(guarantee, "cleanup") + .WithIdentity(grainId, name) + .WithExpected("row absent") + .WithObserved(Describe(row)) + .WithETags(row.ETag) + .Throw(); + } + } + + private void AssertCounts( + string guarantee, + IReminderServiceTestGrain grain, + string name, + int? owners, + int ticks) + { + var diagnosticTicks = _harness.GetTickCount(grain.GetGrainId(), name); + var actualOwners = _harness.GetOwners(grain.GetGrainId(), name); + if ((owners is { } expectedOwners && actualOwners.Count != expectedOwners) + || diagnosticTicks != ticks) + { + Fail(guarantee, "exact counters") + .WithIdentity(grain.GetGrainId(), name) + .WithExpected($"owners={(owners?.ToString(CultureInfo.InvariantCulture) ?? "")}, completedTicks={ticks}") + .WithObserved( + $"owners={actualOwners.Count} [{string.Join(", ", actualOwners)}], " + + $"completedTicks={diagnosticTicks}") + .Throw(); + } + } + + private ReminderFailureReport OwnershipFailure(string guarantee, GrainId grainId, string name, int expected) + => Fail(guarantee, "ownership reconciliation") + .WithIdentity(grainId, name) + .WithExpected($"exactly {expected} local owner(s)") + .WithObserved($"owners=[{string.Join(", ", _harness.GetOwners(grainId, name))}]"); + + private ReminderFailureReport Fail(string guarantee, string operation) + => ReminderFailureReport.Create(ProviderName, guarantee, operation) + .WithDetail("seed", _seed.ToString(CultureInfo.InvariantCulture)) + .WithDetail("clock", _harness.UtcNow.ToString("O", CultureInfo.InvariantCulture)) + .WithDetail("activeSilos", string.Join(", ", _harness.ActiveSilos)); + + private static string Describe(ReminderEntry row) + => $"(GrainId={row.GrainId}, ReminderName='{row.ReminderName}', StartAt={row.StartAt:O}, " + + $"Period={row.Period}, ETag='{row.ETag}')"; +} diff --git a/src/Orleans.Reminders/Orleans.Reminders.csproj b/src/Orleans.Reminders/Orleans.Reminders.csproj index 734c52a609..f7d5c66186 100644 --- a/src/Orleans.Reminders/Orleans.Reminders.csproj +++ b/src/Orleans.Reminders/Orleans.Reminders.csproj @@ -21,9 +21,9 @@ + - diff --git a/src/Orleans.Runtime/Orleans.Runtime.csproj b/src/Orleans.Runtime/Orleans.Runtime.csproj index a541e66f68..66035b1b7a 100644 --- a/src/Orleans.Runtime/Orleans.Runtime.csproj +++ b/src/Orleans.Runtime/Orleans.Runtime.csproj @@ -40,6 +40,7 @@ + diff --git a/test/Extensions/Orleans.AWS.Tests/Orleans.AWS.Tests.csproj b/test/Extensions/Orleans.AWS.Tests/Orleans.AWS.Tests.csproj index 6e467d28a3..714ddf05d5 100644 --- a/test/Extensions/Orleans.AWS.Tests/Orleans.AWS.Tests.csproj +++ b/test/Extensions/Orleans.AWS.Tests/Orleans.AWS.Tests.csproj @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/test/Extensions/Orleans.AWS.Tests/Reminder/DynamoDBRemindersTableTests.cs b/test/Extensions/Orleans.AWS.Tests/Reminder/DynamoDBRemindersTableTests.cs index 38fda15b49..01e9e2b63e 100644 --- a/test/Extensions/Orleans.AWS.Tests/Reminder/DynamoDBRemindersTableTests.cs +++ b/test/Extensions/Orleans.AWS.Tests/Reminder/DynamoDBRemindersTableTests.cs @@ -2,14 +2,75 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Orleans.Configuration; +using Orleans.Hosting; using Orleans.Reminders.DynamoDB; +using Orleans.Testing.Reminders; +using Orleans.TestingHost; using TestExtensions; using UnitTests; using UnitTests.RemindersTest; +using UnitTests.TimerTests; using Xunit; namespace AWSUtils.Tests.RemindersTest { + public sealed class DynamoDBReminderServiceLifecycleFixture : BaseInProcessTestClusterFixture + { + private ReminderTestClock? _clock; + + public ReminderTestClock Clock + { + get + { + EnsurePreconditionsMet(); + return _clock ?? throw new InvalidOperationException("The reminder clock has not been configured."); + } + } + + protected override void CheckPreconditionsOrThrow() + { + if (!AWSTestConstants.IsDynamoDbAvailable) + { + throw Xunit.Sdk.SkipException.ForSkip("Unable to connect to AWS DynamoDB simulator"); + } + } + + protected override void ConfigureTestCluster(InProcessTestClusterBuilder builder) + { + _clock = builder.AddReminderTestClock(); + builder.ConfigureSilo((_, siloBuilder) => + siloBuilder.UseDynamoDBReminderService(options => + options.ParseConnectionString($"Service={AWSTestConstants.DynamoDbService}"))); + } + + public override async ValueTask DisposeAsync() + { + try + { + await base.DisposeAsync(); + } + finally + { + _clock?.Dispose(); + } + } + } + + [TestCategory("Reminders"), TestCategory("AWS"), TestCategory("DynamoDb")] + [Collection(TestEnvironmentFixture.DefaultCollection)] + [TestSuite("Functional")] + [TestProvider("DynamoDB")] + [TestArea("Reminders")] + public sealed class DynamoDBReminderServiceLifecycleTests + : ReminderServiceLifecycleTestsBase, IClassFixture + { + public DynamoDBReminderServiceLifecycleTests(DynamoDBReminderServiceLifecycleFixture fixture) + : base(fixture.Clock, fixture.HostedCluster, "DynamoDB") + { + fixture.EnsurePreconditionsMet(); + } + } + /// /// Tests DynamoDB implementation of the Orleans reminders table for storing and retrieving grain reminders. /// diff --git a/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderServiceLifecycleTests_AdoNet.cs b/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderServiceLifecycleTests_AdoNet.cs new file mode 100644 index 0000000000..c6f8e9682a --- /dev/null +++ b/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderServiceLifecycleTests_AdoNet.cs @@ -0,0 +1,110 @@ +using Orleans.Hosting; +using Orleans.Tests.SqlUtils; +using Orleans.Testing.Reminders; +using Orleans.TestingHost; +using TestExtensions; +using UnitTests.TimerTests; + +namespace Tester.AdoNet.Reminders; + +public abstract class AdoNetReminderServiceLifecycleFixture : BaseInProcessTestClusterFixture +{ + private string _connectionString = null!; + private ReminderTestClock? _clock; + + protected abstract string Invariant { get; } + + protected abstract string DatabaseName { get; } + + public ReminderTestClock Clock + { + get + { + EnsurePreconditionsMet(); + return _clock ?? throw new InvalidOperationException("The reminder clock has not been configured."); + } + } + + protected override void CheckPreconditionsOrThrow() + => UnitTests.General.RelationalStorageForTesting.CheckPreconditionsOrThrow(Invariant); + + public override async ValueTask InitializeAsync() + { + if (!PreconditionsMet) + { + return; + } + + var relationalStorage = await UnitTests.General.RelationalStorageForTesting.SetupInstance( + Invariant, + DatabaseName, + cancellationToken: TestContext.Current.CancellationToken); + _connectionString = relationalStorage.CurrentConnectionString; + await base.InitializeAsync(); + } + + protected override void ConfigureTestCluster(InProcessTestClusterBuilder builder) + { + _clock = builder.AddReminderTestClock(); + builder.ConfigureSilo((_, siloBuilder) => + siloBuilder.UseAdoNetReminderService(options => + { + options.ConnectionString = _connectionString; + options.Invariant = Invariant; + })); + } + + public override async ValueTask DisposeAsync() + { + try + { + await base.DisposeAsync(); + } + finally + { + _clock?.Dispose(); + } + } +} + +public sealed class PostgreSqlReminderServiceLifecycleFixture : AdoNetReminderServiceLifecycleFixture +{ + protected override string Invariant => AdoNetInvariants.InvariantNamePostgreSql; + + protected override string DatabaseName => "OrleansTest_PostgreSql_ReminderLifecycle"; +} + +public sealed class MySqlReminderServiceLifecycleFixture : AdoNetReminderServiceLifecycleFixture +{ + protected override string Invariant => AdoNetInvariants.InvariantNameMySql; + + protected override string DatabaseName => "OrleansTest_MySql_ReminderLifecycle"; +} + +[TestSuite("Functional")] +[TestProvider("PostgreSql")] +[TestArea("Reminders")] +[TestCategory("Functional"), TestCategory("Reminders"), TestCategory("AdoNet"), TestCategory("PostgreSql")] +public sealed class PostgreSqlReminderServiceLifecycleTests + : ReminderServiceLifecycleTestsBase, IClassFixture +{ + public PostgreSqlReminderServiceLifecycleTests(PostgreSqlReminderServiceLifecycleFixture fixture) + : base(fixture.Clock, fixture.HostedCluster, "AdoNet.PostgreSql") + { + fixture.EnsurePreconditionsMet(); + } +} + +[TestSuite("Functional")] +[TestProvider("MySql")] +[TestArea("Reminders")] +[TestCategory("Functional"), TestCategory("Reminders"), TestCategory("AdoNet"), TestCategory("MySql")] +public sealed class MySqlReminderServiceLifecycleTests + : ReminderServiceLifecycleTestsBase, IClassFixture +{ + public MySqlReminderServiceLifecycleTests(MySqlReminderServiceLifecycleFixture fixture) + : base(fixture.Clock, fixture.HostedCluster, "AdoNet.MySql") + { + fixture.EnsurePreconditionsMet(); + } +} diff --git a/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderTests_AdoNet_SqlServer.cs b/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderTests_AdoNet_SqlServer.cs index c48915abe0..db4114f470 100644 --- a/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderTests_AdoNet_SqlServer.cs +++ b/test/Extensions/Orleans.AdoNet.Tests/Reminders/ReminderTests_AdoNet_SqlServer.cs @@ -17,6 +17,20 @@ namespace Tester.AdoNet.Reminders { + [TestSuite("Functional")] + [TestProvider("SqlServer")] + [TestArea("Reminders")] + [TestCategory("Reminders"), TestCategory("AdoNet"), TestCategory("SqlServer")] + public sealed class ReminderServiceLifecycleTests_AdoNet_SqlServer + : ReminderServiceLifecycleTestsBase, IClassFixture + { + public ReminderServiceLifecycleTests_AdoNet_SqlServer(ReminderTests_AdoNet_SqlServer.Fixture fixture) + : base(fixture.ReminderClock, fixture.HostedCluster, "AdoNet.SqlServer") + { + fixture.EnsurePreconditionsMet(); + } + } + /// /// Integration tests for Orleans reminders functionality using SQL Server as the reminder service backend. /// @@ -101,7 +115,7 @@ public async ValueTask InitializeAsync() await ClearReminderTableAsync(TestContext.Current.CancellationToken) .WaitAsync(TestConstants.InitTimeout, TestContext.Current.CancellationToken); } - + // Basic tests [Fact] diff --git a/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs b/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs index 74569db556..29e17e0d4f 100644 --- a/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs +++ b/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs @@ -14,6 +14,20 @@ namespace Tester.AzureUtils.TimerTests { + [TestSuite("Functional")] + [TestProvider("AzureStorage")] + [TestArea("Reminders")] + [TestCategory("Reminders"), TestCategory("AzureStorage")] + public sealed class ReminderServiceLifecycleTests_AzureTable + : ReminderServiceLifecycleTestsBase, IClassFixture + { + public ReminderServiceLifecycleTests_AzureTable(ReminderTests_AzureTable.Fixture fixture) + : base(fixture.ReminderClock, fixture.HostedCluster, "AzureStorage") + { + fixture.EnsurePreconditionsMet(); + } + } + /// /// Tests for Azure Table Storage-based reminder service, including basic operations, failover, and multi-grain scenarios. /// diff --git a/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs b/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs index 59320f9c42..630958f363 100644 --- a/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs +++ b/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs @@ -11,6 +11,20 @@ namespace Tester.Cosmos.Reminders; +[TestSuite("Functional")] +[TestProvider("Cosmos")] +[TestArea("Reminders")] +[TestCategory("Reminders"), TestCategory("Cosmos")] +public sealed class ReminderServiceLifecycleTests_Cosmos + : ReminderServiceLifecycleTestsBase, IClassFixture +{ + public ReminderServiceLifecycleTests_Cosmos(ReminderTests_Cosmos.Fixture fixture) + : base(fixture.ReminderClock, fixture.HostedCluster, "Cosmos") + { + fixture.EnsurePreconditionsMet(); + } +} + /// /// Tests for Orleans reminders functionality using Azure Cosmos DB as the reminder service backing store. /// diff --git a/test/Extensions/Orleans.Redis.Tests/Orleans.Redis.Tests.csproj b/test/Extensions/Orleans.Redis.Tests/Orleans.Redis.Tests.csproj index 2234e120da..bd826f71e0 100644 --- a/test/Extensions/Orleans.Redis.Tests/Orleans.Redis.Tests.csproj +++ b/test/Extensions/Orleans.Redis.Tests/Orleans.Redis.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/test/Extensions/Orleans.Redis.Tests/Reminders/RedisReminderTableTests.cs b/test/Extensions/Orleans.Redis.Tests/Reminders/RedisReminderTableTests.cs index b45a18f920..aa67670f45 100644 --- a/test/Extensions/Orleans.Redis.Tests/Reminders/RedisReminderTableTests.cs +++ b/test/Extensions/Orleans.Redis.Tests/Reminders/RedisReminderTableTests.cs @@ -3,16 +3,75 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Orleans.Configuration; +using Orleans.Hosting; using Orleans.Reminders.Redis; using Orleans.Runtime; +using Orleans.Testing.Reminders; +using Orleans.TestingHost; using StackExchange.Redis; using TestExtensions; using UnitTests; using UnitTests.RemindersTest; +using UnitTests.TimerTests; using Xunit; namespace Tester.Redis.Reminders { + public sealed class RedisReminderServiceLifecycleFixture : BaseInProcessTestClusterFixture + { + private ReminderTestClock? _clock; + + public ReminderTestClock Clock + { + get + { + EnsurePreconditionsMet(); + return _clock ?? throw new InvalidOperationException("The reminder clock has not been configured."); + } + } + + protected override void CheckPreconditionsOrThrow() => TestUtils.CheckForRedis(); + + protected override void ConfigureTestCluster(InProcessTestClusterBuilder builder) + { + _clock = builder.AddReminderTestClock(); + builder.ConfigureSilo((_, siloBuilder) => + siloBuilder.UseRedisReminderService(options => + { + options.ConfigurationOptions = ConfigurationOptions.Parse( + TestDefaultConfiguration.RedisConnectionString!); + options.EntryExpiry = TimeSpan.FromHours(1); + })); + } + + public override async ValueTask DisposeAsync() + { + try + { + await base.DisposeAsync(); + } + finally + { + _clock?.Dispose(); + } + } + } + + [TestCategory("Redis"), TestCategory("Reminders"), TestCategory("Functional")] + [Collection(TestEnvironmentFixture.DefaultCollection)] + [TestSuite("Functional")] + [TestProvider("Redis")] + [TestArea("Reminders")] + public sealed class RedisReminderServiceLifecycleTests + : ReminderServiceLifecycleTestsBase, IClassFixture + { + public RedisReminderServiceLifecycleTests(RedisReminderServiceLifecycleFixture fixture) + : base(fixture.Clock, fixture.HostedCluster, "Redis") + { + fixture.EnsurePreconditionsMet(); + } + } + /// /// Tests for Redis reminder table implementation. /// @@ -23,7 +82,7 @@ namespace Tester.Redis.Reminders [TestArea("Reminders")] public class RedisRemindersTableTests : ReminderTableTestsBase { - public RedisRemindersTableTests(ConnectionStringFixture fixture, CommonFixture clusterFixture) : base (fixture, clusterFixture, CreateFilters()) + public RedisRemindersTableTests(ConnectionStringFixture fixture, CommonFixture clusterFixture) : base(fixture, clusterFixture, CreateFilters()) { TestUtils.CheckForRedis(); } @@ -46,7 +105,7 @@ protected override IReminderTable CreateRemindersTable() { ConfigurationOptions = ConfigurationOptions.Parse(GetConnectionString().Result), EntryExpiry = TimeSpan.FromHours(1) - })); + })); if (reminderTable == null) { diff --git a/test/Extensions/Orleans.Reminders.Firestore.Tests/FirestoreRemindersTests.cs b/test/Extensions/Orleans.Reminders.Firestore.Tests/FirestoreRemindersTests.cs index 40697a25c1..1a297ffd98 100644 --- a/test/Extensions/Orleans.Reminders.Firestore.Tests/FirestoreRemindersTests.cs +++ b/test/Extensions/Orleans.Reminders.Firestore.Tests/FirestoreRemindersTests.cs @@ -1,14 +1,71 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using UnitTests; -using TestExtensions; -using UnitTests.RemindersTest; +using Orleans.Hosting; using Orleans.Reminders.Firestore; using Orleans.Runtime; - +using Orleans.Testing.Reminders; +using Orleans.TestingHost; +using TestExtensions; +using UnitTests; +using UnitTests.RemindersTest; +using UnitTests.TimerTests; namespace Orleans.Reminders.Firestore.Tests; +public sealed class FirestoreReminderServiceLifecycleFixture : BaseInProcessTestClusterFixture +{ + private ReminderTestClock? _clock; + + public ReminderTestClock Clock + { + get + { + EnsurePreconditionsMet(); + return _clock ?? throw new InvalidOperationException("The reminder clock has not been configured."); + } + } + + protected override void CheckPreconditionsOrThrow() => _ = GoogleEmulatorHost.FirestoreEndpoint; + + protected override void ConfigureTestCluster(InProcessTestClusterBuilder builder) + { + _clock = builder.AddReminderTestClock(); + builder.ConfigureSilo((_, siloBuilder) => + siloBuilder.UseFirestoreReminderService(options => + { + options.ProjectId = GoogleEmulatorHost.ProjectId; + options.EmulatorHost = GoogleEmulatorHost.FirestoreEndpoint; + })); + } + + public override async ValueTask DisposeAsync() + { + try + { + await base.DisposeAsync(); + } + finally + { + _clock?.Dispose(); + } + } +} + +[TestSuite("Functional")] +[TestProvider("GoogleCloud")] +[TestArea("Reminders")] +[TestCategory("Reminders"), TestCategory("Firestore"), TestCategory("GoogleCloud"), TestCategory("Functional")] +[Collection(TestEnvironmentFixture.DefaultCollection)] +public sealed class FirestoreReminderServiceLifecycleTests + : ReminderServiceLifecycleTestsBase, IClassFixture +{ + public FirestoreReminderServiceLifecycleTests(FirestoreReminderServiceLifecycleFixture fixture) + : base(fixture.Clock, fixture.HostedCluster, "Firestore") + { + fixture.EnsurePreconditionsMet(); + } +} + [TestSuite("Functional")] [TestProvider("GoogleCloud")] [TestArea("Reminders")] diff --git a/test/Extensions/Orleans.Reminders.Firestore.Tests/Orleans.Reminders.Firestore.Tests.csproj b/test/Extensions/Orleans.Reminders.Firestore.Tests/Orleans.Reminders.Firestore.Tests.csproj index 494bc07168..3731341103 100644 --- a/test/Extensions/Orleans.Reminders.Firestore.Tests/Orleans.Reminders.Firestore.Tests.csproj +++ b/test/Extensions/Orleans.Reminders.Firestore.Tests/Orleans.Reminders.Firestore.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/Orleans.Reminders.TestKit.Tests/ReminderServiceLifecycleConformanceTests.cs b/test/Orleans.Reminders.TestKit.Tests/ReminderServiceLifecycleConformanceTests.cs new file mode 100644 index 0000000000..6b557e074d --- /dev/null +++ b/test/Orleans.Reminders.TestKit.Tests/ReminderServiceLifecycleConformanceTests.cs @@ -0,0 +1,407 @@ +using Microsoft.Extensions.DependencyInjection; +using Orleans.Configuration; +using Orleans.Reminders.TestKit; +using Orleans.Runtime; +using Orleans.Testing.Reminders; +using Orleans.TestingHost; +using Xunit; + +namespace Orleans.Reminders.TestKit.Tests; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class ReminderServiceLifecycleCollection +{ + public const string Name = "Reminder service lifecycle conformance"; +} + +public sealed class ReminderServiceLifecycleFixture : IAsyncLifetime +{ + private static readonly TimeSpan LoadingWindow = TimeSpan.FromSeconds(5); + private InProcessTestCluster? _cluster; + private ReminderTestClock? _clock; + + public ReminderServiceLifecycleFixture() + { + } + + internal ReminderServiceLifecycleFixture(ReminderTestClock clock) + { + _clock = clock; + } + + public ReminderServiceLifecycleHarness Harness { get; private set; } = null!; + + public async ValueTask InitializeAsync() + { + try + { + var builder = new InProcessTestClusterBuilder(1); + builder.ConfigureSilo((_, siloBuilder) => + siloBuilder.Configure(options => options.UseVirtualBucketsConsistentRing = false)); + builder.UseIdealizedReminderTable( + configureReminderOptions: options => options.ReminderLoadingWindow = LoadingWindow); + _clock = ReminderTestClock.Attach( + builder, + minimumReminderPeriod: TimeSpan.FromSeconds(1), + refreshReminderListPeriod: TimeSpan.FromSeconds(1)); + _cluster = builder.Build(); + await _cluster.DeployAsync(TestContext.Current.CancellationToken); + Harness = new ReminderServiceLifecycleHarness( + _cluster, + _clock, + _clock.DiagnosticObserver, + LoadingWindow); + } + catch (Exception initializationException) + { + try + { + await DisposeAsync(); + } + catch (Exception cleanupException) + { + initializationException.Data["ReminderServiceLifecycleFixture.CleanupException"] = cleanupException; + } + + throw; + } + } + + public async ValueTask DisposeAsync() + { + var cluster = _cluster; + var clock = _clock; + _cluster = null; + _clock = null; + + try + { + if (cluster is not null) + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + await cluster.StopAllSilosAsync(cancellation.Token); + } + } + finally + { + try + { + if (cluster is not null) + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + await cluster.DisposeAsync().AsTask().WaitAsync(cancellation.Token); + } + } + finally + { + clock?.Dispose(); + } + } + } +} + +[TestSuite("BVT")] +[TestProvider("None")] +[TestArea("Reminders")] +[TestCategory("BVT"), TestCategory("Reminders"), TestCategory("ReminderTestKit")] +[Collection(ReminderServiceLifecycleCollection.Name)] +public sealed class ReminderServiceLifecycleConformanceTests +{ + [Fact] + public Task ReminderService_StartupReadiness() + => RunAsync((runner, token) => runner.RunReminderService_StartupReadiness(token)); + + [Fact] + public Task ReminderService_RegistrationHasSingleOwner() + => RunAsync((runner, token) => runner.RunReminderService_RegistrationHasSingleOwner(token)); + + [Fact] + public Task ReminderService_UpdateDoesNotRestartLocalOwner() + => RunAsync((runner, token) => runner.RunReminderService_UpdateDoesNotRestartLocalOwner(token)); + + [Fact] + public Task ReminderService_RemovalReachesQuiescence() + => RunAsync((runner, token) => runner.RunReminderService_RemovalReachesQuiescence(token)); + + [Fact] + public Task ReminderService_ExactDueRecovery() + => RunAsync((runner, token) => runner.RunReminderService_ExactDueRecovery(token)); + + [Fact] + public Task ReminderService_StaleOwnerRegistrationReconciles() + => RunAsync((runner, token) => runner.RunReminderService_StaleOwnerRegistrationReconciles(token)); + + [Fact] + public Task ReminderService_OneSiloJoinLeaveTransfersOwnership() + => RunAsync((runner, token) => runner.RunReminderService_OneSiloJoinLeaveTransfersOwnership(token)); + + [Fact] + public Task ReminderService_CleanupIsIsolated() + => RunAsync((runner, token) => runner.RunReminderService_CleanupIsIsolated(token)); + + private static async Task RunAsync( + Func scenario) + { + var fixture = new ReminderServiceLifecycleFixture(); + await fixture.InitializeAsync(); + try + { + using var cancellation = CancellationTokenSource.CreateLinkedTokenSource( + TestContext.Current.CancellationToken); + cancellation.CancelAfter(TimeSpan.FromMinutes(2)); + await scenario( + new LifecycleRunner(fixture.Harness, "IdealizedReminderTable"), + cancellation.Token); + } + finally + { + await fixture.DisposeAsync(); + } + } + + private sealed class LifecycleRunner(IReminderServiceLifecycleHarness harness, string providerName) + : ReminderServiceLifecycleTestRunner(harness, providerName, seed: 42); +} + +[TestSuite("BVT")] +[TestProvider("None")] +[TestArea("Reminders")] +[TestCategory("BVT"), TestCategory("Reminders"), TestCategory("ReminderTestKit")] +[Collection(ReminderServiceLifecycleCollection.Name)] +public sealed class FaultyReminderServiceLifecycleTests +{ + [Fact] + public async Task PartiallyInitializedFixtureDisposesClock() + { + var clock = ReminderTestClock.Attach(new InProcessTestClusterBuilder(1)); + var fixture = new ReminderServiceLifecycleFixture(clock); + + await fixture.DisposeAsync(); + + await Assert.ThrowsAsync( + () => clock.AdvanceAsync(TimeSpan.Zero, TestContext.Current.CancellationToken)); + } + + [Fact] + public Task DuplicateOwnerImplementationIsRejected() + => RunFaultAsync( + harness => new LifecycleRunner(new DuplicateOwnerHarness(harness), "DuplicateOwner"), + async (runner, token) => + { + var exception = await Assert.ThrowsAsync( + () => runner.RunReminderService_RegistrationHasSingleOwner(token)); + Assert.Contains("exactly 1 local owner", exception.Message, StringComparison.Ordinal); + Assert.Contains("owners=[", exception.Message, StringComparison.Ordinal); + }); + + [Fact] + public Task RestartingUpdateImplementationIsRejected() + => RunFaultAsync( + harness => new LifecycleRunner(new RestartingUpdateHarness(harness), "RestartingUpdate"), + async (runner, token) => + { + var exception = await Assert.ThrowsAsync( + () => runner.RunReminderService_UpdateDoesNotRestartLocalOwner(token)); + Assert.Contains("same single owner", exception.Message, StringComparison.Ordinal); + Assert.Contains("starts=2", exception.Message, StringComparison.Ordinal); + }); + + [Fact] + public async Task CanceledScenarioPreservesCancellationAndStillCleansItsRows() + { + var fixture = new ReminderServiceLifecycleFixture(); + await fixture.InitializeAsync(); + try + { + var runner = new LifecycleRunner(new BlockingScheduleHarness(fixture.Harness), "CanceledScenario"); + using var cancellation = CancellationTokenSource.CreateLinkedTokenSource( + TestContext.Current.CancellationToken); + cancellation.CancelAfter(TimeSpan.FromMilliseconds(100)); + + await Assert.ThrowsAnyAsync( + () => runner.RunReminderService_RegistrationHasSingleOwner(cancellation.Token)); + + var table = Assert.IsType(fixture.Harness.ReminderTable); + Assert.Empty(table.Snapshot()); + } + finally + { + await fixture.DisposeAsync(); + } + } + + [Fact] + public Task UpdateWaitsForRefreshBeforeExpectingScheduleReconciliation() + { + RefreshGatedScheduleHarness? gatedHarness = null; + return RunFaultAsync( + harness => new LifecycleRunner( + gatedHarness = new RefreshGatedScheduleHarness(harness), + "RefreshGatedUpdate"), + async (runner, token) => + { + await runner.RunReminderService_UpdateDoesNotRestartLocalOwner(token); + Assert.NotNull(gatedHarness); + Assert.True(gatedHarness.ReleasedByRefresh); + }); + } + + [Fact] + public Task JoinLeaveAdvancesToTheExactRemainingDueTime() + { + RecordingAdvanceHarness? recordingHarness = null; + return RunFaultAsync( + harness => new LifecycleRunner( + recordingHarness = new RecordingAdvanceHarness(harness), + "ExactJoinLeaveDue"), + async (runner, token) => + { + await runner.RunReminderService_OneSiloJoinLeaveTransfersOwnership(token); + Assert.NotNull(recordingHarness); + Assert.Equal([TimeSpan.FromSeconds(3)], recordingHarness.Advances); + }); + } + + private static async Task RunFaultAsync( + Func createRunner, + Func scenario) + { + var fixture = new ReminderServiceLifecycleFixture(); + await fixture.InitializeAsync(); + try + { + using var cancellation = CancellationTokenSource.CreateLinkedTokenSource( + TestContext.Current.CancellationToken); + cancellation.CancelAfter(TimeSpan.FromMinutes(2)); + await scenario(createRunner(fixture.Harness), cancellation.Token); + } + finally + { + await fixture.DisposeAsync(); + } + } + + private sealed class LifecycleRunner(IReminderServiceLifecycleHarness harness, string providerName) + : ReminderServiceLifecycleTestRunner(harness, providerName); + + private class DelegatingHarness(IReminderServiceLifecycleHarness inner) : IReminderServiceLifecycleHarness + { + protected IReminderServiceLifecycleHarness Inner { get; } = inner; + + public IGrainFactory GrainFactory => Inner.GrainFactory; + public IReminderTable ReminderTable => Inner.ReminderTable; + public DateTimeOffset UtcNow => Inner.UtcNow; + public TimeSpan ReminderLoadingWindow => Inner.ReminderLoadingWindow; + public TimeSpan ReminderRefreshPeriod => Inner.ReminderRefreshPeriod; + public IReadOnlyList ActiveSilos => Inner.ActiveSilos; + public Task WaitForStartupReadinessAsync(CancellationToken cancellationToken) => Inner.WaitForStartupReadinessAsync(cancellationToken); + public virtual Task AdvanceAsync(TimeSpan amount, CancellationToken cancellationToken) => Inner.AdvanceAsync(amount, cancellationToken); + public virtual Task RefreshAsync(CancellationToken cancellationToken) => Inner.RefreshAsync(cancellationToken); + public virtual Task WaitForOwnerCountAsync(GrainId grainId, string reminderName, int count, CancellationToken cancellationToken) => Inner.WaitForOwnerCountAsync(grainId, reminderName, count, cancellationToken); + public virtual IReadOnlyList GetOwners(GrainId grainId, string reminderName) => Inner.GetOwners(grainId, reminderName); + public bool IsOwner(SiloAddress siloAddress, GrainId grainId) => Inner.IsOwner(siloAddress, grainId); + public virtual Task WaitForScheduleAsync(GrainId grainId, string reminderName, CancellationToken cancellationToken) => Inner.WaitForScheduleAsync(grainId, reminderName, cancellationToken); + public virtual int GetLocalStartCount(GrainId grainId, string reminderName) => Inner.GetLocalStartCount(grainId, reminderName); + public int GetLocalStopCount(GrainId grainId, string reminderName) => Inner.GetLocalStopCount(grainId, reminderName); + public int GetScheduleChangeCount(GrainId grainId, string reminderName) => Inner.GetScheduleChangeCount(grainId, reminderName); + public virtual Task WaitForScheduleChangeCountAsync(GrainId grainId, string reminderName, int count, CancellationToken cancellationToken) => Inner.WaitForScheduleChangeCountAsync(grainId, reminderName, count, cancellationToken); + public Task WaitForTickCountAsync(GrainId grainId, string reminderName, int count, CancellationToken cancellationToken) => Inner.WaitForTickCountAsync(grainId, reminderName, count, cancellationToken); + public int GetTickCount(GrainId grainId, string reminderName) => Inner.GetTickCount(grainId, reminderName); + public Task JoinOneSiloAsync(CancellationToken cancellationToken) => Inner.JoinOneSiloAsync(cancellationToken); + public Task LeaveSiloAsync(SiloAddress siloAddress, CancellationToken cancellationToken) => Inner.LeaveSiloAsync(siloAddress, cancellationToken); + public Task WaitForTopologyReconciliationAsync(CancellationToken cancellationToken) => Inner.WaitForTopologyReconciliationAsync(cancellationToken); + } + + private sealed class DuplicateOwnerHarness(IReminderServiceLifecycleHarness inner) : DelegatingHarness(inner) + { + private object? _duplicateIdentity; + + public override async Task WaitForOwnerCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken) + { + var harness = Assert.IsType(Inner); + if (count == 0 && _duplicateIdentity is { } identity) + { + harness.RemoveDuplicateOwnerForTesting(grainId, reminderName, identity); + _duplicateIdentity = null; + } + + await base.WaitForOwnerCountAsync(grainId, reminderName, count, cancellationToken); + if (count == 1 && _duplicateIdentity is null) + { + _duplicateIdentity = harness.AddDuplicateOwnerForTesting(grainId, reminderName); + } + } + } + + private sealed class RestartingUpdateHarness(IReminderServiceLifecycleHarness inner) : DelegatingHarness(inner) + { + private bool _updated; + + public override int GetLocalStartCount(GrainId grainId, string reminderName) + => base.GetLocalStartCount(grainId, reminderName) + (_updated ? 1 : 0); + + public override async Task WaitForScheduleChangeCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken) + { + await base.WaitForScheduleChangeCountAsync(grainId, reminderName, count, cancellationToken); + _updated = true; + } + } + + private sealed class BlockingScheduleHarness(IReminderServiceLifecycleHarness inner) : DelegatingHarness(inner) + { + public override Task WaitForScheduleAsync( + GrainId grainId, + string reminderName, + CancellationToken cancellationToken) + => Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + + private sealed class RefreshGatedScheduleHarness(IReminderServiceLifecycleHarness inner) : DelegatingHarness(inner) + { + private TaskCompletionSource? _refreshGate; + + public bool ReleasedByRefresh { get; private set; } + + public override async Task WaitForScheduleChangeCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken) + { + var innerWait = base.WaitForScheduleChangeCountAsync( + grainId, + reminderName, + count, + cancellationToken); + var refreshGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _refreshGate = refreshGate; + await Task.WhenAll(innerWait, refreshGate.Task.WaitAsync(cancellationToken)); + ReleasedByRefresh = true; + } + + public override async Task RefreshAsync(CancellationToken cancellationToken) + { + await base.RefreshAsync(cancellationToken); + Interlocked.Exchange(ref _refreshGate, null)?.TrySetResult(); + } + } + + private sealed class RecordingAdvanceHarness(IReminderServiceLifecycleHarness inner) : DelegatingHarness(inner) + { + public List Advances { get; } = []; + + public override async Task AdvanceAsync(TimeSpan amount, CancellationToken cancellationToken) + { + Advances.Add(amount); + await base.AdvanceAsync(amount, cancellationToken); + } + } +} diff --git a/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs b/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs index 432490ea4e..651f27ddac 100644 --- a/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs +++ b/test/Orleans.Reminders.Tests/Diagnostics/ReminderEventsTests.cs @@ -262,6 +262,47 @@ public async Task ReminderDiagnosticObserver_GlobalQuiescenceIgnoresUnrelatedSil unrelatedSilo); } + [TestSuite("BVT")] + [TestProvider("None")] + [Fact, TestCategory("BVT")] + public async Task ReminderDiagnosticObserver_CountsDuplicateInstancesOnOneSilo() + { + using var observer = ReminderDiagnosticObserver.Create(); + var grainId = GrainId.Create("test", "duplicate-owner"); + const string reminderName = "reminder"; + var siloAddress = SiloAddress.New(new IPEndPoint(IPAddress.Loopback, 14010), 11); + var firstIdentity = new object(); + var secondIdentity = new object(); + + var twoOwners = observer.WaitForActiveReminderCountAsync( + grainId, + 2, + TestContext.Current.CancellationToken, + reminderName, + [siloAddress]); + ReminderEvents.EmitLocalReminderStarted(grainId, reminderName, firstIdentity, siloAddress); + ReminderEvents.EmitLocalReminderStarted(grainId, reminderName, secondIdentity, siloAddress); + + await twoOwners; + Assert.Equal( + [siloAddress, siloAddress], + observer.GetActiveReminderOwnerSilos(grainId, reminderName)); + Assert.Single(observer.GetActiveReminderSilos(grainId, reminderName)); + + ReminderEvents.EmitLocalReminderStopped( + grainId, + reminderName, + firstIdentity, + ReminderEvents.LocalReminderStopReason.Unregistered, + siloAddress); + ReminderEvents.EmitLocalReminderStopped( + grainId, + reminderName, + secondIdentity, + ReminderEvents.LocalReminderStopReason.Unregistered, + siloAddress); + } + [TestSuite("BVT")] [TestProvider("None")] [Fact, TestCategory("BVT")] diff --git a/test/Orleans.Reminders.Tests/TimerTests/ReminderServiceLifecycleTestsBase.cs b/test/Orleans.Reminders.Tests/TimerTests/ReminderServiceLifecycleTestsBase.cs new file mode 100644 index 0000000000..e2ab1f2e20 --- /dev/null +++ b/test/Orleans.Reminders.Tests/TimerTests/ReminderServiceLifecycleTestsBase.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans.Configuration; +using Orleans.Reminders.TestKit; +using Orleans.Testing.Reminders; +using Orleans.TestingHost; +using TestExtensions; +using Xunit; + +namespace UnitTests.TimerTests; + +/// +/// Exposes the shared reminder-service lifecycle contract without inheriting provider whole-table cleanup. +/// +[Collection(TestEnvironmentFixture.DefaultCollection)] +public abstract class ReminderServiceLifecycleTestsBase +{ + private static readonly TimeSpan ScenarioTimeout = TimeSpan.FromMinutes(2); + private readonly ReminderServiceLifecycleTestRunner _runner; + + protected ReminderServiceLifecycleTestsBase( + ReminderTestClock reminderClock, + InProcessTestCluster cluster, + string providerName) + { + ArgumentNullException.ThrowIfNull(reminderClock); + ArgumentNullException.ThrowIfNull(cluster); + var services = cluster.GetActiveSilos().First().ServiceProvider; + var options = services.GetRequiredService>().Value; + var harness = new ReminderServiceLifecycleHarness( + cluster, + reminderClock, + reminderClock.DiagnosticObserver, + options.ReminderLoadingWindow); + _runner = new ProviderRunner(harness, providerName); + } + + [Fact] + public Task ReminderService_StartupReadiness() + => RunAsync(_runner.RunReminderService_StartupReadiness); + + [Fact] + public Task ReminderService_RegistrationHasSingleOwner() + => RunAsync(_runner.RunReminderService_RegistrationHasSingleOwner); + + [Fact] + public Task ReminderService_UpdateDoesNotRestartLocalOwner() + => RunAsync(_runner.RunReminderService_UpdateDoesNotRestartLocalOwner); + + [Fact] + public Task ReminderService_RemovalReachesQuiescence() + => RunAsync(_runner.RunReminderService_RemovalReachesQuiescence); + + [Fact] + public Task ReminderService_ExactDueRecovery() + => RunAsync(_runner.RunReminderService_ExactDueRecovery); + + [Fact] + public Task ReminderService_StaleOwnerRegistrationReconciles() + => RunAsync(_runner.RunReminderService_StaleOwnerRegistrationReconciles); + + [Fact] + public Task ReminderService_OneSiloJoinLeaveTransfersOwnership() + => RunAsync(_runner.RunReminderService_OneSiloJoinLeaveTransfersOwnership); + + [Fact] + public Task ReminderService_CleanupIsIsolated() + => RunAsync(_runner.RunReminderService_CleanupIsIsolated); + + private static async Task RunAsync(Func scenario) + { + using var cancellation = CancellationTokenSource.CreateLinkedTokenSource( + TestContext.Current.CancellationToken); + cancellation.CancelAfter(ScenarioTimeout); + await scenario(cancellation.Token); + } + + private sealed class ProviderRunner(IReminderServiceLifecycleHarness harness, string providerName) + : ReminderServiceLifecycleTestRunner(harness, providerName); +} diff --git a/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs b/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs index 99c3384b8d..da3ac658ee 100644 --- a/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs +++ b/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs @@ -20,6 +20,19 @@ namespace UnitTests.TimerTests { + [TestSuite("Functional")] + [TestProvider("None")] + [TestArea("Reminders")] + [TestCategory("Functional"), TestCategory("Reminders")] + public sealed class ReminderServiceLifecycleTests_TableGrain + : ReminderServiceLifecycleTestsBase, IClassFixture + { + public ReminderServiceLifecycleTests_TableGrain(ReminderTests_TableGrain.Fixture fixture) + : base(fixture.ReminderClock, fixture.HostedCluster, "InMemory") + { + } + } + /// /// Tests for grain-based reminder functionality using in-memory reminder service as table storage. /// diff --git a/test/Orleans.Testing.Reminders/Orleans.Testing.Reminders.csproj b/test/Orleans.Testing.Reminders/Orleans.Testing.Reminders.csproj index b9e72c34b0..e97e267810 100644 --- a/test/Orleans.Testing.Reminders/Orleans.Testing.Reminders.csproj +++ b/test/Orleans.Testing.Reminders/Orleans.Testing.Reminders.csproj @@ -15,7 +15,12 @@ + + + + + diff --git a/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs b/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs index 67f6aa2b92..eeee6ba5df 100644 --- a/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs +++ b/test/Orleans.Testing.Reminders/ReminderDiagnosticObserver.cs @@ -29,11 +29,15 @@ public sealed class ReminderDiagnosticObserver : IDisposable private readonly IDisposable _storageSubscription; private readonly Dictionary _tickCountsByGrain = []; private readonly Dictionary _tickCountsByReminder = []; + private readonly Dictionary _localStartCounts = []; + private readonly Dictionary _localStopCounts = []; + private readonly Dictionary _scheduleChangeCounts = []; private readonly Dictionary> _activeLocalReminders = []; private readonly List _tickCountWaiters = []; private readonly List _activeReminderCountWaiters = []; private readonly List _localReminderScheduleWaiters = []; private readonly List _globalQuiescenceWaiters = []; + private readonly List _scheduleChangeCountWaiters = []; /// /// Creates a new instance of the observer and starts listening for reminder diagnostic events. @@ -84,6 +88,7 @@ private void StoreEvent(ReminderEvents.ReminderEvent value) break; case ReminderEvents.LocalReminderStarted localReminderStarted: var startedKey = new ReminderTickKey(localReminderStarted.GrainId, localReminderStarted.ReminderName); + _localStartCounts[startedKey] = _localStartCounts.GetValueOrDefault(startedKey) + 1; if (!_activeLocalReminders.TryGetValue(startedKey, out var startedInstances)) { startedInstances = new Dictionary(); @@ -97,6 +102,7 @@ private void StoreEvent(ReminderEvents.ReminderEvent value) break; case ReminderEvents.LocalReminderStopped localReminderStopped: var stoppedKey = new ReminderTickKey(localReminderStopped.GrainId, localReminderStopped.ReminderName); + _localStopCounts[stoppedKey] = _localStopCounts.GetValueOrDefault(stoppedKey) + 1; if (_activeLocalReminders.TryGetValue(stoppedKey, out var stoppedInstances)) { stoppedInstances.Remove(new LocalReminderInstanceKey( @@ -114,6 +120,7 @@ private void StoreEvent(ReminderEvents.ReminderEvent value) break; case ReminderEvents.LocalReminderScheduleChanged localReminderScheduleChanged: var changedKey = new ReminderTickKey(localReminderScheduleChanged.GrainId, localReminderScheduleChanged.ReminderName); + _scheduleChangeCounts[changedKey] = _scheduleChangeCounts.GetValueOrDefault(changedKey) + 1; if (TryGetLocalReminderInstance(changedKey, localReminderScheduleChanged.Identity, out var changedInstance)) { changedInstance.ScheduleVersion = Math.Max( @@ -121,6 +128,7 @@ private void StoreEvent(ReminderEvents.ReminderEvent value) localReminderScheduleChanged.ScheduleVersion); } + ReleaseReadyScheduleChangeWaiters(ready); break; case ReminderEvents.LocalReminderTickWaitArmed localReminderTickWaitArmed: var tickWaitArmedKey = new ReminderTickKey(localReminderTickWaitArmed.GrainId, localReminderTickWaitArmed.ReminderName); @@ -245,6 +253,63 @@ public int GetTickCount(GrainId grainId, string? reminderName = null) } } + /// Gets the number of local reminder instances started for a reminder. + public int GetLocalStartCount(GrainId grainId, string reminderName) + { + lock (_lock) + { + return _localStartCounts.GetValueOrDefault(new ReminderTickKey(grainId, reminderName)); + } + } + + /// Gets the number of local reminder instances stopped for a reminder. + public int GetLocalStopCount(GrainId grainId, string reminderName) + { + lock (_lock) + { + return _localStopCounts.GetValueOrDefault(new ReminderTickKey(grainId, reminderName)); + } + } + + /// Gets the number of local schedule changes for a reminder. + public int GetScheduleChangeCount(GrainId grainId, string reminderName) + { + lock (_lock) + { + return _scheduleChangeCounts.GetValueOrDefault(new ReminderTickKey(grainId, reminderName)); + } + } + + /// Waits for the requested number of local schedule changes for a reminder. + public Task WaitForScheduleChangeCountAsync( + GrainId grainId, + string reminderName, + int expectedCount, + CancellationToken cancellationToken) + { + ArgumentOutOfRangeException.ThrowIfNegative(expectedCount); + ArgumentException.ThrowIfNullOrEmpty(reminderName); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + ScheduleChangeCountWaiter waiter; + lock (_lock) + { + if (_scheduleChangeCounts.GetValueOrDefault(new ReminderTickKey(grainId, reminderName)) >= expectedCount) + { + return Task.CompletedTask; + } + + waiter = new ScheduleChangeCountWaiter(grainId, reminderName, expectedCount); + _scheduleChangeCountWaiters.Add(waiter); + RegisterCancellation(waiter, _scheduleChangeCountWaiters, cancellationToken); + } + + return waiter.TaskSource.Task; + } + /// /// Gets the count of active local reminder owners for a specific reminder. /// @@ -273,6 +338,29 @@ public SiloAddress[] GetActiveReminderSilos(GrainId grainId, string reminderName } } + /// + /// Gets one silo address per active local reminder instance, preserving duplicate instances on the same silo. + /// + public SiloAddress[] GetActiveReminderOwnerSilos( + GrainId grainId, + string reminderName, + IEnumerable? activeSilos = null) + { + ArgumentException.ThrowIfNullOrEmpty(reminderName); + var activeSiloSet = activeSilos?.ToHashSet(); + + lock (_lock) + { + return _activeLocalReminders.TryGetValue(new ReminderTickKey(grainId, reminderName), out var instances) + ? instances.Values + .Select(instance => instance.SiloAddress) + .OfType() + .Where(siloAddress => activeSiloSet is null || activeSiloSet.Contains(siloAddress)) + .ToArray() + : []; + } + } + /// /// Waits for a specific number of active local reminder owners for a reminder. /// @@ -280,7 +368,28 @@ public Task WaitForActiveReminderCountAsync(GrainId grainId, int expectedCount, { ArgumentOutOfRangeException.ThrowIfNegative(expectedCount); ArgumentException.ThrowIfNullOrEmpty(reminderName); - return WaitForActiveReminderCountCoreAsync(grainId, expectedCount, reminderName, cancellationToken); + return WaitForActiveReminderCountCoreAsync(grainId, expectedCount, reminderName, activeSilos: null, cancellationToken); + } + + /// + /// Waits for a specific number of active local reminder instances on . + /// + public Task WaitForActiveReminderCountAsync( + GrainId grainId, + int expectedCount, + CancellationToken cancellationToken, + string reminderName, + IEnumerable activeSilos) + { + ArgumentOutOfRangeException.ThrowIfNegative(expectedCount); + ArgumentException.ThrowIfNullOrEmpty(reminderName); + ArgumentNullException.ThrowIfNull(activeSilos); + return WaitForActiveReminderCountCoreAsync( + grainId, + expectedCount, + reminderName, + activeSilos.ToHashSet(), + cancellationToken); } /// @@ -298,7 +407,7 @@ public Task WaitForLocalReminderScheduleAsync(GrainId grainId, string reminderNa public Task WaitForReminderQuiescenceAsync(GrainId grainId, string reminderName, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(reminderName); - return WaitForActiveReminderCountCoreAsync(grainId, 0, reminderName, cancellationToken); + return WaitForActiveReminderCountCoreAsync(grainId, 0, reminderName, activeSilos: null, cancellationToken); } /// @@ -423,7 +532,12 @@ private Task WaitForTickCountCoreAsync(GrainId grainId, int targetCount, string? return waiter.TaskSource.Task; } - private Task WaitForActiveReminderCountCoreAsync(GrainId grainId, int targetCount, string reminderName, CancellationToken cancellationToken) + private Task WaitForActiveReminderCountCoreAsync( + GrainId grainId, + int targetCount, + string reminderName, + HashSet? activeSilos, + CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -433,12 +547,12 @@ private Task WaitForActiveReminderCountCoreAsync(GrainId grainId, int targetCoun ActiveReminderCountWaiter? waiter; lock (_lock) { - if (GetActiveReminderCountCore(grainId, reminderName) == targetCount) + if (GetActiveReminderCountCore(grainId, reminderName, activeSilos) == targetCount) { return Task.CompletedTask; } - waiter = new ActiveReminderCountWaiter(grainId, reminderName, targetCount); + waiter = new ActiveReminderCountWaiter(grainId, reminderName, targetCount, activeSilos); _activeReminderCountWaiters.Add(waiter); RegisterCancellation(waiter, _activeReminderCountWaiters, cancellationToken); } @@ -501,10 +615,15 @@ private int GetTickCountCore(GrainId grainId, string? reminderName) return _tickCountsByReminder.GetValueOrDefault(new ReminderTickKey(grainId, reminderName)); } - private int GetActiveReminderCountCore(GrainId grainId, string reminderName) + private int GetActiveReminderCountCore( + GrainId grainId, + string reminderName, + HashSet? activeSilos = null) { return _activeLocalReminders.TryGetValue(new ReminderTickKey(grainId, reminderName), out var instances) - ? instances.Count + ? instances.Values.Count(instance => + instance.SiloAddress is { } siloAddress + && (activeSilos is null || activeSilos.Contains(siloAddress))) : 0; } @@ -562,7 +681,7 @@ private void ReleaseReadyActiveReminderWaiters(List ready) for (var i = _activeReminderCountWaiters.Count - 1; i >= 0; i--) { var waiter = _activeReminderCountWaiters[i]; - if (GetActiveReminderCountCore(waiter.GrainId, waiter.ReminderName) != waiter.TargetCount) + if (GetActiveReminderCountCore(waiter.GrainId, waiter.ReminderName, waiter.ActiveSilos) != waiter.TargetCount) { continue; } @@ -609,6 +728,23 @@ private bool IsGloballyQuiescentCore(IReadOnlySet siloAddresses) .Any(instance => instance.SiloAddress is { } address && siloAddresses.Contains(address)); } + private void ReleaseReadyScheduleChangeWaiters(List ready) + { + for (var i = _scheduleChangeCountWaiters.Count - 1; i >= 0; i--) + { + var waiter = _scheduleChangeCountWaiters[i]; + var count = _scheduleChangeCounts.GetValueOrDefault( + new ReminderTickKey(waiter.GrainId, waiter.ReminderName)); + if (count < waiter.TargetCount) + { + continue; + } + + _scheduleChangeCountWaiters.RemoveAt(i); + ready.Add(waiter); + } + } + private readonly record struct ReminderTickKey(GrainId GrainId, string ReminderName); private readonly record struct LocalReminderInstanceKey(object Identity) { @@ -645,11 +781,16 @@ private sealed class TickCountWaiter(GrainId grainId, string? reminderName, int public int TargetCount { get; } = targetCount; } - private sealed class ActiveReminderCountWaiter(GrainId grainId, string reminderName, int targetCount) : Waiter + private sealed class ActiveReminderCountWaiter( + GrainId grainId, + string reminderName, + int targetCount, + HashSet? activeSilos) : Waiter { public GrainId GrainId { get; } = grainId; public string ReminderName { get; } = reminderName; public int TargetCount { get; } = targetCount; + public HashSet? ActiveSilos { get; } = activeSilos; } private sealed class LocalReminderScheduleWaiter(GrainId grainId, string reminderName) : Waiter @@ -663,6 +804,16 @@ private sealed class GlobalQuiescenceWaiter(IReadOnlySet siloAddres public IReadOnlySet SiloAddresses { get; } = siloAddresses.ToHashSet(); } + private sealed class ScheduleChangeCountWaiter( + GrainId grainId, + string reminderName, + int targetCount) : Waiter + { + public GrainId GrainId { get; } = grainId; + public string ReminderName { get; } = reminderName; + public int TargetCount { get; } = targetCount; + } + /// public void Dispose() { diff --git a/test/Orleans.Testing.Reminders/ReminderServiceLifecycleHarness.cs b/test/Orleans.Testing.Reminders/ReminderServiceLifecycleHarness.cs new file mode 100644 index 0000000000..6b05fcf3fa --- /dev/null +++ b/test/Orleans.Testing.Reminders/ReminderServiceLifecycleHarness.cs @@ -0,0 +1,208 @@ +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Orleans.Reminders.Diagnostics; +using Orleans.Reminders.TestKit; +using Orleans.Runtime; +using Orleans.Runtime.ConsistentRing; +using Orleans.Runtime.ReminderService; +using Orleans.TestingHost; + +namespace Orleans.Testing.Reminders; + +/// +/// Adapts an in-process cluster, , and +/// to the shared service lifecycle conformance runner. +/// +public sealed class ReminderServiceLifecycleHarness : IReminderServiceLifecycleHarness +{ + private readonly InProcessTestCluster _cluster; + private readonly ReminderTestClock _clock; + private readonly ReminderDiagnosticObserver _observer; + + /// Initializes the harness. + public ReminderServiceLifecycleHarness( + InProcessTestCluster cluster, + ReminderTestClock clock, + ReminderDiagnosticObserver observer, + TimeSpan reminderLoadingWindow) + { + _cluster = cluster ?? throw new ArgumentNullException(nameof(cluster)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _observer = observer ?? throw new ArgumentNullException(nameof(observer)); + ReminderLoadingWindow = reminderLoadingWindow; + } + + /// + public IGrainFactory GrainFactory => _cluster.Client; + + /// + public IReminderTable ReminderTable + => _cluster.GetActiveSilos().First().ServiceProvider.GetRequiredService(); + + /// + public DateTimeOffset UtcNow => _clock.UtcNow; + + /// + public TimeSpan ReminderLoadingWindow { get; } + + /// + public TimeSpan ReminderRefreshPeriod => _clock.RefreshReminderListPeriod; + + /// + public IReadOnlyList ActiveSilos + => _cluster.GetActiveSilos().Select(silo => silo.SiloAddress).Order().ToArray(); + + /// + public async Task WaitForStartupReadinessAsync(CancellationToken cancellationToken) + { + var startupEvents = ActiveSilos + .Select(silo => _observer.WaitForReminderServiceStartedAsync(cancellationToken, silo)) + .ToArray(); + await Task.WhenAll(startupEvents); + await WaitForTopologyReconciliationAsync(cancellationToken); + } + + /// + public Task AdvanceAsync(TimeSpan amount, CancellationToken cancellationToken) + => _clock.AdvanceAsync(amount, cancellationToken); + + /// + public async Task RefreshAsync(CancellationToken cancellationToken) + { + foreach (var silo in _cluster.GetActiveSilos()) + { + await silo.ServiceProvider.GetRequiredService() + .TestOnlyRefresh() + .WaitAsync(cancellationToken); + } + } + + /// + public Task WaitForOwnerCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken) + => _observer.WaitForActiveReminderCountAsync( + grainId, + count, + cancellationToken, + reminderName, + ActiveSilos); + + /// + public IReadOnlyList GetOwners(GrainId grainId, string reminderName) + { + return _observer.GetActiveReminderOwnerSilos(grainId, reminderName, ActiveSilos); + } + + /// + public bool IsOwner(SiloAddress siloAddress, GrainId grainId) + { + var silo = _cluster.GetSiloForAddress(siloAddress) + ?? throw new InvalidOperationException($"Silo {siloAddress} is not active."); + return silo.ServiceProvider + .GetRequiredService() + .GetMyRange() + .InRange(grainId); + } + + /// + public Task WaitForScheduleAsync(GrainId grainId, string reminderName, CancellationToken cancellationToken) + => _observer.WaitForLocalReminderScheduleAsync(grainId, reminderName, cancellationToken); + + /// + public int GetLocalStartCount(GrainId grainId, string reminderName) + => _observer.GetLocalStartCount(grainId, reminderName); + + /// + public int GetLocalStopCount(GrainId grainId, string reminderName) + => _observer.GetLocalStopCount(grainId, reminderName); + + /// + public int GetScheduleChangeCount(GrainId grainId, string reminderName) + => _observer.GetScheduleChangeCount(grainId, reminderName); + + /// + public Task WaitForScheduleChangeCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken) + => _observer.WaitForScheduleChangeCountAsync(grainId, reminderName, count, cancellationToken); + + /// + public Task WaitForTickCountAsync( + GrainId grainId, + string reminderName, + int count, + CancellationToken cancellationToken) + => _observer.WaitForTickCountAsync(grainId, count, cancellationToken, reminderName); + + /// + public int GetTickCount(GrainId grainId, string reminderName) + => _observer.GetTickCount(grainId, reminderName); + + /// + public async Task JoinOneSiloAsync(CancellationToken cancellationToken) + { + var silo = AssertSingle(await _cluster.StartSilosAsync(1).WaitAsync(cancellationToken)); + await Task.WhenAll( + _observer.WaitForReminderServiceStartedAsync(cancellationToken, silo.SiloAddress), + _cluster.WaitForLivenessToStabilizeAsync().WaitAsync(cancellationToken), + _cluster.WaitForClusterManifestToStabilizeAsync().WaitAsync(cancellationToken)); + return silo.SiloAddress; + } + + /// + public async Task LeaveSiloAsync(SiloAddress siloAddress, CancellationToken cancellationToken) + { + var silo = _cluster.GetSiloForAddress(siloAddress) + ?? throw new InvalidOperationException($"Silo {siloAddress} is not active."); + await _cluster.StopSiloAsync(silo, cancellationToken); + await Task.WhenAll( + _cluster.WaitForLivenessToStabilizeAsync().WaitAsync(cancellationToken), + _cluster.WaitForClusterManifestToStabilizeAsync().WaitAsync(cancellationToken)); + } + + /// + public async Task WaitForTopologyReconciliationAsync(CancellationToken cancellationToken) + { + await Task.WhenAll( + _cluster.WaitForLivenessToStabilizeAsync().WaitAsync(cancellationToken), + _cluster.WaitForClusterManifestToStabilizeAsync().WaitAsync(cancellationToken)); + var barriers = _cluster.GetActiveSilos().Select(silo => + silo.ServiceProvider.GetRequiredService() + .TestOnlyWaitForRangeChangeReconciliation(cancellationToken)); + await Task.WhenAll(barriers); + await RefreshAsync(cancellationToken); + } + + private static InProcessSiloHandle AssertSingle(IReadOnlyList silos) + => silos.Count == 1 + ? silos[0] + : throw new InvalidOperationException($"Expected one new silo, observed {silos.Count}."); + + internal object AddDuplicateOwnerForTesting(GrainId grainId, string reminderName) + { + var identity = new object(); + ReminderEvents.EmitLocalReminderStarted( + grainId, + reminderName, + identity, + ActiveSilos.First()); + return identity; + } + + internal void RemoveDuplicateOwnerForTesting( + GrainId grainId, + string reminderName, + object identity) + => ReminderEvents.EmitLocalReminderStopped( + grainId, + reminderName, + identity, + ReminderEvents.LocalReminderStopReason.Unregistered, + ActiveSilos.First()); +} diff --git a/test/Orleans.Testing.Reminders/ReminderTestClock.cs b/test/Orleans.Testing.Reminders/ReminderTestClock.cs index 5e8734f350..72c7c8e4a1 100644 --- a/test/Orleans.Testing.Reminders/ReminderTestClock.cs +++ b/test/Orleans.Testing.Reminders/ReminderTestClock.cs @@ -28,6 +28,7 @@ private ReminderTestClock( TimeSpan initializationTimeout) { TimeProvider = new FakeTimeProvider(initialTime); + DiagnosticObserver = ReminderDiagnosticObserver.Create(); MinimumReminderPeriod = minimumReminderPeriod; RefreshReminderListPeriod = refreshReminderListPeriod; InitializationTimeout = initializationTimeout; @@ -35,6 +36,12 @@ private ReminderTestClock( internal FakeTimeProvider TimeProvider { get; } + /// + /// Gets the diagnostic observer attached before the cluster is built, so reminder-service startup events are + /// available to deterministic lifecycle tests. + /// + public ReminderDiagnosticObserver DiagnosticObserver { get; } + /// /// Gets the current reminder clock time. /// @@ -146,6 +153,7 @@ public void Dispose() } _disposed = true; + DiagnosticObserver.Dispose(); _advanceLock.Dispose(); } From 5da028b34ea77206ae16ad64bbff0edcbd19abb8 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:49:00 -0700 Subject: [PATCH 03/10] fix(test): keep observer cancellation targets alive (#10897) --- .../ObserverCancellationTokenTests.cs | 169 ++++++++++-------- 1 file changed, 96 insertions(+), 73 deletions(-) diff --git a/test/Orleans.Runtime.Tests/CancellationTests/ObserverCancellationTokenTests.cs b/test/Orleans.Runtime.Tests/CancellationTests/ObserverCancellationTokenTests.cs index bd30920a3f..ebd8c85416 100644 --- a/test/Orleans.Runtime.Tests/CancellationTests/ObserverCancellationTokenTests.cs +++ b/test/Orleans.Runtime.Tests/CancellationTests/ObserverCancellationTokenTests.cs @@ -82,30 +82,36 @@ public async Task ObserverTaskCancellation(bool cancelImmediately) var grain = fixture.GrainFactory.GetGrain(Guid.NewGuid()); var observer = new LongRunningObserver(); var reference = fixture.GrainFactory.CreateObjectReference(observer); - await grain.Subscribe(reference); + try + { + await grain.Subscribe(reference); - using var cts = new CancellationTokenSource(); - var callId = Guid.NewGuid(); - var grainTask = grain.NotifyLongWait(TimeSpan.FromSeconds(10), callId, cts.Token); + using var cts = new CancellationTokenSource(); + var callId = Guid.NewGuid(); + var grainTask = grain.NotifyLongWait(TimeSpan.FromSeconds(10), callId, cts.Token); - if (cancelImmediately) - { - await cts.CancelAsync(); - } - else - { - await observer.WaitForCallToStart(callId); - await cts.CancelAsync(); - } + if (cancelImmediately) + { + await cts.CancelAsync(); + } + else + { + await observer.WaitForCallToStart(callId); + await cts.CancelAsync(); + } - await Assert.ThrowsAnyAsync(() => grainTask); - if (!cancelImmediately) + await Assert.ThrowsAnyAsync(() => grainTask); + if (!cancelImmediately) + { + await observer.WaitForCancellation(callId); + } + } + finally { - await observer.WaitForCancellation(callId); + await grain.Unsubscribe(reference); + fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } - - await grain.Unsubscribe(reference); - fixture.GrainFactory.DeleteObjectReference(reference); } /// @@ -128,6 +134,7 @@ public async Task PreCancelledTokenPassing() await grain.Unsubscribe(reference); fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } /// @@ -153,6 +160,7 @@ public async Task TokenPassingWithoutCancellation_NoExceptionShouldBeThrown() await grain.Unsubscribe(reference); fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } /// @@ -186,6 +194,7 @@ public async Task CancellationTokenCallbacksExecutionContext() await observer.WaitForCancellation(callId); await grain.Unsubscribe(reference); fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } /// @@ -199,50 +208,55 @@ public async Task MultipleObserversCancellation(bool cancelImmediately) // Create multiple grains each with an observer using var cts = new CancellationTokenSource(); var grains = new List<(IObserverWithCancellationGrain Grain, LongRunningObserver Observer, ILongRunningObserver Reference, Guid CallId)>(); - - for (int i = 0; i < 5; i++) + try { - var grain = fixture.GrainFactory.GetGrain(Guid.NewGuid()); - var observer = new LongRunningObserver(); - var reference = fixture.GrainFactory.CreateObjectReference(observer); - await grain.Subscribe(reference); - grains.Add((grain, observer, reference, Guid.NewGuid())); - } - - var notifyTasks = grains - .Select(g => g.Grain.NotifyLongWait(TimeSpan.FromSeconds(10), g.CallId, cts.Token)) - .ToList(); + for (int i = 0; i < 5; i++) + { + var grain = fixture.GrainFactory.GetGrain(Guid.NewGuid()); + var observer = new LongRunningObserver(); + var reference = fixture.GrainFactory.CreateObjectReference(observer); + grains.Add((grain, observer, reference, Guid.NewGuid())); + await grain.Subscribe(reference); + } - if (cancelImmediately) - { - await cts.CancelAsync(); - } - else - { - await Task.WhenAll(grains.Select(g => g.Observer.WaitForCallToStart(g.CallId))); - await cts.CancelAsync(); - } + var notifyTasks = grains + .Select(g => g.Grain.NotifyLongWait(TimeSpan.FromSeconds(10), g.CallId, cts.Token)) + .ToList(); - foreach (var task in notifyTasks) - { - await Assert.ThrowsAnyAsync(() => task); - } + if (cancelImmediately) + { + await cts.CancelAsync(); + } + else + { + await Task.WhenAll(grains.Select(g => g.Observer.WaitForCallToStart(g.CallId))); + await cts.CancelAsync(); + } - if (!cancelImmediately) - { - for (int i = 0; i < grains.Count; i++) + foreach (var task in notifyTasks) { - await grains[i].Observer.WaitForCancellation(grains[i].CallId); + await Assert.ThrowsAnyAsync(() => task); } - } - foreach (var g in grains) - { - await g.Grain.Unsubscribe(g.Reference); + if (!cancelImmediately) + { + for (int i = 0; i < grains.Count; i++) + { + await grains[i].Observer.WaitForCancellation(grains[i].CallId); + } + } } - foreach (var g in grains) + finally { - fixture.GrainFactory.DeleteObjectReference(g.Reference); + foreach (var g in grains) + { + await g.Grain.Unsubscribe(g.Reference); + } + foreach (var g in grains) + { + fixture.GrainFactory.DeleteObjectReference(g.Reference); + GC.KeepAlive(g.Observer); + } } } @@ -283,6 +297,7 @@ public async Task CancelWaitingRequest() await grain.Unsubscribe(reference); fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } /// @@ -297,30 +312,36 @@ public async Task InterleavingObserverTaskCancellation(bool cancelImmediately) var grain = fixture.GrainFactory.GetGrain(Guid.NewGuid()); var observer = new LongRunningObserver(); var reference = fixture.GrainFactory.CreateObjectReference(observer); - await grain.Subscribe(reference); + try + { + await grain.Subscribe(reference); - using var cts = new CancellationTokenSource(); - var callId = Guid.NewGuid(); - var grainTask = grain.NotifyInterleavingLongWait(TimeSpan.FromSeconds(10), callId, cts.Token); + using var cts = new CancellationTokenSource(); + var callId = Guid.NewGuid(); + var grainTask = grain.NotifyInterleavingLongWait(TimeSpan.FromSeconds(10), callId, cts.Token); - if (cancelImmediately) - { - await cts.CancelAsync(); - } - else - { - await observer.WaitForCallToStart(callId); - await cts.CancelAsync(); - } + if (cancelImmediately) + { + await cts.CancelAsync(); + } + else + { + await observer.WaitForCallToStart(callId); + await cts.CancelAsync(); + } - await Assert.ThrowsAnyAsync(() => grainTask); - if (!cancelImmediately) + await Assert.ThrowsAnyAsync(() => grainTask); + if (!cancelImmediately) + { + await observer.WaitForCancellation(callId); + } + } + finally { - await observer.WaitForCancellation(callId); + await grain.Unsubscribe(reference); + fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } - - await grain.Unsubscribe(reference); - fixture.GrainFactory.DeleteObjectReference(reference); } /// @@ -370,6 +391,7 @@ await Task.WhenAll( await grain.Unsubscribe(reference); fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } /// @@ -411,6 +433,7 @@ public async Task CancelInterleavingWhileRegularRequestRunning() await grain.Unsubscribe(reference); fixture.GrainFactory.DeleteObjectReference(reference); + GC.KeepAlive(observer); } /// From 3483396871e99396eefd7f9b5335e431814a0c60 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:49:19 -0700 Subject: [PATCH 04/10] fix(test): retry SQL Server deadlock victims (#10901) --- .../SqlServerStorageForTesting.cs | 14 ++++++++++++-- .../SqlServerStorageForTestingTests.cs | 9 +++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTesting.cs b/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTesting.cs index be03661f8e..b9f4757a22 100644 --- a/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTesting.cs +++ b/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTesting.cs @@ -7,6 +7,9 @@ namespace UnitTests.General { public class SqlServerStorageForTesting : RelationalStorageForTesting { + private const int DeadlockVictimError = 1205; + private const int DatabaseInUseError = 3702; + protected override string ProviderMoniker => "SQLServer"; public SqlServerStorageForTesting(string connectionString) @@ -180,14 +183,21 @@ protected override async Task DropDatabaseAsync(string databaseName, Cancellatio await base.DropDatabaseAsync(databaseName, cancellationToken); return; } - catch (SqlException exception) when (exception.Number == 3702 && attempt < maxAttempts) + catch (SqlException exception) when (IsRetryableDatabaseResetError(exception.Number) && attempt < maxAttempts) { - Console.WriteLine("SQL Server database '{0}' remained in use after reset attempt {1}; retrying.", databaseName, attempt); + Console.WriteLine( + "SQL Server database '{0}' reset failed with transient error {1} on attempt {2}; retrying.", + databaseName, + exception.Number, + attempt); PrepareForDatabaseReset(databaseName); } } } + internal static bool IsRetryableDatabaseResetError(int errorNumber) => + errorNumber is DeadlockVictimError or DatabaseInUseError; + protected override string ExistsDatabaseTemplate { get diff --git a/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTestingTests.cs b/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTestingTests.cs index 4e08fbfb29..97e8811e46 100644 --- a/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTestingTests.cs +++ b/test/Extensions/Orleans.AdoNet.Tests/RelationalUtilities/SqlServerStorageForTestingTests.cs @@ -11,6 +11,15 @@ public class SqlServerStorageForTestingTests { private const string TestDatabaseName = "OrleansSqlServerSetupTest"; + [Theory] + [InlineData(1205, true)] + [InlineData(3702, true)] + [InlineData(18456, false)] + public void ClassifiesRetryableDatabaseResetErrors(int errorNumber, bool expected) + { + Assert.Equal(expected, SqlServerStorageForTesting.IsRetryableDatabaseResetError(errorNumber)); + } + [Fact] public async Task RecreatesDatabaseWithActivePooledConnection() { From c6a5040a4b21a13b3f29a11e04c5f7ed6f832e79 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:49:47 -0700 Subject: [PATCH 05/10] test(reminders): enable provider failover coverage (#10895) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Orleans.TestingHost/InProcTestCluster.cs | 43 ++++ .../Orleans.TestingHost.csproj | 1 + .../Reminder/ReminderTests_AzureTable.cs | 25 +- .../ReminderTests_Cosmos.cs | 59 +++-- .../TimerTests/ReminderTestsBase.cs | 221 +++++++++++++++++- .../TimerTests/ReminderTests_TableGrain.cs | 60 +++++ 6 files changed, 355 insertions(+), 54 deletions(-) diff --git a/src/Orleans.TestingHost/InProcTestCluster.cs b/src/Orleans.TestingHost/InProcTestCluster.cs index dc6144793f..37b13162ba 100644 --- a/src/Orleans.TestingHost/InProcTestCluster.cs +++ b/src/Orleans.TestingHost/InProcTestCluster.cs @@ -410,6 +410,49 @@ public async Task WaitForClusterManifestToStabilizeAsync(bool didKill = false) } } + internal async Task WaitForTopologyToConvergeAsync( + CancellationToken cancellationToken, + bool didKill = false) + { + var clusterMembershipOptions = Client!.ServiceProvider + .GetRequiredService>().Value; + var timeout = GetLivenessStabilizationTime(clusterMembershipOptions, didKill); + var activeSilos = GetActiveSilos().ToArray(); + var expectedSilos = string.Join(", ", activeSilos.Select(static silo => silo.SiloAddress)); + var testHooks = activeSilos + .Select(static silo => (ITestHooks)silo.ServiceProvider.GetRequiredService()) + .ToArray(); + var gatewayManager = Client.ServiceProvider.GetRequiredService(); + if (!GrainDirectoryObserver.CanObserve(activeSilos)) + { + throw new InvalidOperationException( + $"The grain directory cannot report convergence for the expected topology: {expectedSilos}."); + } + + var topologyConverged = await LivenessStabilizationHelper + .WaitForExpectedActiveSilosAndGatewaysAsync( + activeSilos, + testHooks, + gatewayManager, + timeout, + remaining => _grainDirectoryObserver.WaitForConvergenceAsync(activeSilos, remaining)) + .WaitAsync(cancellationToken); + if (!topologyConverged) + { + throw new TimeoutException( + $"Membership, gateway, and grain-directory views did not converge within {timeout}. Expected active silos: {expectedSilos}."); + } + + var manifestConverged = await ClusterManifestStabilizationHelper + .WaitForExpectedClusterManifestAsync(activeSilos, testHooks, timeout) + .WaitAsync(cancellationToken); + if (!manifestConverged) + { + throw new TimeoutException( + $"Cluster manifests did not converge within {timeout}. Expected active silos: {expectedSilos}."); + } + } + /// /// Get the timeout value to use to wait for the silo liveness sub-system to detect and act on any recent cluster membership changes. /// diff --git a/src/Orleans.TestingHost/Orleans.TestingHost.csproj b/src/Orleans.TestingHost/Orleans.TestingHost.csproj index 71a0f347d4..f9d7c2f4c4 100644 --- a/src/Orleans.TestingHost/Orleans.TestingHost.csproj +++ b/src/Orleans.TestingHost/Orleans.TestingHost.csproj @@ -26,6 +26,7 @@ + diff --git a/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs b/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs index 29e17e0d4f..9a621664d1 100644 --- a/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs +++ b/test/Extensions/Orleans.Azure.Tests/Reminder/ReminderTests_AzureTable.cs @@ -353,31 +353,10 @@ public async Task Rem_Azure_GT_Basic() await StopReminderAndWaitForQuiescenceAsync(g2, DR, g2.StopReminder, cts.Token); } - [Fact(Skip = "https://github.com/dotnet/orleans/issues/4319"), TestCategory("Functional")] + [Fact, TestCategory("Functional")] public async Task Rem_Azure_GT_1F1J_MultiGrain() { - using var cts = CancellationTokenSource.CreateLinkedTokenSource( - TestContext.Current.CancellationToken); - cts.CancelAfter(ENDWAIT); - _ = await this.StartAdditionalSilosAndWaitForReminderServicesAsync(1, cts.Token); - - IReminderTestGrain2 g1 = this.GrainFactory.GetGrain(Guid.NewGuid()); - IReminderTestGrain2 g2 = this.GrainFactory.GetGrain(Guid.NewGuid()); - IReminderTestCopyGrain g3 = this.GrainFactory.GetGrain(Guid.NewGuid()); - IReminderTestCopyGrain g4 = this.GrainFactory.GetGrain(Guid.NewGuid()); - - IAddressable[] grains = [g1, g2, g3, g4]; - await PrepareForGrainFailureAsync(cts.Token, grains); - - var siloToKill = this.GetReminderOwner(g1, DR); - // stop a silo and join a new one in parallel - await using (await PauseReminderTimeAsync(cts.Token)) - { - log.LogInformation("Stopping a silo and joining a silo"); - await this.StopSiloAndStartAdditionalSiloAsync(siloToKill, cts.Token); - } - - await CompleteGrainFailureTestAsync(cts.Token, grains); + await Test_Reminders_GT_1F1J_MultiGrain(TestContext.Current.CancellationToken); } [Fact, TestCategory("Functional")] diff --git a/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs b/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs index 630958f363..530a36a993 100644 --- a/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs +++ b/test/Extensions/Orleans.Cosmos.Tests/ReminderTests_Cosmos.cs @@ -35,7 +35,9 @@ public class ReminderTests_Cosmos : ReminderTestsBase, IClassFixture _startupObserver.WaitForReminderServiceStartedAsync(cancellation.Token, silo.SiloAddress)) + .ToArray(); + + try + { + await Task.WhenAll(startedTasks); + } + catch (OperationCanceledException) when ( + cancellation.IsCancellationRequested + && !TestContext.Current.CancellationToken.IsCancellationRequested) + { + var missing = silos + .Where((_, index) => !startedTasks[index].IsCompletedSuccessfully) + .Select(silo => silo.SiloAddress); + throw new TimeoutException( + $"Cosmos reminder services did not start within {ReminderServiceStartupTimeout}. Missing silos: {string.Join(", ", missing)}."); + } + } + public override async ValueTask DisposeAsync() { try @@ -68,6 +102,7 @@ public override async ValueTask DisposeAsync() finally { _reminderClock?.Dispose(); + _startupObserver.Dispose(); } } } @@ -330,30 +365,10 @@ public async Task Rem_Azure_GT_Basic() } [TestSuite("Functional")] - [Fact(Skip = "https://github.com/dotnet/orleans/issues/4319"), TestCategory("Functional")] + [Fact, TestCategory("Functional")] public async Task Rem_Azure_GT_1F1J_MultiGrain() { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); - cts.CancelAfter(ENDWAIT); - _ = await StartAdditionalSilosAndWaitForReminderServicesAsync(1, cts.Token); - - IReminderTestGrain2 g1 = GrainFactory.GetGrain(Guid.NewGuid()); - IReminderTestGrain2 g2 = GrainFactory.GetGrain(Guid.NewGuid()); - IReminderTestCopyGrain g3 = GrainFactory.GetGrain(Guid.NewGuid()); - IReminderTestCopyGrain g4 = GrainFactory.GetGrain(Guid.NewGuid()); - - IAddressable[] grains = [g1, g2, g3, g4]; - await PrepareForGrainFailureAsync(cts.Token, grains); - - var siloToKill = GetReminderOwner(g1, DR); - // stop a silo and join a new one in parallel - await using (await PauseReminderTimeAsync(cts.Token)) - { - log.LogInformation("Stopping a silo and joining a silo"); - await StopSiloAndStartAdditionalSiloAsync(siloToKill, cts.Token); - } - - await CompleteGrainFailureTestAsync(cts.Token, grains); + await Test_Reminders_GT_1F1J_MultiGrain(TestContext.Current.CancellationToken); } [TestSuite("Functional")] diff --git a/test/Orleans.Reminders.Tests/TimerTests/ReminderTestsBase.cs b/test/Orleans.Reminders.Tests/TimerTests/ReminderTestsBase.cs index aaf5d4fe18..6a53f60f02 100644 --- a/test/Orleans.Reminders.Tests/TimerTests/ReminderTestsBase.cs +++ b/test/Orleans.Reminders.Tests/TimerTests/ReminderTestsBase.cs @@ -257,7 +257,7 @@ await Test_Reminders_MultiGrainMultiReminders( } finally { - await CleanupAdditionalSilosAsync(initialSilos, startSilosTask, startupCancellation); + await CleanupAdditionalSilosAsync(initialSilos, startupCancellation, startSilosTask); } } @@ -300,7 +300,7 @@ await Test_Reminders_MultiGrainMultiReminders( } finally { - await CleanupAdditionalSilosAsync(initialSilos, startSilosTask, startupCancellation); + await CleanupAdditionalSilosAsync(initialSilos, startupCancellation, startSilosTask); } } @@ -349,6 +349,69 @@ public async Task Test_Reminders_UpdateReminder_DoesNotRestartLocalReminder(Canc Assert.Equal(0, observer.GetActiveReminderCount(grainId, DR)); } + public async Task Test_Reminders_GT_1F1J_MultiGrain(CancellationToken cancellationToken) + { + var initialSilos = HostedCluster.GetActiveSilos().ToHashSet(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(CHURN_ENDWAIT); + using var startupCancellation = CancellationTokenSource.CreateLinkedTokenSource(cts.Token); + Task>? setupJoinTask = null; + Task>? failoverJoinTask = null; + + try + { + setupJoinTask = StartAdditionalSilosAsync(1, startupCancellation.Token); + var failedSilo = Assert.Single( + await WaitForAdditionalSilosAndReminderServicesAsync(setupJoinTask, cts.Token)); + + var g1 = await GetGrainOwnedBySiloAsync(failedSilo, cts.Token); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var g3 = GrainFactory.GetGrain(Guid.NewGuid()); + var g4 = GrainFactory.GetGrain(Guid.NewGuid()); + IAddressable[] grains = [g1, g2, g3, g4]; + var reminders = GetReminderIdentities(grains, DR); + + await PrepareForGrainFailureAsync(cts.Token, grains); + await AssertReminderOwnershipAndSchedulesAsync(reminders, failAfter, cts.Token); + Assert.Equal(failedSilo.SiloAddress, GetReminderOwner(g1, DR).SiloAddress); + + await using (await PauseReminderTimeAsync(cts.Token)) + { + log.LogInformation( + "Stopping reminder owner {SiloAddress} while joining a replacement silo", + failedSilo.SiloAddress); + var stopTask = StopSiloAsync(failedSilo); + failoverJoinTask = StartAdditionalSilosAsync(1, startupCancellation.Token); + await Task.WhenAll(stopTask, failoverJoinTask).WaitAsync(cts.Token); + _ = Assert.Single( + await WaitForReminderServicesStartedAsync(failoverJoinTask, cts.Token)); + await InvokeGrainCallsAfterTopologyConvergenceAsync( + cts.Token, + [.. grains.Select(grain => new Func(async () => + { + _ = await GetReminderPeriodAsync(grain, DR).WaitAsync(cts.Token); + }))]); + await AssertReminderOwnershipAndSchedulesAsync(reminders, failAfter, cts.Token); + await AssertReminderCountersAsync(grains, cts.Token, (DR, failAfter)); + Assert.DoesNotContain( + failedSilo.SiloAddress, + reminders.SelectMany(reminder => + observer.GetActiveReminderSilos(reminder.Grain.GetGrainId(), reminder.ReminderName))); + } + + await CompleteGrainFailureTestWithoutReachabilityRetriesAsync(cts.Token, grains); + AssertRemindersStopped(reminders, failCheckAfter); + } + finally + { + await CleanupAdditionalSilosAsync( + initialSilos, + startupCancellation, + setupJoinTask, + failoverJoinTask); + } + } + protected Task> StartAdditionalSilosAsync( int silosToStart, CancellationToken cancellationToken, @@ -377,10 +440,18 @@ protected async Task> StartAdditionalSilosAndWaitForRe private async Task> WaitForAdditionalSilosAndReminderServicesAsync( Task> startSilosTask, CancellationToken cancellationToken) + { + var result = await WaitForReminderServicesStartedAsync(startSilosTask, cancellationToken); + await WaitForTopologyConvergenceAsync(cancellationToken); + return result; + } + + private async Task> WaitForReminderServicesStartedAsync( + Task> startSilosTask, + CancellationToken cancellationToken) { var result = await startSilosTask.WaitAsync(cancellationToken); - await observer.WaitForTopologyReconciledAsync( - WaitForLivenessToStabilizeAsync(), + await observer.WaitForServicesReadyAsync( result, CHURN_ENDWAIT, cancellationToken); @@ -389,10 +460,10 @@ await observer.WaitForTopologyReconciledAsync( private async Task CleanupAdditionalSilosAsync( HashSet initialSilos, - Task>? startSilosTask, - CancellationTokenSource startupCancellation) + CancellationTokenSource startupCancellation, + params Task>?[] startSilosTasks) { - if (startSilosTask is null) + if (startSilosTasks.All(static task => task is null)) { return; } @@ -400,13 +471,22 @@ private async Task CleanupAdditionalSilosAsync( using var cleanupCts = new CancellationTokenSource(CHURN_ENDWAIT); await ReminderLifecycleHarness.CleanupPartialStartupAsync( initialSilos, - startSilosTask, + Task.WhenAll(startSilosTasks.OfType>>()), () => HostedCluster.GetActiveSilos().ToArray(), StopSiloAsync, - () => WaitForLivenessToStabilizeAsync(didKill: true), + () => observer.WaitForTopologyReconciledAsync( + WaitForLivenessToStabilizeAsync(didKill: true), + [], + CHURN_ENDWAIT, + cleanupCts.Token), log, startupCancellation, cleanupCts.Token); + + var activeSilos = HostedCluster.GetActiveSilos().ToHashSet(); + Assert.True( + initialSilos.SetEquals(activeSilos), + $"Reminder test did not restore its baseline topology. Expected: {string.Join(", ", initialSilos.Select(static silo => silo.SiloAddress))}; actual: {string.Join(", ", activeSilos.Select(static silo => silo.SiloAddress))}."); } protected async Task StopSiloAndStartAdditionalSiloAsync( @@ -523,6 +603,13 @@ protected async Task CompleteGrainFailureTestAsync(CancellationToken cancellatio Assert.NotEmpty(grains); await WaitForGrainsReachableAsync(cancellationToken, grains); + await CompleteGrainFailureTestWithoutReachabilityRetriesAsync(cancellationToken, grains); + } + + private async Task CompleteGrainFailureTestWithoutReachabilityRetriesAsync( + CancellationToken cancellationToken, + params IAddressable[] grains) + { await AdvanceRemindersByTicksAsync((int)(failCheckAfter - failAfter), cancellationToken, GetReminderIdentities(grains, DR)); await AssertReminderCountersAsync(grains, cancellationToken, (DR, failCheckAfter)); @@ -533,6 +620,122 @@ await GetReminderPeriodAsync(grains[0], DR).WaitAsync(cancellationToken), await AssertReminderCountersAsync(grains, cancellationToken, (DR, failCheckAfter)); } + private async Task AssertReminderOwnershipAndSchedulesAsync( + (IAddressable Grain, string ReminderName)[] reminders, + long expectedTickCount, + CancellationToken cancellationToken) + { + await SynchronizeReminderSchedulesAsync(cancellationToken, reminders); + var activeSilos = HostedCluster.GetActiveSilos(); + + foreach (var reminder in reminders) + { + var grainId = reminder.Grain.GetGrainId(); + var actualOwner = Assert.Single(observer.GetActiveReminderSilos(grainId, reminder.ReminderName)); + + Assert.Equal(1, observer.GetActiveReminderCount(grainId, reminder.ReminderName)); + Assert.Contains(activeSilos, silo => silo.SiloAddress == actualOwner); + Assert.Equal(expectedTickCount, (long)observer.GetTickCount(grainId, reminder.ReminderName)); + } + } + + private void AssertRemindersStopped( + (IAddressable Grain, string ReminderName)[] reminders, + long expectedTickCount) + { + foreach (var reminder in reminders) + { + var grainId = reminder.Grain.GetGrainId(); + Assert.Equal(0, observer.GetActiveReminderCount(grainId, reminder.ReminderName)); + Assert.Empty(observer.GetActiveReminderSilos(grainId, reminder.ReminderName)); + Assert.Equal(expectedTickCount, (long)observer.GetTickCount(grainId, reminder.ReminderName)); + } + } + + private async Task GetGrainOwnedBySiloAsync( + InProcessSiloHandle owner, + CancellationToken cancellationToken) + where TGrainInterface : IGrainWithGuidKey + { + const int maximumAttempts = 1_000; + + // Select a key in the joined silo's range so the test always removes a real reminder owner + // while leaving every baseline silo available for subsequent shared-fixture tests. + for (var attempt = 0; attempt < maximumAttempts; attempt++) + { + var grain = GrainFactory.GetGrain(Guid.NewGuid()); + var reminderStarted = false; + var selected = false; + Exception? selectionException = null; + try + { + await StartReminderAsync(grain, DR).WaitAsync(cancellationToken); + reminderStarted = true; + await SynchronizeReminderSchedulesAsync(cancellationToken, (grain, DR)); + if (GetReminderOwner(grain, DR).SiloAddress == owner.SiloAddress) + { + selected = true; + return grain; + } + } + catch (Exception exception) + { + selectionException = exception; + throw; + } + finally + { + if (reminderStarted && !selected) + { + using var cleanupCts = new CancellationTokenSource(ENDWAIT); + try + { + await StopRemindersAsync([grain], DR, cleanupCts.Token); + } + catch (Exception cleanupException) when (selectionException is not null) + { + log.LogWarning( + cleanupException, + "Failed to clean up reminder candidate {GrainId} after selection failed", + grain.GetGrainId()); + } + } + } + } + + throw new InvalidOperationException( + $"Could not select a {typeof(TGrainInterface).Name} reminder grain owned by {owner.SiloAddress} after {maximumAttempts} attempts."); + } + + protected async Task InvokeGrainCallsAfterTopologyConvergenceAsync( + CancellationToken cancellationToken, + params Func[] grainCalls) + => await InvokeGrainCallsAfterTopologyConvergenceAsync( + WaitForTopologyConvergenceAsync, + cancellationToken, + grainCalls); + + protected static async Task InvokeGrainCallsAfterTopologyConvergenceAsync( + Func topologyConvergence, + CancellationToken cancellationToken, + params Func[] grainCalls) + { + await topologyConvergence(cancellationToken); + + var callTasks = grainCalls.Select(static grainCall => grainCall()).ToArray(); + await Task.WhenAll(callTasks).WaitAsync(cancellationToken); + } + + private async Task WaitForTopologyConvergenceAsync(CancellationToken cancellationToken) + { + await HostedCluster.WaitForTopologyToConvergeAsync(cancellationToken); + await observer.WaitForTopologyReconciledAsync( + Task.CompletedTask, + [], + CHURN_ENDWAIT, + cancellationToken); + } + private async Task WaitForGrainsReachableAsync(CancellationToken cancellationToken, params IAddressable[] grains) { Exception? lastException = null; diff --git a/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs b/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs index da3ac658ee..4f4a883ef3 100644 --- a/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs +++ b/test/Orleans.Reminders.Tests/TimerTests/ReminderTests_TableGrain.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Orleans.Internal; using Orleans.Reminders; +using Orleans.Runtime; using Orleans.Runtime.ReminderService; using Orleans.Testing.Reminders; using Orleans.TestingHost; @@ -219,6 +220,65 @@ await Assert.ThrowsAsync( await StopReminderAndWaitForQuiescenceAsync(grain, DR, grain.StopReminder, cancellation.Token); } + [Fact] + public async Task Rem_Grain_GT_1F1J_MultiGrain() + { + await Test_Reminders_GT_1F1J_MultiGrain(TestContext.Current.CancellationToken); + } + + [Fact] + public async Task Rem_Grain_PostTopologyConvergence_DoesNotSuppressMessageRejection() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + cts.CancelAfter(TestConstants.InitTimeout); + var rejection = (OrleansMessageRejectionException)Activator.CreateInstance( + typeof(OrleansMessageRejectionException), + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, + binder: null, + args: ["Controlled post-convergence rejection."], + culture: null)!; + var callCounts = new int[4]; + + var actual = await Assert.ThrowsAsync(() => + InvokeGrainCallsAfterTopologyConvergenceAsync( + cts.Token, + CreateCall(0), + CreateCall(1), + CreateCall(2, rejection), + CreateCall(3))); + + Assert.Same(rejection, actual); + Assert.All(callCounts, count => Assert.Equal(1, count)); + + Func CreateCall(int index, Exception? exception = null) => () => + { + callCounts[index]++; + return exception is null ? Task.CompletedTask : Task.FromException(exception); + }; + } + + [Fact] + public async Task Rem_Grain_TopologyConvergenceTimeout_PreventsGrainCalls() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + cts.CancelAfter(TestConstants.InitTimeout); + var timeout = new TimeoutException("Controlled topology convergence timeout."); + var callCount = 0; + + var actual = await Assert.ThrowsAsync(() => + InvokeGrainCallsAfterTopologyConvergenceAsync( + _ => Task.FromException(timeout), + cts.Token, + () => + { + callCount++; + return Task.CompletedTask; + })); + + Assert.Same(timeout, actual); + Assert.Equal(0, callCount); + } + [Fact] public async Task Rem_Grain_CanRestartBeforeRemovedReminderIsPurged() { From 29acb791b86785965f73928b65a1295b1c0f477b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:28:58 -0700 Subject: [PATCH 06/10] feat(analyzers): regenerate Orleans contracts project-wide (#10869) --- .../src/content/docs/diagnostics/index.md | 4 + .../content/docs/diagnostics/orleans0016.md | 4 +- .../content/docs/diagnostics/orleans0017.md | 4 +- .../content/docs/diagnostics/orleans0018.md | 8 +- .../content/docs/diagnostics/orleans0019.md | 4 +- .../content/docs/diagnostics/orleans0020.md | 6 +- .../content/docs/diagnostics/orleans0021.md | 6 +- .../content/docs/diagnostics/orleans0022.md | 4 +- .../content/docs/diagnostics/orleans0023.md | 4 +- .../content/docs/diagnostics/orleans0024.md | 4 +- .../content/docs/diagnostics/orleans0025.md | 6 +- .../content/docs/diagnostics/orleans0027.md | 37 + .../contract-compatibility-analyzer.md | 74 +- docs/site/src/content/docs/toc.yml | 2 + .../Orleans.Dashboard/OrleansContracts.txt | 48 +- src/Directory.Build.targets | 1 + .../AnalyzerReleases.Unshipped.md | 1 + .../GrainInterfaceVersionAnalyzer.cs | 533 +++++- .../GrainInterfaceVersionCodeFix.cs | 815 +++++++++- .../Orleans.Analyzers.csproj | 5 + src/Orleans.Analyzers/Resources.Designer.cs | 18 + src/Orleans.Analyzers/Resources.resx | 25 +- .../build/Microsoft.Orleans.Analyzers.props | 1 + .../OrleansContracts.txt | 18 +- .../MethodIdProvider.cs | 62 + .../GeneratedCodeUtilities.cs | 59 +- .../Orleans.CodeGenerator.csproj | 1 + .../OrleansContracts.txt | 36 +- src/Orleans.Core/OrleansContracts.txt | 163 +- src/Orleans.DurableJobs/OrleansContracts.txt | 18 +- .../OrleansContracts.txt | 24 +- .../OrleansContracts.txt | 12 +- src/Orleans.Reminders/OrleansContracts.txt | 51 +- src/Orleans.Runtime/OrleansContracts.txt | 93 +- src/Orleans.Streaming/OrleansContracts.txt | 99 +- src/Orleans.TestingHost/OrleansContracts.txt | 30 +- .../OrleansContracts.txt | 132 +- src/Orleans.Transactions/OrleansContracts.txt | 36 +- .../GrainInterfaceVersionAnalyzerTest.cs | 1442 +++++++++++++++-- 39 files changed, 3239 insertions(+), 651 deletions(-) create mode 100644 docs/site/src/content/docs/diagnostics/orleans0027.md create mode 100644 src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs diff --git a/docs/site/src/content/docs/diagnostics/index.md b/docs/site/src/content/docs/diagnostics/index.md index b6ddabca00..f26a81062a 100644 --- a/docs/site/src/content/docs/diagnostics/index.md +++ b/docs/site/src/content/docs/diagnostics/index.md @@ -110,3 +110,7 @@ Analyzer help links use `https://aka.ms/orleans/diagnostics` with the diagnostic ## ORLEANS0026 [Invalid invokable base type mapping](orleans0026.md) — Error. A custom grain-call return type mapping cannot generate a valid invokable request. + +## ORLEANS0027 + +[Grain interface member removed from source](orleans0027.md) — Warning. The manifest retains an RPC signature which is absent from source. diff --git a/docs/site/src/content/docs/diagnostics/orleans0016.md b/docs/site/src/content/docs/diagnostics/orleans0016.md index 08bc298b16..2f2c75fdeb 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0016.md +++ b/docs/site/src/content/docs/diagnostics/orleans0016.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0016: Grain interface is not active in OrleansContracts.txt" description: Understand and resolve ORLEANS0016 when a grain interface is missing or retired in the contract manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -27,6 +27,8 @@ Verify the interface identity and version, then apply **Add to OrleansContracts. If the interface was restored accidentally, remove it from source or introduce a separately named replacement instead of reusing a retired identity. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Deployable RPC contracts should remain in the manifest. If the project intentionally does not maintain a contract manifest, disable the contract analyzer for the project instead of suppressing individual interfaces. diff --git a/docs/site/src/content/docs/diagnostics/orleans0017.md b/docs/site/src/content/docs/diagnostics/orleans0017.md index a96a5e70af..983451d8b0 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0017.md +++ b/docs/site/src/content/docs/diagnostics/orleans0017.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0017: Grain interface version mismatch" description: Understand and resolve ORLEANS0017 when a grain interface version differs from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The manifest no longer describes the numeric version used by runtime compatibili Determine whether the source or manifest changed unintentionally. Restore the previous source version, or review the rolling-upgrade implications and apply **Update version in OrleansContracts.txt** when the new version is intentional. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Use suppression only during a short-lived staged edit. Do not release with a source and manifest version mismatch. diff --git a/docs/site/src/content/docs/diagnostics/orleans0018.md b/docs/site/src/content/docs/diagnostics/orleans0018.md index 312f137789..0ff2be4170 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0018.md +++ b/docs/site/src/content/docs/diagnostics/orleans0018.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0018: Grain interface member not declared" description: Understand and resolve ORLEANS0018 when an RPC method signature is missing from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -17,13 +17,17 @@ ms.topic: reference An ordinary grain-interface method has no matching contract signature in `OrleansContracts.txt`. Method identity, generic arity, parameter types and order, and return type are part of the signature. Parameter names are not. +The method identity is the source `[Id]` value, the source `[Alias]` value, or the generated xxHash32 ID used by the Orleans code generator. Recording a generated ID in the manifest does not add an attribute or change the runtime identity. + ## Impact Older activations can receive an unknown RPC, and changed identities or payload types can cause dispatch or serialization failures during a rolling upgrade. ## How to fix -Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the new signature but does not increment `[Version]`. +Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the existing effective wire identity and does not increment `[Version]` or modify source attributes. + +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0019.md b/docs/site/src/content/docs/diagnostics/orleans0019.md index dd6b3a83cc..f258d8b0b3 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0019.md +++ b/docs/site/src/content/docs/diagnostics/orleans0019.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0019: Removed grain interface is not retired" description: Understand and resolve ORLEANS0019 when OrleansContracts.txt contains an active interface that source no longer defines. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The deletion or identity-changing rename is not recorded as intentional, and the Restore the interface if its removal was accidental. Otherwise apply **Mark as *RETIRED* in OrleansContracts.txt**. Preserve retired declarations as contract history. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest and retire every declaration absent from source, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppression is appropriate only when the manifest intentionally contains contracts owned by another compilation. Prefer one manifest per project so ownership remains explicit. diff --git a/docs/site/src/content/docs/diagnostics/orleans0020.md b/docs/site/src/content/docs/diagnostics/orleans0020.md index 7f181a14e7..aa4568d27f 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0020.md +++ b/docs/site/src/content/docs/diagnostics/orleans0020.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0020: OrleansContracts.txt is missing" description: Understand and resolve ORLEANS0020 when contract compatibility analysis is enabled without a manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -11,7 +11,7 @@ ms.topic: reference | --- | --- | | Category | Orleans.Versioning | | Severity | Info | -| Code fix | Not available | +| Code fix | Available | ## Cause @@ -23,7 +23,7 @@ The analyzer has no baseline, so it cannot detect RPC identity, signature, versi ## How to fix -Create `OrleansContracts.txt` at `OrleansContractsPath`, add it to source control, and rebuild. Apply the resulting diagnostics' code fixes to populate interface, method, and class declarations. +Apply **Regenerate OrleansContracts.txt** to create and populate the complete project manifest. Use **Fix all in solution** to create manifests for every affected project, then add the generated files to source control and review the baseline using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0021.md b/docs/site/src/content/docs/diagnostics/orleans0021.md index 193859716a..a8dd3d0d39 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0021.md +++ b/docs/site/src/content/docs/diagnostics/orleans0021.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0021: Duplicate grain interface declaration" description: Understand and resolve ORLEANS0021 when OrleansContracts.txt declares an interface identity more than once. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` repeats an interface CLR name or a non-empty `GrainInterfaceType`, including active and retired declarations with the same identity. +`OrleansContracts.txt` repeats an effective interface identity. The effective identity is `GrainInterfaceType` when present and the identity derived from the recorded CLR name using Orleans conventions for a legacy declaration. ## Impact @@ -23,7 +23,7 @@ The manifest is ambiguous. The parser retains the first declaration, so compatib ## How to fix -Merge the declarations into one canonical entry. Keep one active declaration when the interface exists, or one retired declaration when it has been removed. +Merge declarations which have the same effective identity into one canonical entry. Active and retired declarations can share a CLR name when they record different explicit `GrainInterfaceType` values across an identity migration. ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0022.md b/docs/site/src/content/docs/diagnostics/orleans0022.md index 389b0af22d..e23e1fa69b 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0022.md +++ b/docs/site/src/content/docs/diagnostics/orleans0022.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0022: Grain class is not active in OrleansContracts.txt" description: Understand and resolve ORLEANS0022 when a concrete grain class is missing or retired in the contract manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The implementation identity is not protected by contract review. A CLR rename wi Verify the class's durable grain type, add `[GrainType]` when it must remain independent of the CLR name, and apply **Add to OrleansContracts.txt**. The code fix adds or reactivates the class declaration. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a grain class intentionally excluded from deployment-contract tracking. diff --git a/docs/site/src/content/docs/diagnostics/orleans0023.md b/docs/site/src/content/docs/diagnostics/orleans0023.md index 4b16080040..3fffe564cf 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0023.md +++ b/docs/site/src/content/docs/diagnostics/orleans0023.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0023: Grain class identity mismatch" description: Understand and resolve ORLEANS0023 when a grain class GrainType differs from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -27,6 +27,8 @@ Restore the previous `[GrainType]` when the change was accidental. Update the ma The **Update grain class alias in OrleansContracts.txt** code fix accepts the source identity as the new baseline. Review the identity change before applying it. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a deliberate, documented identity migration. Updating the reviewed baseline is preferable to retaining a suppression. diff --git a/docs/site/src/content/docs/diagnostics/orleans0024.md b/docs/site/src/content/docs/diagnostics/orleans0024.md index 45f3403e94..ecccb23223 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0024.md +++ b/docs/site/src/content/docs/diagnostics/orleans0024.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0024: Removed grain class is not retired" description: Understand and resolve ORLEANS0024 when OrleansContracts.txt contains an active grain class that source no longer defines. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The removal or identity-changing rename is not recorded, and the old grain ident Restore the class if its removal was accidental. Otherwise apply **Mark grain class as *RETIRED* in OrleansContracts.txt** and preserve the declaration. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest and retire every declaration absent from source, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only when the manifest intentionally includes classes owned by another compilation. Prefer separate project manifests. diff --git a/docs/site/src/content/docs/diagnostics/orleans0025.md b/docs/site/src/content/docs/diagnostics/orleans0025.md index 642cc3de57..23fe3dd8ec 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0025.md +++ b/docs/site/src/content/docs/diagnostics/orleans0025.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0025: Duplicate grain class declaration" description: Understand and resolve ORLEANS0025 when OrleansContracts.txt declares a grain identity more than once. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` repeats a grain class CLR name or a non-empty `GrainType`, including active and retired declarations with the same identity. +`OrleansContracts.txt` repeats an effective grain class identity. The effective identity is `GrainType` when present and the identity derived from the recorded CLR name using Orleans conventions for a legacy declaration. ## Impact @@ -23,7 +23,7 @@ The grain identity history becomes ambiguous, and the parser accepts only the fi ## How to fix -Merge or remove duplicates, retaining one canonical active or retired declaration. +Merge declarations which have the same effective identity into one canonical entry. Active and retired declarations can share a CLR name when they record different explicit `GrainType` values across an identity migration. ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0027.md b/docs/site/src/content/docs/diagnostics/orleans0027.md new file mode 100644 index 0000000000..94c1a4e877 --- /dev/null +++ b/docs/site/src/content/docs/diagnostics/orleans0027.md @@ -0,0 +1,37 @@ +--- +title: "ORLEANS0027: Grain interface member removed from source" +description: Understand and resolve ORLEANS0027 when OrleansContracts.txt retains an RPC method which is absent from source. +ms.date: 08/27/2026 +ms.topic: reference +--- + +# ORLEANS0027: Grain interface member removed from source + +| Property | Value | +| --- | --- | +| Category | Orleans.Versioning | +| Severity | Warning | +| Code fix | Not available | + +## Cause + +`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. The manifest identity is an explicit `[Id]` or `[Alias]` value when present in source; otherwise, it is the generated method ID already used by Orleans on the wire. + +## Impact + +Removing an RPC method can break calls from older clients or activations during a rolling upgrade. Regeneration retains the historical signature so the wire-contract removal remains visible and requires an explicit decision. + +## How to fix + +Restore the source method when the removal was accidental. When the removal is intentional, review the mixed-version deployment impact, increment the interface version when appropriate, and explicitly remove the retained signature from `OrleansContracts.txt`. + +See [Orleans contract compatibility analyzer](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + +## Suppress the diagnostic + +Prefer updating the reviewed manifest after accepting the contract removal. Suppress only for a manifest intentionally shared with another compilation. + +```ini +[*] +dotnet_diagnostic.ORLEANS0027.severity = none +``` diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index af2da8618d..ba74ed48b0 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -1,7 +1,7 @@ --- title: Orleans contract compatibility analyzer description: Track grain RPC contracts during development to identify changes which can break rolling upgrades. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: concept-article --- @@ -36,40 +36,69 @@ The path can also be set in `Directory.Build.props` to apply a repository conven ## Create and update the manifest -After opting in, build the project. If no manifest exists, diagnostic `ORLEANS0020` identifies the missing file. Create the file, include it in source control, and apply the Orleans code fixes to add missing interface and class entries. Apply the code fixes again after adding RPC methods. +After opting in, build the project. If no manifest exists, diagnostic `ORLEANS0020` identifies the missing file at the first contract declaration. Apply **Regenerate OrleansContracts.txt** to create and populate the manifest for the project. -Code fixes add the generated-file header, preserve the file's line endings, and write entries in stable ordinal order. The resulting file is deterministic regardless of the order in which fixes are applied. +The regeneration code fix rebuilds every active interface, method, and grain-class entry from the project compilation. It preserves existing `*RETIRED*` entries and marks declarations which are no longer in source as retired. The generated header, line endings, and ordinal entry order are deterministic. ### Regenerate the manifest -Use the analyzer code fixes to regenerate the active contracts: +Apply **Regenerate OrleansContracts.txt** from `ORLEANS0016`, `ORLEANS0017`, `ORLEANS0018`, `ORLEANS0019`, `ORLEANS0020`, `ORLEANS0022`, `ORLEANS0023`, or `ORLEANS0024`. One application regenerates the entire project manifest. In an IDE, use **Fix all in project** or **Fix all in solution** to regenerate every affected project. -1. Preserve every `*RETIRED*` declaration from the existing manifest. Retired identities are historical data and cannot be reconstructed from current source. -2. Create an empty `OrleansContracts.txt`, then restore the retired declarations. -3. Build the project. -4. Apply each `ORLEANS0016` and `ORLEANS0022` code fix to add active interfaces and grain classes. -5. Build again and apply each `ORLEANS0018` code fix to add interface methods. -6. Review the resulting identity, version, and signature diff before committing it. +`ORLEANS0027` intentionally retains a removed method signature, so regeneration isn't offered for that diagnostic. If it is the only remaining diagnostic, restore the source method or explicitly delete the retained signature after reviewing and accepting the wire-compatibility break. -For routine contract changes, keep the existing manifest and apply the reported update or retirement code fix instead of rebuilding it from scratch. +Agents and command-line workflows can regenerate manifests without an IDE: + +```dotnetcli +dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +``` + +Run the command from the repository root. Replace `PATH_TO_PROJECT_OR_SOLUTION` with the path to the owning `.csproj` to regenerate one manifest, or a `.sln`/`.slnx` path to regenerate manifests in every affected project. The `--severity info` option includes `ORLEANS0020`, allowing the command to create a missing manifest. + +Regeneration edits `OrleansContracts.txt` files only. It does not add or change `[Alias]`, `[Id]`, `[GrainType]`, or `[GrainInterfaceType]` attributes in source. Attribute-like syntax in the manifest records the effective identity which Orleans already uses at runtime. + +For a method without `[Id]` or `[Alias]`, the manifest records the same generated xxHash32 method ID which the Orleans code generator already uses on the wire. The preceding comment records the CLR signature so reviewers can map the wire ID back to source. A one-time upgrade from an older manifest format can therefore replace a CLR method name with its existing generated ID; this records the current wire contract and does not change it. + +After the command completes: + +1. Inspect `git diff -- "*OrleansContracts.txt"` and account for every changed identity, version, and method signature. +2. Preserve all `*RETIRED*` declarations and retained removed-method signatures unless the compatibility break is intentional. +3. Run `dotnet build PATH_TO_PROJECT_OR_SOLUTION` and resolve all Orleans contract diagnostics. `ORLEANS0027` remains until a removed method is restored or its retained signature is explicitly deleted after compatibility review. + +Add the generated file to source control and review its diff before committing. Treat every changed contract line as a potential wire-compatibility change: + +- A changed `GrainInterfaceType`, `GrainType`, method identity, parameter type, or return type changes a wire identity or signature. +- A removed source contract becomes `*RETIRED*`, preserving its identity history and preventing accidental reuse. +- A removed RPC method remains in the manifest and reports `ORLEANS0027` until the method is restored or the wire break is explicitly accepted by removing the retained signature. +- A `[Version]` change affects version-aware routing and must align with the rolling-upgrade design. +- A CLR comment-only change records a refactor while the explicit Orleans identity remains stable. + +Coding agents should regenerate the manifest instead of hand-editing active entries, retain retired history, and explain the compatibility impact of each contract diff in the change description. ## Manifest format Interface methods are indented beneath their interface: ```text -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Contoso.Grains.ICartGrain")] Contoso.Grains.ICartGrain [Version(1)] - AddAsync(Contoso.Grains.Item) -> Task - GetAsync() -> Task + # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task + 15793847(Contoso.Grains.Item) -> Task + # Contoso.Grains.ICartGrain.GetAsync() -> Task + 857AC6B2() -> Task class [GrainType("cart")] Contoso.Grains.CartGrain ``` -Each declaration includes both its Orleans identity and CLR type name. A diff which changes both values is a breaking identity change. A diff which changes only the CLR type name preserves the Orleans identity. +Each declaration includes both its Orleans identity and CLR type name. A diff which changes the Orleans identity changes the wire contract. A diff which changes only the CLR type name preserves an explicit Orleans identity. Explicit identities remain visible alongside their CLR names: @@ -77,13 +106,13 @@ Explicit identities remain visible alongside their CLR names: # Contoso.Grains.ICartGrain interface [GrainInterfaceType("cart")] Contoso.Grains.ICartGrain [Version(1)] # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task - add(Contoso.Grains.Item) -> Task + [Alias("add")] add(Contoso.Grains.Item) -> Task # Contoso.Grains.CartGrain class [GrainType("cart")] Contoso.Grains.CartGrain ``` -Comments record CLR names only when they differ from the stable identity. Comments are informational and aren't part of contract matching. +`[Alias("...")]` on a manifest method records that the identity comes from a source `[Alias]` attribute. An unmarked eight-digit hexadecimal method identity is the generated wire ID. Comments record CLR names when they improve traceability; comments are informational and aren't part of contract matching. `*RETIRED*` marks an intentionally removed contract: @@ -93,7 +122,7 @@ Comments record CLR names only when they differ from the stable identity. Commen *RETIRED* class [GrainType("legacy")] Contoso.Grains.LegacyGrain ``` -Don't delete retired entries. They preserve the contract history and prevent a removed identity from being unintentionally reused. +Retired entries preserve contract history and prevent a removed identity from being unintentionally reused. ## Refactor-safe identities @@ -104,7 +133,7 @@ The analyzer uses Orleans identities before CLR names: - or identifies grain methods. - identifies serialized parameter and return types. -When these identities remain unchanged, renaming a CLR class, interface, method, parameter, or aliased data type doesn't require a manifest update. Changing an Orleans identity remains a contract change and produces a diagnostic. +When these identities remain unchanged, a CLR class, interface, method, parameter, or aliased data type can be renamed while preserving the wire identity. Changing an Orleans identity produces a contract diff and diagnostic. Without an explicit stable identity, Orleans derives the identity from the CLR type name. Renaming the CLR type therefore changes the derived identity and the contract. @@ -122,9 +151,10 @@ Without an explicit stable identity, Orleans derives the identity from the CLR t | [`ORLEANS0023`](../../diagnostics/orleans0023.md) | Warning | A grain class identity differs from the manifest. | | [`ORLEANS0024`](../../diagnostics/orleans0024.md) | Warning | A removed grain class isn't marked `*RETIRED*`. | | [`ORLEANS0025`](../../diagnostics/orleans0025.md) | Warning | A grain class is declared more than once. | +| [`ORLEANS0027`](../../diagnostics/orleans0027.md) | Warning | An RPC method remains in the manifest after it is removed from source. | Standard `.editorconfig` diagnostic configuration can change these severities. Prefer fixing contract drift instead of suppressing diagnostics. ## Scope -The analyzer tracks RPC interface signatures, numeric interface versions, and concrete grain class identities. It doesn't prove behavioral compatibility or validate persisted state schemas. Continue to follow the [backward compatibility guidelines](backward-compatibility-guidelines.md) and test mixed-version deployments before production rollout. +The analyzer tracks RPC interface signatures, numeric interface versions, and concrete grain class identities. Behavioral compatibility and persisted state schemas require separate review using the [backward compatibility guidelines](backward-compatibility-guidelines.md) and mixed-version deployment tests. diff --git a/docs/site/src/content/docs/toc.yml b/docs/site/src/content/docs/toc.yml index 6c7037e1fe..ac0f03b888 100644 --- a/docs/site/src/content/docs/toc.yml +++ b/docs/site/src/content/docs/toc.yml @@ -435,6 +435,8 @@ items: href: diagnostics/orleans0025.md - name: ORLEANS0026 href: diagnostics/orleans0026.md + - name: ORLEANS0027 + href: diagnostics/orleans0027.md - name: API context and examples href: resources/api-reference-guide.md - name: Best practices diff --git a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt index 14bcf5a315..181ecb811a 100644 --- a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt +++ b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt @@ -1,32 +1,38 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardGrain")] Orleans.Dashboard.Core.IDashboardGrain [Version(0)] - GetClusterTracing() -> Task>> - GetCounters(string[]) -> Task> - GetGrainState(string?, string?) -> Task> - GetGrainTracing(string) -> Task>>> - GetGrainTypes(string[]) -> Task> - GetSiloTracing(string) -> Task>> - InitializeAsync() -> Task - SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task - TopGrainMethods(int, string[]) -> Task>> + [Alias("GetClusterTracing")] GetClusterTracing() -> Task>> + [Alias("GetCounters")] GetCounters(string[]) -> Task> + [Alias("GetGrainState")] GetGrainState(string?, string?) -> Task> + [Alias("GetGrainTracing")] GetGrainTracing(string) -> Task>>> + [Alias("GetGrainTypes")] GetGrainTypes(string[]) -> Task> + [Alias("GetSiloTracing")] GetSiloTracing(string) -> Task>> + [Alias("InitializeAsync")] InitializeAsync() -> Task + [Alias("SubmitTracing")] SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task + [Alias("TopGrainMethods")] TopGrainMethods(int, string[]) -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardRemindersGrain")] Orleans.Dashboard.Core.IDashboardRemindersGrain [Version(0)] - GetReminders(int, int) -> Task> + [Alias("GetReminders")] GetReminders(int, int) -> Task> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainProxy")] Orleans.Dashboard.Core.ISiloGrainProxy [Version(0)] - GetMetadata() -> Task>> + [Alias("GetMetadata")] GetMetadata() -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainService")] Orleans.Dashboard.Core.ISiloGrainService [Version(0)] - Enable(bool) -> Task - GetCounters() -> Task> - GetExtendedProperties() -> Task>> - GetLifecycleStages() -> Task> - GetRuntimeStatistics() -> Task> - ReportCounters(Orleans.Concurrency.Immutable) -> Task - SetVersion(string, string) -> Task + [Alias("Enable")] Enable(bool) -> Task + [Alias("GetCounters")] GetCounters() -> Task> + [Alias("GetExtendedProperties")] GetExtendedProperties() -> Task>> + [Alias("GetLifecycleStages")] GetLifecycleStages() -> Task> + [Alias("GetRuntimeStatistics")] GetRuntimeStatistics() -> Task> + [Alias("ReportCounters")] ReportCounters(Orleans.Concurrency.Immutable) -> Task + [Alias("SetVersion")] SetVersion(string, string) -> Task class [GrainType("dashboard")] Orleans.Dashboard.Implementation.Grains.DashboardGrain diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 721094b23b..2520dcbd01 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -7,6 +7,7 @@ + diff --git a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md index 28ca7fa6bf..30fabca306 100644 --- a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md @@ -16,3 +16,4 @@ ORLEANS0022 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grai ORLEANS0023 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class alias mismatch ORLEANS0024 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain class not marked as *RETIRED* ORLEANS0025 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate grain class declaration in file +ORLEANS0027 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain interface member remains in OrleansContracts.txt diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index 11d31f780a..888ed99019 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; +using Orleans.CodeGenerator; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -33,6 +34,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer public const string RuleId0023 = "ORLEANS0023"; public const string RuleId0024 = "ORLEANS0024"; public const string RuleId0025 = "ORLEANS0025"; + public const string RuleId0027 = "ORLEANS0027"; // Property bag keys for code fixes internal const string InterfaceNamePropertyKey = "InterfaceName"; @@ -79,6 +81,17 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberNotDeclaredDescription), Resources.ResourceManager, typeof(Resources)), helpLinkUri: Constants.GetDiagnosticHelpLink(RuleId0018)); + private static readonly DiagnosticDescriptor RemovedMemberRule = new( + id: RuleId0027, + title: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedTitle), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedMessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Orleans.Versioning", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedDescription), Resources.ResourceManager, typeof(Resources)), + helpLinkUri: Constants.GetDiagnosticHelpLink(RuleId0027), + customTags: WellKnownDiagnosticTags.CompilationEnd); + private static readonly DiagnosticDescriptor RemovedInterfaceNotRetiredRule = new( id: RuleId0019, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceRemovedNotRetiredTitle), Resources.ResourceManager, typeof(Resources)), @@ -163,7 +176,8 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer GrainClassNotDeclaredRule, GrainClassAliasMismatchRule, RemovedGrainClassNotRetiredRule, - DuplicateGrainClassDeclarationRule); + DuplicateGrainClassDeclarationRule, + RemovedMemberRule); public override void Initialize(AnalysisContext context) { @@ -191,18 +205,19 @@ private void OnCompilationStart(CompilationStartAnalysisContext context) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)); GrainInterfaceData? data = null; + SourceText? sourceText = null; List? fileParseErrors = null; if (grainInterfacesFile is not null) { - var sourceText = grainInterfacesFile.GetText(context.CancellationToken); + sourceText = grainInterfacesFile.GetText(context.CancellationToken); if (sourceText is not null) { (data, fileParseErrors) = GrainInterfaceFileParser.Parse(sourceText, grainInterfacesFile.Path); } } - var impl = new Impl(context.Compilation, data, grainInterfacesFile, fileParseErrors); + var impl = new Impl(context.Compilation, data, grainInterfacesFile, sourceText, fileParseErrors); context.RegisterSymbolAction(impl.AnalyzeNamedType, SymbolKind.NamedType); context.RegisterCompilationEndAction(impl.OnCompilationEnd); @@ -212,21 +227,26 @@ private sealed class Impl { private readonly GrainInterfaceData? _data; private readonly AdditionalText? _grainInterfacesFile; + private readonly SourceText? _grainInterfacesFileText; private readonly List? _fileParseErrors; private readonly ConcurrentDictionary _visitedInterfaces = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _visitedClasses = new(StringComparer.Ordinal); + private readonly ConcurrentBag _removedMemberDiagnostics = new(); private readonly INamedTypeSymbol? _iAddressableType; private readonly INamedTypeSymbol? _aliasAttributeType; private readonly INamedTypeSymbol? _versionAttributeType; + private Location? _firstContractLocation; public Impl( Compilation compilation, GrainInterfaceData? data, AdditionalText? grainInterfacesFile, + SourceText? grainInterfacesFileText, List? fileParseErrors) { _data = data; _grainInterfacesFile = grainInterfacesFile; + _grainInterfacesFileText = grainInterfacesFileText; _fileParseErrors = fileParseErrors; _iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); @@ -262,13 +282,24 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) if (_data is null) { _visitedInterfaces.TryAdd(interfaceName, true); + RecordContractLocation(namedType); // We'll report file missing at compilation end return; } // Check if interface is declared in the file + var explicitGrainInterfaceType = GetStringAttributeValue( + namedType, + Constants.GrainInterfaceTypeAttributeFullyQualifiedName); var grainInterfaceType = GetGrainInterfaceType(namedType); - var declaredInterface = FindDeclaredInterface(interfaceName, grainInterfaceType); + var declaredInterface = FindDeclaredInterface( + interfaceName, + grainInterfaceType, + explicitGrainInterfaceType is null + || string.Equals( + explicitGrainInterfaceType, + GetDefaultGrainInterfaceType(namedType), + StringComparison.Ordinal)); if (declaredInterface is null) { _visitedInterfaces.TryAdd(interfaceName, true); @@ -288,7 +319,7 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) return; } - _visitedInterfaces.TryAdd(declaredInterface.Name, true); + _visitedInterfaces.TryAdd(GetDeclarationKey(declaredInterface), true); // Check if retired if (declaredInterface.IsRetired) @@ -339,38 +370,23 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) // Alias mismatch - could add a separate diagnostic for this } + var sourceMembers = namedType.GetMembers() + .OfType() + .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) + .ToArray(); + // Check members - foreach (var member in namedType.GetMembers().OfType()) + foreach (var member in sourceMembers) { - if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) - { - continue; - } - var memberSignature = GetMethodSignature(member); var memberAlias = GetAliasFromAttribute(member); - if (!declaredInterface.Members.ContainsKey(memberSignature) - && !declaredInterface.Members.Keys.Any(signature => - { - if (string.Equals( - NormalizeStoredMemberSignature(signature, declaredInterface.Name), - memberSignature, - StringComparison.Ordinal)) - { - return true; - } - - var normalized = NormalizeLegacyMethodSignature(signature); - var clrSignature = GetClrMethodSignature(member); - var containingTypePrefix = - $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; - return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) - || string.Equals( - normalized, - NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), - StringComparison.Ordinal); - })) + if (!declaredInterface.Members.Values.Any(declaredMember => + GrainInterfaceVersionAnalyzer.IsMatchingMember( + declaredInterface.Name, + declaredMember.Signature, + declaredMember.Alias, + member))) { // Member not found - interface has changed var properties = ImmutableDictionary.Empty @@ -387,10 +403,38 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) location, properties, memberSignature, - interfaceName)); + interfaceName, + GetClrMethodSignature(member))); } } } + + if (_grainInterfacesFile is not null && _grainInterfacesFileText is { } sourceText) + { + foreach (var declaredMember in declaredInterface.Members.Values) + { + if (sourceMembers.Any(member => + GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( + declaredInterface.Name, + declaredMember.Signature, + declaredMember.Alias, + member))) + { + continue; + } + + var properties = ImmutableDictionary.Empty + .Add(InterfaceNamePropertyKey, interfaceName) + .Add(GrainInterfaceTypePropertyKey, grainInterfaceType) + .Add(MemberNamePropertyKey, declaredMember.Signature); + _removedMemberDiagnostics.Add(Diagnostic.Create( + RemovedMemberRule, + declaredMember.GetLocation(sourceText, _grainInterfacesFile.Path), + properties, + declaredMember.Signature, + interfaceName)); + } + } } public void OnCompilationEnd(CompilationAnalysisContext context) @@ -404,60 +448,66 @@ public void OnCompilationEnd(CompilationAnalysisContext context) } } + foreach (var diagnostic in _removedMemberDiagnostics) + { + context.ReportDiagnostic(diagnostic); + } + // Report file missing if any grain interfaces were found but no file exists if (_data is null && (_visitedInterfaces.Count > 0 || _visitedClasses.Count > 0)) { context.ReportDiagnostic(Diagnostic.Create( OrleansContractsFileMissingRule, - Location.None, + _firstContractLocation ?? Location.None, Constants.OrleansContractsFileName)); } // Check for removed interfaces (in file but not in code) if (_data is not null && _grainInterfacesFile is not null) { - var sourceText = _grainInterfacesFile.GetText(context.CancellationToken); - if (sourceText is not null) + if (_grainInterfacesFileText is { } sourceText) { - foreach (var kvp in _data.Interfaces) + foreach (var declaredInterface in _data.Interfaces) { - if (kvp.Value.IsRetired) + if (declaredInterface.IsRetired) { continue; } - if (!_visitedInterfaces.ContainsKey(kvp.Key)) + if (!_visitedInterfaces.ContainsKey(GetDeclarationKey(declaredInterface))) { // Interface in file but not in code - needs to be retired - var location = kvp.Value.GetLocation(sourceText, _grainInterfacesFile.Path); + var location = declaredInterface.GetLocation(sourceText, _grainInterfacesFile.Path); var properties = ImmutableDictionary.Empty - .Add(InterfaceNamePropertyKey, kvp.Key); + .Add(InterfaceNamePropertyKey, declaredInterface.Name) + .Add(GrainInterfaceTypePropertyKey, declaredInterface.GrainInterfaceType); context.ReportDiagnostic(Diagnostic.Create( RemovedInterfaceNotRetiredRule, location, properties, - kvp.Key)); + declaredInterface.Name)); } } - foreach (var kvp in _data.Classes) + foreach (var declaredClass in _data.Classes) { - if (kvp.Value.IsRetired || _visitedClasses.ContainsKey(kvp.Key)) + if (declaredClass.IsRetired || _visitedClasses.ContainsKey(GetDeclarationKey(declaredClass))) { continue; } - var location = kvp.Value.GetLocation(sourceText, _grainInterfacesFile.Path); + var location = declaredClass.GetLocation(sourceText, _grainInterfacesFile.Path); var properties = ImmutableDictionary.Empty - .Add(ClassNamePropertyKey, kvp.Key); + .Add(ClassNamePropertyKey, declaredClass.Name) + .Add(ActualAliasPropertyKey, declaredClass.Alias); context.ReportDiagnostic(Diagnostic.Create( RemovedGrainClassNotRetiredRule, location, properties, - kvp.Key)); + declaredClass.Name)); } } } @@ -470,6 +520,7 @@ private void AnalyzeGrainClass(SymbolAnalysisContext context, INamedTypeSymbol n if (_data is null) { _visitedClasses.TryAdd(className, true); + RecordContractLocation(namedType); return; } @@ -487,7 +538,7 @@ private void AnalyzeGrainClass(SymbolAnalysisContext context, INamedTypeSymbol n return; } - _visitedClasses.TryAdd(declaredClass.Name, true); + _visitedClasses.TryAdd(GetDeclarationKey(declaredClass), true); if (declaredClass.Alias is null || string.Equals(codeAlias, declaredClass.Alias, StringComparison.Ordinal)) { @@ -520,43 +571,68 @@ private bool IsRpcContract(INamedTypeSymbol type) && type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, _iAddressableType)); } - private DeclaredGrainInterface? FindDeclaredInterface(string interfaceName, string? grainInterfaceType) + private void RecordContractLocation(INamedTypeSymbol type) + { + var location = type.Locations.FirstOrDefault(candidate => candidate.IsInSource); + if (location is not null) + { + Interlocked.CompareExchange(ref _firstContractLocation, location, null); + } + } + + private DeclaredGrainInterface? FindDeclaredInterface( + string interfaceName, + string? grainInterfaceType, + bool allowLegacyNameMatch) { if (grainInterfaceType is not null) { - var stableMatch = _data!.Interfaces.Values.FirstOrDefault(candidate => - string.Equals(candidate.GrainInterfaceType, grainInterfaceType, StringComparison.Ordinal)); + var stableMatch = _data!.Interfaces.FirstOrDefault(candidate => + string.Equals(GetDeclarationKey(candidate), grainInterfaceType, StringComparison.Ordinal)); if (stableMatch is not null) { return stableMatch; } - return _data.Interfaces.TryGetValue(interfaceName, out var legacyMatch) - && legacyMatch.GrainInterfaceType is null - ? legacyMatch - : null; + if (allowLegacyNameMatch) + { + return _data.Interfaces.FirstOrDefault(candidate => + candidate.GrainInterfaceType is null + && string.Equals(candidate.Name, interfaceName, StringComparison.Ordinal)); + } + + return null; } - return _data!.Interfaces.TryGetValue(interfaceName, out var result) ? result : null; + return _data!.Interfaces.FirstOrDefault(candidate => + string.Equals(candidate.Name, interfaceName, StringComparison.Ordinal)); } private DeclaredGrainClass? FindDeclaredClass(string className, string? alias) { if (alias is not null) { - var stableMatch = _data!.Classes.Values.FirstOrDefault(candidate => - string.Equals(candidate.Alias, alias, StringComparison.Ordinal)); + var stableMatch = _data!.Classes.FirstOrDefault(candidate => + string.Equals(GetDeclarationKey(candidate), alias, StringComparison.Ordinal)); if (stableMatch is not null) { return stableMatch; } - return _data.Classes.TryGetValue(className, out var exactNameMatch) ? exactNameMatch : null; + return _data.Classes.FirstOrDefault(candidate => + string.Equals(candidate.Name, className, StringComparison.Ordinal)); } - return _data!.Classes.TryGetValue(className, out var result) ? result : null; + return _data!.Classes.FirstOrDefault(candidate => + string.Equals(candidate.Name, className, StringComparison.Ordinal)); } + private static string GetDeclarationKey(DeclaredGrainInterface declaration) + => declaration.GrainInterfaceType ?? GetDefaultGrainInterfaceType(declaration.Name); + + private static string GetDeclarationKey(DeclaredGrainClass declaration) + => declaration.Alias ?? GetDefaultGrainType(declaration.Name); + private ushort GetVersionFromAttribute(INamedTypeSymbol type) { if (_versionAttributeType is null) @@ -610,21 +686,68 @@ internal static string GetGrainType(INamedTypeSymbol type) return grainType; } - var name = type.MetadataName.ToLowerInvariant(); - var arityIndex = name.IndexOf('`'); - var arity = arityIndex >= 0 ? name.Substring(arityIndex) : string.Empty; - if (arityIndex >= 0) + return GetDefaultGrainType(type); + } + + internal static string GetDefaultGrainType(INamedTypeSymbol type) + { + var name = type.Name.ToLowerInvariant(); + + const string GrainSuffix = "grain"; + if (name.EndsWith(GrainSuffix, StringComparison.Ordinal) && name.Length > GrainSuffix.Length) + { + name = name.Substring(0, name.Length - GrainSuffix.Length); + } + + var arity = 0; + for (var current = type; current is not null; current = current.ContainingType) + { + arity += current.Arity; + } + + return arity > 0 ? $"{name}`{arity}" : name; + } + + internal static string GetDefaultGrainType(string typeName) + { + var simpleNameStart = typeName.LastIndexOf('.') + 1; + var simpleName = typeName.Substring(simpleNameStart); + var genericStart = simpleName.IndexOf('<'); + if (genericStart >= 0) { - name = name.Substring(0, arityIndex); + simpleName = simpleName.Substring(0, genericStart); } + var name = simpleName.ToLowerInvariant(); const string GrainSuffix = "grain"; if (name.EndsWith(GrainSuffix, StringComparison.Ordinal) && name.Length > GrainSuffix.Length) { name = name.Substring(0, name.Length - GrainSuffix.Length); } - return name + arity; + var arity = 0; + var searchIndex = 0; + while ((genericStart = typeName.IndexOf('<', searchIndex)) >= 0) + { + var genericEnd = typeName.IndexOf('>', genericStart + 1); + if (genericEnd < 0) + { + break; + } + + arity++; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (typeName[index] == ',') + { + arity++; + } + } + + searchIndex = genericEnd + 1; + } + + return arity > 0 ? $"{name}`{arity}" : name; } internal static string GetGrainInterfaceType(INamedTypeSymbol type) @@ -634,14 +757,14 @@ internal static string GetGrainInterfaceType(INamedTypeSymbol type) return grainInterfaceType; } - return GetRuntimeTypeName(type); + return GetDefaultGrainInterfaceType(type); } - private static string GetRuntimeTypeName(INamedTypeSymbol type) + internal static string GetDefaultGrainInterfaceType(INamedTypeSymbol type) { if (type.ContainingType is { } containingType) { - return $"{GetRuntimeTypeName(containingType)}+{type.MetadataName}"; + return $"{GetDefaultGrainInterfaceType(containingType)}+{type.MetadataName}"; } return type.ContainingNamespace.IsGlobalNamespace @@ -649,12 +772,52 @@ private static string GetRuntimeTypeName(INamedTypeSymbol type) : $"{type.ContainingNamespace.ToDisplayString()}.{type.MetadataName}"; } + internal static string GetDefaultGrainInterfaceType(string typeName) + { + var segments = typeName.Split('.'); + var firstGenericSegment = Array.FindIndex(segments, segment => segment.IndexOf('<') >= 0); + if (firstGenericSegment < 0) + { + return typeName; + } + + for (var index = firstGenericSegment; index < segments.Length; index++) + { + var genericStart = segments[index].IndexOf('<'); + if (genericStart < 0) + { + continue; + } + + var genericEnd = segments[index].LastIndexOf('>'); + var arity = 1; + for (var characterIndex = genericStart + 1; characterIndex < genericEnd; characterIndex++) + { + if (segments[index][characterIndex] == ',') + { + arity++; + } + } + + segments[index] = $"{segments[index].Substring(0, genericStart)}`{arity}"; + } + + return string.Join(".", segments.Take(firstGenericSegment)) + + (firstGenericSegment > 0 ? "." : string.Empty) + + string.Join("+", segments.Skip(firstGenericSegment)); + } + internal static string GetMethodSignature(IMethodSymbol method) { - var sb = new StringBuilder(); var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName); var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); - sb.Append(methodId ?? methodAlias ?? method.Name); + return GetMethodSignature(method, methodId ?? methodAlias ?? MethodIdProvider.Create(method)); + } + + private static string GetMethodSignature(IMethodSymbol method, object methodId) + { + var sb = new StringBuilder(); + sb.Append(methodId); if (method.Arity > 0) { sb.Append('`'); @@ -706,6 +869,11 @@ internal static bool RequiresClrComment(IMethodSymbol method) { var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName)?.ToString(); var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); + if (methodId is null && methodAlias is null) + { + return true; + } + if (methodId is not null && !string.Equals(methodId, method.Name, StringComparison.Ordinal) || methodAlias is not null && !string.Equals(methodAlias, method.Name, StringComparison.Ordinal)) { @@ -744,6 +912,116 @@ internal static string NormalizeStoredMemberSignature(string signature, string i return Regex.Replace(result, @"alias\(""([^""]+)""\)", "$1"); } + internal static bool IsMatchingMember( + string declaredInterfaceName, + string storedSignature, + string? storedAlias, + IMethodSymbol member) + { + var memberSignature = GetMethodSignature(member); + var sourceMethodId = GetAttributeValue(member, Constants.IdAttributeFullyQualifiedName); + var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); + if (storedAlias is not null) + { + if (!string.Equals( + storedAlias, + sourceMethodAlias, + StringComparison.Ordinal)) + { + return false; + } + + var storedIdentity = storedAlias + GrainInterfaceFileParser.GetMethodAritySuffix(storedSignature); + var parameterListStart = memberSignature.IndexOf('('); + if (parameterListStart < 0 + || !string.Equals( + storedIdentity, + memberSignature.Substring(0, parameterListStart), + StringComparison.Ordinal)) + { + return false; + } + + var canonicalStoredSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( + storedSignature, + storedAlias); + if (string.Equals( + NormalizeStoredMemberSignature(canonicalStoredSignature, declaredInterfaceName), + memberSignature, + StringComparison.Ordinal)) + { + return true; + } + + return string.Equals( + GetNormalizedSignatureSuffix(storedSignature), + GetNormalizedSignatureSuffix(GetClrMethodSignature(member)), + StringComparison.Ordinal); + } + + var normalizedStoredSignature = NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName); + if (sourceMethodAlias is not null) + { + return false; + } + + if (sourceMethodId is not null) + { + return string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal); + } + + if (string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal)) + { + return true; + } + + if (string.Equals( + normalizedStoredSignature, + GetMethodSignature(member, member.Name), + StringComparison.Ordinal)) + { + return true; + } + + var normalized = NormalizeLegacyMethodSignature(storedSignature); + var clrSignature = GetClrMethodSignature(member); + var containingTypePrefix = + $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; + return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) + || string.Equals( + normalized, + NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), + StringComparison.Ordinal); + } + + internal static bool IsMatchingHistoricalMember( + string declaredInterfaceName, + string storedSignature, + string? storedAlias, + IMethodSymbol member) + { + if (IsMatchingMember(declaredInterfaceName, storedSignature, storedAlias, member)) + { + return true; + } + + var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); + return storedAlias is null + && sourceMethodAlias is not null + && string.Equals( + NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName), + GetMethodSignature(member), + StringComparison.Ordinal); + } + + private static string GetNormalizedSignatureSuffix(string signature) + { + var parameterListStart = signature.IndexOf('('); + return parameterListStart < 0 + ? NormalizeLegacyMethodSignature(signature) + : NormalizeLegacyMethodSignature(signature.Substring(parameterListStart)); + } + private static string GetContractTypeName(ITypeSymbol type) { if (type is IArrayTypeSymbol array) @@ -844,9 +1122,9 @@ private static bool HasMeaningfulTypeAlias(ITypeSymbol type) /// internal sealed class GrainInterfaceData { - public Dictionary Interfaces { get; } = new(StringComparer.Ordinal); + public List Interfaces { get; } = new(); - public Dictionary Classes { get; } = new(StringComparer.Ordinal); + public List Classes { get; } = new(); } /// @@ -888,6 +1166,12 @@ public DeclaredGrainMember(string signature) public string Signature { get; } public string? Alias { get; set; } public TextSpan Span { get; set; } + + public Location GetLocation(SourceText sourceText, string filePath) + { + var lineSpan = sourceText.Lines.GetLinePositionSpan(Span); + return Location.Create(filePath, Span, lineSpan); + } } /// @@ -925,13 +1209,13 @@ internal static class GrainInterfaceFileParser // Or with retired: *RETIRED* interface [GrainInterfaceType("x")] Namespace.IInterface [Version(N)] // The name can include generic type parameters like IMyGrain or IMyGrain private static readonly Regex InterfacePattern = new( - @"^(?\*RETIRED\*\s*)?(?:interface\s+)?(\[GrainInterfaceType\(""(?[^""]+)""\)\]\s*)?(\[Alias\(""(?[^""]+)""\)\]\s*)?(?[\w.]+(?:<[\w,\s]+>)?)\s*\[Version\((?\d+)\)\]$", + @"^(?\*RETIRED\*\s*)?(?:interface\s+)?(\[GrainInterfaceType\(""(?[^""]+)""\)\]\s*)?(\[Alias\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)\s*\[Version\((?\d+)\)\]$", RegexOptions.Compiled); // Grain class line: class [GrainType("x")] Namespace.GrainClass // Or with retired: *RETIRED* class [GrainType("x")] Namespace.GrainClass private static readonly Regex GrainClassPattern = new( - @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w.]+(?:<[\w,\s]+>)?)$", + @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)$", RegexOptions.Compiled); // Member line: [Alias("x")] Namespace.IInterface.Method(params) -> ReturnType @@ -979,22 +1263,97 @@ internal static bool TryGetGrainClassName(string line, out string name) return false; } + internal static bool TryGetGrainClassType(string line, out string grainType) + { + var match = GrainClassPattern.Match(StripClrComment(line)); + if (match.Success && match.Groups["alias"].Success) + { + grainType = match.Groups["alias"].Value; + return true; + } + + grainType = string.Empty; + return false; + } + internal static bool TryGetContractName(string line, out string name) => TryGetGrainClassName(line, out name) || TryGetInterfaceName(line, out name); internal static bool TryGetMemberSignature(string line, out string signature) + { + if (TryGetMemberDeclaration(line, out signature, out var alias)) + { + signature = GetCanonicalMemberSignature(signature, alias); + return true; + } + + signature = string.Empty; + return false; + } + + internal static bool TryGetMemberDeclaration(string line, out string signature, out string? alias) { var match = MemberPattern.Match(StripClrComment(line)); if (match.Success) { signature = match.Groups["signature"].Value; + alias = match.Groups["alias"].Success ? match.Groups["alias"].Value : null; return true; } signature = string.Empty; + alias = null; return false; } + internal static string GetCanonicalMemberSignature(string signature, string? alias) + { + if (alias is null) + { + return signature; + } + + var parameterListStart = signature.IndexOf('('); + return parameterListStart < 0 + ? signature + : alias + GetMethodAritySuffix(signature) + signature.Substring(parameterListStart); + } + + internal static string GetMethodAritySuffix(string signature) + { + var parameterListStart = signature.IndexOf('('); + if (parameterListStart < 0) + { + return string.Empty; + } + + var methodStart = signature.LastIndexOf('.', parameterListStart - 1) + 1; + var methodName = signature.Substring(methodStart, parameterListStart - methodStart); + var arityMarker = methodName.LastIndexOf('`'); + if (arityMarker >= 0) + { + return methodName.Substring(arityMarker); + } + + var genericStart = methodName.IndexOf('<'); + var genericEnd = methodName.LastIndexOf('>'); + if (genericStart < 0 || genericEnd <= genericStart) + { + return string.Empty; + } + + var arity = 1; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (methodName[index] == ',') + { + arity++; + } + } + + return $"`{arity}"; + } + internal static string GetClrComment(string line) { const string Prefix = " # CLR: "; @@ -1032,8 +1391,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe currentInterface = null; var name = grainClassMatch.Groups["name"].Value; var alias = grainClassMatch.Groups["alias"].Success ? grainClassMatch.Groups["alias"].Value : null; - if (data.Classes.ContainsKey(name) - || alias is not null && data.Classes.Values.Any(candidate => string.Equals(candidate.Alias, alias, StringComparison.Ordinal))) + var identity = alias ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainType(name); + if (data.Classes.Any(candidate => + string.Equals( + candidate.Alias ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainType(candidate.Name), + identity, + StringComparison.Ordinal))) { errors ??= new List(); var location = Location.Create( @@ -1044,12 +1407,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe continue; } - data.Classes[name] = new DeclaredGrainClass(name) + data.Classes.Add(new DeclaredGrainClass(name) { Alias = alias, IsRetired = grainClassMatch.Groups["retired"].Success, Span = textLine.Span - }; + }); continue; } @@ -1067,9 +1430,13 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe } var isRetired = interfaceMatch.Groups["retired"].Success; - if (data.Interfaces.ContainsKey(name) - || grainInterfaceType is not null - && data.Interfaces.Values.Any(candidate => string.Equals(candidate.GrainInterfaceType, grainInterfaceType, StringComparison.Ordinal))) + var identity = grainInterfaceType ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(name); + if (data.Interfaces.Any(candidate => + string.Equals( + candidate.GrainInterfaceType + ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(candidate.Name), + identity, + StringComparison.Ordinal))) { // Duplicate declaration errors ??= new List(); @@ -1092,7 +1459,7 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe IsRetired = isRetired, Span = textLine.Span }; - data.Interfaces[name] = currentInterface; + data.Interfaces.Add(currentInterface); continue; } diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 4c6203480f..e31560066f 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -24,11 +24,22 @@ namespace Orleans.Analyzers; public class GrainInterfaceVersionCodeFix : CodeFixProvider { private const string DefaultNewLine = "\n"; + private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; + private const string RegenerateCodeActionEquivalenceKey = nameof(RegenerateOrleansContractsFileAsync); + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private const string RegenerationCommand = + "dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024"; private static readonly string[] GeneratedHeader = [ - "# This file is auto-generated by the Orleans contract analyzer.", - "# Update source contracts, then regenerate this file by following:", - "# https://aka.ms/orleans/OrleansContracts.txt" + "# This file is generated by the Orleans contract analyzer.", + "# To regenerate, run this command from the repository root after replacing", + "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:", + $"# {RegenerationCommand}", + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION", + "# The regeneration command edits this manifest only; it does not change source attributes.", + "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.", + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.", + "# Details: https://aka.ms/orleans/OrleansContracts.txt" ]; public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( @@ -36,15 +47,26 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider GrainInterfaceVersionAnalyzer.RuleId0017, // Version mismatch GrainInterfaceVersionAnalyzer.RuleId0018, // Member not declared GrainInterfaceVersionAnalyzer.RuleId0019, // Removed interface not retired + GrainInterfaceVersionAnalyzer.RuleId0020, // Contracts file missing GrainInterfaceVersionAnalyzer.RuleId0022, // Grain class not declared GrainInterfaceVersionAnalyzer.RuleId0023, // Grain class alias mismatch GrainInterfaceVersionAnalyzer.RuleId0024); // Removed grain class not retired - // Note: We don't use BatchFixer because each fix may need to coordinate updates to the same file - public sealed override FixAllProvider? GetFixAllProvider() => null; + public sealed override FixAllProvider GetFixAllProvider() => OrleansContractsFixAllProvider.Instance; public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { + var validDiagnostics = context.Diagnostics.Where(HasRequiredProperties).ToImmutableArray(); + if (!validDiagnostics.IsEmpty) + { + context.RegisterCodeFix( + CodeAction.Create( + title: RegenerateCodeActionTitle, + createChangedSolution: cancellationToken => RegenerateOrleansContractsFileAsync(context.Document.Project, cancellationToken), + equivalenceKey: RegenerateCodeActionEquivalenceKey), + validDiagnostics); + } + foreach (var diagnostic in context.Diagnostics) { switch (diagnostic.Id) @@ -76,6 +98,660 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) return Task.CompletedTask; } + private static bool HasRequiredProperties(Diagnostic diagnostic) + => diagnostic.Id switch + { + GrainInterfaceVersionAnalyzer.RuleId0016 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0017 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ActualVersionPropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0018 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.MemberNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0019 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0020 => true, + GrainInterfaceVersionAnalyzer.RuleId0022 + or GrainInterfaceVersionAnalyzer.RuleId0023 + or GrainInterfaceVersionAnalyzer.RuleId0024 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ClassNamePropertyKey), + _ => false + }; + + private static bool HasProperty(Diagnostic diagnostic, string propertyName) + => diagnostic.Properties.TryGetValue(propertyName, out var value) + && !string.IsNullOrEmpty(value); + + private static async Task RegenerateOrleansContractsFileAsync( + Project project, + CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + return project.Solution; + } + + var contractsFile = FindContractsDocument(project); + var existingText = contractsFile is null + ? null + : await contractsFile.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newLine = existingText is null ? DefaultNewLine : GetNewLine(existingText); + var lines = new List(); + var activeInterfaceIdentities = new HashSet(StringComparer.Ordinal); + var activeConventionInterfaceNames = new HashSet(StringComparer.Ordinal); + var activeInterfaces = new Dictionary(StringComparer.Ordinal); + var activeInterfacesByName = new Dictionary(StringComparer.Ordinal); + var activeClassIdentities = new HashSet(StringComparer.Ordinal); + var activeConventionClassNames = new HashSet(StringComparer.Ordinal); + var iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); + var generatedCodeTrees = new Dictionary(); + var semanticModels = new Dictionary(); + + foreach (var type in GetAllSourceTypes( + compilation.Assembly.GlobalNamespace, + compilation, + generatedCodeTrees, + semanticModels) + .OrderBy(GetFullyQualifiedName, StringComparer.Ordinal)) + { + if (type.TypeKind == TypeKind.Interface + && iAddressableType is not null + && !SymbolEqualityComparer.Default.Equals(type, iAddressableType) + && type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate, iAddressableType))) + { + AppendInterface( + lines, + type, + activeInterfaceIdentities, + activeConventionInterfaceNames, + activeInterfaces, + activeInterfacesByName); + } + else if (type.TypeKind == TypeKind.Class && !type.IsAbstract && type.IsGrainClass()) + { + AppendGrainClass(lines, type, activeClassIdentities, activeConventionClassNames); + } + } + + if (existingText is not null) + { + AppendHistoricalContracts( + lines, + existingText.ToString(), + activeInterfaceIdentities, + activeConventionInterfaceNames, + activeInterfaces, + activeInterfacesByName, + activeClassIdentities, + activeConventionClassNames); + } + + var content = SortContractEntries(string.Join(newLine, lines), newLine); + var newText = SourceText.From(content, Utf8NoBom); + if (contractsFile is not null) + { + return project.Solution.WithAdditionalDocumentText(contractsFile.Id, newText); + } + + var filePath = GetConfiguredContractsPath(project); + return project.Solution.AddAdditionalDocument( + DocumentId.CreateNewId(project.Id), + Path.GetFileName(filePath), + newText, + filePath: filePath); + } + + private static TextDocument? FindContractsDocument(Project project) + { + var configuredPath = GetConfiguredContractsPath(project); + var configuredDocument = project.AdditionalDocuments.FirstOrDefault(document => + PathsEqual(document.FilePath ?? document.Name, configuredPath)); + if (configuredDocument is not null) + { + return configuredDocument; + } + + foreach (var additionalFile in project.AnalyzerOptions.AdditionalFiles) + { + if (!Path.GetFileName(additionalFile.Path) + .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase) + && (!project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(additionalFile) + .TryGetValue("build_metadata.AdditionalFiles.OrleansContractsFile", out var value) + || !string.Equals(value, "true", StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + var document = project.AdditionalDocuments.FirstOrDefault(candidate => + string.Equals(candidate.FilePath, additionalFile.Path, StringComparison.OrdinalIgnoreCase)); + if (document is not null) + { + return document; + } + } + + return project.AdditionalDocuments.FirstOrDefault(document => + Path.GetFileName(document.FilePath ?? document.Name) + .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + } + + private static bool PathsEqual(string left, string right) + { + left = NormalizePathSeparators(left); + right = NormalizePathSeparators(right); + if (Path.IsPathRooted(left) && Path.IsPathRooted(right)) + { + left = Path.GetFullPath(left); + right = Path.GetFullPath(right); + } + + return string.Equals(left, right, StringComparison.OrdinalIgnoreCase); + } + + private static string GetConfiguredContractsPath(Project project) + { + if (project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue( + "build_property.OrleansContractsPath", + out var configuredPath) + && !string.IsNullOrWhiteSpace(configuredPath)) + { + configuredPath = NormalizePathSeparators(configuredPath); + if (Path.IsPathRooted(configuredPath)) + { + return configuredPath; + } + + if (Path.GetDirectoryName(project.FilePath) is { } projectDirectory) + { + return Path.Combine(projectDirectory, configuredPath); + } + + return configuredPath; + } + + return Path.GetDirectoryName(project.FilePath) is { } directory + ? Path.Combine(directory, Constants.OrleansContractsFileName) + : Constants.OrleansContractsFileName; + } + + private static string NormalizePathSeparators(string path) + => path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + + private static IEnumerable GetAllSourceTypes( + INamespaceSymbol @namespace, + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) + { + foreach (var type in @namespace.GetTypeMembers()) + { + foreach (var result in GetSourceTypeAndNestedTypes( + type, + compilation, + generatedCodeTrees, + semanticModels)) + { + yield return result; + } + } + + foreach (var childNamespace in @namespace.GetNamespaceMembers()) + { + foreach (var result in GetAllSourceTypes( + childNamespace, + compilation, + generatedCodeTrees, + semanticModels)) + { + yield return result; + } + } + } + + private static IEnumerable GetSourceTypeAndNestedTypes( + INamedTypeSymbol type, + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) + { + if (!type.IsImplicitlyDeclared + && !IsGeneratedCode(type, compilation, generatedCodeTrees, semanticModels) + && type.Locations.Any(location => location.IsInSource)) + { + yield return type; + } + + foreach (var nestedType in type.GetTypeMembers()) + { + foreach (var result in GetSourceTypeAndNestedTypes( + nestedType, + compilation, + generatedCodeTrees, + semanticModels)) + { + yield return result; + } + } + } + + private static bool IsGeneratedCode( + INamedTypeSymbol type, + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) + { + var declarations = type.DeclaringSyntaxReferences; + return !declarations.IsEmpty + && declarations.All(declaration => + IsGeneratedCode(declaration.SyntaxTree, generatedCodeTrees) + || HasGeneratedCodeAttribute(declaration.GetSyntax(), compilation, semanticModels)); + } + + private static bool HasGeneratedCodeAttribute( + SyntaxNode declaration, + Compilation compilation, + Dictionary semanticModels) + { + if (!semanticModels.TryGetValue(declaration.SyntaxTree, out var semanticModel)) + { + semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree); + semanticModels[declaration.SyntaxTree] = semanticModel; + } + + return declaration.AncestorsAndSelf() + .OfType() + .SelectMany(type => type.AttributeLists) + .SelectMany(list => list.Attributes) + .Select(attribute => semanticModel.GetSymbolInfo(attribute).Symbol as IMethodSymbol) + .Select(constructor => constructor?.ContainingType.ToDisplayString()) + .Any(attributeType => attributeType is + "System.CodeDom.Compiler.GeneratedCodeAttribute" + or "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); + } + + private static bool IsGeneratedCode( + SyntaxTree syntaxTree, + Dictionary generatedCodeTrees) + { + if (generatedCodeTrees.TryGetValue(syntaxTree, out var result)) + { + return result; + } + + var fileName = Path.GetFileName(syntaxTree.FilePath); + result = fileName.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".generated.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".designer.cs", StringComparison.OrdinalIgnoreCase); + if (!result) + { + var text = syntaxTree.GetText(); + var prefixLength = Math.Min(text.Length, 2048); + result = text.ToString(new TextSpan(0, prefixLength)) + .IndexOf("= 0; + } + + generatedCodeTrees[syntaxTree] = result; + return result; + } + + private static void AppendInterface( + List lines, + INamedTypeSymbol type, + HashSet activeInterfaceIdentities, + HashSet activeConventionInterfaceNames, + Dictionary activeInterfaces, + Dictionary activeInterfacesByName) + { + AppendBlockSeparator(lines); + var interfaceName = GetFullyQualifiedName(type); + var explicitInterfaceType = GetGrainInterfaceTypeFromAttributes(type); + var interfaceType = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(type); + activeInterfaceIdentities.Add(interfaceType); + activeInterfaces[interfaceType] = type; + activeInterfacesByName[interfaceName] = type; + if (explicitInterfaceType is null + || string.Equals( + explicitInterfaceType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(type), + StringComparison.Ordinal)) + { + activeConventionInterfaceNames.Add(interfaceName); + } + if (GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitInterfaceType, type)) + { + lines.Add($"# {interfaceName}"); + } + + lines.Add($"interface [GrainInterfaceType(\"{interfaceType}\")] {interfaceName} [Version({GetVersionFromAttributes(type)})]"); + foreach (var member in type.GetMembers() + .OfType() + .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) + .OrderBy(GrainInterfaceVersionAnalyzer.GetMethodSignature, StringComparer.Ordinal)) + { + if (GrainInterfaceVersionAnalyzer.RequiresClrComment(member)) + { + lines.Add($" # {GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member)}"); + } + + lines.Add($" {FormatStoredMember( + GrainInterfaceVersionAnalyzer.GetMethodSignature(member), + GetAliasFromAttributes(member))}"); + } + } + + private static void AppendGrainClass( + List lines, + INamedTypeSymbol type, + HashSet activeClassIdentities, + HashSet activeConventionClassNames) + { + AppendBlockSeparator(lines); + var className = GetFullyQualifiedName(type); + var explicitGrainType = GetGrainTypeFromAttributes(type); + var grainType = GrainInterfaceVersionAnalyzer.GetGrainType(type); + activeClassIdentities.Add(grainType); + if (explicitGrainType is null + || string.Equals( + explicitGrainType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainType(type), + StringComparison.Ordinal)) + { + activeConventionClassNames.Add(className); + } + if (GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitGrainType, type)) + { + lines.Add($"# {className}"); + } + + lines.Add($"class [GrainType(\"{grainType}\")] {className}"); + } + + private static void AppendHistoricalContracts( + List result, + string existingContent, + HashSet activeInterfaceIdentities, + HashSet activeConventionInterfaceNames, + Dictionary activeInterfaces, + Dictionary activeInterfacesByName, + HashSet activeClassIdentities, + HashSet activeConventionClassNames) + { + var lines = existingContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + for (var index = 0; index < lines.Length; index++) + { + var declaration = lines[index].Trim(); + var isInterface = GrainInterfaceFileParser.TryGetInterfaceName(declaration, out var contractName); + var isClass = !isInterface && GrainInterfaceFileParser.TryGetGrainClassName(declaration, out contractName); + if (!isInterface && !isClass) + { + continue; + } + + string? explicitIdentity = null; + if (isInterface && GrainInterfaceFileParser.TryGetGrainInterfaceType(declaration, out var interfaceType)) + { + explicitIdentity = interfaceType; + } + else if (isClass && GrainInterfaceFileParser.TryGetGrainClassType(declaration, out var grainType)) + { + explicitIdentity = grainType; + } + + var isActive = isInterface + ? explicitIdentity is null + ? activeInterfaceIdentities.Contains( + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(contractName)) + || activeConventionInterfaceNames.Contains(contractName) + : activeInterfaceIdentities.Contains(explicitIdentity) + : explicitIdentity is null + ? activeClassIdentities.Contains(GrainInterfaceVersionAnalyzer.GetDefaultGrainType(contractName)) + || activeConventionClassNames.Contains(contractName) + : activeClassIdentities.Contains(explicitIdentity); + var blockEnd = index + 1; + while (blockEnd < lines.Length) + { + if (GrainInterfaceFileParser.TryGetContractName(lines[blockEnd], out _)) + { + break; + } + + if (lines[blockEnd].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && blockEnd + 1 < lines.Length + && GrainInterfaceFileParser.TryGetContractName(lines[blockEnd + 1], out _)) + { + break; + } + + blockEnd++; + } + + if (!isActive) + { + AppendBlockSeparator(result); + if (index > 0 + && lines[index - 1].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && !IsGeneratedHeaderLine(lines[index - 1].TrimStart())) + { + result.Add(lines[index - 1].Trim()); + } + + for (var blockIndex = index; blockIndex < blockEnd; blockIndex++) + { + result.Add(blockIndex == index + && !declaration.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal) + ? $"{GrainInterfaceVersionAnalyzer.RetiredPrefix} {declaration}" + : lines[blockIndex]); + } + } + else if (isInterface) + { + var interfaceIdentity = explicitIdentity + ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(contractName); + if (activeInterfaces.TryGetValue(interfaceIdentity, out var activeInterface) + || activeConventionInterfaceNames.Contains(contractName) + && activeInterfacesByName.TryGetValue(contractName, out activeInterface)) + { + MergeHistoricalMembers( + result, + lines, + index, + blockEnd, + contractName, + activeInterface); + } + } + + index = blockEnd - 1; + } + } + + private static void MergeHistoricalMembers( + List result, + string[] historicalLines, + int historicalDeclarationIndex, + int historicalBlockEnd, + string historicalInterfaceName, + INamedTypeSymbol activeInterface) + { + var generatedDeclarationIndex = -1; + var activeInterfaceIdentity = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(activeInterface); + var activeInterfaceName = GetFullyQualifiedName(activeInterface); + for (var index = 0; index < result.Count; index++) + { + if (!GrainInterfaceFileParser.TryGetInterfaceName(result[index], out var generatedInterfaceName)) + { + continue; + } + + var generatedIdentity = GrainInterfaceFileParser.TryGetGrainInterfaceType( + result[index], + out var generatedInterfaceType) + ? generatedInterfaceType + : GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(generatedInterfaceName); + if (string.Equals(generatedIdentity, activeInterfaceIdentity, StringComparison.Ordinal) + || string.Equals(generatedInterfaceName, activeInterfaceName, StringComparison.Ordinal)) + { + generatedDeclarationIndex = index; + break; + } + } + + if (generatedDeclarationIndex < 0) + { + return; + } + + var generatedBlockEnd = generatedDeclarationIndex + 1; + var generatedMembers = new HashSet(StringComparer.Ordinal); + while (generatedBlockEnd < result.Count) + { + if (GrainInterfaceFileParser.TryGetContractName(result[generatedBlockEnd], out _) + || result[generatedBlockEnd].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && generatedBlockEnd + 1 < result.Count + && GrainInterfaceFileParser.TryGetContractName(result[generatedBlockEnd + 1], out _)) + { + break; + } + + if (GrainInterfaceFileParser.TryGetMemberSignature(result[generatedBlockEnd], out var memberSignature)) + { + generatedMembers.Add(GetHistoricalMemberKey(memberSignature, historicalInterfaceName)); + } + + generatedBlockEnd++; + } + + string? pendingComment = null; + for (var index = historicalDeclarationIndex + 1; index < historicalBlockEnd; index++) + { + var line = historicalLines[index]; + if (line.TrimStart().StartsWith("# ", StringComparison.Ordinal)) + { + pendingComment = line.Trim(); + continue; + } + + if (!GrainInterfaceFileParser.TryGetMemberDeclaration( + line, + out var memberSignature, + out var memberAlias)) + { + pendingComment = null; + continue; + } + + if (activeInterface.GetMembers() + .OfType() + .Any(member => member.MethodKind == MethodKind.Ordinary + && !member.IsStatic + && GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( + historicalInterfaceName, + memberSignature, + memberAlias, + member))) + { + pendingComment = null; + continue; + } + + var normalizedSignature = GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature( + GrainInterfaceFileParser.GetCanonicalMemberSignature(memberSignature, memberAlias), + historicalInterfaceName); + var historicalMemberKey = GetHistoricalMemberKey(normalizedSignature, historicalInterfaceName); + if (generatedMembers.Add(historicalMemberKey)) + { + if (pendingComment is not null) + { + result.Insert(generatedBlockEnd++, $" {pendingComment}"); + } + + result.Insert(generatedBlockEnd++, $" {FormatStoredMember(historicalMemberKey, memberAlias)}"); + } + + pendingComment = null; + } + } + + private static string GetHistoricalMemberKey(string signature, string interfaceName) + => GrainInterfaceVersionAnalyzer.NormalizeLegacyMethodSignature( + GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature(signature, interfaceName)); + + private static void AppendBlockSeparator(List lines) + { + while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[lines.Count - 1])) + { + lines.RemoveAt(lines.Count - 1); + } + + if (lines.Count > 0) + { + lines.Add(string.Empty); + } + } + + private static string GetFullyQualifiedName(INamedTypeSymbol type) + => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + + private sealed class OrleansContractsFixAllProvider : FixAllProvider + { + public static OrleansContractsFixAllProvider Instance { get; } = new(); + + public override Task GetFixAsync(FixAllContext fixAllContext) + { + if (!string.Equals( + fixAllContext.CodeActionEquivalenceKey, + RegenerateCodeActionEquivalenceKey, + StringComparison.Ordinal)) + { + return Task.FromResult(null); + } + + var title = fixAllContext.Scope == FixAllScope.Solution + ? "Regenerate OrleansContracts.txt in solution" + : "Regenerate OrleansContracts.txt in project"; + return Task.FromResult(CodeAction.Create( + title, + cancellationToken => RegenerateFixAllAsync(fixAllContext, cancellationToken), + RegenerateCodeActionEquivalenceKey)); + } + + private static async Task RegenerateFixAllAsync( + FixAllContext fixAllContext, + CancellationToken cancellationToken) + { + var solution = fixAllContext.Solution; + var projectIds = new List(); + if (fixAllContext.Scope == FixAllScope.Solution) + { + foreach (var project in solution.Projects.Where(project => project.Language == LanguageNames.CSharp)) + { + if (!(await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false)).IsEmpty) + { + projectIds.Add(project.Id); + } + } + } + else + { + projectIds.Add(fixAllContext.Project.Id); + } + + foreach (var projectId in projectIds) + { + cancellationToken.ThrowIfCancellationRequested(); + if (solution.GetProject(projectId) is { } project) + { + solution = await RegenerateOrleansContractsFileAsync(project, cancellationToken).ConfigureAwait(false); + } + } + + return solution; + } + } + private static void RegisterAddInterfaceCodeFix(CodeFixContext context, Diagnostic diagnostic) { if (!diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey, out var interfaceName) || @@ -131,12 +807,20 @@ private static void RegisterAddMemberCodeFix(CodeFixContext context, Diagnostic } diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberClrSignaturePropertyKey, out var memberClrSignature); + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberAliasPropertyKey, out var memberAlias); diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( CodeAction.Create( title: Resources.AddToOrleansContractsFileTitle, - createChangedSolution: ct => AddMemberToFileAsync(context.Document, interfaceName!, grainInterfaceType, memberSignature!, memberClrSignature, ct), + createChangedSolution: ct => AddMemberToFileAsync( + context.Document, + interfaceName!, + grainInterfaceType, + memberSignature!, + memberAlias, + memberClrSignature, + ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0018), diagnostic); } @@ -149,10 +833,11 @@ private static void RegisterRetireInterfaceCodeFix(CodeFixContext context, Diagn return; } + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( CodeAction.Create( title: Resources.RetireGrainInterfaceTitle, - createChangedSolution: ct => RetireInterfaceInFileAsync(context.Document, interfaceName!, ct), + createChangedSolution: ct => RetireInterfaceInFileAsync(context.Document, interfaceName!, grainInterfaceType, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0019), diagnostic); } @@ -198,10 +883,11 @@ private static void RegisterRetireGrainClassCodeFix(CodeFixContext context, Diag return; } + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.ActualAliasPropertyKey, out var grainType); context.RegisterCodeFix( CodeAction.Create( title: Resources.RetireGrainClassTitle, - createChangedSolution: ct => RetireGrainClassInFileAsync(context.Document, className!, ct), + createChangedSolution: ct => RetireGrainClassInFileAsync(context.Document, className!, grainType, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0024), diagnostic); } @@ -237,8 +923,7 @@ private static async Task AddGrainClassToFileAsync( var grainType = GrainInterfaceVersionAnalyzer.GetGrainType(classSymbol); var classClrComment = GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitGrainType, classSymbol) ? className : null; var classLine = $"class [GrainType(\"{grainType}\")] {className}"; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return solution; @@ -263,7 +948,7 @@ private static async Task AddGrainClassToFileAsync( var reactivatedContent = SortContractEntries(string.Join(newLine, updatedLines), newLine); return solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Utf8NoBom)); } } @@ -280,7 +965,7 @@ private static async Task AddGrainClassToFileAsync( content = SortContractEntries(content + classLine, newLine); return solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task UpdateGrainClassAliasInFileAsync( @@ -290,8 +975,7 @@ private static async Task UpdateGrainClassAliasInFileAsync( CancellationToken cancellationToken) { var project = document.Project; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return project.Solution; @@ -323,17 +1007,17 @@ private static async Task UpdateGrainClassAliasInFileAsync( var content = SortContractEntries(string.Join(newLine, lines), newLine); return project.Solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task RetireGrainClassInFileAsync( Document document, string className, + string? grainType, CancellationToken cancellationToken) { var project = document.Project; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return project.Solution; @@ -350,8 +1034,7 @@ private static async Task RetireGrainClassInFileAsync( for (var i = 0; i < lines.Length; i++) { var trimmedLine = lines[i].Trim(); - if (GrainInterfaceFileParser.TryGetGrainClassName(trimmedLine, out var declaredName) - && string.Equals(declaredName, className, StringComparison.Ordinal) + if (IsMatchingGrainClass(trimmedLine, className, grainType) && !trimmedLine.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal)) { lines[i] = $"{GrainInterfaceVersionAnalyzer.RetiredPrefix} {trimmedLine}"; @@ -362,7 +1045,7 @@ private static async Task RetireGrainClassInFileAsync( var content = SortContractEntries(string.Join(newLine, lines), newLine); return project.Solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task AddInterfaceToFileAsync( @@ -423,7 +1106,7 @@ private static async Task AddInterfaceToFileAsync( var interfaceLine = sb.ToString(); // Build member lines - var memberLines = new List<(string Signature, string? ClrSignature)>(); + var memberLines = new List<(string Signature, string? Alias, string? ClrSignature)>(); foreach (var member in symbol.GetMembers().OfType()) { if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) @@ -434,14 +1117,14 @@ private static async Task AddInterfaceToFileAsync( var memberSignature = GrainInterfaceVersionAnalyzer.GetMethodSignature(member); memberLines.Add(( memberSignature, + GetAliasFromAttributes(member), GrainInterfaceVersionAnalyzer.RequiresClrComment(member) ? GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member) : null)); } // Find or create the OrleansContracts.txt file - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is not null) { @@ -458,7 +1141,7 @@ private static async Task AddInterfaceToFileAsync( i = SetClrCommentBefore(updatedLines, i, interfaceClrComment); updatedLines[i] = interfaceLine; var reactivatedContent = SortContractEntries(string.Join(newLine, updatedLines), newLine); - var reactivatedText = Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Encoding.UTF8); + var reactivatedText = Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, reactivatedText); } } @@ -479,11 +1162,11 @@ private static async Task AddInterfaceToFileAsync( { newContent += $"{newLine} # {member.ClrSignature}"; } - newContent += $"{newLine} {member.Signature}"; + newContent += $"{newLine} {FormatStoredMember(member.Signature, member.Alias)}"; } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); solution = solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } else @@ -500,18 +1183,15 @@ private static async Task AddInterfaceToFileAsync( { AppendLine(content, $" # {member.ClrSignature}", DefaultNewLine); } - AppendLine(content, $" {member.Signature}", DefaultNewLine); + AppendLine(content, $" {FormatStoredMember(member.Signature, member.Alias)}", DefaultNewLine); } - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Encoding.UTF8); - var projectDir = Path.GetDirectoryName(project.FilePath); - var filePath = projectDir is not null - ? Path.Combine(projectDir, Constants.OrleansContractsFileName) - : Constants.OrleansContractsFileName; + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Utf8NoBom); + var filePath = GetConfiguredContractsPath(project); solution = solution.AddAdditionalDocument( DocumentId.CreateNewId(project.Id), - Constants.OrleansContractsFileName, + Path.GetFileName(filePath), newText, filePath: filePath); } @@ -529,8 +1209,7 @@ private static async Task UpdateVersionInFileAsync( var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -578,7 +1257,7 @@ private static async Task UpdateVersionInFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -587,14 +1266,14 @@ private static async Task AddMemberToFileAsync( string interfaceName, string? grainInterfaceType, string memberSignature, + string? memberAlias, string? memberClrSignature, CancellationToken cancellationToken) { var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -624,7 +1303,7 @@ private static async Task AddMemberToFileAsync( || trimmedLine.StartsWith("#", StringComparison.Ordinal) || GrainInterfaceFileParser.TryGetContractName(trimmedLine, out _))) { - AppendMember(newLines, memberSignature, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); insertedMember = true; } @@ -640,7 +1319,7 @@ private static async Task AddMemberToFileAsync( // If we didn't insert the member yet, append it at the end if (foundInterface && !insertedMember) { - AppendMember(newLines, memberSignature, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); } // Remove trailing newline added by AppendLine @@ -651,20 +1330,20 @@ private static async Task AddMemberToFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } private static async Task RetireInterfaceInFileAsync( Document document, string interfaceName, + string? grainInterfaceType, CancellationToken cancellationToken) { var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -686,8 +1365,7 @@ private static async Task RetireInterfaceInFileAsync( var trimmedLine = line.Trim(); // Check if this line contains the interface declaration - if (GrainInterfaceFileParser.TryGetInterfaceName(trimmedLine, out var declaredInterfaceName) - && string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal) && + if (IsMatchingInterface(trimmedLine, interfaceName, grainInterfaceType) && !trimmedLine.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal)) { // Add *RETIRED* prefix @@ -706,7 +1384,7 @@ private static async Task RetireInterfaceInFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -734,12 +1412,30 @@ private static bool IsMatchingInterface(string line, string interfaceName, strin { return GrainInterfaceFileParser.TryGetGrainInterfaceType(line, out var declaredGrainInterfaceType) ? string.Equals(declaredGrainInterfaceType, grainInterfaceType, StringComparison.Ordinal) - : string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); + : string.Equals( + grainInterfaceType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(interfaceName), + StringComparison.Ordinal) + && string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); } return string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); } + private static bool IsMatchingGrainClass(string line, string className, string? grainType) + { + if (!GrainInterfaceFileParser.TryGetGrainClassName(line, out var declaredClassName) + || !string.Equals(declaredClassName, className, StringComparison.Ordinal)) + { + return false; + } + + return grainType is null + ? !GrainInterfaceFileParser.TryGetGrainClassType(line, out _) + : GrainInterfaceFileParser.TryGetGrainClassType(line, out var declaredGrainType) + && string.Equals(declaredGrainType, grainType, StringComparison.Ordinal); + } + private static bool IdentityDiffersFromClrName(string? identity, string fullName) { if (identity is null) @@ -823,13 +1519,20 @@ private static string SortContractEntries(string content, string newLine) } preamble.Add(line); } - else if (GrainInterfaceFileParser.TryGetMemberSignature(line, out var memberSignature)) + else if (GrainInterfaceFileParser.TryGetMemberDeclaration( + line, + out var storedMemberSignature, + out var memberAlias)) { + var memberSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( + storedMemberSignature, + memberAlias); var inlineComment = GrainInterfaceFileParser.GetClrComment(line); var normalizedMemberLine = NormalizeMemberLine(memberSignature, currentBlock.Name); currentBlock.Members.Add(( normalizedMemberLine, normalizedMemberLine, + memberAlias, pendingComment ?? (inlineComment.Length > 0 ? NormalizeComment(inlineComment) : null))); pendingComment = null; } @@ -882,7 +1585,7 @@ private static string SortContractEntries(string content, string newLine) { result.Add($" {member.ClrComment}"); } - result.Add($" {member.Line}"); + result.Add($" {FormatStoredMember(member.Line, member.Alias)}"); } result.AddRange(block.OtherLines); } @@ -893,6 +1596,10 @@ private static string SortContractEntries(string content, string newLine) private static bool IsGeneratedHeaderLine(string line) => GeneratedHeader.Contains(line, StringComparer.Ordinal) || line is "# OrleansContracts.txt" + or "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope." + or "# This file is auto-generated by the Orleans contract analyzer." + or "# Update source contracts, then regenerate this file by following:" + or "# https://aka.ms/orleans/OrleansContracts.txt" or "# This file tracks grain interface versions for compatibility during rolling upgrades." or "# Format:" or "# # Namespace.GrainClass" @@ -941,6 +1648,7 @@ private static void AppendLine(StringBuilder builder, string value, string newLi private static void AppendMember( StringBuilder builder, string memberSignature, + string? memberAlias, string? memberClrSignature, string newLine) { @@ -949,9 +1657,14 @@ private static void AppendMember( AppendLine(builder, $" # {memberClrSignature}", newLine); } - AppendLine(builder, $" {memberSignature}", newLine); + AppendLine(builder, $" {FormatStoredMember(memberSignature, memberAlias)}", newLine); } + private static string FormatStoredMember(string memberSignature, string? memberAlias) + => memberAlias is null + ? memberSignature + : $"[Alias(\"{memberAlias}\")] {memberSignature}"; + private static ushort GetVersionFromAttributes(ISymbol symbol) { foreach (var attribute in symbol.GetAttributes()) @@ -1031,7 +1744,7 @@ public ContractBlock(string name, string declaration, string? clrComment) public string? ClrComment { get; } - public List<(string Signature, string Line, string? ClrComment)> Members { get; } = new(); + public List<(string Signature, string Line, string? Alias, string? ClrComment)> Members { get; } = new(); public List OtherLines { get; } = new(); } diff --git a/src/Orleans.Analyzers/Orleans.Analyzers.csproj b/src/Orleans.Analyzers/Orleans.Analyzers.csproj index 4675a5de0d..d68d00de2e 100644 --- a/src/Orleans.Analyzers/Orleans.Analyzers.csproj +++ b/src/Orleans.Analyzers/Orleans.Analyzers.csproj @@ -19,6 +19,11 @@ + + + + + diff --git a/src/Orleans.Analyzers/Resources.Designer.cs b/src/Orleans.Analyzers/Resources.Designer.cs index c02c461d0f..5b3f26601b 100644 --- a/src/Orleans.Analyzers/Resources.Designer.cs +++ b/src/Orleans.Analyzers/Resources.Designer.cs @@ -409,6 +409,24 @@ internal static string GrainInterfaceMemberNotDeclaredTitle { return ResourceManager.GetString("GrainInterfaceMemberNotDeclaredTitle", resourceCulture); } } + + internal static string GrainInterfaceMemberRemovedDescription { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedDescription", resourceCulture); + } + } + + internal static string GrainInterfaceMemberRemovedMessageFormat { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedMessageFormat", resourceCulture); + } + } + + internal static string GrainInterfaceMemberRemovedTitle { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedTitle", resourceCulture); + } + } /// /// Looks up a localized string similar to All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades.. diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 4cbbf0c2f8..6cfbcce209 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -218,7 +218,7 @@ Grain interface is not active in OrleansContracts.txt - Grain interface '{0}' does not have an active declaration in OrleansContracts.txt + Grain interface '{0}' does not have an active declaration in OrleansContracts.txt. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades. @@ -227,7 +227,7 @@ Grain interface version mismatch - Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code + Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [Version] attribute on the grain interface must match the version declared in OrleansContracts.txt. @@ -236,16 +236,25 @@ Grain interface member not declared in OrleansContracts.txt - Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}' + Grain interface source member '{2}' with wire signature '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. + + Grain interface member removed from source + + + Grain interface member '{0}' is declared in OrleansContracts.txt for interface '{1}' but is absent from source. Restore the method or explicitly remove the manifest entry after reviewing the wire break. See https://aka.ms/orleans/OrleansContracts.txt for details. + + + Removing a grain interface method can break calls during a rolling upgrade. OrleansContracts.txt retains the signature until the removal is explicitly accepted. + Removed grain interface not marked as retired - Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code - mark it as *RETIRED* + Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code. Run the regeneration command in the file header for the owning project or solution to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain interface, mark it as *RETIRED* in OrleansContracts.txt to document that it has been intentionally removed. @@ -254,7 +263,7 @@ OrleansContracts.txt file is missing - The project contains grain interfaces but no {0} file to track them + The project contains Orleans contracts but no {0} file. From the repository root, replace PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path and run 'dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024'. Review the generated baseline. See https://aka.ms/orleans/OrleansContracts.txt for details. Add an OrleansContracts.txt file to track Orleans contracts for compatibility during rolling upgrades. @@ -281,7 +290,7 @@ Grain class is not active in OrleansContracts.txt - Grain class '{0}' does not have an active declaration in OrleansContracts.txt + Grain class '{0}' does not have an active declaration in OrleansContracts.txt. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All concrete grain classes should have an active declaration in OrleansContracts.txt to protect grain type identities during renames. @@ -290,7 +299,7 @@ Grain class alias mismatch - Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code + Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [GrainType] attribute on a grain class must match the alias declared in OrleansContracts.txt. @@ -299,7 +308,7 @@ Removed grain class not marked as retired - Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code - mark it as *RETIRED* + Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code. Run the regeneration command in the file header for the owning project or solution to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain class, mark it as *RETIRED* in OrleansContracts.txt to preserve its contract history. diff --git a/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props b/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props index 7d4869b7bd..bb67bff9c8 100644 --- a/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props +++ b/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props @@ -5,5 +5,6 @@ + diff --git a/src/Orleans.BroadcastChannel/OrleansContracts.txt b/src/Orleans.BroadcastChannel/OrleansContracts.txt index b3edafef67..2df212605c 100644 --- a/src/Orleans.BroadcastChannel/OrleansContracts.txt +++ b/src/Orleans.BroadcastChannel/OrleansContracts.txt @@ -1,7 +1,15 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension")] Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension [Version(0)] - OnError(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task - OnPublished(Orleans.BroadcastChannel.InternalChannelId, object) -> Task + # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnError(InternalChannelId streamId, Exception exception) -> Task + 73F72B20(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task + # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnPublished(InternalChannelId streamId, object item) -> Task + B1E55518(Orleans.BroadcastChannel.InternalChannelId, object) -> Task diff --git a/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs b/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs new file mode 100644 index 0000000000..cd74257e6b --- /dev/null +++ b/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs @@ -0,0 +1,62 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Orleans.CodeGenerator.Hashing; + +namespace Orleans.CodeGenerator; + +internal static class MethodIdProvider +{ + public static string Create(IMethodSymbol method) + { + var signature = Format(method); + var hash = XxHash32.Hash(Encoding.UTF8.GetBytes(signature)); + var result = new StringBuilder(hash.Length * 2); + foreach (var value in hash) + { + result.Append(value.ToString("X2")); + } + + return result.ToString(); + } + + private static string Format(IMethodSymbol method) + { + var result = new StringBuilder(); + result.Append(method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + result.Append('.'); + result.Append(method.Name); + + if (method.IsGenericMethod) + { + result.Append('<'); + for (var index = 0; index < method.TypeArguments.Length; index++) + { + if (index > 0) + { + result.Append(','); + } + + result.Append(method.TypeArguments[index].Name); + } + + result.Append('>'); + } + + result.Append('('); + for (var index = 0; index < method.Parameters.Length; index++) + { + if (index > 0) + { + result.Append(','); + } + + var parameterType = method.Parameters[index].Type; + result.Append(parameterType is ITypeParameterSymbol + ? parameterType.Name + : parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + result.Append(')'); + return result.ToString(); + } +} diff --git a/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs b/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs index a7660c72e5..6c1ee36f49 100644 --- a/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs +++ b/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs @@ -1,7 +1,5 @@ -using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Orleans.CodeGenerator.Hashing; using Orleans.CodeGenerator.SyntaxGeneration; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Orleans.CodeGenerator.SyntaxGeneration.SymbolExtensions; @@ -26,62 +24,7 @@ internal static class GeneratedCodeUtilities } internal static string CreateHashedMethodId(IMethodSymbol methodSymbol) - { - var methodSignature = Format(methodSymbol); - var hash = XxHash32.Hash(Encoding.UTF8.GetBytes(methodSignature)); - return $"{HexConverter.ToString(hash)}"; - - static string Format(IMethodSymbol methodInfo) - { - var result = new StringBuilder(); - result.Append(methodInfo.ContainingType.ToDisplayName()); - result.Append('.'); - result.Append(methodInfo.Name); - - if (methodInfo.IsGenericMethod) - { - result.Append('<'); - var first = true; - foreach (var typeArgument in methodInfo.TypeArguments) - { - if (!first) result.Append(','); - else first = false; - result.Append(typeArgument.Name); - } - - result.Append('>'); - } - - { - result.Append('('); - var parameters = methodInfo.Parameters; - var first = true; - foreach (var parameter in parameters) - { - if (!first) - { - result.Append(','); - } - - var parameterType = parameter.Type; - switch (parameterType) - { - case ITypeParameterSymbol _: - result.Append(parameterType.Name); - break; - default: - result.Append(parameterType.ToDisplayName()); - break; - } - - first = false; - } - } - - result.Append(')'); - return result.ToString(); - } - } + => MethodIdProvider.Create(methodSymbol); internal static string? GetAlias(LibraryTypes libraryTypes, ISymbol symbol) => (string?)symbol.GetAttribute(libraryTypes.AliasAttribute)?.ConstructorArguments.First().Value; diff --git a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj index 6fb0ee8e32..805e0e1461 100644 --- a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj +++ b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Orleans.Core.Abstractions/OrleansContracts.txt b/src/Orleans.Core.Abstractions/OrleansContracts.txt index be4406a54a..1f7df80e61 100644 --- a/src/Orleans.Core.Abstractions/OrleansContracts.txt +++ b/src/Orleans.Core.Abstractions/OrleansContracts.txt @@ -1,10 +1,18 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Core.Internal.IGrainManagementExtension")] Orleans.Core.Internal.IGrainManagementExtension [Version(0)] - DeactivateOnIdle() -> ValueTask - MigrateOnIdle() -> ValueTask + # Orleans.Core.Internal.IGrainManagementExtension.DeactivateOnIdle() -> ValueTask + 1B9614D1() -> ValueTask + # Orleans.Core.Internal.IGrainManagementExtension.MigrateOnIdle() -> ValueTask + 4CC93B45() -> ValueTask interface [GrainInterfaceType("Orleans.IGrain")] Orleans.IGrain [Version(0)] @@ -23,14 +31,20 @@ interface [GrainInterfaceType("Orleans.IGrainWithStringKey")] Orleans.IGrainWith interface [GrainInterfaceType("Orleans.ISystemTarget")] Orleans.ISystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.Runtime.IAsyncEnumerableGrainExtension")] Orleans.Runtime.IAsyncEnumerableGrainExtension [Version(0)] - DisposeAsync(System.Guid) -> ValueTask - MoveNext`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - MoveNext`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request) -> ValueTask<(EnumerationResult Status, object? Value)> + 370CD5AB`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.DisposeAsync(Guid requestId) -> ValueTask + 3C6D7209(System.Guid) -> ValueTask + # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> + 8678B466`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId) -> ValueTask<(EnumerationResult Status, object? Value)> + A7FA7E30`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> + E60EA75B`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> interface [GrainInterfaceType("Orleans.Runtime.ICancellationSourcesExtension")] Orleans.Runtime.ICancellationSourcesExtension [Version(0)] - CancelRemoteToken(System.Guid) -> Task + # Orleans.Runtime.ICancellationSourcesExtension.CancelRemoteToken(Guid tokenId) -> Task + 50F75C16(System.Guid) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainExtension")] Orleans.Runtime.IGrainExtension [Version(0)] diff --git a/src/Orleans.Core/OrleansContracts.txt b/src/Orleans.Core/OrleansContracts.txt index f90b29fe0a..f41e5afc4d 100644 --- a/src/Orleans.Core/OrleansContracts.txt +++ b/src/Orleans.Core/OrleansContracts.txt @@ -1,80 +1,133 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.ClientObservers.IClientGatewayObserver")] Orleans.ClientObservers.IClientGatewayObserver [Version(0)] - StopSendingToGateway(Orleans.Runtime.SiloAddress) -> void + # Orleans.ClientObservers.IClientGatewayObserver.StopSendingToGateway(SiloAddress gateway) -> void + AFB768FD(Orleans.Runtime.SiloAddress) -> void interface [GrainInterfaceType("Orleans.IMembershipTableSystemTarget")] Orleans.IMembershipTableSystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.ISiloControl")] Orleans.ISiloControl [Version(0)] - ForceActivationCollection(System.TimeSpan) -> Task - ForceGarbageCollection() -> Task - ForceRuntimeStatisticsCollection() -> Task - GetActivationCount() -> Task - GetActiveGrains(Orleans.Runtime.GrainType) -> Task> - GetDetailedGrainReport(Orleans.Runtime.GrainId) -> Task - GetDetailedGrainStatistics(string[]) -> Task> - GetGrainStatistics() -> Task>> - GetRuntimeStatistics() -> Task - GetSimpleGrainStatistics() -> Task - MigrateRandomActivations(Orleans.Runtime.SiloAddress, int) -> Task - Ping(string) -> Task - SendControlCommandToProvider`1(string, int, object?) -> Task + # Orleans.ISiloControl.ForceRuntimeStatisticsCollection() -> Task + 0C7DBD0C() -> Task + # Orleans.ISiloControl.Ping(string message) -> Task + 1422B0B7(string) -> Task + # Orleans.ISiloControl.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task + 355CA3FA`1(string, int, object?) -> Task + # Orleans.ISiloControl.GetDetailedGrainReport(GrainId grainId) -> Task + 45172562(Orleans.Runtime.GrainId) -> Task + # Orleans.ISiloControl.ForceActivationCollection(TimeSpan ageLimit) -> Task + 45D07D09(System.TimeSpan) -> Task + # Orleans.ISiloControl.GetSimpleGrainStatistics() -> Task + 6DE16EF7() -> Task + # Orleans.ISiloControl.GetActiveGrains(GrainType grainType) -> Task> + 85797C87(Orleans.Runtime.GrainType) -> Task> + # Orleans.ISiloControl.GetDetailedGrainStatistics(string[]? types) -> Task> + B0F4C24B(string[]) -> Task> + # Orleans.ISiloControl.GetActivationCount() -> Task + C4C370A5() -> Task + # Orleans.ISiloControl.MigrateRandomActivations(SiloAddress target, int count) -> Task + E8327F0B(Orleans.Runtime.SiloAddress, int) -> Task + # Orleans.ISiloControl.GetRuntimeStatistics() -> Task + F18EAF24() -> Task + # Orleans.ISiloControl.ForceGarbageCollection() -> Task + F388CED1() -> Task + # Orleans.ISiloControl.GetGrainStatistics() -> Task>> + FF707A30() -> Task>> interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerMonitor")] Orleans.Placement.Rebalancing.IActivationRebalancerMonitor [Version(0)] - Report(RebalancingReport) -> Task + [Alias("Report")] Report(RebalancingReport) -> Task interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerWorker")] Orleans.Placement.Rebalancing.IActivationRebalancerWorker [Version(0)] - GetReport() -> ValueTask - ResumeRebalancing() -> Task - SuspendRebalancing(System.TimeSpan?) -> Task + [Alias("GetReport")] GetReport() -> ValueTask + [Alias("ResumeRebalancing")] ResumeRebalancing() -> Task + [Alias("SuspendRebalancing")] SuspendRebalancing(System.TimeSpan?) -> Task interface [GrainInterfaceType("Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget")] Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget [Version(0)] - AcceptExchangeRequest(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask - FlushBuffers() -> ValueTask - GetActivationCount() -> ValueTask - GetGrainCallFrequencies() -> ValueTask> - ResetCounters() -> ValueTask - SetActivationCountOffset(int) -> ValueTask - TriggerExchangeRequest() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.FlushBuffers() -> ValueTask + 11731652() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.SetActivationCountOffset(int activationCountOffset) -> ValueTask + 135356E5(int) -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.ResetCounters() -> ValueTask + 21852A09() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.AcceptExchangeRequest(AcceptExchangeRequest request) -> ValueTask + 9D8EDC44(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetActivationCount() -> ValueTask + 9FB525F3() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.TriggerExchangeRequest() -> ValueTask + A6EE4757() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetGrainCallFrequencies() -> ValueTask> + C4497899() -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.IClusterManifestSystemTarget")] Orleans.Runtime.IClusterManifestSystemTarget [Version(0)] - GetClusterManifest() -> ValueTask - GetClusterManifestUpdate(Orleans.Metadata.MajorMinorVersion) -> ValueTask + # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifest() -> ValueTask + 40D39F85() -> ValueTask + # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifestUpdate(MajorMinorVersion previousVersion) -> ValueTask + 4EFCA109(Orleans.Metadata.MajorMinorVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IDeploymentLoadPublisher")] Orleans.Runtime.IDeploymentLoadPublisher [Version(0)] - UpdateRuntimeStatistics(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task + # Orleans.Runtime.IDeploymentLoadPublisher.UpdateRuntimeStatistics(SiloAddress siloAddress, SiloRuntimeStatistics siloStats) -> Task + C5255F0C(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationExtension")] Orleans.Runtime.IGrainCallCancellationExtension [Version(0)] - CancelRequestAsync(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask + # Orleans.Runtime.IGrainCallCancellationExtension.CancelRequestAsync(GrainId senderGrainId, CorrelationId messageId) -> ValueTask + FA239824(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IManagementGrain")] Orleans.Runtime.IManagementGrain [Version(0)] - ForceActivationCollection(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task - ForceActivationCollection(System.TimeSpan) -> Task - ForceGarbageCollection(Orleans.Runtime.SiloAddress[]) -> Task - ForceRuntimeStatisticsCollection(Orleans.Runtime.SiloAddress[]) -> Task - GetActivationAddress(Orleans.Runtime.IAddressable) -> ValueTask - GetActiveGrains(Orleans.Runtime.GrainType) -> ValueTask> - GetDetailedGrainStatistics(string[], Orleans.Runtime.SiloAddress[]) -> Task - GetDetailedHosts(bool) -> Task + # Orleans.Runtime.IManagementGrain.GetDetailedGrainStatistics(string[]? types, SiloAddress[]? hostsIds) -> Task + 0A1C0D82(string[], Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetGrainCallFrequencies(SiloAddress[]? hostsIds) -> Task> + 0F06E027(Orleans.Runtime.SiloAddress[]) -> Task> + # Orleans.Runtime.IManagementGrain.GetRuntimeStatistics(SiloAddress[] hostsIds) -> Task + 2D761B36(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetActivationAddress(IAddressable reference) -> ValueTask + 317D82B6(Orleans.Runtime.IAddressable) -> ValueTask + # Orleans.Runtime.IManagementGrain.ForceActivationCollection(SiloAddress[] hostsIds, TimeSpan ageLimit) -> Task + 329F9A1B(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task + # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics(SiloAddress[] hostsIds) -> Task + 3CFF788C(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetActiveGrains(GrainType type) -> ValueTask> + 3DB7923B(Orleans.Runtime.GrainType) -> ValueTask> + # Orleans.Runtime.IManagementGrain.GetHosts(bool onlyActive) -> Task> + 4C0864C2(bool) -> Task> + # Orleans.Runtime.IManagementGrain.ForceActivationCollection(TimeSpan ageLimit) -> Task + 54E6D1D1(System.TimeSpan) -> Task + # Orleans.Runtime.IManagementGrain.ResetGrainCallFrequencies(SiloAddress[]? hostsIds) -> ValueTask + 54FE0FEC(Orleans.Runtime.SiloAddress[]) -> ValueTask + # Orleans.Runtime.IManagementGrain.ForceGarbageCollection(SiloAddress[] hostsIds) -> Task + 5922EB76(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics() -> Task + ACCE9D6A() -> Task # Orleans.Runtime.IManagementGrain.GetGrainActivationCount(GrainReference grainReference) -> Task - GetGrainActivationCount(GrainRef) -> Task - GetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> Task> - GetHosts(bool) -> Task> - GetRuntimeStatistics(Orleans.Runtime.SiloAddress[]) -> Task - GetSimpleGrainStatistics() -> Task - GetSimpleGrainStatistics(Orleans.Runtime.SiloAddress[]) -> Task - GetTotalActivationCount() -> Task - ResetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> ValueTask - SendControlCommandToProvider`1(string, int, object?) -> Task + AEDE93F6(GrainRef) -> Task + # Orleans.Runtime.IManagementGrain.ForceRuntimeStatisticsCollection(SiloAddress[] siloAddresses) -> Task + B761B345(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetDetailedHosts(bool onlyActive) -> Task + CC6CCBC3(bool) -> Task + # Orleans.Runtime.IManagementGrain.GetTotalActivationCount() -> Task + D7365B43() -> Task + # Orleans.Runtime.IManagementGrain.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task + F67965CC`1(string, int, object?) -> Task interface [GrainInterfaceType("Orleans.Runtime.IMembershipService")] Orleans.Runtime.IMembershipService [Version(0)] - MembershipChangeNotification(Orleans.Runtime.MembershipTableSnapshot) -> Task - Ping(int) -> Task - ProbeIndirectly(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task + # Orleans.Runtime.IMembershipService.ProbeIndirectly(SiloAddress target, TimeSpan probeTimeout, int probeNumber) -> Task + 0F85FAAF(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task + # Orleans.Runtime.IMembershipService.MembershipChangeNotification(MembershipTableSnapshot snapshot) -> Task + 22A02D46(Orleans.Runtime.MembershipTableSnapshot) -> Task + # Orleans.Runtime.IMembershipService.Ping(int pingNumber) -> Task + 39AB7071(int) -> Task interface [GrainInterfaceType("Orleans.Storage.IMemoryStorageGrain")] Orleans.Storage.IMemoryStorageGrain [Version(0)] - DeleteStateAsync`1(string, string?) -> Task - ReadStateAsync`1(string) -> Task> - WriteStateAsync`1(string, Orleans.IGrainState) -> Task + # Orleans.Storage.IMemoryStorageGrain.ReadStateAsync(string grainStoreKey) -> Task?> + 45659318`1(string) -> Task> + # Orleans.Storage.IMemoryStorageGrain.WriteStateAsync(string grainStoreKey, IGrainState grainState) -> Task + 7CC6CA25`1(string, Orleans.IGrainState) -> Task + # Orleans.Storage.IMemoryStorageGrain.DeleteStateAsync(string grainStoreKey, string? eTag) -> Task + B7CADD03`1(string, string?) -> Task diff --git a/src/Orleans.DurableJobs/OrleansContracts.txt b/src/Orleans.DurableJobs/OrleansContracts.txt index 756611fbcb..49d70fce8d 100644 --- a/src/Orleans.DurableJobs/OrleansContracts.txt +++ b/src/Orleans.DurableJobs/OrleansContracts.txt @@ -1,11 +1,19 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.DurableJobs.IDurableJobReceiverExtension")] Orleans.DurableJobs.IDurableJobReceiverExtension [Version(0)] - HandleDurableJobAsync(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask + # Orleans.DurableJobs.IDurableJobReceiverExtension.HandleDurableJobAsync(IJobRunContext context, CancellationToken attemptCancellationToken) -> ValueTask + 703DB2D4(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget")] Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget [Version(0)] - CancelAsync(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task + # Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget.CancelAsync(DurableJob job, CancellationToken requestCancellationToken) -> Task + 4D559F22(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task class [GrainType("localdurablejobmanager")] Orleans.DurableJobs.LocalDurableJobManager diff --git a/src/Orleans.EventSourcing/OrleansContracts.txt b/src/Orleans.EventSourcing/OrleansContracts.txt index c3cd2d2988..db96365c6b 100644 --- a/src/Orleans.EventSourcing/OrleansContracts.txt +++ b/src/Orleans.EventSourcing/OrleansContracts.txt @@ -1,11 +1,21 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.EventSourcing.ILogConsistencyProtocolParticipant")] Orleans.EventSourcing.ILogConsistencyProtocolParticipant [Version(0)] - DeactivateProtocolParticipant() -> Task - PostActivateProtocolParticipant() -> Task - PreActivateProtocolParticipant() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PreActivateProtocolParticipant() -> Task + 0DB087C8() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PostActivateProtocolParticipant() -> Task + 22FD7D72() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.DeactivateProtocolParticipant() -> Task + A36FC884() -> Task interface [GrainInterfaceType("Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway")] Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway [Version(0)] - RelayMessage(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task + # Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway.RelayMessage(GrainId id, ILogConsistencyProtocolMessage payload) -> Task + C86A1066(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task diff --git a/src/Orleans.Persistence.Memory/OrleansContracts.txt b/src/Orleans.Persistence.Memory/OrleansContracts.txt index bb17fc7aff..524150db70 100644 --- a/src/Orleans.Persistence.Memory/OrleansContracts.txt +++ b/src/Orleans.Persistence.Memory/OrleansContracts.txt @@ -1,5 +1,11 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("memorystorage")] Orleans.Storage.MemoryStorageGrain diff --git a/src/Orleans.Reminders/OrleansContracts.txt b/src/Orleans.Reminders/OrleansContracts.txt index 2db089842d..f877ceebf1 100644 --- a/src/Orleans.Reminders/OrleansContracts.txt +++ b/src/Orleans.Reminders/OrleansContracts.txt @@ -1,25 +1,44 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.IRemindable")] Orleans.IRemindable [Version(0)] - ReceiveReminder(string, Orleans.Runtime.TickStatus) -> Task + # Orleans.IRemindable.ReceiveReminder(string reminderName, TickStatus status) -> Task + 6461BF2F(string, Orleans.Runtime.TickStatus) -> Task interface [GrainInterfaceType("Orleans.IReminderService")] Orleans.IReminderService [Version(0)] - GetReminder(Orleans.Runtime.GrainId, string) -> Task - GetReminders(Orleans.Runtime.GrainId) -> Task> - RegisterOrUpdateReminder(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task - Start() -> Task - Stop() -> Task - UnregisterReminder(Orleans.Runtime.IGrainReminder) -> Task + # Orleans.IReminderService.RegisterOrUpdateReminder(GrainId grainId, string reminderName, TimeSpan dueTime, TimeSpan period) -> Task + 1281C86D(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task + # Orleans.IReminderService.GetReminders(GrainId grainId) -> Task> + 419EB51E(Orleans.Runtime.GrainId) -> Task> + # Orleans.IReminderService.Start() -> Task + 5CF78F8A() -> Task + # Orleans.IReminderService.UnregisterReminder(IGrainReminder reminder) -> Task + A7AF84A8(Orleans.Runtime.IGrainReminder) -> Task + # Orleans.IReminderService.GetReminder(GrainId grainId, string reminderName) -> Task + AC622EEB(Orleans.Runtime.GrainId, string) -> Task + # Orleans.IReminderService.Stop() -> Task + DCFCA00D() -> Task interface [GrainInterfaceType("Orleans.IReminderTableGrain")] Orleans.IReminderTableGrain [Version(0)] - ReadRow(Orleans.Runtime.GrainId, string) -> Task - ReadRows(Orleans.Runtime.GrainId) -> Task - ReadRows(uint, uint) -> Task - RemoveRow(Orleans.Runtime.GrainId, string, string) -> Task - TestOnlyClearTable() -> Task - UpsertRow(Orleans.ReminderEntry) -> Task + # Orleans.IReminderTableGrain.ReadRows(uint begin, uint end) -> Task + 13558B55(uint, uint) -> Task + # Orleans.IReminderTableGrain.UpsertRow(ReminderEntry entry) -> Task + 873299B5(Orleans.ReminderEntry) -> Task + # Orleans.IReminderTableGrain.TestOnlyClearTable() -> Task + 8EBE0523() -> Task + # Orleans.IReminderTableGrain.ReadRow(GrainId grainId, string reminderName) -> Task + ECA791DE(Orleans.Runtime.GrainId, string) -> Task + # Orleans.IReminderTableGrain.ReadRows(GrainId grainId) -> Task + EEEF6FCA(Orleans.Runtime.GrainId) -> Task + # Orleans.IReminderTableGrain.RemoveRow(GrainId grainId, string reminderName, string eTag) -> Task + FF391E0B(Orleans.Runtime.GrainId, string, string) -> Task class [GrainType("localreminderservice")] Orleans.Runtime.ReminderService.LocalReminderService diff --git a/src/Orleans.Runtime/OrleansContracts.txt b/src/Orleans.Runtime/OrleansContracts.txt index 75bdb2c2fe..99064daf66 100644 --- a/src/Orleans.Runtime/OrleansContracts.txt +++ b/src/Orleans.Runtime/OrleansContracts.txt @@ -1,6 +1,12 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("activationmigrationmanager")] Orleans.Runtime.ActivationMigrationManager @@ -13,7 +19,8 @@ class [GrainType("deploymentloadpublisher")] Orleans.Runtime.DeploymentLoadPubli class [GrainType("developmentleaseprovider")] Orleans.Runtime.Development.DevelopmentLeaseProviderGrain interface [GrainInterfaceType("Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain")] Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain [Version(0)] - Reset() -> Task + # Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain.Reset() -> Task + 847FCE12() -> Task class [GrainType("graincallcancellationmanager")] Orleans.Runtime.GrainCallCancellationManager @@ -26,25 +33,27 @@ class [GrainType("distributedremotegraindirectory")] Orleans.Runtime.GrainDirect class [GrainType("graindirectorypartition")] Orleans.Runtime.GrainDirectory.GrainDirectoryPartition interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryClient")] Orleans.Runtime.GrainDirectory.IGrainDirectoryClient [Version(0)] - GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> - RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> + [Alias("GetRegisteredActivations")] GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> + [Alias("RecoverRegisteredActivations")] RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition")] Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition [Version(0)] - AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask - DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> - GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask - LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> - RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> + [Alias("AcknowledgeSnapshotTransferAsync")] AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask + [Alias("DeregisterAsync")] DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> + [Alias("GetSnapshotAsync")] GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask + [Alias("LookupAsync")] LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> + [Alias("RegisterAsync")] RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks")] Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks [Version(0)] - CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> - CheckIntegrityAsync() -> ValueTask - RecoverAndCheckIntegrityAsync() -> ValueTask - WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask + [Alias("CheckActivationsAsync")] CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> + [Alias("CheckIntegrityAsync")] CheckIntegrityAsync() -> ValueTask + [Alias("RecoverAndCheckIntegrityAsync")] RecoverAndCheckIntegrityAsync() -> ValueTask + [Alias("WaitForMembershipVersionAsync")] WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IRemoteClientDirectory")] Orleans.Runtime.GrainDirectory.IRemoteClientDirectory [Version(0)] - GetClientRoutes(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> - OnUpdateClientRoutes(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task + # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.OnUpdateClientRoutes(ImmutableDictionary ConnectedClients, long Version)> update) -> Task + 972F9953(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task + # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.GetClientRoutes(ImmutableDictionary knownRoutes) -> Task ConnectedClients, long Version)>> + A6E49CD1(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> class [GrainType("localgraindirectoryclientcompatibility")] Orleans.Runtime.GrainDirectory.LocalGrainDirectoryClientCompatibility @@ -53,24 +62,32 @@ class [GrainType("localgraindirectorypartitioncompatibility")] Orleans.Runtime.G class [GrainType("remotegraindirectory")] Orleans.Runtime.GrainDirectory.RemoteGrainDirectory interface [GrainInterfaceType("Orleans.Runtime.IActivationMigrationManagerSystemTarget")] Orleans.Runtime.IActivationMigrationManagerSystemTarget [Version(0)] - AcceptMigratingGrains(System.Collections.Generic.List) -> ValueTask + # Orleans.Runtime.IActivationMigrationManagerSystemTarget.AcceptMigratingGrains(List migratingGrains) -> ValueTask + 29E9E63F(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.ICatalog")] Orleans.Runtime.ICatalog [Version(0)] - DeleteActivations(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task + # Orleans.Runtime.ICatalog.DeleteActivations(List activationAddresses, DeactivationReasonCode reasonCode, string reasonText) -> Task + C4A56D7C(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationManagerSystemTarget")] Orleans.Runtime.IGrainCallCancellationManagerSystemTarget [Version(0)] - CancelCallsAsync(System.Collections.Generic.List) -> ValueTask + # Orleans.Runtime.IGrainCallCancellationManagerSystemTarget.CancelCallsAsync(List cancellationRequests) -> ValueTask + AF79F3FA(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IGrainTimerInvoker")] Orleans.Runtime.IGrainTimerInvoker [Version(0)] - InvokeCallbackAsync() -> Task + # Orleans.Runtime.IGrainTimerInvoker.InvokeCallbackAsync() -> Task + 3F6C2672() -> Task interface [GrainInterfaceType("Orleans.Runtime.IRemoteGrainDirectory")] Orleans.Runtime.IRemoteGrainDirectory [Version(0)] - AcceptSplitPartition(System.Collections.Generic.List) -> Task - LookUpMany(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> - RegisterMany(System.Collections.Generic.List) -> Task + # Orleans.Runtime.IRemoteGrainDirectory.LookUpMany(List<(GrainId GrainId, int Version)> grainAndETagList) -> Task> + 7DF50601(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> + # Orleans.Runtime.IRemoteGrainDirectory.AcceptSplitPartition(List singleActivations) -> Task + 9ABE3793(System.Collections.Generic.List) -> Task + # Orleans.Runtime.IRemoteGrainDirectory.RegisterMany(List addresses) -> Task + CD06EAEE(System.Collections.Generic.List) -> Task interface [GrainInterfaceType("Orleans.Runtime.ISiloManifestSystemTarget")] Orleans.Runtime.ISiloManifestSystemTarget [Version(0)] - GetSiloManifest() -> ValueTask + # Orleans.Runtime.ISiloManifestSystemTarget.GetSiloManifest() -> ValueTask + 1857A4C8() -> ValueTask class [GrainType("management")] Orleans.Runtime.Management.ManagementGrain @@ -79,7 +96,7 @@ class [GrainType("membershipsystemtarget")] Orleans.Runtime.MembershipService.Me class [GrainType("membershiptablesystemtarget")] Orleans.Runtime.MembershipService.MembershipTableSystemTarget interface [GrainInterfaceType("Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget")] Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget [Version(0)] - GetSiloMetadata() -> Task + [Alias("GetSiloMetadata")] GetSiloMetadata() -> Task class [GrainType("silometadatasystemtarget")] Orleans.Runtime.MembershipService.SiloMetadata.SiloMetadataSystemTarget @@ -96,13 +113,21 @@ interface [GrainInterfaceType("Orleans.Runtime.TestHooks.ITestHooksSystemTarget" class [GrainType("testhookssystemtarget")] Orleans.Runtime.TestHooks.TestHooksSystemTarget interface [GrainInterfaceType("Orleans.Runtime.Versions.IVersionStoreGrain")] Orleans.Runtime.Versions.IVersionStoreGrain [Version(0)] - GetCompatibilityStrategies() -> Task> - GetCompatibilityStrategy() -> Task - GetSelectorStrategies() -> Task> - GetSelectorStrategy() -> Task - SetCompatibilityStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - SetCompatibilityStrategy(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - SetSelectorStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task - SetSelectorStrategy(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(GrainInterfaceType interfaceType, CompatibilityStrategy strategy) -> Task + 1B7F13C8(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(GrainInterfaceType interfaceType, VersionSelectorStrategy strategy) -> Task + 3E6DDE3E(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(CompatibilityStrategy strategy) -> Task + 67A0B5AA(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategy() -> Task + 67EF9A39() -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategies() -> Task> + 7261373F() -> Task> + # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategies() -> Task> + 743D88ED() -> Task> + # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategy() -> Task + 8A72848A() -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(VersionSelectorStrategy strategy) -> Task + E7532DE3(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task class [GrainType("versionstore")] Orleans.Runtime.Versions.VersionStoreGrain diff --git a/src/Orleans.Streaming/OrleansContracts.txt b/src/Orleans.Streaming/OrleansContracts.txt index de3f623bf0..60fe142410 100644 --- a/src/Orleans.Streaming/OrleansContracts.txt +++ b/src/Orleans.Streaming/OrleansContracts.txt @@ -1,10 +1,18 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Providers.IMemoryStreamQueueGrain")] Orleans.Providers.IMemoryStreamQueueGrain [Version(0)] - Dequeue(int) -> Task> - Enqueue(Orleans.Providers.MemoryMessageData) -> Task + # Orleans.Providers.IMemoryStreamQueueGrain.Enqueue(MemoryMessageData data) -> Task + 74D60341(Orleans.Providers.MemoryMessageData) -> Task + # Orleans.Providers.IMemoryStreamQueueGrain.Dequeue(int maxCount) -> Task> + 7A8F8C1A(int) -> Task> class [GrainType("memorystreamqueue")] Orleans.Providers.MemoryStreamQueueGrain @@ -14,43 +22,70 @@ class [GrainType("stream.checkpoint.configured")] Orleans.Streams.ConfiguredStre interface [GrainInterfaceType("Orleans.Streams.IConfiguredStreamCheckpointerGrain")] Orleans.Streams.IConfiguredStreamCheckpointerGrain [Version(0)] interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingAgent")] Orleans.Streams.IPersistentStreamPullingAgent [Version(0)] - Initialize() -> Task - Shutdown() -> Task + # Orleans.Streams.IPersistentStreamPullingAgent.Initialize() -> Task + 06009D9C() -> Task + # Orleans.Streams.IPersistentStreamPullingAgent.Shutdown() -> Task + 620FF905() -> Task interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingManager")] Orleans.Streams.IPersistentStreamPullingManager [Version(0)] - ExecuteCommand(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task - Initialize() -> Task - StartAgents() -> Task - Stop() -> Task - StopAgents() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.Initialize() -> Task + 455AB850() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.StartAgents() -> Task + 54E9E970() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.StopAgents() -> Task + BBD50CFF() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.ExecuteCommand(PersistentStreamProviderCommand command, object? arg) -> Task + DE756D95(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task + # Orleans.Streams.IPersistentStreamPullingManager.Stop() -> Task + F4B5B5AA() -> Task interface [GrainInterfaceType("Orleans.Streams.IPubSubRendezvousGrain")] Orleans.Streams.IPubSubRendezvousGrain [Version(0)] - ConsumerCount(Orleans.Runtime.QualifiedStreamId) -> Task - DiagGetConsumers(Orleans.Runtime.QualifiedStreamId) -> Task - FaultSubscription(Orleans.Runtime.GuidId) -> Task - GetAllSubscriptions(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - ProducerCount(Orleans.Runtime.QualifiedStreamId) -> Task - RegisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - RegisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - UnregisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task - UnregisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task - Validate() -> Task + # Orleans.Streams.IPubSubRendezvousGrain.Validate() -> Task + 20AA72BF() -> Task + # Orleans.Streams.IPubSubRendezvousGrain.FaultSubscription(GuidId subscriptionId) -> Task + 2821FCF5(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.ProducerCount(QualifiedStreamId streamId) -> Task + 29B61035(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.RegisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task + 5E7E20BC(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.ConsumerCount(QualifiedStreamId streamId) -> Task + 5F72C5CF(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.GetAllSubscriptions(QualifiedStreamId streamId, GrainId streamConsumer) -> Task> + 7DBE84FA(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + # Orleans.Streams.IPubSubRendezvousGrain.DiagGetConsumers(QualifiedStreamId streamId) -> Task + 8A033955(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.UnregisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId) -> Task + 974334B6(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.RegisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task> + B5FFB7F3(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + # Orleans.Streams.IPubSubRendezvousGrain.UnregisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task + C017B47D(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamCheckpointerGrain")] Orleans.Streams.IStreamCheckpointerGrain [Version(0)] - Load(System.Threading.CancellationToken) -> ValueTask - Update(string, string, System.Threading.CancellationToken) -> ValueTask + # Orleans.Streams.IStreamCheckpointerGrain.Update(string offset, string expectedCheckpoint, CancellationToken cancellationToken) -> ValueTask + 7AB50A87(string, string, System.Threading.CancellationToken) -> ValueTask + # Orleans.Streams.IStreamCheckpointerGrain.Load(CancellationToken cancellationToken) -> ValueTask + DE3727A1(System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.Streams.IStreamConsumerExtension")] Orleans.Streams.IStreamConsumerExtension [Version(0)] - CompleteStream(Orleans.Runtime.GuidId) -> Task - DeliverBatch(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task - DeliverImmutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - DeliverMutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - ErrorInStream(Orleans.Runtime.GuidId, System.Exception) -> Task - GetSequenceToken(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverMutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task + 31840DDE(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.CompleteStream(GuidId subscriptionId) -> Task + 49F94A48(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IStreamConsumerExtension.ErrorInStream(GuidId subscriptionId, Exception exc) -> Task + 4C676CAF(Orleans.Runtime.GuidId, System.Exception) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverImmutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task + 6D8FAEB2(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverBatch(GuidId subscriptionId, QualifiedStreamId streamId, IBatchContainer item, StreamHandshakeToken? handshakeToken) -> Task + B9CFF2C9(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.GetSequenceToken(GuidId subscriptionId) -> Task + C265B3CB(Orleans.Runtime.GuidId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamProducerExtension")] Orleans.Streams.IStreamProducerExtension [Version(0)] - AddSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - RemoveSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IStreamProducerExtension.AddSubscriber(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task + 1341E3D4(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + # Orleans.Streams.IStreamProducerExtension.RemoveSubscriber(GuidId subscriptionId, QualifiedStreamId streamId) -> Task + B98BA876(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task class [GrainType("persistentstreampullingagent")] Orleans.Streams.PersistentStreamPullingAgent diff --git a/src/Orleans.TestingHost/OrleansContracts.txt b/src/Orleans.TestingHost/OrleansContracts.txt index eb810b7770..d1d33c7d44 100644 --- a/src/Orleans.TestingHost/OrleansContracts.txt +++ b/src/Orleans.TestingHost/OrleansContracts.txt @@ -1,13 +1,25 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.TestingHost.IStorageFaultGrain")] Orleans.TestingHost.IStorageFaultGrain [Version(0)] - AddFaultOnClear(Orleans.Runtime.GrainId, System.Exception) -> Task - AddFaultOnRead(Orleans.Runtime.GrainId, System.Exception) -> Task - AddFaultOnWrite(Orleans.Runtime.GrainId, System.Exception) -> Task - OnClear(Orleans.Runtime.GrainId) -> Task - OnRead(Orleans.Runtime.GrainId) -> Task - OnWrite(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnRead(GrainId grainId, Exception exception) -> Task + 1150D526(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnClear(GrainId grainId, Exception exception) -> Task + 1A607A31(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnRead(GrainId grainId) -> Task + 5D91E1AF(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnWrite(GrainId grainId, Exception exception) -> Task + B9852E6E(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnClear(GrainId grainId) -> Task + C94BA77C(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnWrite(GrainId grainId) -> Task + E8594820(Orleans.Runtime.GrainId) -> Task class [GrainType("storagefault")] Orleans.TestingHost.StorageFaultGrain diff --git a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt index cb8e54cb07..1fcbada73e 100644 --- a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt +++ b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt @@ -1,19 +1,29 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("consistencytest")] Orleans.Transactions.TestKit.Consistency.ConsistencyTestGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain")] Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain [Version(0)] - Run(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task + # Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain.Run(ConsistencyTestOptions options, int depth, string stack, int max, DateTime stopAfter) -> Task + 2EB318CB(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task # Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain class [GrainType("txn-correctness-DoubleStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain")] Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain [Version(0)] - Get() -> Task> - Ping() -> Task - SetBit(int) -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.SetBit(int newValue) -> Task + 0183C2F5(int) -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Ping() -> Task + 9A5740F1() -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Get() -> Task> + B821F3B1() -> Task> # Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain class [GrainType("txn-correctness-MaxStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain @@ -37,68 +47,104 @@ class [GrainType("exclusivelocktransactiontest")] Orleans.Transactions.TestKit.E class [GrainType("faultinjectiontransactioncoordinator")] Orleans.Transactions.TestKit.FaultInjectionTransactionCoordinatorGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateAttributionGrain")] Orleans.Transactions.TestKit.ICreateAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ICreateAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 3EFBDD5D(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain")] Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + C9B8ECB8(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain")] Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain [Version(0)] - ReadThenWrite(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - ReadThenWriteWithExclusiveLock(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWrite(ITransactionTestGrain grain, int value) -> Task + 148E55F3(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWriteWithExclusiveLock(IExclusiveLockTransactionTestGrain grain, int value) -> Task + F880C5FF(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain")] Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain [Version(0)] - Add(int) -> Task - Get() -> Task - Set(int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Get() -> Task + 16E53FE3() -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Add(int numberToAdd) -> Task + 81B05CD8(int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Set(int newValue) -> Task + BD3AA4D0(int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain [Version(0)] - MultiGrainAddAndFaultInjection(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - MultiGrainSet(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task + 70FF7C60(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainAddAndFaultInjection(List grains, int numberToAdd, FaultInjectionControl? faultInjection) -> Task + E67D54A5(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain [Version(0)] - Add(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - Deactivate() -> Task - Get() -> Task - Set(int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Set(int newValue) -> Task + 8389970A(int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Add(int numberToAdd, FaultInjectionControl? faultInjectionControl) -> Task + A4CAE05C(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Deactivate() -> Task + A6C1652E() -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Get() -> Task + C752DF7D() -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IJoinAttributionGrain")] Orleans.Transactions.TestKit.IJoinAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.IJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + B1619F67(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INoAttributionGrain")] Orleans.Transactions.TestKit.INoAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.INoAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + BC7E3A79(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INotAllowedAttributionGrain")] Orleans.Transactions.TestKit.INotAllowedAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.INotAllowedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 891D027E(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISupportedAttributionGrain")] Orleans.Transactions.TestKit.ISupportedAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ISupportedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + BC7DBC0A(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISuppressAttributionGrain")] Orleans.Transactions.TestKit.ISuppressAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ISuppressAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 5A02311D(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCommitterTestGrain")] Orleans.Transactions.TestKit.ITransactionCommitterTestGrain [Version(0)] - Commit(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task + # Orleans.Transactions.TestKit.ITransactionCommitterTestGrain.Commit(ITransactionCommitOperation operation) -> Task + C44BE2A4(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCoordinatorGrain")] Orleans.Transactions.TestKit.ITransactionCoordinatorGrain [Version(0)] - AddAndThrow(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - MultiGrainAdd(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task - MultiGrainAdd(System.Collections.Generic.List, int) -> Task - MultiGrainAddAndThrow(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task - MultiGrainDouble(System.Collections.Generic.List) -> Task - MultiGrainDoubleByRWRW(System.Collections.Generic.List, int) -> Task - MultiGrainDoubleByWRWR(System.Collections.Generic.List, int) -> Task - MultiGrainSet(System.Collections.Generic.List, int) -> Task - MultiGrainSetBit(System.Collections.Generic.List, int) -> Task - OrphanCallTransaction() -> Task - UpdateViolated(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAddAndThrow(List grain, List grains, int numberToAdd) -> Task + 2760260D(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(List grains, int numberToAdd) -> Task + 3A6B9237(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.UpdateViolated(ITransactionTestGrain grains, int numberToAdd) -> Task + 485592B2(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDouble(List grains) -> Task + 5FC2E7A1(System.Collections.Generic.List) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSetBit(List grains, int bitIndex) -> Task + 5FF4F216(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task + 78D54907(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(ITransactionCommitterTestGrain committer, ITransactionCommitOperation operation, List grains, int numberToAdd) -> Task + 8EE5E563(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByRWRW(List grains, int numberToAdd) -> Task + 9EFEA7F3(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByWRWR(List grains, int numberToAdd) -> Task + B4376B4D(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.AddAndThrow(ITransactionTestGrain grain, int numberToAdd) -> Task + D3EF444F(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.OrphanCallTransaction() -> Task + EDCC120B() -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionTestGrain")] Orleans.Transactions.TestKit.ITransactionTestGrain [Version(0)] - Add(int) -> Task - AddAndThrow(int) -> Task - Deactivate() -> Task - Get() -> Task - Set(int) -> Task - SetAndThrow(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.AddAndThrow(int numberToAdd) -> Task + 25B066B5(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.SetAndThrow(int numberToSet) -> Task + 35C87F81(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Deactivate() -> Task + 35D6FD32() -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Get() -> Task + 8DAA79AA() -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Set(int newValue) -> Task + CE9EC80B(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Add(int numberToAdd) -> Task + DC07DAEA(int) -> Task class [GrainType("joinattribution")] Orleans.Transactions.TestKit.JoinAttributionGrain diff --git a/src/Orleans.Transactions/OrleansContracts.txt b/src/Orleans.Transactions/OrleansContracts.txt index 6521d3b3a1..02e544c9e5 100644 --- a/src/Orleans.Transactions/OrleansContracts.txt +++ b/src/Orleans.Transactions/OrleansContracts.txt @@ -1,15 +1,29 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionManagerExtension")] Orleans.Transactions.Abstractions.ITransactionManagerExtension [Version(0)] - Ping(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task - PrepareAndCommit(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task - Prepared(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Prepared(string resourceId, Guid transactionId, DateTime timestamp, ParticipantId resource, TransactionalStatus status) -> Task + 12BEFA17(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Ping(string resourceId, Guid transactionId, DateTime timeStamp, ParticipantId resource) -> Task + AC4A9AEB(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.PrepareAndCommit(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, List writeResources, int totalParticipants) -> Task + B024EFA6(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionalResourceExtension")] Orleans.Transactions.Abstractions.ITransactionalResourceExtension [Version(0)] - Abort(string, System.Guid) -> Task - Cancel(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task - CommitReadOnly(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task - Confirm(string, System.Guid, System.DateTime) -> Task - Prepare(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.CommitReadOnly(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp) -> Task + 1BB071FE(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Prepare(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, ParticipantId transactionManager) -> Task + 2ADCC608(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Confirm(string resourceId, Guid transactionId, DateTime timeStamp) -> Task + 5DDDE6F0(string, System.Guid, System.DateTime) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Cancel(string resourceId, Guid transactionId, DateTime timeStamp, TransactionalStatus status) -> Task + 80028AB9(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Abort(string resourceId, Guid transactionId) -> Task + BD051D23(string, System.Guid) -> Task diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index 9189e7b03f..e152c04844 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -23,10 +24,18 @@ namespace Analyzers.Tests; public class GrainInterfaceVersionAnalyzerTest { private const string OrleansContractsFileName = "OrleansContracts.txt"; + private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; + private const string RegenerateCodeActionEquivalenceKey = "RegenerateOrleansContractsFileAsync"; private const string GeneratedHeader = - "# This file is auto-generated by the Orleans contract analyzer.\n" + - "# Update source contracts, then regenerate this file by following:\n" + - "# https://aka.ms/orleans/OrleansContracts.txt\n\n"; + "# This file is generated by the Orleans contract analyzer.\n" + + "# To regenerate, run this command from the repository root after replacing\n" + + "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:\n" + + "# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024\n" + + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION\n" + + "# The regeneration command edits this manifest only; it does not change source attributes.\n" + + "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.\n" + + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.\n" + + "# Details: https://aka.ms/orleans/OrleansContracts.txt\n\n"; private static readonly string[] Usings = new[] { @@ -354,6 +363,7 @@ IMyGrain [Version(1)] Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); var diagnostic = diagnostics.First(d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); Assert.Contains("NewMethod", diagnostic.GetMessage()); + Assert.Contains("8E43BF4F() -> Task", diagnostic.GetMessage()); } [Fact] @@ -520,6 +530,162 @@ interface IMyGrain [Version(1)] Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); } + [Fact] + public async Task RemovedMember_ReportsDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + var diagnostic = Assert.Single( + diagnostics, + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains("RemovedAsync() -> Task", diagnostic.GetMessage()); + Assert.Equal(OrleansContractsFileName, Path.GetFileName(diagnostic.Location.GetLineSpan().Path)); + } + + [Fact] + public async Task LegacyAliasedMemberRename_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-method"")] + Task NewName(string renamed); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""stable-method"")] IMyGrain.OldName(string original) -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task UnmarkedSameNameAlias_ReportsManifestUpgrade() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""Method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task AliasedMemberIdentityChange_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""new-method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""old-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task ExplicitId_DoesNotMatchLegacyMethodName() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Id(42)] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Ping() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task RemovedAlias_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""old-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task AliasedMemberGenericArityChange_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""stable-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + #endregion #region ORLEANS0019 - Removed Interface Not Retired @@ -576,6 +742,9 @@ public interface IMyGrain : IGrain var diagnostic = Assert.Single(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0020); Assert.Contains(OrleansContractsFileName, diagnostic.GetMessage()); + Assert.True(diagnostic.Location.IsInSource); + Assert.Contains("dotnet format PATH_TO_PROJECT_OR_SOLUTION", diagnostic.GetMessage()); + Assert.Contains("https://aka.ms/orleans/OrleansContracts.txt", diagnostic.GetMessage()); } [Fact] @@ -953,188 +1122,1093 @@ public sealed class NewRequest { } [Alias(""response"")] public sealed class NewResponse { } -[GrainInterfaceType(""stable-interface"")] -public interface INewGrain : IGrain +[GrainInterfaceType(""stable-interface"")] +public interface INewGrain : IGrain +{ + [Alias(""stable-method"")] + Task NewMethod(NewRequest renamedParameter); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + Assert.Empty(diagnostics); + Assert.Contains("# IOldGrain\ninterface [GrainInterfaceType(\"stable-interface\")] IOldGrain [Version(0)]", contractsFile); + Assert.Contains( + " [Alias(\"stable-method\")] stable-method(request) -> Task", + contractsFile); + Assert.Contains("# IOldGrain", contractsFile); + Assert.Contains("# IOldGrain.OldMethod", contractsFile); + } + + [Fact] + public async Task StableInterfaceIdentityChange_IsBreaking() + { + const string source = @" +[GrainInterfaceType(""new-identity"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = @" +[GrainInterfaceType(""old-identity"")] IMyGrain [Version(0)] +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0016); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0019); + } + + [Fact] + public async Task StableGrainTypeChange_IsBreaking() + { + const string source = @" +[GrainType(""new-identity"")] +public class MyGrain : Grain, IGrainWithStringKey +{ +} +"; + const string contractsFile = "class [GrainType(\"old-identity\")] MyGrain"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0023); + } + + [Fact] + public async Task MethodRenameWithoutAlias_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task OldName(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task NewName(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains(diagnostics, diagnostic => diagnostic.GetMessage().Contains("NewName", StringComparison.Ordinal)); + Assert.Contains("# IMyGrain.OldName() -> Task", contractsFile); + } + + [Fact] + public async Task MethodReturnTypeChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Read(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Read(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterTypeChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string value); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(int value); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterOrderChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string name, int count); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(int count, string name); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterRename_PreservesGeneratedIdentity() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string oldName); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string newName); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MethodIdChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + [Id(1)] + Task Update(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + [Id(2)] + Task Update(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task PayloadAliasChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +[Alias(""request"")] +public sealed class Request { } + +public interface IMyGrain : IGrain +{ + [Alias(""update"")] + Task Update(Request request); +} +"; + const string newSource = @" +[Alias(""request-v2"")] +public sealed class Request { } + +public interface IMyGrain : IGrain +{ + [Alias(""update"")] + Task Update(Request request); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task InterfaceRenameWithoutStableIdentity_ReportsAddedAndRemovedContracts() + { + const string oldSource = "public interface IOldGrain : IGrain { }"; + const string newSource = "public interface INewGrain : IGrain { }"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0016, + GrainInterfaceVersionAnalyzer.RuleId0019); + } + + [Fact] + public async Task GrainClassRenameWithoutStableIdentity_ReportsAddedAndRemovedContracts() + { + const string oldSource = "public class OldGrain : Grain, IGrainWithStringKey { }"; + const string newSource = "public class NewGrain : Grain, IGrainWithStringKey { }"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0022); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0022, + GrainInterfaceVersionAnalyzer.RuleId0024); + } + + #endregion + + #region Code Fix Tests Infrastructure + + private async Task<(Solution ChangedSolution, DocumentId? AdditionalDocumentId)> ApplyCodeFixAsync( + string source, + string? grainInterfacesFileContent, + string expectedDiagnosticId, + string? codeActionTitle = null, + string? configuredContractsPath = null, + string? generatedSource = null) + { + var project = CreateProjectWithAdditionalFilesForCodeFix( + source, + grainInterfacesFileContent, + configuredContractsPath, + generatedSource); + var document = project.Documents.First(); + var compilation = await project.GetCompilationAsync(); + + Assert.NotNull(compilation); + var errors = compilation!.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error); + Assert.Empty(errors); + + var analyzer = new GrainInterfaceVersionAnalyzer(); + + // Build analyzer options with additional files + var additionalFiles = grainInterfacesFileContent is not null + ? ImmutableArray.Create(new TestAdditionalText(OrleansContractsFileName, grainInterfacesFileContent)) + : ImmutableArray.Empty; + + var analyzerOptions = CreateAnalyzerOptions(additionalFiles, analyzerEnabled: true); + + var compilationWithAnalyzers = compilation + .WithOptions(compilation.Options.WithSpecificDiagnosticOptions( + analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default))) + .WithAnalyzers(ImmutableArray.Create(analyzer), analyzerOptions); + + var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); + var diagnostic = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnosticId); + + Assert.NotNull(diagnostic); + + // Apply code fix + var codeFixer = new GrainInterfaceVersionCodeFix(); + var actions = new List(); + var context = new CodeFixContext( + document, + diagnostic!, + (action, _) => actions.Add(action), + CancellationToken.None); + + await codeFixer.RegisterCodeFixesAsync(context); + Assert.NotEmpty(actions); + + var action = codeActionTitle is null + ? actions.FirstOrDefault(candidate => !string.Equals(candidate.Title, RegenerateCodeActionTitle, StringComparison.Ordinal)) + ?? actions.First() + : actions.Single(candidate => string.Equals(candidate.Title, codeActionTitle, StringComparison.Ordinal)); + var operations = await action.GetOperationsAsync(CancellationToken.None); + var changedSolution = operations.OfType().Single().ChangedSolution; + + var additionalDocumentId = changedSolution.GetProject(project.Id)?.AdditionalDocumentIds.FirstOrDefault(); + + return (changedSolution, additionalDocumentId); + } + + private async Task ApplyCodeFixAndGetContractsAsync( + string source, + string contractsFileContent, + string expectedDiagnosticId) + { + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, contractsFileContent, expectedDiagnosticId); + Assert.NotNull(additionalDocumentId); + + var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId!); + Assert.NotNull(changedDocument); + + return (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + } + + private static void AssertContainsGeneratedMethod( + string content, + string clrSignature, + string contractSignatureSuffix) + { + var pattern = + $"(?m)^ # {Regex.Escape(clrSignature)}\\r?$\\n" + + $" [0-9A-F]{{8}}{Regex.Escape(contractSignatureSuffix)}\\r?$"; + Assert.Matches(pattern, content); + } + + private static void AssertDiagnosticIds(IEnumerable diagnostics, params string[] expected) + => Assert.Equal( + expected.OrderBy(id => id, StringComparer.Ordinal), + diagnostics.Select(diagnostic => diagnostic.Id).OrderBy(id => id, StringComparer.Ordinal)); + + private static Project CreateProjectWithAdditionalFilesForCodeFix( + string source, + string? grainInterfacesFileContent, + string? configuredContractsPath = null, + string? generatedSource = null) + { + const string fileName = "Test.cs"; + + // Prepend usings + var sb = new StringBuilder(); + foreach (var @using in Usings) + { + sb.AppendLine($"using {@using};"); + } + sb.AppendLine(source); + var fullSource = sb.ToString(); + + var projectId = ProjectId.CreateNewId(debugName: "TestProject"); + var documentId = DocumentId.CreateNewId(projectId, fileName); + + var assemblies = new[] + { + typeof(Task).Assembly, + typeof(Orleans.IGrain).Assembly, + typeof(Orleans.Grain).Assembly, + typeof(Attribute).Assembly, + typeof(int).Assembly, + typeof(object).Assembly, + }; + + var metadataReferences = assemblies + .SelectMany(x => x.GetReferencedAssemblies().Select(Assembly.Load)) + .Concat(assemblies) + .Distinct() + .Select(x => MetadataReference.CreateFromFile(x.Location)) + .Cast() + .ToList(); + + var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); + + var solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp) + .AddMetadataReferences(projectId, metadataReferences) + .AddDocument(documentId, fileName, SourceText.From(fullSource)); + + if (generatedSource is not null) + { + solution = solution.AddDocument( + DocumentId.CreateNewId(projectId, "Generated.g.cs"), + "Generated.g.cs", + SourceText.From(generatedSource), + filePath: "Generated.g.cs"); + } + + // Add additional document if content is provided + if (grainInterfacesFileContent is not null) + { + var contractsDocumentPath = configuredContractsPath is null + ? OrleansContractsFileName + : configuredContractsPath.Replace('/', Path.DirectorySeparatorChar); + var contractsDocumentName = Path.GetFileName(contractsDocumentPath); + var additionalDocumentId = DocumentId.CreateNewId(projectId, contractsDocumentName); + solution = solution.AddAdditionalDocument( + additionalDocumentId, + contractsDocumentName, + SourceText.From(grainInterfacesFileContent), + filePath: contractsDocumentPath); + } + + if (configuredContractsPath is not null) + { + var analyzerConfigDirectory = Path.GetDirectoryName(configuredContractsPath); + var analyzerConfigPath = string.IsNullOrEmpty(analyzerConfigDirectory) + ? Path.Combine(Path.GetTempPath(), ".globalconfig") + : Path.Combine(analyzerConfigDirectory, ".globalconfig"); + solution = solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId, ".globalconfig"), + ".globalconfig", + SourceText.From( + $"is_global = true{Environment.NewLine}" + + $"build_property.OrleansContractsPath = {configuredContractsPath}{Environment.NewLine}"), + filePath: analyzerConfigPath); + } + + return solution.GetProject(projectId)! + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + #endregion + + #region Code Fix Tests - Regenerate + + [Fact] + public async Task CodeFix_RegenerateMissingFile_CreatesCompleteManifest() + { + const string source = @" +[Version(2)] +[GrainInterfaceType(""cart"")] +public interface ICartGrain : IGrain +{ + [Alias(""read"")] + Task GetAsync(int itemId); +} + +[GrainType(""cart"")] +public class CartGrain : Grain, IGrainWithStringKey +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + Assert.NotNull(additionalDocumentId); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"cart\")] ICartGrain [Version(2)]", content); + Assert.Contains(" [Alias(\"read\")] read(int) -> Task", content); + Assert.Contains("class [GrainType(\"cart\")] CartGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateMissingFile_UsesConfiguredManifestPath() + { + var configuredPath = Path.Combine(Path.GetTempPath(), "contracts", "rpc-contracts.txt"); + var configuredPathWithAlternateSeparators = configuredPath.Replace('\\', '/'); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); }", + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + configuredPathWithAlternateSeparators); + + Assert.NotNull(additionalDocumentId); + Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); + } + + [Fact] + public async Task CodeFix_RegenerateMissingFile_UsesFilenameOnlyConfiguredManifestPath() + { + const string configuredPath = "rpc-contracts.txt"; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); }", + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + configuredPath); + + Assert.NotNull(additionalDocumentId); + Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); + } + + [Fact] + public async Task CodeFix_RegenerateExistingFile_UsesConfiguredCustomFilename() + { + var configuredPath = Path.Combine(Path.GetTempPath(), "contracts", "rpc-contracts.txt"); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); Task Pong(); }", + "interface IMyGrain [Version(0)]\n Ping() -> Task\n", + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle, + configuredPath); + + Assert.NotNull(additionalDocumentId); + var project = changedSolution.GetProject(changedSolution.ProjectIds.Single())!; + var document = Assert.Single(project.AdditionalDocuments); + Assert.Equal(configuredPath, document.FilePath); + Assert.Equal("rpc-contracts.txt", document.Name); + AssertContainsGeneratedMethod( + (await document.GetTextAsync(TestContext.Current.CancellationToken)).ToString(), + "IMyGrain.Pong() -> Task", + "() -> Task"); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesContractHistory() + { + const string source = @" +public interface ICurrentGrain : IGrain +{ + Task ReadAsync(); +} +"; + const string contractsFile = @" +interface ILegacyGrain [Version(1)] + WriteAsync(int) -> Task + +*RETIRED* class [GrainType(""retired"")] RetiredGrain +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"ICurrentGrain\")] ICurrentGrain [Version(0)]", content); + AssertContainsGeneratedMethod(content, "ICurrentGrain.ReadAsync() -> Task", "() -> Task"); + Assert.Contains("*RETIRED* interface ILegacyGrain [Version(1)]", content); + Assert.Contains(" WriteAsync(int) -> Task", content); + Assert.Contains("*RETIRED* class [GrainType(\"retired\")] RetiredGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DistinguishesClrNamesFromExplicitIdentities() + { + const string source = @" +[GrainInterfaceType(""current-id"")] +public interface LegacyName : IGrain +{ +} +"; + const string contractsFile = @" +interface [GrainInterfaceType(""LegacyName"")] OldContract [Version(0)] +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"current-id\")] LegacyName [Version(0)]", content); + Assert.Contains( + "*RETIRED* interface [GrainInterfaceType(\"LegacyName\")] OldContract [Version(0)]", + content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesLegacyIdentityWhenExplicitIdentityChanges() + { + const string source = @" +[GrainInterfaceType(""new-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = "interface IMyGrain [Version(0)]"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"new-id\")] IMyGrain [Version(0)]", content); + Assert.Contains("*RETIRED* interface IMyGrain [Version(0)]", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesLegacyIdentityAfterClrRename() + { + const string source = @" +[GrainInterfaceType(""OldName"")] +public interface NewName : IGrain +{ +} + +public interface ITriggerGrain : IGrain +{ +} +"; + const string contractsFile = "interface OldName [Version(0)]"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"OldName\")] NewName [Version(0)]", content); + Assert.DoesNotContain("*RETIRED* interface OldName", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_ExcludesGeneratedGrainProxies() + { + const string source = @" +public interface IMyGrain : IGrain +{ + Task Ping(); +} +"; + const string generatedSource = @" +public sealed class GeneratedProxy : IMyGrain +{ + public System.Threading.Tasks.Task Ping() => System.Threading.Tasks.Task.CompletedTask; +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + generatedSource: generatedSource); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"IMyGrain\")] IMyGrain [Version(0)]", content); + Assert.DoesNotContain("GeneratedProxy", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesAliasedGeneratedCodeAttribute() + { + const string source = @" +using GC = System.CodeDom.Compiler.GeneratedCodeAttribute; + +public interface IMyGrain : IGrain +{ +} + +[GC(""Test"", ""1.0"")] +public sealed class GeneratedProxy : IMyGrain +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.DoesNotContain("GeneratedProxy", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DoesNotTreatSimilarlyNamedAttributeAsGeneratedCode() + { + const string source = @" +public sealed class CustomGeneratedCodeAttribute : Attribute +{ +} + +[CustomGeneratedCode] +public sealed class MyGrain : Grain, IGrainWithStringKey { - [Alias(""stable-method"")] - Task NewMethod(NewRequest renamedParameter); } "; - var contractsFile = await ApplyCodeFixAndGetContractsAsync( - oldSource, - "# OrleansContracts.txt\n", - GrainInterfaceVersionAnalyzer.RuleId0016); - var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); - Assert.Empty(diagnostics); - Assert.Contains("# IOldGrain\ninterface [GrainInterfaceType(\"stable-interface\")] IOldGrain [Version(0)]", contractsFile); - Assert.Contains( - " stable-method(request) -> Task", - contractsFile); - Assert.Contains("# IOldGrain", contractsFile); - Assert.Contains("# IOldGrain.OldMethod", contractsFile); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"my\")] MyGrain", content); } [Fact] - public async Task StableInterfaceIdentityChange_IsBreaking() + public async Task CodeFix_RegenerateProject_IncludesSourcePartialGrainWithGeneratedPartial() { const string source = @" -[GrainInterfaceType(""new-identity"")] -public interface IMyGrain : IGrain +public partial class MyGrain : Grain, IGrainWithStringKey { } "; - const string contractsFile = @" -[GrainInterfaceType(""old-identity"")] IMyGrain [Version(0)] + const string generatedSource = @" +[System.CodeDom.Compiler.GeneratedCode(""Test"", ""1.0"")] +public partial class MyGrain +{ +} "; - var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + generatedSource: generatedSource); - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0016); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"my\")] MyGrain", content); + + var sourceText = (await changedSolution.Projects.Single().Documents + .Single(document => document.Name == "Test.cs") + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.DoesNotContain("[Alias(", sourceText); + Assert.DoesNotContain("[Id(", sourceText); + Assert.DoesNotContain("[GrainType(", sourceText); + Assert.DoesNotContain("[GrainInterfaceType(", sourceText); } [Fact] - public async Task StableGrainTypeChange_IsBreaking() + public async Task CodeFix_RegenerateProject_RecognizesLegacyGrainTypeAfterClrRename() { const string source = @" -[GrainType(""new-identity"")] -public class MyGrain : Grain, IGrainWithStringKey +[GrainType(""old"")] +public class NewGrain : Grain, IGrainWithStringKey +{ +} + +public interface ITriggerGrain : IGrain { } "; - const string contractsFile = "class [GrainType(\"old-identity\")] MyGrain"; + const string contractsFile = "class OldGrain"; - var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0023); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"old\")] NewGrain", content); + Assert.DoesNotContain("*RETIRED* class OldGrain", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); } - #endregion - - #region Code Fix Tests Infrastructure - - private async Task<(Solution ChangedSolution, DocumentId? AdditionalDocumentId)> ApplyCodeFixAsync( - string source, - string? grainInterfacesFileContent, - string expectedDiagnosticId) + [Fact] + public async Task CodeFix_RegenerateProject_RoundTripsContractsNestedInGenericTypes() { - var project = CreateProjectWithAdditionalFilesForCodeFix(source, grainInterfacesFileContent); - var document = project.Documents.First(); - var compilation = await project.GetCompilationAsync(); - - Assert.NotNull(compilation); - var errors = compilation!.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error); - Assert.Empty(errors); + const string source = @" +public class Outer +{ + public interface IInnerGrain : IGrain + { + Task Ping(T value); + } - var analyzer = new GrainInterfaceVersionAnalyzer(); + public class InnerGrain : Grain, IGrainWithStringKey + { + } +} +"; - // Build analyzer options with additional files - var additionalFiles = grainInterfacesFileContent is not null - ? ImmutableArray.Create(new TestAdditionalText(OrleansContractsFileName, grainInterfacesFileContent)) - : ImmutableArray.Empty; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); - var analyzerOptions = CreateAnalyzerOptions(additionalFiles, analyzerEnabled: true); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains( + "interface [GrainInterfaceType(\"Outer`1+IInnerGrain\")] Outer.IInnerGrain [Version(0)]", + content); + Assert.Contains("class [GrainType(\"inner`1\")] Outer.InnerGrain", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } - var compilationWithAnalyzers = compilation - .WithOptions(compilation.Options.WithSpecificDiagnosticOptions( - analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default))) - .WithAnalyzers(ImmutableArray.Create(analyzer), analyzerOptions); + [Fact] + public async Task CodeFix_RegenerateProject_PreservesRemovedMemberSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; - var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); - var diagnostic = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnosticId); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); - Assert.NotNull(diagnostic); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + AssertContainsGeneratedMethod(content, "IMyGrain.ExistingAsync() -> Task", "() -> Task"); + AssertContainsGeneratedMethod(content, "IMyGrain.NewAsync() -> Task", "() -> Task"); + Assert.Contains(" RemovedAsync() -> Task", content); - // Apply code fix - var codeFixer = new GrainInterfaceVersionCodeFix(); - var actions = new List(); - var context = new CodeFixContext( - document, - diagnostic!, - (action, _) => actions.Add(action), - CancellationToken.None); + var diagnostics = await GetDiagnosticsAsync(source, content); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } - await codeFixer.RegisterCodeFixesAsync(context); - Assert.NotEmpty(actions); + [Fact] + public async Task CodeFix_RegenerateProject_PreservesAliasesAndDeduplicatesLegacyMembers() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + [Alias(""removed"")] IMyGrain.RemovedAsync(string value) -> Task + removed(string) -> Task +"; - var operations = await actions.First().GetOperationsAsync(CancellationToken.None); - var changedSolution = operations.OfType().Single().ChangedSolution; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); - var additionalDocumentId = changedSolution.GetProject(project.Id)?.AdditionalDocumentIds.FirstOrDefault(); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Equal(1, content.Split(new[] { "removed(string) -> Task" }, StringSplitOptions.None).Length - 1); + Assert.Contains("[Alias(\"removed\")] removed(string) -> Task", content); - return (changedSolution, additionalDocumentId); + var diagnostics = await GetDiagnosticsAsync(source, content); + Assert.Single(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } - private async Task ApplyCodeFixAndGetContractsAsync( - string source, - string contractsFileContent, - string expectedDiagnosticId) + [Fact] + public async Task CodeFix_RegenerateProject_DeduplicatesSemanticallyEquivalentLegacyMembers() { - var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, contractsFileContent, expectedDiagnosticId); - Assert.NotNull(additionalDocumentId); + const string source = @" +namespace Models +{ + public sealed class Request { } +} - var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId!); - Assert.NotNull(changedDocument); +[Version(1)] +public interface IMyGrain : IGrain +{ + Task Method(Models.Request request); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Method(Request value) -> Task +"; - return (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + AssertContainsGeneratedMethod( + content, + "IMyGrain.Method(Request request) -> Task", + "(Models.Request) -> Task"); + Assert.DoesNotContain(" Method(Request) -> Task", content); + Assert.DoesNotContain( + await GetDiagnosticsAsync(source, content), + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } - private static Project CreateProjectWithAdditionalFilesForCodeFix(string source, string? grainInterfacesFileContent) + [Fact] + public async Task CodeFix_RegenerateProject_PreservesRemovedMembersOnNestedLegacyInterfaces() { - const string fileName = "Test.cs"; - - // Prepend usings - var sb = new StringBuilder(); - foreach (var @using in Usings) - { - sb.AppendLine($"using {@using};"); - } - sb.AppendLine(source); - var fullSource = sb.ToString(); + const string source = @" +public class Outer +{ + [Version(1)] + public interface IInnerGrain : IGrain + { + Task ExistingAsync(); + Task NewAsync(); + } +} +"; + const string contractsFile = @" +interface Outer.IInnerGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; - var projectId = ProjectId.CreateNewId(debugName: "TestProject"); - var documentId = DocumentId.CreateNewId(projectId, fileName); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); - var assemblies = new[] + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains(" RemovedAsync() -> Task", content); + Assert.Contains( + await GetDiagnosticsAsync(source, content), + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task FixAll_RegenerateSolution_UpdatesEveryProjectWithDiagnostics() + { + var firstProject = CreateProjectWithAdditionalFilesForCodeFix( + "public interface IFirstGrain : IGrain { Task Ping(); }", + "# OrleansContracts.txt\n"); + var solution = firstProject.Solution; + var secondProjectId = ProjectId.CreateNewId("SecondProject"); + var secondDocumentId = DocumentId.CreateNewId(secondProjectId, "Second.cs"); + var secondContractsId = DocumentId.CreateNewId(secondProjectId, OrleansContractsFileName); + var secondSource = string.Join( + Environment.NewLine, + Usings.Select(@using => $"using {@using};").Append( + "public interface ISecondGrain : IGrain { Task Pong(); }")); + solution = solution + .AddProject(secondProjectId, "SecondProject", "SecondProject", LanguageNames.CSharp) + .AddMetadataReferences(secondProjectId, firstProject.MetadataReferences) + .WithProjectCompilationOptions(secondProjectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddDocument(secondDocumentId, "Second.cs", SourceText.From(secondSource)) + .AddAdditionalDocument(secondContractsId, OrleansContractsFileName, SourceText.From("# OrleansContracts.txt\n")); + + firstProject = solution.GetProject(firstProject.Id)!; + var diagnostics = new Dictionary> { - typeof(Task).Assembly, - typeof(Orleans.IGrain).Assembly, - typeof(Orleans.Grain).Assembly, - typeof(Attribute).Assembly, - typeof(int).Assembly, - typeof(object).Assembly, + [firstProject.Id] = new[] { CreateFixAllDiagnostic() }, + [secondProjectId] = new[] { CreateFixAllDiagnostic() } }; + var codeFixer = new GrainInterfaceVersionCodeFix(); + var context = new FixAllContext( + firstProject.Documents.First(), + codeFixer, + FixAllScope.Solution, + RegenerateCodeActionEquivalenceKey, + codeFixer.FixableDiagnosticIds, + new TestFixAllDiagnosticProvider(diagnostics), + TestContext.Current.CancellationToken); - var metadataReferences = assemblies - .SelectMany(x => x.GetReferencedAssemblies().Select(Assembly.Load)) - .Concat(assemblies) - .Distinct() - .Select(x => MetadataReference.CreateFromFile(x.Location)) - .Cast() - .ToList(); - - var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll"))); - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll"))); - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll"))); - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); - - var solution = new AdhocWorkspace() - .CurrentSolution - .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp) - .AddMetadataReferences(projectId, metadataReferences) - .AddDocument(documentId, fileName, SourceText.From(fullSource)); - - // Add additional document if content is provided - if (grainInterfacesFileContent is not null) - { - var additionalDocumentId = DocumentId.CreateNewId(projectId, OrleansContractsFileName); - solution = solution.AddAdditionalDocument(additionalDocumentId, OrleansContractsFileName, SourceText.From(grainInterfacesFileContent)); - } - - return solution.GetProject(projectId)! - .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var action = await codeFixer.GetFixAllProvider().GetFixAsync(context); + Assert.NotNull(action); + var operations = await action!.GetOperationsAsync(TestContext.Current.CancellationToken); + var changedSolution = operations.OfType().Single().ChangedSolution; + var firstContent = (await changedSolution.GetProject(firstProject.Id)!.AdditionalDocuments.Single() + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var secondContent = (await changedSolution.GetProject(secondProjectId)!.AdditionalDocuments.Single() + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + + Assert.Contains("interface [GrainInterfaceType(\"IFirstGrain\")] IFirstGrain [Version(0)]", firstContent); + Assert.Contains("interface [GrainInterfaceType(\"ISecondGrain\")] ISecondGrain [Version(0)]", secondContent); + } + + private static Diagnostic CreateFixAllDiagnostic() + => Diagnostic.Create( + new DiagnosticDescriptor( + GrainInterfaceVersionAnalyzer.RuleId0016, + "Contract missing", + "Contract missing", + "Orleans.Versioning", + DiagnosticSeverity.Warning, + isEnabledByDefault: true), + Location.None); + + private sealed class TestFixAllDiagnosticProvider( + IReadOnlyDictionary> diagnostics) : FixAllContext.DiagnosticProvider + { + public override Task> GetDocumentDiagnosticsAsync( + Document document, + CancellationToken cancellationToken) + => Task.FromResult(Enumerable.Empty()); + + public override Task> GetProjectDiagnosticsAsync( + Project project, + CancellationToken cancellationToken) + => Task.FromResult(Enumerable.Empty()); + + public override Task> GetAllDiagnosticsAsync( + Project project, + CancellationToken cancellationToken) + => Task.FromResult( + diagnostics.TryGetValue(project.Id, out var result) + ? result + : Enumerable.Empty()); } #endregion @@ -1318,7 +2392,7 @@ public interface IMyGrain : IGrain // Should contain the new interface Assert.Contains("IMyGrain [Version(1)]", content); - Assert.Contains("\n DoSomething() -> Task", content); + AssertContainsGeneratedMethod(content, "IMyGrain.DoSomething() -> Task", "() -> Task"); Assert.DoesNotContain("Utility", content); } @@ -1347,7 +2421,10 @@ public interface IMyGrain : IGrain Assert.DoesNotContain("[Alias(", content); Assert.Contains("IMyGrain [Version(2)]", content); - Assert.Contains("\n DoSomething(string) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.DoSomething(string name) -> Task", + "(string) -> Task"); } [Fact] @@ -1425,22 +2502,12 @@ public interface IMiddle : IGrain var alphaInterface = content.IndexOf("IAlpha [Version(1)]", StringComparison.Ordinal); var middleInterface = content.IndexOf("IMiddle [Version(1)]", StringComparison.Ordinal); var zuluInterface = content.IndexOf("IZulu [Version(1)]", StringComparison.Ordinal); - var alphaMember = content.IndexOf(" Alpha() -> Task", StringComparison.Ordinal); - var zetaMember = content.IndexOf(" Zeta() -> Task", StringComparison.Ordinal); - Assert.True(alphaInterface < middleInterface); Assert.True(middleInterface < zuluInterface); - Assert.True(alphaMember < zetaMember); - Assert.Equal( - GeneratedHeader + - "interface IAlpha [Version(1)]\n" + - " Method() -> Task\n\n" + - "interface [GrainInterfaceType(\"IMiddle\")] IMiddle [Version(1)]\n" + - " Alpha() -> Task\n" + - " Zeta() -> Task\n\n" + - "interface IZulu [Version(1)]\n" + - " Method() -> Task\n", - content); + AssertContainsGeneratedMethod(content, "IMiddle.Alpha() -> Task", "() -> Task"); + AssertContainsGeneratedMethod(content, "IMiddle.Zeta() -> Task", "() -> Task"); + Assert.Contains("interface IAlpha [Version(1)]\n Method() -> Task", content); + Assert.Contains("interface IZulu [Version(1)]\n Method() -> Task", content); } [Fact] @@ -1533,6 +2600,26 @@ public interface INewGrain : IGrain Assert.Equal(1, content.Split(new[] { "stable-interface" }, StringSplitOptions.None).Length - 1); } + [Fact] + public async Task CodeFix_AddInterface_PreservesLegacyDeclarationWhenIdentityChanges() + { + const string source = @" +[GrainInterfaceType(""new-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = "interface IMyGrain [Version(0)]"; + + var content = await ApplyCodeFixAndGetContractsAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016); + + Assert.Contains("interface [GrainInterfaceType(\"new-id\")] IMyGrain [Version(0)]", content); + Assert.Contains("interface IMyGrain [Version(0)]", content); + } + #endregion #region Code Fix Tests - ORLEANS0017 Update Version @@ -1655,7 +2742,10 @@ IMyGrain [Version(1)] var content = changedText.ToString(); // Should contain the new member - Assert.Contains("\n NewMethod(int) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.NewMethod(int value) -> Task", + "(int) -> Task"); } [Fact] @@ -1684,7 +2774,7 @@ IMyGrain [Version(1)] var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); var content = changedText.ToString(); - Assert.Contains("\n new-method(int) -> Task", content); + Assert.Contains("\n [Alias(\"new-method\")] new-method(int) -> Task", content); Assert.Contains(" # IMyGrain.NewMethod(int value) -> Task", content); } @@ -1706,7 +2796,9 @@ public interface IMyGrain : IGrain contractsFile, GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.Equal(GeneratedHeader + "interface IMyGrain [Version(1)]\n NewMethod() -> Task\n", content); + Assert.Equal( + GeneratedHeader + "interface IMyGrain [Version(1)]\n [Alias(\"NewMethod\")] NewMethod() -> Task\n", + content); } [Fact] @@ -1726,9 +2818,11 @@ public interface IMyGrain : IGrain contractsFile, GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.Equal( - GeneratedHeader + "interface IMyGrain [Version(1)]\n ReadStateAsync`1(T) -> Task\n", - content); + Assert.StartsWith(GeneratedHeader + "interface IMyGrain [Version(1)]\n", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.ReadStateAsync(T value) -> Task", + "`1(T) -> Task"); } [Fact] @@ -1908,6 +3002,32 @@ IFoo [Version(1)] Assert.DoesNotContain("*RETIRED* interface IFooBar", content); } + [Fact] + public async Task CodeFix_RetireInterface_UsesStableIdentityWhenClrNamesMatch() + { + const string source = @" +[GrainInterfaceType(""current-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string grainInterfacesFile = @" +interface [GrainInterfaceType(""current-id"")] IMyGrain [Version(0)] +interface [GrainInterfaceType(""old-id"")] IMyGrain [Version(0)] +"; + + var content = await ApplyCodeFixAndGetContractsAsync( + source, + grainInterfacesFile, + GrainInterfaceVersionAnalyzer.RuleId0019); + + Assert.Contains("interface [GrainInterfaceType(\"current-id\")] IMyGrain [Version(0)]", content); + Assert.DoesNotContain("*RETIRED* interface [GrainInterfaceType(\"current-id\")]", content); + Assert.Contains( + "*RETIRED* interface [GrainInterfaceType(\"old-id\")] IMyGrain [Version(0)]", + content); + } + #endregion #region Inherited Interfaces @@ -2049,10 +3169,8 @@ public interface IMyGrain : IGrain } "; const string contractsFile = "# OrleansContracts.txt\r\nIMyGrain [Version(1)]\r\nIMyGrain.DoSomething() -> Task\r\n"; - const string expectedContractsFile = - "# This file is auto-generated by the Orleans contract analyzer.\r\n" + - "# Update source contracts, then regenerate this file by following:\r\n" + - "# https://aka.ms/orleans/OrleansContracts.txt\r\n\r\n" + + var expectedContractsFile = + GeneratedHeader.Replace("\n", "\r\n", StringComparison.Ordinal) + "interface IMyGrain [Version(2)]\r\n" + " DoSomething() -> Task\r\n"; var properties = ImmutableDictionary.Empty @@ -2066,7 +3184,9 @@ public interface IMyGrain : IGrain var codeFixer = new GrainInterfaceVersionCodeFix(); await codeFixer.RegisterCodeFixesAsync(context); - var action = Assert.Single(actions); + var action = Assert.Single( + actions, + action => !string.Equals(action.Title, RegenerateCodeActionTitle, StringComparison.Ordinal)); var operations = await action.GetOperationsAsync(TestContext.Current.CancellationToken); var changedSolution = Assert.Single(operations.OfType()).ChangedSolution; var changedProject = changedSolution.GetProject(context.Document.Project.Id); @@ -2074,10 +3194,12 @@ public interface IMyGrain : IGrain var additionalDocumentId = Assert.Single(changedProject!.AdditionalDocumentIds); var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId); Assert.NotNull(changedDocument); - var actualContractsFile = (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); + var actualContractsFile = changedText.ToString(); Assert.Equal(expectedContractsFile, actualContractsFile); Assert.DoesNotContain("\n", actualContractsFile.Replace("\r\n", string.Empty, StringComparison.Ordinal)); + Assert.Empty(changedText.Encoding?.GetPreamble() ?? []); } [Theory] From 137d9acc17830f15b13a4eb0058d6cee633cad5e Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:02:19 -0700 Subject: [PATCH 07/10] fix(analyzers): clarify Orleans contract identities (#10910) --- .../content/docs/diagnostics/orleans0016.md | 2 +- .../content/docs/diagnostics/orleans0017.md | 2 +- .../content/docs/diagnostics/orleans0018.md | 8 +- .../content/docs/diagnostics/orleans0019.md | 2 +- .../content/docs/diagnostics/orleans0020.md | 2 +- .../content/docs/diagnostics/orleans0021.md | 2 +- .../content/docs/diagnostics/orleans0022.md | 2 +- .../content/docs/diagnostics/orleans0023.md | 2 +- .../content/docs/diagnostics/orleans0024.md | 2 +- .../content/docs/diagnostics/orleans0025.md | 2 +- .../content/docs/diagnostics/orleans0027.md | 6 +- .../contract-compatibility-analyzer.md | 32 +- .../Orleans.Dashboard/OrleansContracts.txt | 40 +- .../AnalyzerReleases.Unshipped.md | 22 +- .../GrainInterfaceVersionAnalyzer.cs | 402 ++++++++------- .../GrainInterfaceVersionCodeFix.cs | 155 +++--- src/Orleans.Analyzers/Resources.Designer.cs | 2 +- src/Orleans.Analyzers/Resources.resx | 2 +- .../OrleansContracts.txt | 10 +- .../OrleansContracts.txt | 28 +- src/Orleans.Core/OrleansContracts.txt | 156 ++---- src/Orleans.DurableJobs/OrleansContracts.txt | 10 +- .../OrleansContracts.txt | 16 +- .../OrleansContracts.txt | 4 +- src/Orleans.Reminders/OrleansContracts.txt | 43 +- src/Orleans.Runtime/OrleansContracts.txt | 85 ++- src/Orleans.Streaming/OrleansContracts.txt | 91 ++-- src/Orleans.TestingHost/OrleansContracts.txt | 22 +- .../OrleansContracts.txt | 124 ++--- src/Orleans.Transactions/OrleansContracts.txt | 28 +- .../GrainInterfaceVersionAnalyzerTest.cs | 482 +++++++++++++++--- 31 files changed, 988 insertions(+), 798 deletions(-) diff --git a/docs/site/src/content/docs/diagnostics/orleans0016.md b/docs/site/src/content/docs/diagnostics/orleans0016.md index 2f2c75fdeb..1bf27ecaf1 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0016.md +++ b/docs/site/src/content/docs/diagnostics/orleans0016.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0017.md b/docs/site/src/content/docs/diagnostics/orleans0017.md index 983451d8b0..26bc4c8160 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0017.md +++ b/docs/site/src/content/docs/diagnostics/orleans0017.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0018.md b/docs/site/src/content/docs/diagnostics/orleans0018.md index 0ff2be4170..bb3f0033e5 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0018.md +++ b/docs/site/src/content/docs/diagnostics/orleans0018.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0018: Grain interface member not declared" description: Understand and resolve ORLEANS0018 when an RPC method signature is missing from OrleansContracts.txt. -ms.date: 08/27/2026 +ms.date: 08/28/2026 ms.topic: reference --- @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | @@ -17,7 +17,7 @@ ms.topic: reference An ordinary grain-interface method has no matching contract signature in `OrleansContracts.txt`. Method identity, generic arity, parameter types and order, and return type are part of the signature. Parameter names are not. -The method identity is the source `[Id]` value, the source `[Alias]` value, or the generated xxHash32 ID used by the Orleans code generator. Recording a generated ID in the manifest does not add an attribute or change the runtime identity. +The method identity is the source `[Id]` value, the source `[Alias]` value, or the generated xxHash32 ID used by the Orleans code generator. The manifest records the resulting identifier before the colon, independent of how source declares it. ## Impact @@ -25,7 +25,7 @@ Older activations can receive an unknown RPC, and changed identities or payload ## How to fix -Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the existing effective wire identity and does not increment `[Version]` or modify source attributes. +Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the CLR signature and effective wire identity in the manifest. Source attributes remain unchanged. Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). diff --git a/docs/site/src/content/docs/diagnostics/orleans0019.md b/docs/site/src/content/docs/diagnostics/orleans0019.md index f258d8b0b3..4723b680a8 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0019.md +++ b/docs/site/src/content/docs/diagnostics/orleans0019.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0020.md b/docs/site/src/content/docs/diagnostics/orleans0020.md index aa4568d27f..bcaf46b507 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0020.md +++ b/docs/site/src/content/docs/diagnostics/orleans0020.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Info | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0021.md b/docs/site/src/content/docs/diagnostics/orleans0021.md index a8dd3d0d39..34aa12b433 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0021.md +++ b/docs/site/src/content/docs/diagnostics/orleans0021.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Not available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0022.md b/docs/site/src/content/docs/diagnostics/orleans0022.md index e23e1fa69b..6373c7d789 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0022.md +++ b/docs/site/src/content/docs/diagnostics/orleans0022.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0023.md b/docs/site/src/content/docs/diagnostics/orleans0023.md index 3fffe564cf..24b2709977 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0023.md +++ b/docs/site/src/content/docs/diagnostics/orleans0023.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0024.md b/docs/site/src/content/docs/diagnostics/orleans0024.md index ecccb23223..16996ca824 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0024.md +++ b/docs/site/src/content/docs/diagnostics/orleans0024.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0025.md b/docs/site/src/content/docs/diagnostics/orleans0025.md index 23fe3dd8ec..0d3edcaa71 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0025.md +++ b/docs/site/src/content/docs/diagnostics/orleans0025.md @@ -9,7 +9,7 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Not available | diff --git a/docs/site/src/content/docs/diagnostics/orleans0027.md b/docs/site/src/content/docs/diagnostics/orleans0027.md index 94c1a4e877..1fd34de1cc 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0027.md +++ b/docs/site/src/content/docs/diagnostics/orleans0027.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0027: Grain interface member removed from source" description: Understand and resolve ORLEANS0027 when OrleansContracts.txt retains an RPC method which is absent from source. -ms.date: 08/27/2026 +ms.date: 08/28/2026 ms.topic: reference --- @@ -9,13 +9,13 @@ ms.topic: reference | Property | Value | | --- | --- | -| Category | Orleans.Versioning | +| Category | Versioning | | Severity | Warning | | Code fix | Not available | ## Cause -`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. The manifest identity is an explicit `[Id]` or `[Alias]` value when present in source; otherwise, it is the generated method ID already used by Orleans on the wire. +`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. The value before the colon is the effective method identity Orleans uses on the wire. ## Impact diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index ba74ed48b0..4ad39c9adb 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -1,7 +1,7 @@ --- title: Orleans contract compatibility analyzer description: Track grain RPC contracts during development to identify changes which can break rolling upgrades. -ms.date: 08/27/2026 +ms.date: 08/28/2026 ms.topic: concept-article --- @@ -19,6 +19,15 @@ The analyzer is **disabled by default**. Enable it explicitly in a project file Projects which use `Microsoft.Orleans.Sdk`, `Microsoft.Orleans.Client`, or `Microsoft.Orleans.Server` already receive the Orleans analyzers through those packages. A project which references `Microsoft.Orleans.Analyzers` directly can use the same property. +To promote every contract diagnostic, configure the standard `Versioning` category: + +```ini +[*.cs] +dotnet_analyzer_diagnostic.category-Versioning.severity = error +``` + +This also promotes informational diagnostics such as `ORLEANS0020`. Configure `dotnet_diagnostic.ORLEANS####.severity` entries instead when only selected contract diagnostics should change severity. + ## Configure the manifest path By default, the analyzer looks for `OrleansContracts.txt` beside the project file. The analyzer package automatically adds an existing file at that location as a compiler `AdditionalFile`; no explicit `AdditionalFiles` item is required. @@ -54,9 +63,9 @@ dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostic Run the command from the repository root. Replace `PATH_TO_PROJECT_OR_SOLUTION` with the path to the owning `.csproj` to regenerate one manifest, or a `.sln`/`.slnx` path to regenerate manifests in every affected project. The `--severity info` option includes `ORLEANS0020`, allowing the command to create a missing manifest. -Regeneration edits `OrleansContracts.txt` files only. It does not add or change `[Alias]`, `[Id]`, `[GrainType]`, or `[GrainInterfaceType]` attributes in source. Attribute-like syntax in the manifest records the effective identity which Orleans already uses at runtime. +Regeneration edits `OrleansContracts.txt` files only. Source `[Alias]`, `[Id]`, `[GrainType]`, and `[GrainInterfaceType]` attributes remain unchanged. -For a method without `[Id]` or `[Alias]`, the manifest records the same generated xxHash32 method ID which the Orleans code generator already uses on the wire. The preceding comment records the CLR signature so reviewers can map the wire ID back to source. A one-time upgrade from an older manifest format can therefore replace a CLR method name with its existing generated ID; this records the current wire contract and does not change it. +Every method line places the effective wire identity before a colon, followed by the CLR method name and signature. The identity is the source `[Id]` value, source `[Alias]` value, or the generated xxHash32 method ID already used by the Orleans code generator. The stable identity appears first so contract-breaking changes are prominent in diffs, while CLR-only renames keep the same leading value. After the command completes: @@ -70,7 +79,7 @@ Add the generated file to source control and review its diff before committing. - A removed source contract becomes `*RETIRED*`, preserving its identity history and preventing accidental reuse. - A removed RPC method remains in the manifest and reports `ORLEANS0027` until the method is restored or the wire break is explicitly accepted by removing the retained signature. - A `[Version]` change affects version-aware routing and must align with the rolling-upgrade design. -- A CLR comment-only change records a refactor while the explicit Orleans identity remains stable. +- A changed CLR method name with an unchanged identity records a refactor while the Orleans wire contract remains stable. Coding agents should regenerate the manifest instead of hand-editing active entries, retain retired history, and explain the compatibility impact of each contract diff in the change description. @@ -85,15 +94,15 @@ Interface methods are indented beneath their interface: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Contoso.Grains.ICartGrain")] Contoso.Grains.ICartGrain [Version(1)] - # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task - 15793847(Contoso.Grains.Item) -> Task - # Contoso.Grains.ICartGrain.GetAsync() -> Task - 857AC6B2() -> Task + 15793847: AddAsync(Contoso.Grains.Item) -> Task + 857AC6B2: GetAsync() -> Task class [GrainType("cart")] Contoso.Grains.CartGrain ``` @@ -105,14 +114,13 @@ Explicit identities remain visible alongside their CLR names: ```text # Contoso.Grains.ICartGrain interface [GrainInterfaceType("cart")] Contoso.Grains.ICartGrain [Version(1)] - # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task - [Alias("add")] add(Contoso.Grains.Item) -> Task + add: AddAsync(Contoso.Grains.Item) -> Task # Contoso.Grains.CartGrain class [GrainType("cart")] Contoso.Grains.CartGrain ``` -`[Alias("...")]` on a manifest method records that the identity comes from a source `[Alias]` attribute. An unmarked eight-digit hexadecimal method identity is the generated wire ID. Comments record CLR names when they improve traceability; comments are informational and aren't part of contract matching. +The value before the colon is the effective runtime identity. A source `[Id(42)]`, source `[Alias("42")]`, and generated method ID `42` describe the same wire identity. The manifest records that result as `42:` and keeps source provenance in source code. Syntax-sensitive characters in aliases are backslash-escaped. `*RETIRED*` marks an intentionally removed contract: diff --git a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt index 181ecb811a..90173c6df0 100644 --- a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt +++ b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt @@ -4,35 +4,37 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardGrain")] Orleans.Dashboard.Core.IDashboardGrain [Version(0)] - [Alias("GetClusterTracing")] GetClusterTracing() -> Task>> - [Alias("GetCounters")] GetCounters(string[]) -> Task> - [Alias("GetGrainState")] GetGrainState(string?, string?) -> Task> - [Alias("GetGrainTracing")] GetGrainTracing(string) -> Task>>> - [Alias("GetGrainTypes")] GetGrainTypes(string[]) -> Task> - [Alias("GetSiloTracing")] GetSiloTracing(string) -> Task>> - [Alias("InitializeAsync")] InitializeAsync() -> Task - [Alias("SubmitTracing")] SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task - [Alias("TopGrainMethods")] TopGrainMethods(int, string[]) -> Task>> + GetClusterTracing: GetClusterTracing() -> Task>> + GetCounters: GetCounters(string[]) -> Task> + GetGrainState: GetGrainState(string?, string?) -> Task> + GetGrainTracing: GetGrainTracing(string) -> Task>>> + GetGrainTypes: GetGrainTypes(string[]) -> Task> + GetSiloTracing: GetSiloTracing(string) -> Task>> + InitializeAsync: InitializeAsync() -> Task + SubmitTracing: SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task + TopGrainMethods: TopGrainMethods(int, string[]) -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardRemindersGrain")] Orleans.Dashboard.Core.IDashboardRemindersGrain [Version(0)] - [Alias("GetReminders")] GetReminders(int, int) -> Task> + GetReminders: GetReminders(int, int) -> Task> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainProxy")] Orleans.Dashboard.Core.ISiloGrainProxy [Version(0)] - [Alias("GetMetadata")] GetMetadata() -> Task>> + GetMetadata: GetMetadata() -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainService")] Orleans.Dashboard.Core.ISiloGrainService [Version(0)] - [Alias("Enable")] Enable(bool) -> Task - [Alias("GetCounters")] GetCounters() -> Task> - [Alias("GetExtendedProperties")] GetExtendedProperties() -> Task>> - [Alias("GetLifecycleStages")] GetLifecycleStages() -> Task> - [Alias("GetRuntimeStatistics")] GetRuntimeStatistics() -> Task> - [Alias("ReportCounters")] ReportCounters(Orleans.Concurrency.Immutable) -> Task - [Alias("SetVersion")] SetVersion(string, string) -> Task + Enable: Enable(bool) -> Task + GetCounters: GetCounters() -> Task> + GetExtendedProperties: GetExtendedProperties() -> Task>> + GetLifecycleStages: GetLifecycleStages() -> Task> + GetRuntimeStatistics: GetRuntimeStatistics() -> Task> + ReportCounters: ReportCounters(Orleans.Concurrency.Immutable) -> Task + SetVersion: SetVersion(string, string) -> Task class [GrainType("dashboard")] Orleans.Dashboard.Implementation.Grains.DashboardGrain diff --git a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md index 30fabca306..826a1e4f6a 100644 --- a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md @@ -6,14 +6,14 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- ORLEANS0026 | Usage | Error | Invalid invokable base type mapping ORLEANS0014 | Usage | Warning | ConfigureAwaitAnalyzer, Grain code should not use ConfigureAwait(false) or ConfigureAwait without ContinueOnCapturedContext -ORLEANS0016 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface not declared in OrleansContracts.txt -ORLEANS0017 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface version mismatch between code and file -ORLEANS0018 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface member not declared in OrleansContracts.txt -ORLEANS0019 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed interface not marked as *RETIRED* -ORLEANS0020 | Orleans.Versioning | Info | GrainInterfaceVersionAnalyzer, OrleansContracts.txt file is missing -ORLEANS0021 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate interface declaration in file -ORLEANS0022 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class not declared in OrleansContracts.txt -ORLEANS0023 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class alias mismatch -ORLEANS0024 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain class not marked as *RETIRED* -ORLEANS0025 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate grain class declaration in file -ORLEANS0027 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain interface member remains in OrleansContracts.txt +ORLEANS0016 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface not declared in OrleansContracts.txt +ORLEANS0017 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface version mismatch between code and file +ORLEANS0018 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain interface member not declared in OrleansContracts.txt +ORLEANS0019 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed interface not marked as *RETIRED* +ORLEANS0020 | Versioning | Info | GrainInterfaceVersionAnalyzer, OrleansContracts.txt file is missing +ORLEANS0021 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate interface declaration in file +ORLEANS0022 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class not declared in OrleansContracts.txt +ORLEANS0023 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class alias mismatch +ORLEANS0024 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain class not marked as *RETIRED* +ORLEANS0025 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate grain class declaration in file +ORLEANS0027 | Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain interface member remains in OrleansContracts.txt diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index 888ed99019..b5a6331704 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -8,6 +8,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -23,6 +24,7 @@ namespace Orleans.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer { + private const string DiagnosticCategory = "Versioning"; public const string EnableAnalyzerPropertyName = "EnableOrleansContractsAnalyzer"; public const string RuleId0016 = "ORLEANS0016"; public const string RuleId0017 = "ORLEANS0017"; @@ -39,8 +41,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer // Property bag keys for code fixes internal const string InterfaceNamePropertyKey = "InterfaceName"; internal const string MemberNamePropertyKey = "MemberName"; - internal const string MemberAliasPropertyKey = "MemberAlias"; - internal const string MemberClrSignaturePropertyKey = "MemberClrSignature"; + internal const string MemberWireIdentityPropertyKey = "MemberWireIdentity"; internal const string ExpectedVersionPropertyKey = "ExpectedVersion"; internal const string ActualVersionPropertyKey = "ActualVersion"; internal const string ExpectedSignaturePropertyKey = "ExpectedSignature"; @@ -55,7 +56,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0016, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceNotDeclaredTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceNotDeclaredMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainInterfaceNotDeclaredDescription), Resources.ResourceManager, typeof(Resources)), @@ -65,7 +66,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0017, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceVersionMismatchTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceVersionMismatchMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainInterfaceVersionMismatchDescription), Resources.ResourceManager, typeof(Resources)), @@ -75,7 +76,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0018, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberNotDeclaredTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberNotDeclaredMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberNotDeclaredDescription), Resources.ResourceManager, typeof(Resources)), @@ -85,7 +86,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0027, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedDescription), Resources.ResourceManager, typeof(Resources)), @@ -96,7 +97,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0019, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceRemovedNotRetiredTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceRemovedNotRetiredMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainInterfaceRemovedNotRetiredDescription), Resources.ResourceManager, typeof(Resources)), @@ -107,7 +108,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0020, title: new LocalizableResourceString(nameof(Resources.OrleansContractsFileMissingTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.OrleansContractsFileMissingMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.OrleansContractsFileMissingDescription), Resources.ResourceManager, typeof(Resources)), @@ -118,7 +119,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0021, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceDuplicateDeclarationTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceDuplicateDeclarationMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainInterfaceDuplicateDeclarationDescription), Resources.ResourceManager, typeof(Resources)), @@ -128,7 +129,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0022, title: new LocalizableResourceString(nameof(Resources.GrainClassNotDeclaredTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainClassNotDeclaredMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainClassNotDeclaredDescription), Resources.ResourceManager, typeof(Resources)), @@ -138,7 +139,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0023, title: new LocalizableResourceString(nameof(Resources.GrainClassAliasMismatchTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainClassAliasMismatchMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainClassAliasMismatchDescription), Resources.ResourceManager, typeof(Resources)), @@ -148,7 +149,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0024, title: new LocalizableResourceString(nameof(Resources.GrainClassRemovedNotRetiredTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainClassRemovedNotRetiredMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainClassRemovedNotRetiredDescription), Resources.ResourceManager, typeof(Resources)), @@ -159,7 +160,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer id: RuleId0025, title: new LocalizableResourceString(nameof(Resources.GrainClassDuplicateDeclarationTitle), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.GrainClassDuplicateDeclarationMessageFormat), Resources.ResourceManager, typeof(Resources)), - category: "Orleans.Versioning", + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.GrainClassDuplicateDeclarationDescription), Resources.ResourceManager, typeof(Resources)), @@ -378,23 +379,22 @@ explicitGrainInterfaceType is null // Check members foreach (var member in sourceMembers) { - var memberSignature = GetMethodSignature(member); - var memberAlias = GetAliasFromAttribute(member); - - if (!declaredInterface.Members.Values.Any(declaredMember => - GrainInterfaceVersionAnalyzer.IsMatchingMember( + var declaredMember = declaredInterface.Members.Values + .Where(candidate => GrainInterfaceVersionAnalyzer.IsMatchingMember( declaredInterface.Name, - declaredMember.Signature, - declaredMember.Alias, - member))) + candidate.Signature, + candidate.WireIdentity, + member)) + .FirstOrDefault(); + + if (declaredMember is null) { // Member not found - interface has changed var properties = ImmutableDictionary.Empty .Add(InterfaceNamePropertyKey, interfaceName) .Add(GrainInterfaceTypePropertyKey, grainInterfaceType) - .Add(MemberNamePropertyKey, memberSignature) - .Add(MemberAliasPropertyKey, memberAlias) - .Add(MemberClrSignaturePropertyKey, RequiresClrComment(member) ? GetClrMethodSignature(member) : null); + .Add(MemberNamePropertyKey, GetManifestMethodSignature(member)) + .Add(MemberWireIdentityPropertyKey, GetMethodWireIdentity(member).ToManifestMetadata()); foreach (var location in member.Locations.Where(l => l.IsInSource)) { @@ -402,9 +402,9 @@ explicitGrainInterfaceType is null MemberNotDeclaredRule, location, properties, - memberSignature, - interfaceName, - GetClrMethodSignature(member))); + GetClrMethodSignature(member), + GetMethodWireIdentity(member).Value, + interfaceName)); } } } @@ -417,7 +417,7 @@ explicitGrainInterfaceType is null GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( declaredInterface.Name, declaredMember.Signature, - declaredMember.Alias, + declaredMember.WireIdentity, member))) { continue; @@ -427,11 +427,14 @@ explicitGrainInterfaceType is null .Add(InterfaceNamePropertyKey, interfaceName) .Add(GrainInterfaceTypePropertyKey, grainInterfaceType) .Add(MemberNamePropertyKey, declaredMember.Signature); + var declaredMemberLine = GrainInterfaceFileParser.FormatMemberDeclaration( + declaredMember.Signature, + declaredMember.WireIdentity); _removedMemberDiagnostics.Add(Diagnostic.Create( RemovedMemberRule, declaredMember.GetLocation(sourceText, _grainInterfacesFile.Path), properties, - declaredMember.Signature, + declaredMemberLine, interfaceName)); } } @@ -809,9 +812,8 @@ internal static string GetDefaultGrainInterfaceType(string typeName) internal static string GetMethodSignature(IMethodSymbol method) { - var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName); - var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); - return GetMethodSignature(method, methodId ?? methodAlias ?? MethodIdProvider.Create(method)); + var identity = GetMethodWireIdentity(method); + return GetMethodSignature(method, identity.Value); } private static string GetMethodSignature(IMethodSymbol method, object methodId) @@ -838,6 +840,24 @@ private static string GetMethodSignature(IMethodSymbol method, object methodId) return sb.ToString(); } + internal static string GetManifestMethodSignature(IMethodSymbol method) + => GetMethodSignature(method, method.Name); + + internal static MethodWireIdentity GetMethodWireIdentity(IMethodSymbol method) + { + if (GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName) is { } methodId) + { + return new MethodWireIdentity(Convert.ToString(methodId, CultureInfo.InvariantCulture)!); + } + + if (GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName) is { } methodAlias) + { + return new MethodWireIdentity(methodAlias); + } + + return new MethodWireIdentity(MethodIdProvider.Create(method)); + } + internal static string GetClrMethodSignature(IMethodSymbol method) { var sb = new StringBuilder(); @@ -865,25 +885,6 @@ internal static string GetClrMethodSignature(IMethodSymbol method) return sb.ToString(); } - internal static bool RequiresClrComment(IMethodSymbol method) - { - var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName)?.ToString(); - var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); - if (methodId is null && methodAlias is null) - { - return true; - } - - if (methodId is not null && !string.Equals(methodId, method.Name, StringComparison.Ordinal) - || methodAlias is not null && !string.Equals(methodAlias, method.Name, StringComparison.Ordinal)) - { - return true; - } - - return method.Parameters.Any(parameter => HasMeaningfulTypeAlias(parameter.Type)) - || HasMeaningfulTypeAlias(method.ReturnType); - } - internal static bool IdentityDiffersFromClrName(string? identity, INamedTypeSymbol type) { if (identity is null) @@ -897,9 +898,6 @@ internal static bool IdentityDiffersFromClrName(string? identity, INamedTypeSymb && !string.Equals(identity, fullName, StringComparison.Ordinal); } - internal static string NormalizeLegacyMethodSignature(string signature) - => Regex.Replace(signature, @"\s+[A-Za-z_]\w*(?=\s*[,)\]])", ""); - internal static string NormalizeStoredMemberSignature(string signature, string interfaceName) { var result = signature; @@ -915,111 +913,37 @@ internal static string NormalizeStoredMemberSignature(string signature, string i internal static bool IsMatchingMember( string declaredInterfaceName, string storedSignature, - string? storedAlias, + MethodWireIdentity storedWireIdentity, IMethodSymbol member) { - var memberSignature = GetMethodSignature(member); - var sourceMethodId = GetAttributeValue(member, Constants.IdAttributeFullyQualifiedName); - var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); - if (storedAlias is not null) - { - if (!string.Equals( - storedAlias, - sourceMethodAlias, - StringComparison.Ordinal)) - { - return false; - } - - var storedIdentity = storedAlias + GrainInterfaceFileParser.GetMethodAritySuffix(storedSignature); - var parameterListStart = memberSignature.IndexOf('('); - if (parameterListStart < 0 - || !string.Equals( - storedIdentity, - memberSignature.Substring(0, parameterListStart), - StringComparison.Ordinal)) - { - return false; - } - - var canonicalStoredSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( - storedSignature, - storedAlias); - if (string.Equals( - NormalizeStoredMemberSignature(canonicalStoredSignature, declaredInterfaceName), - memberSignature, - StringComparison.Ordinal)) - { - return true; - } - - return string.Equals( - GetNormalizedSignatureSuffix(storedSignature), - GetNormalizedSignatureSuffix(GetClrMethodSignature(member)), - StringComparison.Ordinal); - } - - var normalizedStoredSignature = NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName); - if (sourceMethodAlias is not null) - { - return false; - } - - if (sourceMethodId is not null) - { - return string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal); - } - - if (string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal)) - { - return true; - } - - if (string.Equals( - normalizedStoredSignature, - GetMethodSignature(member, member.Name), - StringComparison.Ordinal)) - { - return true; - } - - var normalized = NormalizeLegacyMethodSignature(storedSignature); - var clrSignature = GetClrMethodSignature(member); - var containingTypePrefix = - $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; - return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) - || string.Equals( - normalized, - NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), + var sourceWireIdentity = GetMethodWireIdentity(member); + return string.Equals( + storedWireIdentity.Value, + sourceWireIdentity.Value, + StringComparison.Ordinal) + && string.Equals( + GrainInterfaceFileParser.GetMethodAritySuffix(storedSignature), + GrainInterfaceFileParser.GetMethodAritySuffix(GetManifestMethodSignature(member)), + StringComparison.Ordinal) + && string.Equals( + GetSignatureSuffix(storedSignature), + GetSignatureSuffix(GetManifestMethodSignature(member)), StringComparison.Ordinal); } internal static bool IsMatchingHistoricalMember( string declaredInterfaceName, string storedSignature, - string? storedAlias, + MethodWireIdentity storedWireIdentity, IMethodSymbol member) { - if (IsMatchingMember(declaredInterfaceName, storedSignature, storedAlias, member)) - { - return true; - } - - var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); - return storedAlias is null - && sourceMethodAlias is not null - && string.Equals( - NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName), - GetMethodSignature(member), - StringComparison.Ordinal); + return IsMatchingMember(declaredInterfaceName, storedSignature, storedWireIdentity, member); } - private static string GetNormalizedSignatureSuffix(string signature) + private static string GetSignatureSuffix(string signature) { var parameterListStart = signature.IndexOf('('); - return parameterListStart < 0 - ? NormalizeLegacyMethodSignature(signature) - : NormalizeLegacyMethodSignature(signature.Substring(parameterListStart)); + return parameterListStart < 0 ? signature : signature.Substring(parameterListStart); } private static string GetContractTypeName(ITypeSymbol type) @@ -1087,27 +1011,6 @@ private static string GetContractTypeName(ITypeSymbol type) return $"{genericName}<{string.Join(", ", namedType.TypeArguments.Select(GetContractTypeName))}>"; } - private static bool HasMeaningfulTypeAlias(ITypeSymbol type) - { - if (type is IArrayTypeSymbol array) - { - return HasMeaningfulTypeAlias(array.ElementType); - } - - if (type is not INamedTypeSymbol namedType) - { - return false; - } - - var alias = GetStringAttributeValue(namedType.OriginalDefinition, Constants.AliasAttributeFullyQualifiedName); - if (IdentityDiffersFromClrName(alias, namedType.OriginalDefinition)) - { - return true; - } - - return namedType.TypeArguments.Any(HasMeaningfulTypeAlias); - } - private static object? GetAttributeValue(ISymbol symbol, string attributeName) => symbol.GetAttributes() .FirstOrDefault(attribute => string.Equals(attribute.AttributeClass?.ToDisplayString(), attributeName, StringComparison.Ordinal)) @@ -1125,6 +1028,7 @@ internal sealed class GrainInterfaceData public List Interfaces { get; } = new(); public List Classes { get; } = new(); + } /// @@ -1158,13 +1062,14 @@ public Location GetLocation(SourceText sourceText, string filePath) /// internal sealed class DeclaredGrainMember { - public DeclaredGrainMember(string signature) + public DeclaredGrainMember(string signature, MethodWireIdentity wireIdentity) { Signature = signature; + WireIdentity = wireIdentity; } public string Signature { get; } - public string? Alias { get; set; } + public MethodWireIdentity WireIdentity { get; } public TextSpan Span { get; set; } public Location GetLocation(SourceText sourceText, string filePath) @@ -1174,6 +1079,40 @@ public Location GetLocation(SourceText sourceText, string filePath) } } +internal readonly struct MethodWireIdentity +{ + public MethodWireIdentity(string value) + { + Value = value; + } + + public string Value { get; } + + public string ToManifestMetadata() + { + var result = new StringBuilder(Value.Length); + foreach (var character in Value) + { + result.Append(character switch + { + '\\' => @"\\", + ':' => @"\:", + '#' => @"\#", + ' ' => @"\s", + '\r' => @"\r", + '\n' => @"\n", + '\t' => @"\t", + _ when char.IsControl(character) || char.IsWhiteSpace(character) + => $"\\u{((int)character).ToString("X4", CultureInfo.InvariantCulture)}", + _ => character.ToString() + }); + } + + return result.ToString(); + } + +} + /// /// Represents a declared grain class in the OrleansContracts.txt file. /// @@ -1218,10 +1157,10 @@ internal static class GrainInterfaceFileParser @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)$", RegexOptions.Compiled); - // Member line: [Alias("x")] Namespace.IInterface.Method(params) -> ReturnType - // The signature includes the full interface name (possibly generic) and method + // Member line: 0123ABCD: Method(params) -> ReturnType + // The value is the effective wire identity, independent of how source declares it. private static readonly Regex MemberPattern = new( - @"^(\[Alias\(""(?[^""]+)""\)\]\s*)?(?.+\(.*\)\s*->\s*.+)$", + @"^(?.+\(.*\)\s*->\s*.+)$", RegexOptions.Compiled); internal static bool TryGetInterfaceName(string line, out string name) @@ -1281,9 +1220,9 @@ internal static bool TryGetContractName(string line, out string name) internal static bool TryGetMemberSignature(string line, out string signature) { - if (TryGetMemberDeclaration(line, out signature, out var alias)) + if (TryGetMemberDeclaration(line, out signature, out var wireIdentity)) { - signature = GetCanonicalMemberSignature(signature, alias); + signature = GetCanonicalMemberSignature(signature, wireIdentity); return true; } @@ -1291,32 +1230,52 @@ internal static bool TryGetMemberSignature(string line, out string signature) return false; } - internal static bool TryGetMemberDeclaration(string line, out string signature, out string? alias) + internal static bool TryGetMemberDeclaration( + string line, + out string signature, + out MethodWireIdentity wireIdentity) { - var match = MemberPattern.Match(StripClrComment(line)); + var declaration = StripClrComment(line); + var identityDelimiter = FindIdentityDelimiter(declaration); + if (identityDelimiter < 0) + { + signature = string.Empty; + wireIdentity = default; + return false; + } + + wireIdentity = new MethodWireIdentity( + UnescapeWireIdentity(declaration.Substring(0, identityDelimiter))); + declaration = declaration.Substring(identityDelimiter + 2); + var match = MemberPattern.Match(declaration); if (match.Success) { signature = match.Groups["signature"].Value; - alias = match.Groups["alias"].Success ? match.Groups["alias"].Value : null; return true; } signature = string.Empty; - alias = null; + wireIdentity = default; return false; } - internal static string GetCanonicalMemberSignature(string signature, string? alias) + internal static string GetCanonicalMemberSignature( + string signature, + MethodWireIdentity wireIdentity) { - if (alias is null) - { - return signature; - } - var parameterListStart = signature.IndexOf('('); return parameterListStart < 0 ? signature - : alias + GetMethodAritySuffix(signature) + signature.Substring(parameterListStart); + : $"{wireIdentity.Value.Length}:{wireIdentity.Value}" + + GetMethodAritySuffix(signature) + + signature.Substring(parameterListStart); + } + + internal static string FormatMemberDeclaration( + string signature, + MethodWireIdentity wireIdentity) + { + return $"{wireIdentity.ToManifestMetadata()}: {signature}"; } internal static string GetMethodAritySuffix(string signature) @@ -1368,6 +1327,70 @@ internal static string StripClrComment(string line) return (index < 0 ? line : line.Substring(0, index)).Trim(); } + private static int FindIdentityDelimiter(string declaration) + { + var escaped = false; + for (var index = 0; index < declaration.Length - 1; index++) + { + if (escaped) + { + escaped = false; + continue; + } + + if (declaration[index] == '\\') + { + escaped = true; + continue; + } + + if (declaration[index] == ':' && declaration[index + 1] == ' ') + { + return index; + } + } + + return -1; + } + + private static string UnescapeWireIdentity(string value) + { + var result = new StringBuilder(value.Length); + for (var index = 0; index < value.Length; index++) + { + if (value[index] == '\\' && index + 1 < value.Length) + { + if (value[index + 1] == 'u' + && index + 5 < value.Length + && ushort.TryParse( + value.Substring(index + 2, 4), + NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out var codePoint)) + { + result.Append((char)codePoint); + index += 5; + continue; + } + + index++; + result.Append(value[index] switch + { + 'r' => '\r', + 'n' => '\n', + 't' => '\t', + 's' => ' ', + _ => value[index] + }); + continue; + } + + result.Append(value[index]); + } + + return result.ToString(); + } + public static (GrainInterfaceData Data, List? Errors) Parse(SourceText sourceText, string filePath) { var data = new GrainInterfaceData(); @@ -1464,15 +1487,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe } // Try to match member declaration - var memberMatch = MemberPattern.Match(lineText); - if (memberMatch.Success && currentInterface is not null) + if (currentInterface is not null + && TryGetMemberDeclaration(lineText, out var signature, out var wireIdentity)) { - var signature = memberMatch.Groups["signature"].Value; - var alias = memberMatch.Groups["alias"].Success ? memberMatch.Groups["alias"].Value : null; - - currentInterface.Members[signature] = new DeclaredGrainMember(signature) + var key = GetCanonicalMemberSignature(signature, wireIdentity); + currentInterface.Members[key] = new DeclaredGrainMember(signature, wireIdentity) { - Alias = alias, Span = textLine.Span }; } diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index e31560066f..00c53b0db9 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -37,7 +37,9 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider $"# {RegenerationCommand}", "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION", "# The regeneration command edits this manifest only; it does not change source attributes.", - "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.", + "# OrleansContracts format: 2", + "# Method lines use: wire-identity: CLR-signature.", + "# The identity is the identifier Orleans uses at runtime, whether generated or declared in source.", "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.", "# Details: https://aka.ms/orleans/OrleansContracts.txt" ]; @@ -108,7 +110,8 @@ private static bool HasRequiredProperties(Diagnostic diagnostic) && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ActualVersionPropertyKey), GrainInterfaceVersionAnalyzer.RuleId0018 => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) - && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.MemberNamePropertyKey), + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.MemberNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.MemberWireIdentityPropertyKey), GrainInterfaceVersionAnalyzer.RuleId0019 => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), GrainInterfaceVersionAnalyzer.RuleId0020 => true, @@ -429,16 +432,11 @@ private static void AppendInterface( foreach (var member in type.GetMembers() .OfType() .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) - .OrderBy(GrainInterfaceVersionAnalyzer.GetMethodSignature, StringComparer.Ordinal)) + .OrderBy(GrainInterfaceVersionAnalyzer.GetManifestMethodSignature, StringComparer.Ordinal)) { - if (GrainInterfaceVersionAnalyzer.RequiresClrComment(member)) - { - lines.Add($" # {GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member)}"); - } - - lines.Add($" {FormatStoredMember( - GrainInterfaceVersionAnalyzer.GetMethodSignature(member), - GetAliasFromAttributes(member))}"); + lines.Add($" {GrainInterfaceFileParser.FormatMemberDeclaration( + GrainInterfaceVersionAnalyzer.GetManifestMethodSignature(member), + GrainInterfaceVersionAnalyzer.GetMethodWireIdentity(member))}"); } } @@ -616,9 +614,15 @@ private static void MergeHistoricalMembers( break; } - if (GrainInterfaceFileParser.TryGetMemberSignature(result[generatedBlockEnd], out var memberSignature)) + if (GrainInterfaceFileParser.TryGetMemberDeclaration( + result[generatedBlockEnd], + out var memberSignature, + out var memberWireIdentity)) { - generatedMembers.Add(GetHistoricalMemberKey(memberSignature, historicalInterfaceName)); + generatedMembers.Add(GetHistoricalMemberKey( + memberSignature, + memberWireIdentity, + historicalInterfaceName)); } generatedBlockEnd++; @@ -637,7 +641,7 @@ private static void MergeHistoricalMembers( if (!GrainInterfaceFileParser.TryGetMemberDeclaration( line, out var memberSignature, - out var memberAlias)) + out var memberWireIdentity)) { pendingComment = null; continue; @@ -650,7 +654,7 @@ private static void MergeHistoricalMembers( && GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( historicalInterfaceName, memberSignature, - memberAlias, + memberWireIdentity, member))) { pendingComment = null; @@ -658,9 +662,12 @@ private static void MergeHistoricalMembers( } var normalizedSignature = GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature( - GrainInterfaceFileParser.GetCanonicalMemberSignature(memberSignature, memberAlias), + memberSignature, + historicalInterfaceName); + var historicalMemberKey = GetHistoricalMemberKey( + memberSignature, + memberWireIdentity, historicalInterfaceName); - var historicalMemberKey = GetHistoricalMemberKey(normalizedSignature, historicalInterfaceName); if (generatedMembers.Add(historicalMemberKey)) { if (pendingComment is not null) @@ -668,16 +675,22 @@ private static void MergeHistoricalMembers( result.Insert(generatedBlockEnd++, $" {pendingComment}"); } - result.Insert(generatedBlockEnd++, $" {FormatStoredMember(historicalMemberKey, memberAlias)}"); + result.Insert(generatedBlockEnd++, $" {GrainInterfaceFileParser.FormatMemberDeclaration( + normalizedSignature, + memberWireIdentity)}"); } pendingComment = null; } } - private static string GetHistoricalMemberKey(string signature, string interfaceName) - => GrainInterfaceVersionAnalyzer.NormalizeLegacyMethodSignature( - GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature(signature, interfaceName)); + private static string GetHistoricalMemberKey( + string signature, + MethodWireIdentity wireIdentity, + string interfaceName) + => GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature( + GrainInterfaceFileParser.GetCanonicalMemberSignature(signature, wireIdentity), + interfaceName); private static void AppendBlockSeparator(List lines) { @@ -806,8 +819,9 @@ private static void RegisterAddMemberCodeFix(CodeFixContext context, Diagnostic return; } - diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberClrSignaturePropertyKey, out var memberClrSignature); - diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberAliasPropertyKey, out var memberAlias); + diagnostic.Properties.TryGetValue( + GrainInterfaceVersionAnalyzer.MemberWireIdentityPropertyKey, + out var memberWireIdentity); diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( @@ -818,8 +832,7 @@ private static void RegisterAddMemberCodeFix(CodeFixContext context, Diagnostic interfaceName!, grainInterfaceType, memberSignature!, - memberAlias, - memberClrSignature, + memberWireIdentity!, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0018), diagnostic); @@ -1106,7 +1119,7 @@ private static async Task AddInterfaceToFileAsync( var interfaceLine = sb.ToString(); // Build member lines - var memberLines = new List<(string Signature, string? Alias, string? ClrSignature)>(); + var memberLines = new List<(string Signature, MethodWireIdentity WireIdentity)>(); foreach (var member in symbol.GetMembers().OfType()) { if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) @@ -1114,13 +1127,10 @@ private static async Task AddInterfaceToFileAsync( continue; } - var memberSignature = GrainInterfaceVersionAnalyzer.GetMethodSignature(member); + var memberSignature = GrainInterfaceVersionAnalyzer.GetManifestMethodSignature(member); memberLines.Add(( memberSignature, - GetAliasFromAttributes(member), - GrainInterfaceVersionAnalyzer.RequiresClrComment(member) - ? GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member) - : null)); + GrainInterfaceVersionAnalyzer.GetMethodWireIdentity(member))); } // Find or create the OrleansContracts.txt file @@ -1158,11 +1168,9 @@ private static async Task AddInterfaceToFileAsync( newContent += interfaceLine; foreach (var member in memberLines) { - if (member.ClrSignature is not null) - { - newContent += $"{newLine} # {member.ClrSignature}"; - } - newContent += $"{newLine} {FormatStoredMember(member.Signature, member.Alias)}"; + newContent += $"{newLine} {GrainInterfaceFileParser.FormatMemberDeclaration( + member.Signature, + member.WireIdentity)}"; } newContent = SortContractEntries(newContent, newLine); @@ -1179,11 +1187,10 @@ private static async Task AddInterfaceToFileAsync( AppendLine(content, interfaceLine, DefaultNewLine); foreach (var member in memberLines) { - if (member.ClrSignature is not null) - { - AppendLine(content, $" # {member.ClrSignature}", DefaultNewLine); - } - AppendLine(content, $" {FormatStoredMember(member.Signature, member.Alias)}", DefaultNewLine); + AppendLine( + content, + $" {GrainInterfaceFileParser.FormatMemberDeclaration(member.Signature, member.WireIdentity)}", + DefaultNewLine); } var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Utf8NoBom); @@ -1266,8 +1273,7 @@ private static async Task AddMemberToFileAsync( string interfaceName, string? grainInterfaceType, string memberSignature, - string? memberAlias, - string? memberClrSignature, + string memberWireIdentity, CancellationToken cancellationToken) { var project = document.Project; @@ -1303,7 +1309,7 @@ private static async Task AddMemberToFileAsync( || trimmedLine.StartsWith("#", StringComparison.Ordinal) || GrainInterfaceFileParser.TryGetContractName(trimmedLine, out _))) { - AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberWireIdentity, newLine); insertedMember = true; } @@ -1319,7 +1325,7 @@ private static async Task AddMemberToFileAsync( // If we didn't insert the member yet, append it at the end if (foundInterface && !insertedMember) { - AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberWireIdentity, newLine); } // Remove trailing newline added by AppendLine @@ -1522,18 +1528,17 @@ private static string SortContractEntries(string content, string newLine) else if (GrainInterfaceFileParser.TryGetMemberDeclaration( line, out var storedMemberSignature, - out var memberAlias)) + out var memberWireIdentity)) { - var memberSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( - storedMemberSignature, - memberAlias); var inlineComment = GrainInterfaceFileParser.GetClrComment(line); - var normalizedMemberLine = NormalizeMemberLine(memberSignature, currentBlock.Name); + var memberComment = pendingComment + ?? (inlineComment.Length > 0 ? NormalizeComment(inlineComment) : null); + var normalizedMemberLine = NormalizeMemberLine(storedMemberSignature, currentBlock.Name); currentBlock.Members.Add(( normalizedMemberLine, normalizedMemberLine, - memberAlias, - pendingComment ?? (inlineComment.Length > 0 ? NormalizeComment(inlineComment) : null))); + memberWireIdentity, + memberComment)); pendingComment = null; } else if (!string.IsNullOrWhiteSpace(line)) @@ -1579,13 +1584,15 @@ private static string SortContractEntries(string content, string newLine) result.Add(block.Declaration); foreach (var member in block.Members .OrderBy(member => member.Signature, StringComparer.Ordinal) - .ThenBy(member => member.Line, StringComparer.Ordinal)) + .ThenBy(member => member.WireIdentity.Value, StringComparer.Ordinal)) { if (member.ClrComment is not null) { result.Add($" {member.ClrComment}"); } - result.Add($" {FormatStoredMember(member.Line, member.Alias)}"); + result.Add($" {GrainInterfaceFileParser.FormatMemberDeclaration( + member.Line, + member.WireIdentity)}"); } result.AddRange(block.OtherLines); } @@ -1600,6 +1607,7 @@ private static bool IsGeneratedHeaderLine(string line) or "# This file is auto-generated by the Orleans contract analyzer." or "# Update source contracts, then regenerate this file by following:" or "# https://aka.ms/orleans/OrleansContracts.txt" + or "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash." or "# This file tracks grain interface versions for compatibility during rolling upgrades." or "# Format:" or "# # Namespace.GrainClass" @@ -1648,22 +1656,12 @@ private static void AppendLine(StringBuilder builder, string value, string newLi private static void AppendMember( StringBuilder builder, string memberSignature, - string? memberAlias, - string? memberClrSignature, + string memberWireIdentity, string newLine) - { - if (!string.IsNullOrEmpty(memberClrSignature)) - { - AppendLine(builder, $" # {memberClrSignature}", newLine); - } - - AppendLine(builder, $" {FormatStoredMember(memberSignature, memberAlias)}", newLine); - } - - private static string FormatStoredMember(string memberSignature, string? memberAlias) - => memberAlias is null - ? memberSignature - : $"[Alias(\"{memberAlias}\")] {memberSignature}"; + => AppendLine( + builder, + $" {memberWireIdentity}: {memberSignature}", + newLine); private static ushort GetVersionFromAttributes(ISymbol symbol) { @@ -1682,23 +1680,6 @@ private static ushort GetVersionFromAttributes(ISymbol symbol) return 0; } - private static string? GetAliasFromAttributes(ISymbol symbol) - { - foreach (var attribute in symbol.GetAttributes()) - { - if (string.Equals(attribute.AttributeClass?.ToDisplayString(), Constants.AliasAttributeFullyQualifiedName, StringComparison.Ordinal)) - { - if (attribute.ConstructorArguments.Length > 0 && - attribute.ConstructorArguments[0].Value is string alias) - { - return alias; - } - } - } - - return null; - } - private static string? GetGrainTypeFromAttributes(ISymbol symbol) { foreach (var attribute in symbol.GetAttributes()) @@ -1744,7 +1725,7 @@ public ContractBlock(string name, string declaration, string? clrComment) public string? ClrComment { get; } - public List<(string Signature, string Line, string? Alias, string? ClrComment)> Members { get; } = new(); + public List<(string Signature, string Line, MethodWireIdentity WireIdentity, string? ClrComment)> Members { get; } = new(); public List OtherLines { get; } = new(); } diff --git a/src/Orleans.Analyzers/Resources.Designer.cs b/src/Orleans.Analyzers/Resources.Designer.cs index 5b3f26601b..dc7719f8b8 100644 --- a/src/Orleans.Analyzers/Resources.Designer.cs +++ b/src/Orleans.Analyzers/Resources.Designer.cs @@ -481,7 +481,7 @@ internal static string GrainInterfaceRemovedNotRetiredTitle { return ResourceManager.GetString("GrainInterfaceRemovedNotRetiredTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Add an OrleansContracts.txt file to track Orleans contracts for compatibility during rolling upgrades.. /// diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 6cfbcce209..488428fac4 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -236,7 +236,7 @@ Grain interface member not declared in OrleansContracts.txt - Grain interface source member '{2}' with wire signature '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain interface source member '{0}' with effective wire identity '{1}' is not declared in OrleansContracts.txt for interface '{2}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. diff --git a/src/Orleans.BroadcastChannel/OrleansContracts.txt b/src/Orleans.BroadcastChannel/OrleansContracts.txt index 2df212605c..2c6d837293 100644 --- a/src/Orleans.BroadcastChannel/OrleansContracts.txt +++ b/src/Orleans.BroadcastChannel/OrleansContracts.txt @@ -4,12 +4,12 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension")] Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension [Version(0)] - # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnError(InternalChannelId streamId, Exception exception) -> Task - 73F72B20(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task - # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnPublished(InternalChannelId streamId, object item) -> Task - B1E55518(Orleans.BroadcastChannel.InternalChannelId, object) -> Task + 73F72B20: OnError(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task + B1E55518: OnPublished(Orleans.BroadcastChannel.InternalChannelId, object) -> Task diff --git a/src/Orleans.Core.Abstractions/OrleansContracts.txt b/src/Orleans.Core.Abstractions/OrleansContracts.txt index 1f7df80e61..c0155d3cbe 100644 --- a/src/Orleans.Core.Abstractions/OrleansContracts.txt +++ b/src/Orleans.Core.Abstractions/OrleansContracts.txt @@ -4,15 +4,15 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Core.Internal.IGrainManagementExtension")] Orleans.Core.Internal.IGrainManagementExtension [Version(0)] - # Orleans.Core.Internal.IGrainManagementExtension.DeactivateOnIdle() -> ValueTask - 1B9614D1() -> ValueTask - # Orleans.Core.Internal.IGrainManagementExtension.MigrateOnIdle() -> ValueTask - 4CC93B45() -> ValueTask + 1B9614D1: DeactivateOnIdle() -> ValueTask + 4CC93B45: MigrateOnIdle() -> ValueTask interface [GrainInterfaceType("Orleans.IGrain")] Orleans.IGrain [Version(0)] @@ -31,20 +31,14 @@ interface [GrainInterfaceType("Orleans.IGrainWithStringKey")] Orleans.IGrainWith interface [GrainInterfaceType("Orleans.ISystemTarget")] Orleans.ISystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.Runtime.IAsyncEnumerableGrainExtension")] Orleans.Runtime.IAsyncEnumerableGrainExtension [Version(0)] - # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request) -> ValueTask<(EnumerationResult Status, object? Value)> - 370CD5AB`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - # Orleans.Runtime.IAsyncEnumerableGrainExtension.DisposeAsync(Guid requestId) -> ValueTask - 3C6D7209(System.Guid) -> ValueTask - # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> - 8678B466`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId) -> ValueTask<(EnumerationResult Status, object? Value)> - A7FA7E30`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> - E60EA75B`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + 3C6D7209: DisposeAsync(System.Guid) -> ValueTask + A7FA7E30: MoveNext`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + E60EA75B: MoveNext`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + 370CD5AB: StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + 8678B466: StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> interface [GrainInterfaceType("Orleans.Runtime.ICancellationSourcesExtension")] Orleans.Runtime.ICancellationSourcesExtension [Version(0)] - # Orleans.Runtime.ICancellationSourcesExtension.CancelRemoteToken(Guid tokenId) -> Task - 50F75C16(System.Guid) -> Task + 50F75C16: CancelRemoteToken(System.Guid) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainExtension")] Orleans.Runtime.IGrainExtension [Version(0)] diff --git a/src/Orleans.Core/OrleansContracts.txt b/src/Orleans.Core/OrleansContracts.txt index f41e5afc4d..8ab2713ef7 100644 --- a/src/Orleans.Core/OrleansContracts.txt +++ b/src/Orleans.Core/OrleansContracts.txt @@ -4,130 +4,84 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.ClientObservers.IClientGatewayObserver")] Orleans.ClientObservers.IClientGatewayObserver [Version(0)] - # Orleans.ClientObservers.IClientGatewayObserver.StopSendingToGateway(SiloAddress gateway) -> void - AFB768FD(Orleans.Runtime.SiloAddress) -> void + AFB768FD: StopSendingToGateway(Orleans.Runtime.SiloAddress) -> void interface [GrainInterfaceType("Orleans.IMembershipTableSystemTarget")] Orleans.IMembershipTableSystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.ISiloControl")] Orleans.ISiloControl [Version(0)] - # Orleans.ISiloControl.ForceRuntimeStatisticsCollection() -> Task - 0C7DBD0C() -> Task - # Orleans.ISiloControl.Ping(string message) -> Task - 1422B0B7(string) -> Task - # Orleans.ISiloControl.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task - 355CA3FA`1(string, int, object?) -> Task - # Orleans.ISiloControl.GetDetailedGrainReport(GrainId grainId) -> Task - 45172562(Orleans.Runtime.GrainId) -> Task - # Orleans.ISiloControl.ForceActivationCollection(TimeSpan ageLimit) -> Task - 45D07D09(System.TimeSpan) -> Task - # Orleans.ISiloControl.GetSimpleGrainStatistics() -> Task - 6DE16EF7() -> Task - # Orleans.ISiloControl.GetActiveGrains(GrainType grainType) -> Task> - 85797C87(Orleans.Runtime.GrainType) -> Task> - # Orleans.ISiloControl.GetDetailedGrainStatistics(string[]? types) -> Task> - B0F4C24B(string[]) -> Task> - # Orleans.ISiloControl.GetActivationCount() -> Task - C4C370A5() -> Task - # Orleans.ISiloControl.MigrateRandomActivations(SiloAddress target, int count) -> Task - E8327F0B(Orleans.Runtime.SiloAddress, int) -> Task - # Orleans.ISiloControl.GetRuntimeStatistics() -> Task - F18EAF24() -> Task - # Orleans.ISiloControl.ForceGarbageCollection() -> Task - F388CED1() -> Task - # Orleans.ISiloControl.GetGrainStatistics() -> Task>> - FF707A30() -> Task>> + 45D07D09: ForceActivationCollection(System.TimeSpan) -> Task + F388CED1: ForceGarbageCollection() -> Task + 0C7DBD0C: ForceRuntimeStatisticsCollection() -> Task + C4C370A5: GetActivationCount() -> Task + 85797C87: GetActiveGrains(Orleans.Runtime.GrainType) -> Task> + 45172562: GetDetailedGrainReport(Orleans.Runtime.GrainId) -> Task + B0F4C24B: GetDetailedGrainStatistics(string[]) -> Task> + FF707A30: GetGrainStatistics() -> Task>> + F18EAF24: GetRuntimeStatistics() -> Task + 6DE16EF7: GetSimpleGrainStatistics() -> Task + E8327F0B: MigrateRandomActivations(Orleans.Runtime.SiloAddress, int) -> Task + 1422B0B7: Ping(string) -> Task + 355CA3FA: SendControlCommandToProvider`1(string, int, object?) -> Task interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerMonitor")] Orleans.Placement.Rebalancing.IActivationRebalancerMonitor [Version(0)] - [Alias("Report")] Report(RebalancingReport) -> Task + Report: Report(RebalancingReport) -> Task interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerWorker")] Orleans.Placement.Rebalancing.IActivationRebalancerWorker [Version(0)] - [Alias("GetReport")] GetReport() -> ValueTask - [Alias("ResumeRebalancing")] ResumeRebalancing() -> Task - [Alias("SuspendRebalancing")] SuspendRebalancing(System.TimeSpan?) -> Task + GetReport: GetReport() -> ValueTask + ResumeRebalancing: ResumeRebalancing() -> Task + SuspendRebalancing: SuspendRebalancing(System.TimeSpan?) -> Task interface [GrainInterfaceType("Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget")] Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget [Version(0)] - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.FlushBuffers() -> ValueTask - 11731652() -> ValueTask - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.SetActivationCountOffset(int activationCountOffset) -> ValueTask - 135356E5(int) -> ValueTask - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.ResetCounters() -> ValueTask - 21852A09() -> ValueTask - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.AcceptExchangeRequest(AcceptExchangeRequest request) -> ValueTask - 9D8EDC44(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetActivationCount() -> ValueTask - 9FB525F3() -> ValueTask - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.TriggerExchangeRequest() -> ValueTask - A6EE4757() -> ValueTask - # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetGrainCallFrequencies() -> ValueTask> - C4497899() -> ValueTask> + 9D8EDC44: AcceptExchangeRequest(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask + 11731652: FlushBuffers() -> ValueTask + 9FB525F3: GetActivationCount() -> ValueTask + C4497899: GetGrainCallFrequencies() -> ValueTask> + 21852A09: ResetCounters() -> ValueTask + 135356E5: SetActivationCountOffset(int) -> ValueTask + A6EE4757: TriggerExchangeRequest() -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IClusterManifestSystemTarget")] Orleans.Runtime.IClusterManifestSystemTarget [Version(0)] - # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifest() -> ValueTask - 40D39F85() -> ValueTask - # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifestUpdate(MajorMinorVersion previousVersion) -> ValueTask - 4EFCA109(Orleans.Metadata.MajorMinorVersion) -> ValueTask + 40D39F85: GetClusterManifest() -> ValueTask + 4EFCA109: GetClusterManifestUpdate(Orleans.Metadata.MajorMinorVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IDeploymentLoadPublisher")] Orleans.Runtime.IDeploymentLoadPublisher [Version(0)] - # Orleans.Runtime.IDeploymentLoadPublisher.UpdateRuntimeStatistics(SiloAddress siloAddress, SiloRuntimeStatistics siloStats) -> Task - C5255F0C(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task + C5255F0C: UpdateRuntimeStatistics(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationExtension")] Orleans.Runtime.IGrainCallCancellationExtension [Version(0)] - # Orleans.Runtime.IGrainCallCancellationExtension.CancelRequestAsync(GrainId senderGrainId, CorrelationId messageId) -> ValueTask - FA239824(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask + FA239824: CancelRequestAsync(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IManagementGrain")] Orleans.Runtime.IManagementGrain [Version(0)] - # Orleans.Runtime.IManagementGrain.GetDetailedGrainStatistics(string[]? types, SiloAddress[]? hostsIds) -> Task - 0A1C0D82(string[], Orleans.Runtime.SiloAddress[]) -> Task - # Orleans.Runtime.IManagementGrain.GetGrainCallFrequencies(SiloAddress[]? hostsIds) -> Task> - 0F06E027(Orleans.Runtime.SiloAddress[]) -> Task> - # Orleans.Runtime.IManagementGrain.GetRuntimeStatistics(SiloAddress[] hostsIds) -> Task - 2D761B36(Orleans.Runtime.SiloAddress[]) -> Task - # Orleans.Runtime.IManagementGrain.GetActivationAddress(IAddressable reference) -> ValueTask - 317D82B6(Orleans.Runtime.IAddressable) -> ValueTask - # Orleans.Runtime.IManagementGrain.ForceActivationCollection(SiloAddress[] hostsIds, TimeSpan ageLimit) -> Task - 329F9A1B(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task - # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics(SiloAddress[] hostsIds) -> Task - 3CFF788C(Orleans.Runtime.SiloAddress[]) -> Task - # Orleans.Runtime.IManagementGrain.GetActiveGrains(GrainType type) -> ValueTask> - 3DB7923B(Orleans.Runtime.GrainType) -> ValueTask> - # Orleans.Runtime.IManagementGrain.GetHosts(bool onlyActive) -> Task> - 4C0864C2(bool) -> Task> - # Orleans.Runtime.IManagementGrain.ForceActivationCollection(TimeSpan ageLimit) -> Task - 54E6D1D1(System.TimeSpan) -> Task - # Orleans.Runtime.IManagementGrain.ResetGrainCallFrequencies(SiloAddress[]? hostsIds) -> ValueTask - 54FE0FEC(Orleans.Runtime.SiloAddress[]) -> ValueTask - # Orleans.Runtime.IManagementGrain.ForceGarbageCollection(SiloAddress[] hostsIds) -> Task - 5922EB76(Orleans.Runtime.SiloAddress[]) -> Task - # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics() -> Task - ACCE9D6A() -> Task - # Orleans.Runtime.IManagementGrain.GetGrainActivationCount(GrainReference grainReference) -> Task - AEDE93F6(GrainRef) -> Task - # Orleans.Runtime.IManagementGrain.ForceRuntimeStatisticsCollection(SiloAddress[] siloAddresses) -> Task - B761B345(Orleans.Runtime.SiloAddress[]) -> Task - # Orleans.Runtime.IManagementGrain.GetDetailedHosts(bool onlyActive) -> Task - CC6CCBC3(bool) -> Task - # Orleans.Runtime.IManagementGrain.GetTotalActivationCount() -> Task - D7365B43() -> Task - # Orleans.Runtime.IManagementGrain.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task - F67965CC`1(string, int, object?) -> Task + 329F9A1B: ForceActivationCollection(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task + 54E6D1D1: ForceActivationCollection(System.TimeSpan) -> Task + 5922EB76: ForceGarbageCollection(Orleans.Runtime.SiloAddress[]) -> Task + B761B345: ForceRuntimeStatisticsCollection(Orleans.Runtime.SiloAddress[]) -> Task + 317D82B6: GetActivationAddress(Orleans.Runtime.IAddressable) -> ValueTask + 3DB7923B: GetActiveGrains(Orleans.Runtime.GrainType) -> ValueTask> + 0A1C0D82: GetDetailedGrainStatistics(string[], Orleans.Runtime.SiloAddress[]) -> Task + CC6CCBC3: GetDetailedHosts(bool) -> Task + AEDE93F6: GetGrainActivationCount(GrainRef) -> Task + 0F06E027: GetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> Task> + 4C0864C2: GetHosts(bool) -> Task> + 2D761B36: GetRuntimeStatistics(Orleans.Runtime.SiloAddress[]) -> Task + ACCE9D6A: GetSimpleGrainStatistics() -> Task + 3CFF788C: GetSimpleGrainStatistics(Orleans.Runtime.SiloAddress[]) -> Task + D7365B43: GetTotalActivationCount() -> Task + 54FE0FEC: ResetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> ValueTask + F67965CC: SendControlCommandToProvider`1(string, int, object?) -> Task interface [GrainInterfaceType("Orleans.Runtime.IMembershipService")] Orleans.Runtime.IMembershipService [Version(0)] - # Orleans.Runtime.IMembershipService.ProbeIndirectly(SiloAddress target, TimeSpan probeTimeout, int probeNumber) -> Task - 0F85FAAF(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task - # Orleans.Runtime.IMembershipService.MembershipChangeNotification(MembershipTableSnapshot snapshot) -> Task - 22A02D46(Orleans.Runtime.MembershipTableSnapshot) -> Task - # Orleans.Runtime.IMembershipService.Ping(int pingNumber) -> Task - 39AB7071(int) -> Task + 22A02D46: MembershipChangeNotification(Orleans.Runtime.MembershipTableSnapshot) -> Task + 39AB7071: Ping(int) -> Task + 0F85FAAF: ProbeIndirectly(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task interface [GrainInterfaceType("Orleans.Storage.IMemoryStorageGrain")] Orleans.Storage.IMemoryStorageGrain [Version(0)] - # Orleans.Storage.IMemoryStorageGrain.ReadStateAsync(string grainStoreKey) -> Task?> - 45659318`1(string) -> Task> - # Orleans.Storage.IMemoryStorageGrain.WriteStateAsync(string grainStoreKey, IGrainState grainState) -> Task - 7CC6CA25`1(string, Orleans.IGrainState) -> Task - # Orleans.Storage.IMemoryStorageGrain.DeleteStateAsync(string grainStoreKey, string? eTag) -> Task - B7CADD03`1(string, string?) -> Task + B7CADD03: DeleteStateAsync`1(string, string?) -> Task + 45659318: ReadStateAsync`1(string) -> Task> + 7CC6CA25: WriteStateAsync`1(string, Orleans.IGrainState) -> Task diff --git a/src/Orleans.DurableJobs/OrleansContracts.txt b/src/Orleans.DurableJobs/OrleansContracts.txt index 49d70fce8d..fc77c7606e 100644 --- a/src/Orleans.DurableJobs/OrleansContracts.txt +++ b/src/Orleans.DurableJobs/OrleansContracts.txt @@ -4,16 +4,16 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.DurableJobs.IDurableJobReceiverExtension")] Orleans.DurableJobs.IDurableJobReceiverExtension [Version(0)] - # Orleans.DurableJobs.IDurableJobReceiverExtension.HandleDurableJobAsync(IJobRunContext context, CancellationToken attemptCancellationToken) -> ValueTask - 703DB2D4(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask + 703DB2D4: HandleDurableJobAsync(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget")] Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget [Version(0)] - # Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget.CancelAsync(DurableJob job, CancellationToken requestCancellationToken) -> Task - 4D559F22(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task + 4D559F22: CancelAsync(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task class [GrainType("localdurablejobmanager")] Orleans.DurableJobs.LocalDurableJobManager diff --git a/src/Orleans.EventSourcing/OrleansContracts.txt b/src/Orleans.EventSourcing/OrleansContracts.txt index db96365c6b..a9505bb5a0 100644 --- a/src/Orleans.EventSourcing/OrleansContracts.txt +++ b/src/Orleans.EventSourcing/OrleansContracts.txt @@ -4,18 +4,16 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.EventSourcing.ILogConsistencyProtocolParticipant")] Orleans.EventSourcing.ILogConsistencyProtocolParticipant [Version(0)] - # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PreActivateProtocolParticipant() -> Task - 0DB087C8() -> Task - # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PostActivateProtocolParticipant() -> Task - 22FD7D72() -> Task - # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.DeactivateProtocolParticipant() -> Task - A36FC884() -> Task + A36FC884: DeactivateProtocolParticipant() -> Task + 22FD7D72: PostActivateProtocolParticipant() -> Task + 0DB087C8: PreActivateProtocolParticipant() -> Task interface [GrainInterfaceType("Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway")] Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway [Version(0)] - # Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway.RelayMessage(GrainId id, ILogConsistencyProtocolMessage payload) -> Task - C86A1066(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task + C86A1066: RelayMessage(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task diff --git a/src/Orleans.Persistence.Memory/OrleansContracts.txt b/src/Orleans.Persistence.Memory/OrleansContracts.txt index 524150db70..0ade0088d7 100644 --- a/src/Orleans.Persistence.Memory/OrleansContracts.txt +++ b/src/Orleans.Persistence.Memory/OrleansContracts.txt @@ -4,7 +4,9 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Reminders/OrleansContracts.txt b/src/Orleans.Reminders/OrleansContracts.txt index f877ceebf1..a948172d16 100644 --- a/src/Orleans.Reminders/OrleansContracts.txt +++ b/src/Orleans.Reminders/OrleansContracts.txt @@ -4,41 +4,30 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.IRemindable")] Orleans.IRemindable [Version(0)] - # Orleans.IRemindable.ReceiveReminder(string reminderName, TickStatus status) -> Task - 6461BF2F(string, Orleans.Runtime.TickStatus) -> Task + 6461BF2F: ReceiveReminder(string, Orleans.Runtime.TickStatus) -> Task interface [GrainInterfaceType("Orleans.IReminderService")] Orleans.IReminderService [Version(0)] - # Orleans.IReminderService.RegisterOrUpdateReminder(GrainId grainId, string reminderName, TimeSpan dueTime, TimeSpan period) -> Task - 1281C86D(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task - # Orleans.IReminderService.GetReminders(GrainId grainId) -> Task> - 419EB51E(Orleans.Runtime.GrainId) -> Task> - # Orleans.IReminderService.Start() -> Task - 5CF78F8A() -> Task - # Orleans.IReminderService.UnregisterReminder(IGrainReminder reminder) -> Task - A7AF84A8(Orleans.Runtime.IGrainReminder) -> Task - # Orleans.IReminderService.GetReminder(GrainId grainId, string reminderName) -> Task - AC622EEB(Orleans.Runtime.GrainId, string) -> Task - # Orleans.IReminderService.Stop() -> Task - DCFCA00D() -> Task + AC622EEB: GetReminder(Orleans.Runtime.GrainId, string) -> Task + 419EB51E: GetReminders(Orleans.Runtime.GrainId) -> Task> + 1281C86D: RegisterOrUpdateReminder(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task + 5CF78F8A: Start() -> Task + DCFCA00D: Stop() -> Task + A7AF84A8: UnregisterReminder(Orleans.Runtime.IGrainReminder) -> Task interface [GrainInterfaceType("Orleans.IReminderTableGrain")] Orleans.IReminderTableGrain [Version(0)] - # Orleans.IReminderTableGrain.ReadRows(uint begin, uint end) -> Task - 13558B55(uint, uint) -> Task - # Orleans.IReminderTableGrain.UpsertRow(ReminderEntry entry) -> Task - 873299B5(Orleans.ReminderEntry) -> Task - # Orleans.IReminderTableGrain.TestOnlyClearTable() -> Task - 8EBE0523() -> Task - # Orleans.IReminderTableGrain.ReadRow(GrainId grainId, string reminderName) -> Task - ECA791DE(Orleans.Runtime.GrainId, string) -> Task - # Orleans.IReminderTableGrain.ReadRows(GrainId grainId) -> Task - EEEF6FCA(Orleans.Runtime.GrainId) -> Task - # Orleans.IReminderTableGrain.RemoveRow(GrainId grainId, string reminderName, string eTag) -> Task - FF391E0B(Orleans.Runtime.GrainId, string, string) -> Task + ECA791DE: ReadRow(Orleans.Runtime.GrainId, string) -> Task + EEEF6FCA: ReadRows(Orleans.Runtime.GrainId) -> Task + 13558B55: ReadRows(uint, uint) -> Task + FF391E0B: RemoveRow(Orleans.Runtime.GrainId, string, string) -> Task + 8EBE0523: TestOnlyClearTable() -> Task + 873299B5: UpsertRow(Orleans.ReminderEntry) -> Task class [GrainType("localreminderservice")] Orleans.Runtime.ReminderService.LocalReminderService diff --git a/src/Orleans.Runtime/OrleansContracts.txt b/src/Orleans.Runtime/OrleansContracts.txt index 99064daf66..92777fa3ab 100644 --- a/src/Orleans.Runtime/OrleansContracts.txt +++ b/src/Orleans.Runtime/OrleansContracts.txt @@ -4,7 +4,9 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt @@ -19,8 +21,7 @@ class [GrainType("deploymentloadpublisher")] Orleans.Runtime.DeploymentLoadPubli class [GrainType("developmentleaseprovider")] Orleans.Runtime.Development.DevelopmentLeaseProviderGrain interface [GrainInterfaceType("Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain")] Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain [Version(0)] - # Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain.Reset() -> Task - 847FCE12() -> Task + 847FCE12: Reset() -> Task class [GrainType("graincallcancellationmanager")] Orleans.Runtime.GrainCallCancellationManager @@ -33,27 +34,25 @@ class [GrainType("distributedremotegraindirectory")] Orleans.Runtime.GrainDirect class [GrainType("graindirectorypartition")] Orleans.Runtime.GrainDirectory.GrainDirectoryPartition interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryClient")] Orleans.Runtime.GrainDirectory.IGrainDirectoryClient [Version(0)] - [Alias("GetRegisteredActivations")] GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> - [Alias("RecoverRegisteredActivations")] RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> + GetRegisteredActivations: GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> + RecoverRegisteredActivations: RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition")] Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition [Version(0)] - [Alias("AcknowledgeSnapshotTransferAsync")] AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask - [Alias("DeregisterAsync")] DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> - [Alias("GetSnapshotAsync")] GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask - [Alias("LookupAsync")] LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> - [Alias("RegisterAsync")] RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> + AcknowledgeSnapshotTransferAsync: AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask + DeregisterAsync: DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> + GetSnapshotAsync: GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask + LookupAsync: LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> + RegisterAsync: RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks")] Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks [Version(0)] - [Alias("CheckActivationsAsync")] CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> - [Alias("CheckIntegrityAsync")] CheckIntegrityAsync() -> ValueTask - [Alias("RecoverAndCheckIntegrityAsync")] RecoverAndCheckIntegrityAsync() -> ValueTask - [Alias("WaitForMembershipVersionAsync")] WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask + CheckActivationsAsync: CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> + CheckIntegrityAsync: CheckIntegrityAsync() -> ValueTask + RecoverAndCheckIntegrityAsync: RecoverAndCheckIntegrityAsync() -> ValueTask + WaitForMembershipVersionAsync: WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IRemoteClientDirectory")] Orleans.Runtime.GrainDirectory.IRemoteClientDirectory [Version(0)] - # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.OnUpdateClientRoutes(ImmutableDictionary ConnectedClients, long Version)> update) -> Task - 972F9953(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task - # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.GetClientRoutes(ImmutableDictionary knownRoutes) -> Task ConnectedClients, long Version)>> - A6E49CD1(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> + A6E49CD1: GetClientRoutes(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> + 972F9953: OnUpdateClientRoutes(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task class [GrainType("localgraindirectoryclientcompatibility")] Orleans.Runtime.GrainDirectory.LocalGrainDirectoryClientCompatibility @@ -62,32 +61,24 @@ class [GrainType("localgraindirectorypartitioncompatibility")] Orleans.Runtime.G class [GrainType("remotegraindirectory")] Orleans.Runtime.GrainDirectory.RemoteGrainDirectory interface [GrainInterfaceType("Orleans.Runtime.IActivationMigrationManagerSystemTarget")] Orleans.Runtime.IActivationMigrationManagerSystemTarget [Version(0)] - # Orleans.Runtime.IActivationMigrationManagerSystemTarget.AcceptMigratingGrains(List migratingGrains) -> ValueTask - 29E9E63F(System.Collections.Generic.List) -> ValueTask + 29E9E63F: AcceptMigratingGrains(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.ICatalog")] Orleans.Runtime.ICatalog [Version(0)] - # Orleans.Runtime.ICatalog.DeleteActivations(List activationAddresses, DeactivationReasonCode reasonCode, string reasonText) -> Task - C4A56D7C(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task + C4A56D7C: DeleteActivations(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationManagerSystemTarget")] Orleans.Runtime.IGrainCallCancellationManagerSystemTarget [Version(0)] - # Orleans.Runtime.IGrainCallCancellationManagerSystemTarget.CancelCallsAsync(List cancellationRequests) -> ValueTask - AF79F3FA(System.Collections.Generic.List) -> ValueTask + AF79F3FA: CancelCallsAsync(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IGrainTimerInvoker")] Orleans.Runtime.IGrainTimerInvoker [Version(0)] - # Orleans.Runtime.IGrainTimerInvoker.InvokeCallbackAsync() -> Task - 3F6C2672() -> Task + 3F6C2672: InvokeCallbackAsync() -> Task interface [GrainInterfaceType("Orleans.Runtime.IRemoteGrainDirectory")] Orleans.Runtime.IRemoteGrainDirectory [Version(0)] - # Orleans.Runtime.IRemoteGrainDirectory.LookUpMany(List<(GrainId GrainId, int Version)> grainAndETagList) -> Task> - 7DF50601(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> - # Orleans.Runtime.IRemoteGrainDirectory.AcceptSplitPartition(List singleActivations) -> Task - 9ABE3793(System.Collections.Generic.List) -> Task - # Orleans.Runtime.IRemoteGrainDirectory.RegisterMany(List addresses) -> Task - CD06EAEE(System.Collections.Generic.List) -> Task + 9ABE3793: AcceptSplitPartition(System.Collections.Generic.List) -> Task + 7DF50601: LookUpMany(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> + CD06EAEE: RegisterMany(System.Collections.Generic.List) -> Task interface [GrainInterfaceType("Orleans.Runtime.ISiloManifestSystemTarget")] Orleans.Runtime.ISiloManifestSystemTarget [Version(0)] - # Orleans.Runtime.ISiloManifestSystemTarget.GetSiloManifest() -> ValueTask - 1857A4C8() -> ValueTask + 1857A4C8: GetSiloManifest() -> ValueTask class [GrainType("management")] Orleans.Runtime.Management.ManagementGrain @@ -96,7 +87,7 @@ class [GrainType("membershipsystemtarget")] Orleans.Runtime.MembershipService.Me class [GrainType("membershiptablesystemtarget")] Orleans.Runtime.MembershipService.MembershipTableSystemTarget interface [GrainInterfaceType("Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget")] Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget [Version(0)] - [Alias("GetSiloMetadata")] GetSiloMetadata() -> Task + GetSiloMetadata: GetSiloMetadata() -> Task class [GrainType("silometadatasystemtarget")] Orleans.Runtime.MembershipService.SiloMetadata.SiloMetadataSystemTarget @@ -113,21 +104,13 @@ interface [GrainInterfaceType("Orleans.Runtime.TestHooks.ITestHooksSystemTarget" class [GrainType("testhookssystemtarget")] Orleans.Runtime.TestHooks.TestHooksSystemTarget interface [GrainInterfaceType("Orleans.Runtime.Versions.IVersionStoreGrain")] Orleans.Runtime.Versions.IVersionStoreGrain [Version(0)] - # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(GrainInterfaceType interfaceType, CompatibilityStrategy strategy) -> Task - 1B7F13C8(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(GrainInterfaceType interfaceType, VersionSelectorStrategy strategy) -> Task - 3E6DDE3E(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task - # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(CompatibilityStrategy strategy) -> Task - 67A0B5AA(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategy() -> Task - 67EF9A39() -> Task - # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategies() -> Task> - 7261373F() -> Task> - # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategies() -> Task> - 743D88ED() -> Task> - # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategy() -> Task - 8A72848A() -> Task - # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(VersionSelectorStrategy strategy) -> Task - E7532DE3(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + 7261373F: GetCompatibilityStrategies() -> Task> + 67EF9A39: GetCompatibilityStrategy() -> Task + 743D88ED: GetSelectorStrategies() -> Task> + 8A72848A: GetSelectorStrategy() -> Task + 1B7F13C8: SetCompatibilityStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + 67A0B5AA: SetCompatibilityStrategy(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + 3E6DDE3E: SetSelectorStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + E7532DE3: SetSelectorStrategy(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task class [GrainType("versionstore")] Orleans.Runtime.Versions.VersionStoreGrain diff --git a/src/Orleans.Streaming/OrleansContracts.txt b/src/Orleans.Streaming/OrleansContracts.txt index 60fe142410..3d056b829e 100644 --- a/src/Orleans.Streaming/OrleansContracts.txt +++ b/src/Orleans.Streaming/OrleansContracts.txt @@ -4,15 +4,15 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Providers.IMemoryStreamQueueGrain")] Orleans.Providers.IMemoryStreamQueueGrain [Version(0)] - # Orleans.Providers.IMemoryStreamQueueGrain.Enqueue(MemoryMessageData data) -> Task - 74D60341(Orleans.Providers.MemoryMessageData) -> Task - # Orleans.Providers.IMemoryStreamQueueGrain.Dequeue(int maxCount) -> Task> - 7A8F8C1A(int) -> Task> + 7A8F8C1A: Dequeue(int) -> Task> + 74D60341: Enqueue(Orleans.Providers.MemoryMessageData) -> Task class [GrainType("memorystreamqueue")] Orleans.Providers.MemoryStreamQueueGrain @@ -22,70 +22,43 @@ class [GrainType("stream.checkpoint.configured")] Orleans.Streams.ConfiguredStre interface [GrainInterfaceType("Orleans.Streams.IConfiguredStreamCheckpointerGrain")] Orleans.Streams.IConfiguredStreamCheckpointerGrain [Version(0)] interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingAgent")] Orleans.Streams.IPersistentStreamPullingAgent [Version(0)] - # Orleans.Streams.IPersistentStreamPullingAgent.Initialize() -> Task - 06009D9C() -> Task - # Orleans.Streams.IPersistentStreamPullingAgent.Shutdown() -> Task - 620FF905() -> Task + 06009D9C: Initialize() -> Task + 620FF905: Shutdown() -> Task interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingManager")] Orleans.Streams.IPersistentStreamPullingManager [Version(0)] - # Orleans.Streams.IPersistentStreamPullingManager.Initialize() -> Task - 455AB850() -> Task - # Orleans.Streams.IPersistentStreamPullingManager.StartAgents() -> Task - 54E9E970() -> Task - # Orleans.Streams.IPersistentStreamPullingManager.StopAgents() -> Task - BBD50CFF() -> Task - # Orleans.Streams.IPersistentStreamPullingManager.ExecuteCommand(PersistentStreamProviderCommand command, object? arg) -> Task - DE756D95(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task - # Orleans.Streams.IPersistentStreamPullingManager.Stop() -> Task - F4B5B5AA() -> Task + DE756D95: ExecuteCommand(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task + 455AB850: Initialize() -> Task + 54E9E970: StartAgents() -> Task + F4B5B5AA: Stop() -> Task + BBD50CFF: StopAgents() -> Task interface [GrainInterfaceType("Orleans.Streams.IPubSubRendezvousGrain")] Orleans.Streams.IPubSubRendezvousGrain [Version(0)] - # Orleans.Streams.IPubSubRendezvousGrain.Validate() -> Task - 20AA72BF() -> Task - # Orleans.Streams.IPubSubRendezvousGrain.FaultSubscription(GuidId subscriptionId) -> Task - 2821FCF5(Orleans.Runtime.GuidId) -> Task - # Orleans.Streams.IPubSubRendezvousGrain.ProducerCount(QualifiedStreamId streamId) -> Task - 29B61035(Orleans.Runtime.QualifiedStreamId) -> Task - # Orleans.Streams.IPubSubRendezvousGrain.RegisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task - 5E7E20BC(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - # Orleans.Streams.IPubSubRendezvousGrain.ConsumerCount(QualifiedStreamId streamId) -> Task - 5F72C5CF(Orleans.Runtime.QualifiedStreamId) -> Task - # Orleans.Streams.IPubSubRendezvousGrain.GetAllSubscriptions(QualifiedStreamId streamId, GrainId streamConsumer) -> Task> - 7DBE84FA(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - # Orleans.Streams.IPubSubRendezvousGrain.DiagGetConsumers(QualifiedStreamId streamId) -> Task - 8A033955(Orleans.Runtime.QualifiedStreamId) -> Task - # Orleans.Streams.IPubSubRendezvousGrain.UnregisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId) -> Task - 974334B6(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task - # Orleans.Streams.IPubSubRendezvousGrain.RegisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task> - B5FFB7F3(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - # Orleans.Streams.IPubSubRendezvousGrain.UnregisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task - C017B47D(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task + 5F72C5CF: ConsumerCount(Orleans.Runtime.QualifiedStreamId) -> Task + 8A033955: DiagGetConsumers(Orleans.Runtime.QualifiedStreamId) -> Task + 2821FCF5: FaultSubscription(Orleans.Runtime.GuidId) -> Task + 7DBE84FA: GetAllSubscriptions(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + 29B61035: ProducerCount(Orleans.Runtime.QualifiedStreamId) -> Task + 5E7E20BC: RegisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + B5FFB7F3: RegisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + 974334B6: UnregisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + C017B47D: UnregisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task + 20AA72BF: Validate() -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamCheckpointerGrain")] Orleans.Streams.IStreamCheckpointerGrain [Version(0)] - # Orleans.Streams.IStreamCheckpointerGrain.Update(string offset, string expectedCheckpoint, CancellationToken cancellationToken) -> ValueTask - 7AB50A87(string, string, System.Threading.CancellationToken) -> ValueTask - # Orleans.Streams.IStreamCheckpointerGrain.Load(CancellationToken cancellationToken) -> ValueTask - DE3727A1(System.Threading.CancellationToken) -> ValueTask + DE3727A1: Load(System.Threading.CancellationToken) -> ValueTask + 7AB50A87: Update(string, string, System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.Streams.IStreamConsumerExtension")] Orleans.Streams.IStreamConsumerExtension [Version(0)] - # Orleans.Streams.IStreamConsumerExtension.DeliverMutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task - 31840DDE(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - # Orleans.Streams.IStreamConsumerExtension.CompleteStream(GuidId subscriptionId) -> Task - 49F94A48(Orleans.Runtime.GuidId) -> Task - # Orleans.Streams.IStreamConsumerExtension.ErrorInStream(GuidId subscriptionId, Exception exc) -> Task - 4C676CAF(Orleans.Runtime.GuidId, System.Exception) -> Task - # Orleans.Streams.IStreamConsumerExtension.DeliverImmutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task - 6D8FAEB2(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - # Orleans.Streams.IStreamConsumerExtension.DeliverBatch(GuidId subscriptionId, QualifiedStreamId streamId, IBatchContainer item, StreamHandshakeToken? handshakeToken) -> Task - B9CFF2C9(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task - # Orleans.Streams.IStreamConsumerExtension.GetSequenceToken(GuidId subscriptionId) -> Task - C265B3CB(Orleans.Runtime.GuidId) -> Task + 49F94A48: CompleteStream(Orleans.Runtime.GuidId) -> Task + B9CFF2C9: DeliverBatch(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task + 6D8FAEB2: DeliverImmutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + 31840DDE: DeliverMutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + 4C676CAF: ErrorInStream(Orleans.Runtime.GuidId, System.Exception) -> Task + C265B3CB: GetSequenceToken(Orleans.Runtime.GuidId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamProducerExtension")] Orleans.Streams.IStreamProducerExtension [Version(0)] - # Orleans.Streams.IStreamProducerExtension.AddSubscriber(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task - 1341E3D4(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - # Orleans.Streams.IStreamProducerExtension.RemoveSubscriber(GuidId subscriptionId, QualifiedStreamId streamId) -> Task - B98BA876(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + 1341E3D4: AddSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + B98BA876: RemoveSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task class [GrainType("persistentstreampullingagent")] Orleans.Streams.PersistentStreamPullingAgent diff --git a/src/Orleans.TestingHost/OrleansContracts.txt b/src/Orleans.TestingHost/OrleansContracts.txt index d1d33c7d44..9b1aacfa1d 100644 --- a/src/Orleans.TestingHost/OrleansContracts.txt +++ b/src/Orleans.TestingHost/OrleansContracts.txt @@ -4,22 +4,18 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.TestingHost.IStorageFaultGrain")] Orleans.TestingHost.IStorageFaultGrain [Version(0)] - # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnRead(GrainId grainId, Exception exception) -> Task - 1150D526(Orleans.Runtime.GrainId, System.Exception) -> Task - # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnClear(GrainId grainId, Exception exception) -> Task - 1A607A31(Orleans.Runtime.GrainId, System.Exception) -> Task - # Orleans.TestingHost.IStorageFaultGrain.OnRead(GrainId grainId) -> Task - 5D91E1AF(Orleans.Runtime.GrainId) -> Task - # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnWrite(GrainId grainId, Exception exception) -> Task - B9852E6E(Orleans.Runtime.GrainId, System.Exception) -> Task - # Orleans.TestingHost.IStorageFaultGrain.OnClear(GrainId grainId) -> Task - C94BA77C(Orleans.Runtime.GrainId) -> Task - # Orleans.TestingHost.IStorageFaultGrain.OnWrite(GrainId grainId) -> Task - E8594820(Orleans.Runtime.GrainId) -> Task + 1A607A31: AddFaultOnClear(Orleans.Runtime.GrainId, System.Exception) -> Task + 1150D526: AddFaultOnRead(Orleans.Runtime.GrainId, System.Exception) -> Task + B9852E6E: AddFaultOnWrite(Orleans.Runtime.GrainId, System.Exception) -> Task + C94BA77C: OnClear(Orleans.Runtime.GrainId) -> Task + 5D91E1AF: OnRead(Orleans.Runtime.GrainId) -> Task + E8594820: OnWrite(Orleans.Runtime.GrainId) -> Task class [GrainType("storagefault")] Orleans.TestingHost.StorageFaultGrain diff --git a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt index 1fcbada73e..2a1fdec8ba 100644 --- a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt +++ b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt @@ -4,26 +4,24 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("consistencytest")] Orleans.Transactions.TestKit.Consistency.ConsistencyTestGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain")] Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain [Version(0)] - # Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain.Run(ConsistencyTestOptions options, int depth, string stack, int max, DateTime stopAfter) -> Task - 2EB318CB(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task + 2EB318CB: Run(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task # Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain class [GrainType("txn-correctness-DoubleStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain")] Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain [Version(0)] - # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.SetBit(int newValue) -> Task - 0183C2F5(int) -> Task - # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Ping() -> Task - 9A5740F1() -> Task - # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Get() -> Task> - B821F3B1() -> Task> + B821F3B1: Get() -> Task> + 9A5740F1: Ping() -> Task + 0183C2F5: SetBit(int) -> Task # Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain class [GrainType("txn-correctness-MaxStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain @@ -47,104 +45,68 @@ class [GrainType("exclusivelocktransactiontest")] Orleans.Transactions.TestKit.E class [GrainType("faultinjectiontransactioncoordinator")] Orleans.Transactions.TestKit.FaultInjectionTransactionCoordinatorGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateAttributionGrain")] Orleans.Transactions.TestKit.ICreateAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.ICreateAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - 3EFBDD5D(int, System.Collections.Generic.List[]) -> Task[]> + 3EFBDD5D: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain")] Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - C9B8ECB8(int, System.Collections.Generic.List[]) -> Task[]> + C9B8ECB8: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain")] Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain [Version(0)] - # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWrite(ITransactionTestGrain grain, int value) -> Task - 148E55F3(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWriteWithExclusiveLock(IExclusiveLockTransactionTestGrain grain, int value) -> Task - F880C5FF(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task + 148E55F3: ReadThenWrite(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + F880C5FF: ReadThenWriteWithExclusiveLock(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain")] Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain [Version(0)] - # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Get() -> Task - 16E53FE3() -> Task - # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Add(int numberToAdd) -> Task - 81B05CD8(int) -> Task - # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Set(int newValue) -> Task - BD3AA4D0(int) -> Task + 81B05CD8: Add(int) -> Task + 16E53FE3: Get() -> Task + BD3AA4D0: Set(int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain [Version(0)] - # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task - 70FF7C60(System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainAddAndFaultInjection(List grains, int numberToAdd, FaultInjectionControl? faultInjection) -> Task - E67D54A5(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task + E67D54A5: MultiGrainAddAndFaultInjection(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task + 70FF7C60: MultiGrainSet(System.Collections.Generic.List, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain [Version(0)] - # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Set(int newValue) -> Task - 8389970A(int) -> Task - # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Add(int numberToAdd, FaultInjectionControl? faultInjectionControl) -> Task - A4CAE05C(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Deactivate() -> Task - A6C1652E() -> Task - # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Get() -> Task - C752DF7D() -> Task + A4CAE05C: Add(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task + A6C1652E: Deactivate() -> Task + C752DF7D: Get() -> Task + 8389970A: Set(int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IJoinAttributionGrain")] Orleans.Transactions.TestKit.IJoinAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.IJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - B1619F67(int, System.Collections.Generic.List[]) -> Task[]> + B1619F67: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INoAttributionGrain")] Orleans.Transactions.TestKit.INoAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.INoAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - BC7E3A79(int, System.Collections.Generic.List[]) -> Task[]> + BC7E3A79: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INotAllowedAttributionGrain")] Orleans.Transactions.TestKit.INotAllowedAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.INotAllowedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - 891D027E(int, System.Collections.Generic.List[]) -> Task[]> + 891D027E: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISupportedAttributionGrain")] Orleans.Transactions.TestKit.ISupportedAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.ISupportedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - BC7DBC0A(int, System.Collections.Generic.List[]) -> Task[]> + BC7DBC0A: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISuppressAttributionGrain")] Orleans.Transactions.TestKit.ISuppressAttributionGrain [Version(0)] - # Orleans.Transactions.TestKit.ISuppressAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> - 5A02311D(int, System.Collections.Generic.List[]) -> Task[]> + 5A02311D: GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCommitterTestGrain")] Orleans.Transactions.TestKit.ITransactionCommitterTestGrain [Version(0)] - # Orleans.Transactions.TestKit.ITransactionCommitterTestGrain.Commit(ITransactionCommitOperation operation) -> Task - C44BE2A4(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task + C44BE2A4: Commit(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCoordinatorGrain")] Orleans.Transactions.TestKit.ITransactionCoordinatorGrain [Version(0)] - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAddAndThrow(List grain, List grains, int numberToAdd) -> Task - 2760260D(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(List grains, int numberToAdd) -> Task - 3A6B9237(System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.UpdateViolated(ITransactionTestGrain grains, int numberToAdd) -> Task - 485592B2(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDouble(List grains) -> Task - 5FC2E7A1(System.Collections.Generic.List) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSetBit(List grains, int bitIndex) -> Task - 5FF4F216(System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task - 78D54907(System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(ITransactionCommitterTestGrain committer, ITransactionCommitOperation operation, List grains, int numberToAdd) -> Task - 8EE5E563(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByRWRW(List grains, int numberToAdd) -> Task - 9EFEA7F3(System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByWRWR(List grains, int numberToAdd) -> Task - B4376B4D(System.Collections.Generic.List, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.AddAndThrow(ITransactionTestGrain grain, int numberToAdd) -> Task - D3EF444F(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.OrphanCallTransaction() -> Task - EDCC120B() -> Task + D3EF444F: AddAndThrow(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + 8EE5E563: MultiGrainAdd(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task + 3A6B9237: MultiGrainAdd(System.Collections.Generic.List, int) -> Task + 2760260D: MultiGrainAddAndThrow(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task + 5FC2E7A1: MultiGrainDouble(System.Collections.Generic.List) -> Task + 9EFEA7F3: MultiGrainDoubleByRWRW(System.Collections.Generic.List, int) -> Task + B4376B4D: MultiGrainDoubleByWRWR(System.Collections.Generic.List, int) -> Task + 78D54907: MultiGrainSet(System.Collections.Generic.List, int) -> Task + 5FF4F216: MultiGrainSetBit(System.Collections.Generic.List, int) -> Task + EDCC120B: OrphanCallTransaction() -> Task + 485592B2: UpdateViolated(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionTestGrain")] Orleans.Transactions.TestKit.ITransactionTestGrain [Version(0)] - # Orleans.Transactions.TestKit.ITransactionTestGrain.AddAndThrow(int numberToAdd) -> Task - 25B066B5(int) -> Task - # Orleans.Transactions.TestKit.ITransactionTestGrain.SetAndThrow(int numberToSet) -> Task - 35C87F81(int) -> Task - # Orleans.Transactions.TestKit.ITransactionTestGrain.Deactivate() -> Task - 35D6FD32() -> Task - # Orleans.Transactions.TestKit.ITransactionTestGrain.Get() -> Task - 8DAA79AA() -> Task - # Orleans.Transactions.TestKit.ITransactionTestGrain.Set(int newValue) -> Task - CE9EC80B(int) -> Task - # Orleans.Transactions.TestKit.ITransactionTestGrain.Add(int numberToAdd) -> Task - DC07DAEA(int) -> Task + DC07DAEA: Add(int) -> Task + 25B066B5: AddAndThrow(int) -> Task + 35D6FD32: Deactivate() -> Task + 8DAA79AA: Get() -> Task + CE9EC80B: Set(int) -> Task + 35C87F81: SetAndThrow(int) -> Task class [GrainType("joinattribution")] Orleans.Transactions.TestKit.JoinAttributionGrain diff --git a/src/Orleans.Transactions/OrleansContracts.txt b/src/Orleans.Transactions/OrleansContracts.txt index 02e544c9e5..50d5bd7ab3 100644 --- a/src/Orleans.Transactions/OrleansContracts.txt +++ b/src/Orleans.Transactions/OrleansContracts.txt @@ -4,26 +4,20 @@ # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # The regeneration command edits this manifest only; it does not change source attributes. -# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# OrleansContracts format: 2 +# Method lines use: wire-identity: CLR-signature. +# The identity is the identifier Orleans uses at runtime, whether generated or declared in source. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionManagerExtension")] Orleans.Transactions.Abstractions.ITransactionManagerExtension [Version(0)] - # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Prepared(string resourceId, Guid transactionId, DateTime timestamp, ParticipantId resource, TransactionalStatus status) -> Task - 12BEFA17(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task - # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Ping(string resourceId, Guid transactionId, DateTime timeStamp, ParticipantId resource) -> Task - AC4A9AEB(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task - # Orleans.Transactions.Abstractions.ITransactionManagerExtension.PrepareAndCommit(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, List writeResources, int totalParticipants) -> Task - B024EFA6(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task + AC4A9AEB: Ping(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + B024EFA6: PrepareAndCommit(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task + 12BEFA17: Prepared(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionalResourceExtension")] Orleans.Transactions.Abstractions.ITransactionalResourceExtension [Version(0)] - # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.CommitReadOnly(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp) -> Task - 1BB071FE(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task - # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Prepare(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, ParticipantId transactionManager) -> Task - 2ADCC608(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task - # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Confirm(string resourceId, Guid transactionId, DateTime timeStamp) -> Task - 5DDDE6F0(string, System.Guid, System.DateTime) -> Task - # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Cancel(string resourceId, Guid transactionId, DateTime timeStamp, TransactionalStatus status) -> Task - 80028AB9(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task - # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Abort(string resourceId, Guid transactionId) -> Task - BD051D23(string, System.Guid) -> Task + BD051D23: Abort(string, System.Guid) -> Task + 80028AB9: Cancel(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task + 1BB071FE: CommitReadOnly(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task + 5DDDE6F0: Confirm(string, System.Guid, System.DateTime) -> Task + 2ADCC608: Prepare(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index e152c04844..12fee0febd 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Immutable; +using System.Globalization; using System.Reflection; using System.Text; using System.Text.RegularExpressions; @@ -33,7 +34,9 @@ public class GrainInterfaceVersionAnalyzerTest "# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024\n" + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION\n" + "# The regeneration command edits this manifest only; it does not change source attributes.\n" + - "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.\n" + + "# OrleansContracts format: 2\n" + + "# Method lines use: wire-identity: CLR-signature.\n" + + "# The identity is the identifier Orleans uses at runtime, whether generated or declared in source.\n" + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.\n" + "# Details: https://aka.ms/orleans/OrleansContracts.txt\n\n"; @@ -179,6 +182,7 @@ private sealed class TestAnalyzerConfigOptions(ImmutableDictionary Task +41F3F487: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -288,7 +292,7 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task +41F3F487: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -312,7 +316,7 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task +41F3F487: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -356,14 +360,14 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task +41F3F487: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); var diagnostic = diagnostics.First(d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); Assert.Contains("NewMethod", diagnostic.GetMessage()); - Assert.Contains("8E43BF4F() -> Task", diagnostic.GetMessage()); + Assert.Contains("8E43BF4F", diagnostic.GetMessage()); } [Fact] @@ -379,13 +383,191 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task +41F3F487: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); Assert.DoesNotContain(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); } + [Fact] + public async Task CanonicalGeneratedIdentity_InFile_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task NewMethod(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + 8E43BF4F: NewMethod() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task CanonicalIdIdentity_InFile_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Id(42)] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + 42: Ping() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task CanonicalIdIdentity_UsesInvariantFormatting() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Id(42)] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + 42: Ping() -> Task +"; + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("ar-SA"); + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public async Task CanonicalAliasIdentity_InFile_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-ping"")] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + stable-ping: Ping() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task CanonicalAliasIdentity_WithEscapedDelimiter_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias("" # CLR: stable:ping\\v2\u0085next "")] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + \s\#\sCLR\:\sstable\:ping\\v2\u0085next\s: Ping() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task CanonicalMemberKeys_SeparateIdentityFromGenericArity() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""a"")] + Task First(); + + [Alias(""a`1"")] + Task Second(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + a: First`1() -> Task + a`1: Second() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task EquivalentGeneratedAndAliasIdentity_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""8E43BF4F"")] + Task NewMethod(); +} +"; + const string contractsFile = + "interface IMyGrain [Version(1)]\n 8E43BF4F: NewMethod() -> Task\n"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + + [Theory] + [InlineData("[Id(46529181)]")] + [InlineData("")] + public async Task EquivalentGeneratedAndIdIdentity_NoDiagnostic(string sourceAttribute) + { + var source = $@" +[Version(1)] +public interface IMyGrain : IGrain +{{ + {sourceAttribute} + Task Method26(); +}} +"; + const string contractsFile = + "interface IMyGrain [Version(1)]\n 46529181: Method26() -> Task\n"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Empty(diagnostics); + } + [Fact] public async Task MemberWithParameters_InFile_NoDiagnostic() { @@ -399,7 +581,7 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething(string name, int count) -> Task +2DB8A137: DoSomething(string, int) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -407,18 +589,19 @@ IMyGrain [Version(1)] } [Fact] - public async Task LegacyMemberParameterRename_NoDiagnostic() + public async Task AliasedMemberParameterRename_NoDiagnostic() { const string source = @" [Version(1)] public interface IMyGrain : IGrain { + [Alias(""stable-method"")] Task DoSomething(string renamed, int newCount); } "; const string contractsFile = @" IMyGrain [Version(1)] -IMyGrain.DoSomething(string original, int oldCount) -> Task +stable-method: DoSomething(string, int) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -433,13 +616,14 @@ public async Task MemberWithTupleParameter_InFile_NoDiagnostic() [Version(1)] public interface IMyGrain : IGrain { + [Alias(""tuple-method"")] Task DoSomething((int X, int Y) value); } "; const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething((int X, int Y) value) -> Task +tuple-method: DoSomething((int, int)) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -458,13 +642,14 @@ public sealed class Request { } [Version(1)] public interface IMyGrain : IGrain { + [Alias(""stable-method"")] Task DoSomething(NamespaceA.Request request); } "; const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething(NamespaceB.Request request) -> Task +stable-method: DoSomething(NamespaceB.Request) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -502,7 +687,7 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - ReadStateAsync`1(T) -> Task + 0105B5F1: ReadStateAsync`1(T) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -517,12 +702,13 @@ public async Task GenericMethodArityChanged_ReportsDiagnostic() [Version(1)] public interface IMyGrain : IGrain { + [Alias(""read-state"")] Task ReadStateAsync(T1 first, T2 second); } "; const string contractsFile = @" interface IMyGrain [Version(1)] - ReadStateAsync`1(T) -> Task + read-state: ReadStateAsync`1(T) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -542,8 +728,8 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - ExistingAsync() -> Task - RemovedAsync() -> Task + 629F4AD1: ExistingAsync() -> Task + C1BEB0D0: RemovedAsync() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -556,7 +742,7 @@ interface IMyGrain [Version(1)] } [Fact] - public async Task LegacyAliasedMemberRename_NoDiagnostic() + public async Task AliasedMemberRename_NoDiagnostic() { const string source = @" [Version(1)] @@ -568,7 +754,7 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - [Alias(""stable-method"")] IMyGrain.OldName(string original) -> Task + stable-method: OldName(string) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -578,7 +764,7 @@ interface IMyGrain [Version(1)] } [Fact] - public async Task UnmarkedSameNameAlias_ReportsManifestUpgrade() + public async Task AliasIdentityMismatch_ReportsAddedAndRemovedSignatures() { const string source = @" [Version(1)] @@ -590,13 +776,13 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - Method() -> Task + old-method: Method() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } [Fact] @@ -612,7 +798,7 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - [Alias(""old-method"")] IMyGrain.Method() -> Task + old-method: Method() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -622,7 +808,7 @@ interface IMyGrain [Version(1)] } [Fact] - public async Task ExplicitId_DoesNotMatchLegacyMethodName() + public async Task ExplicitId_DoesNotMatchDifferentWireIdentity() { const string source = @" [Version(1)] @@ -634,7 +820,7 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - Ping() -> Task + Ping: Ping() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -655,7 +841,7 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - [Alias(""old-method"")] IMyGrain.Method() -> Task + old-method: Method() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -677,7 +863,7 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - [Alias(""stable-method"")] IMyGrain.Method() -> Task + stable-method: Method() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, contractsFile); @@ -818,7 +1004,7 @@ public interface IMyObserver : IGrainObserver const string grainInterfacesFile = @" # OrleansContracts.txt IMyObserver [Version(0)] -IMyObserver.OnEvent(string value) -> void +3DCBC519: OnEvent(string) -> void "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -866,7 +1052,7 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt MyApp.Grains.IMyGrain [Version(1)] -MyApp.Grains.IMyGrain.DoSomething() -> Task +4057D20A: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -894,7 +1080,7 @@ public interface IMyGrain : IGrain # Another comment IMyGrain [Version(1)] # Comment between entries -IMyGrain.DoSomething() -> Task +41F3F487: DoSomething() -> Task # Final comment "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -1139,10 +1325,9 @@ public interface INewGrain : IGrain Assert.Empty(diagnostics); Assert.Contains("# IOldGrain\ninterface [GrainInterfaceType(\"stable-interface\")] IOldGrain [Version(0)]", contractsFile); Assert.Contains( - " [Alias(\"stable-method\")] stable-method(request) -> Task", + " stable-method: OldMethod(request) -> Task", contractsFile); Assert.Contains("# IOldGrain", contractsFile); - Assert.Contains("# IOldGrain.OldMethod", contractsFile); } [Fact] @@ -1207,7 +1392,8 @@ public interface IMyGrain : IGrain GrainInterfaceVersionAnalyzer.RuleId0018, GrainInterfaceVersionAnalyzer.RuleId0027); Assert.Contains(diagnostics, diagnostic => diagnostic.GetMessage().Contains("NewName", StringComparison.Ordinal)); - Assert.Contains("# IMyGrain.OldName() -> Task", contractsFile); + Assert.Contains(": OldName() -> Task", contractsFile); + Assert.Contains("OldName() -> Task", contractsFile); } [Fact] @@ -1507,9 +1693,33 @@ private static void AssertContainsGeneratedMethod( string clrSignature, string contractSignatureSuffix) { + var parameterListStart = clrSignature.IndexOf('('); + var methodStart = clrSignature.LastIndexOf('.', parameterListStart - 1) + 1; + var methodName = clrSignature.Substring(methodStart, parameterListStart - methodStart); + var genericStart = methodName.IndexOf('<'); + if (genericStart >= 0) + { + var genericEnd = methodName.LastIndexOf('>'); + var arity = 1; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (methodName[index] == ',') + { + arity++; + } + } + + methodName = $"{methodName.Substring(0, genericStart)}`{arity}"; + var suffixArityEnd = contractSignatureSuffix.IndexOf('('); + if (contractSignatureSuffix.StartsWith("`", StringComparison.Ordinal) + && suffixArityEnd >= 0) + { + contractSignatureSuffix = contractSignatureSuffix.Substring(suffixArityEnd); + } + } + var pattern = - $"(?m)^ # {Regex.Escape(clrSignature)}\\r?$\\n" + - $" [0-9A-F]{{8}}{Regex.Escape(contractSignatureSuffix)}\\r?$"; + $"(?m)^ [0-9A-F]{{8}}: {Regex.Escape(methodName + contractSignatureSuffix)}\\r?$"; Assert.Matches(pattern, content); } @@ -1643,7 +1853,7 @@ public class CartGrain : Grain, IGrainWithStringKey var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); Assert.Contains("interface [GrainInterfaceType(\"cart\")] ICartGrain [Version(2)]", content); - Assert.Contains(" [Alias(\"read\")] read(int) -> Task", content); + Assert.Contains(" read: GetAsync(int) -> Task", content); Assert.Contains("class [GrainType(\"cart\")] CartGrain", content); } @@ -1700,6 +1910,96 @@ public async Task CodeFix_RegenerateExistingFile_UsesConfiguredCustomFilename() "() -> Task"); } + [Fact] + public async Task CodeFix_RegenerateProject_MigratesHashFirstManifest() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task NewMethod(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(0)] + # IMyGrain.NewMethod() -> Task + 8E43BF4F() -> Task +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0017, + RegenerateCodeActionTitle); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + + Assert.Contains(" 8E43BF4F: NewMethod() -> Task", content); + Assert.DoesNotContain("# IMyGrain.NewMethod", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_MigratesNameBasedManifest() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task NewMethod(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(0)] + IMyGrain.NewMethod() -> Task +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0017, + RegenerateCodeActionTitle); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + + Assert.Contains(" 8E43BF4F: NewMethod() -> Task", content); + Assert.DoesNotContain("IMyGrain.NewMethod", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_EmitsSourceIdentityMetadataWithoutEditingSource() + { + const string source = @" +public interface IMyGrain : IGrain +{ + [Id(42)] + Task Ping(); + + [Alias(""stable-pong"")] + Task Pong(); +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains(" 42: Ping() -> Task", content); + Assert.Contains(" stable-pong: Pong() -> Task", content); + + var sourceText = (await changedSolution.Projects.Single().Documents + .Single(document => document.Name == "Test.cs") + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("[Id(42)]", sourceText); + Assert.Contains("[Alias(\"stable-pong\")]", sourceText); + Assert.DoesNotContain("stable-pong: Pong", sourceText); + } + [Fact] public async Task CodeFix_RegenerateProject_PreservesContractHistory() { @@ -2001,8 +2301,8 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - ExistingAsync() -> Task - RemovedAsync() -> Task + 629F4AD1: ExistingAsync() -> Task + C1BEB0D0: RemovedAsync() -> Task "; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( @@ -2015,7 +2315,7 @@ interface IMyGrain [Version(1)] .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); AssertContainsGeneratedMethod(content, "IMyGrain.ExistingAsync() -> Task", "() -> Task"); AssertContainsGeneratedMethod(content, "IMyGrain.NewAsync() -> Task", "() -> Task"); - Assert.Contains(" RemovedAsync() -> Task", content); + Assert.Contains(" C1BEB0D0: RemovedAsync() -> Task", content); var diagnostics = await GetDiagnosticsAsync(source, content); Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); @@ -2023,7 +2323,7 @@ interface IMyGrain [Version(1)] } [Fact] - public async Task CodeFix_RegenerateProject_PreservesAliasesAndDeduplicatesLegacyMembers() + public async Task CodeFix_RegenerateProject_PreservesAliasesAndDeduplicatesMembers() { const string source = @" [Version(1)] @@ -2035,9 +2335,9 @@ public interface IMyGrain : IGrain "; const string contractsFile = @" interface IMyGrain [Version(1)] - ExistingAsync() -> Task - [Alias(""removed"")] IMyGrain.RemovedAsync(string value) -> Task - removed(string) -> Task + 629F4AD1: ExistingAsync() -> Task + removed: RemovedAsync(string) -> Task + removed: RemovedAsync(string) -> Task "; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( @@ -2048,15 +2348,15 @@ interface IMyGrain [Version(1)] var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); - Assert.Equal(1, content.Split(new[] { "removed(string) -> Task" }, StringSplitOptions.None).Length - 1); - Assert.Contains("[Alias(\"removed\")] removed(string) -> Task", content); + Assert.Equal(1, content.Split(new[] { "removed:" }, StringSplitOptions.None).Length - 1); + Assert.Contains("removed: RemovedAsync(string) -> Task", content); var diagnostics = await GetDiagnosticsAsync(source, content); Assert.Single(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } [Fact] - public async Task CodeFix_RegenerateProject_DeduplicatesSemanticallyEquivalentLegacyMembers() + public async Task CodeFix_RegenerateProject_DropsUnrecognizedPreReleaseMemberSyntax() { const string source = @" namespace Models @@ -2095,7 +2395,7 @@ await GetDiagnosticsAsync(source, content), } [Fact] - public async Task CodeFix_RegenerateProject_PreservesRemovedMembersOnNestedLegacyInterfaces() + public async Task CodeFix_RegenerateProject_PreservesRemovedMembersOnNestedInterfaces() { const string source = @" public class Outer @@ -2110,8 +2410,8 @@ public interface IInnerGrain : IGrain "; const string contractsFile = @" interface Outer.IInnerGrain [Version(1)] - ExistingAsync() -> Task - RemovedAsync() -> Task + D4692090: ExistingAsync() -> Task + 5F9C2FBA: RemovedAsync() -> Task "; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( @@ -2122,7 +2422,7 @@ interface Outer.IInnerGrain [Version(1)] var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); - Assert.Contains(" RemovedAsync() -> Task", content); + Assert.Contains(" 5F9C2FBA: RemovedAsync() -> Task", content); Assert.Contains( await GetDiagnosticsAsync(source, content), diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); @@ -2184,7 +2484,7 @@ private static Diagnostic CreateFixAllDiagnostic() GrainInterfaceVersionAnalyzer.RuleId0016, "Contract missing", "Contract missing", - "Orleans.Versioning", + "Versioning", DiagnosticSeverity.Warning, isEnabledByDefault: true), Location.None); @@ -2486,9 +2786,9 @@ public interface IMiddle : IGrain "; const string grainInterfacesFile = "# OrleansContracts.txt\n" + "IZulu [Version(1)]\n" + - "IZulu.Method() -> Task\n\n" + + "zulu: Method() -> Task\n\n" + "interface IAlpha [Version(1)]\n" + - "IAlpha.Method() -> Task"; + "alpha: Method() -> Task"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0016); @@ -2506,8 +2806,43 @@ public interface IMiddle : IGrain Assert.True(middleInterface < zuluInterface); AssertContainsGeneratedMethod(content, "IMiddle.Alpha() -> Task", "() -> Task"); AssertContainsGeneratedMethod(content, "IMiddle.Zeta() -> Task", "() -> Task"); - Assert.Contains("interface IAlpha [Version(1)]\n Method() -> Task", content); - Assert.Contains("interface IZulu [Version(1)]\n Method() -> Task", content); + Assert.Contains("interface IAlpha [Version(1)]\n alpha: Method() -> Task", content); + Assert.Contains("interface IZulu [Version(1)]\n zulu: Method() -> Task", content); + } + + [Fact] + public async Task CodeFix_SortsDuplicateClrSignaturesByWireIdentity() + { + const string source = @" +public interface IMyGrain : IGrain +{ + Task NewAsync(); +} +"; + const string alphaFirst = @" +interface IMyGrain [Version(0)] + alpha: RemovedAsync() -> Task + zeta: RemovedAsync() -> Task +"; + const string zetaFirst = @" +interface IMyGrain [Version(0)] + zeta: RemovedAsync() -> Task + alpha: RemovedAsync() -> Task +"; + + var first = await ApplyCodeFixAndGetContractsAsync( + source, + alphaFirst, + GrainInterfaceVersionAnalyzer.RuleId0018); + var second = await ApplyCodeFixAndGetContractsAsync( + source, + zetaFirst, + GrainInterfaceVersionAnalyzer.RuleId0018); + + Assert.Equal(first, second); + Assert.True( + first.IndexOf("alpha: RemovedAsync()", StringComparison.Ordinal) + < first.IndexOf("zeta: RemovedAsync()", StringComparison.Ordinal)); } [Fact] @@ -2636,7 +2971,7 @@ public interface IMyGrain : IGrain "; const string grainInterfacesFile = @"# OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task"; +41F3F487: DoSomething() -> Task"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0017); @@ -2670,10 +3005,10 @@ public interface IFooBar : IGrain "; const string grainInterfacesFile = @"# OrleansContracts.txt IFooBar [Version(1)] -IFooBar.DoSomething() -> Task +E0A3D19A: DoSomething() -> Task IFoo [Version(1)] -IFoo.DoSomething() -> Task"; +4BC6AD4C: DoSomething() -> Task"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0017); @@ -2698,7 +3033,7 @@ public interface IMyGrain : IGrain Task DoSomething(); } "; - const string grainInterfacesFile = "# OrleansContracts.txt\nIMyGrain [Version(1)]\nIMyGrain.DoSomething() -> Task\n"; + const string grainInterfacesFile = "# OrleansContracts.txt\nIMyGrain [Version(1)]\n41F3F487: DoSomething() -> Task\n"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0017); @@ -2710,7 +3045,7 @@ public interface IMyGrain : IGrain var content = changedText.ToString(); Assert.DoesNotContain("\r", content); - Assert.Equal(GeneratedHeader + "interface IMyGrain [Version(2)]\n DoSomething() -> Task\n", content); + Assert.Equal(GeneratedHeader + "interface IMyGrain [Version(2)]\n 41F3F487: DoSomething() -> Task\n", content); } #endregion @@ -2730,7 +3065,7 @@ public interface IMyGrain : IGrain "; const string grainInterfacesFile = @"# OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task"; +41F3F487: DoSomething() -> Task"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0018); @@ -2763,7 +3098,7 @@ public interface IMyGrain : IGrain "; const string grainInterfacesFile = @"# OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task"; +41F3F487: DoSomething() -> Task"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0018); @@ -2774,8 +3109,7 @@ IMyGrain [Version(1)] var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); var content = changedText.ToString(); - Assert.Contains("\n [Alias(\"new-method\")] new-method(int) -> Task", content); - Assert.Contains(" # IMyGrain.NewMethod(int value) -> Task", content); + Assert.Contains("\n new-method: NewMethod(int) -> Task", content); } [Fact] @@ -2797,7 +3131,7 @@ public interface IMyGrain : IGrain GrainInterfaceVersionAnalyzer.RuleId0018); Assert.Equal( - GeneratedHeader + "interface IMyGrain [Version(1)]\n [Alias(\"NewMethod\")] NewMethod() -> Task\n", + GeneratedHeader + "interface IMyGrain [Version(1)]\n NewMethod: NewMethod() -> Task\n", content); } @@ -2844,7 +3178,7 @@ public interface IFooBar : IGrain const string grainInterfacesFile = @"# OrleansContracts.txt IFoo [Version(1)] IFooBar [Version(1)] -IFooBar.ExistingMethod() -> Task"; +037556D9: ExistingMethod() -> Task"; var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, grainInterfacesFile, GrainInterfaceVersionAnalyzer.RuleId0018); @@ -2901,7 +3235,7 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething(T value) -> Task +7DDCD155: DoSomething(T) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -2923,7 +3257,7 @@ public interface IMyGrain : IGrain where T : class const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething(T value) -> Task +7DDCD155: DoSomething(T) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -2943,7 +3277,7 @@ public interface IMyGrain : IGrain const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething(TKey key, TValue value) -> Task +96A32DB8: DoSomething(TKey, TValue) -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -3051,10 +3385,10 @@ public interface IDerivedGrain : IBaseGrain const string grainInterfacesFile = @" # OrleansContracts.txt IBaseGrain [Version(1)] -IBaseGrain.DoBase() -> Task +E1000F29: DoBase() -> Task IDerivedGrain [Version(1)] -IDerivedGrain.DoDerived() -> Task +BDD4EFFA: DoDerived() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -3083,7 +3417,7 @@ public interface IDerivedGrain : IBaseGrain const string grainInterfacesFile = @" # OrleansContracts.txt IBaseGrain [Version(1)] -IBaseGrain.DoBase() -> Task +E1000F29: DoBase() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -3106,7 +3440,7 @@ public interface IMyGrain : IGrainWithStringKey const string grainInterfacesFile = @" # OrleansContracts.txt IMyGrain [Version(1)] -IMyGrain.DoSomething() -> Task +41F3F487: DoSomething() -> Task "; var diagnostics = await GetDiagnosticsAsync(source, grainInterfacesFile); @@ -3168,11 +3502,11 @@ public interface IMyGrain : IGrain Task DoSomething(); } "; - const string contractsFile = "# OrleansContracts.txt\r\nIMyGrain [Version(1)]\r\nIMyGrain.DoSomething() -> Task\r\n"; + const string contractsFile = "# OrleansContracts.txt\r\nIMyGrain [Version(1)]\r\n41F3F487: DoSomething() -> Task\r\n"; var expectedContractsFile = GeneratedHeader.Replace("\n", "\r\n", StringComparison.Ordinal) + "interface IMyGrain [Version(2)]\r\n" + - " DoSomething() -> Task\r\n"; + " 41F3F487: DoSomething() -> Task\r\n"; var properties = ImmutableDictionary.Empty .Add("InterfaceName", "IMyGrain") .Add("ActualVersion", "2"); From 0f61fbb5f61d83b67fd661eac9248afba1e8dc52 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 11:44:11 -0700 Subject: [PATCH 08/10] perf(runtime): route local responses directly --- src/Orleans.Core/Messaging/Message.cs | 3 + src/Orleans.Core/Messaging/MessageFactory.cs | 1 + src/Orleans.Core/Runtime/CallbackData.cs | 16 +- .../Runtime/OutsideRuntimeClient.cs | 6 +- .../Runtime/SharedCallbackData.cs | 4 +- src/Orleans.Runtime/Core/CallbackRegistry.cs | 77 ++++++ .../Core/InsideRuntimeClient.cs | 54 ++-- .../Serialization/MessageSerializerTests.cs | 27 ++ .../CallbackDataTests.cs | 2 +- .../CallbackRegistryTests.cs | 245 ++++++++++++++++++ 10 files changed, 398 insertions(+), 37 deletions(-) create mode 100644 src/Orleans.Runtime/Core/CallbackRegistry.cs create mode 100644 test/Orleans.Runtime.Tests/CallbackRegistryTests.cs diff --git a/src/Orleans.Core/Messaging/Message.cs b/src/Orleans.Core/Messaging/Message.cs index f57033e8da..0bc60ae5d7 100644 --- a/src/Orleans.Core/Messaging/Message.cs +++ b/src/Orleans.Core/Messaging/Message.cs @@ -25,6 +25,9 @@ internal sealed class Message : ISpanFormattable public Dictionary? _requestContextData; + [field: NonSerialized] + internal object? ResponseTarget { get; set; } + public SiloAddress? _targetSilo; public GrainId _targetGrain; diff --git a/src/Orleans.Core/Messaging/MessageFactory.cs b/src/Orleans.Core/Messaging/MessageFactory.cs index 1f815944ea..5f862cc13e 100644 --- a/src/Orleans.Core/Messaging/MessageFactory.cs +++ b/src/Orleans.Core/Messaging/MessageFactory.cs @@ -68,6 +68,7 @@ public Message CreateResponseMessage(Message request) CacheInvalidationHeader = request.CacheInvalidationHeader, TimeToLive = request.TimeToLive, RequestContextData = RequestContextExtensions.Export(_deepCopier), + ResponseTarget = request.ResponseTarget, }; _messagingTrace.OnCreateMessage(response); diff --git a/src/Orleans.Core/Runtime/CallbackData.cs b/src/Orleans.Core/Runtime/CallbackData.cs index bfbfb7a28f..070a862edf 100644 --- a/src/Orleans.Core/Runtime/CallbackData.cs +++ b/src/Orleans.Core/Runtime/CallbackData.cs @@ -129,7 +129,7 @@ private void OnCancellation(CancellationToken cancellationToken) stopwatch.Stop(); SignalCancellation(); - shared.Unregister(Message); + shared.Unregister(this); _applicationRequestInstruments.OnAppRequestsEnd((long)stopwatch.Elapsed.TotalMilliseconds); _applicationRequestInstruments.OnAppRequestsCanceled(GetTargetGrainType()); OrleansCallBackDataEvent.Instance.OnCanceled(Message); @@ -150,7 +150,7 @@ public void OnTimeout() SignalCancellation(); } - this.shared.Unregister(this.Message); + this.shared.Unregister(this); DisposeCancellationRegistration(); _applicationRequestInstruments.OnAppRequestsEnd((long)this.stopwatch.Elapsed.TotalMilliseconds); _applicationRequestInstruments.OnAppRequestsTimedOut(GetTargetGrainType()); @@ -175,7 +175,7 @@ public void OnTargetSiloFail() } this.stopwatch.Stop(); - this.shared.Unregister(this.Message); + this.shared.Unregister(this); DisposeCancellationRegistration(); _applicationRequestInstruments.OnAppRequestsEnd((long)this.stopwatch.Elapsed.TotalMilliseconds); @@ -195,7 +195,7 @@ public void OnHostShutdown() } this.stopwatch.Stop(); - this.shared.Unregister(this.Message); + this.shared.Unregister(this); DisposeCancellationRegistration(); _applicationRequestInstruments.OnAppRequestsEnd((long)this.stopwatch.Elapsed.TotalMilliseconds); @@ -205,10 +205,15 @@ public void OnHostShutdown() } public void DoCallback(Message response) + { + TryDoCallback(response); + } + + internal bool TryDoCallback(Message response) { if (!TryComplete()) { - return; + return false; } OrleansCallBackDataEvent.Instance.DoCallback(this.Message); @@ -219,6 +224,7 @@ public void DoCallback(Message response) // do callback outside the CallbackData lock. Just not a good practice to hold a lock for this unrelated operation. ResponseCallback(response, this.context); + return true; } private bool TryComplete() => (Interlocked.Or(ref _state, StateCompleted) & StateCompleted) == 0; diff --git a/src/Orleans.Core/Runtime/OutsideRuntimeClient.cs b/src/Orleans.Core/Runtime/OutsideRuntimeClient.cs index bb86127f63..5cbc5d818b 100644 --- a/src/Orleans.Core/Runtime/OutsideRuntimeClient.cs +++ b/src/Orleans.Core/Runtime/OutsideRuntimeClient.cs @@ -93,7 +93,7 @@ public OutsideRuntimeClient( TimeSpan.FromSeconds(1))); this.callbackTimer = new PeriodicTimer(period, timeProvider); this.sharedCallbackData = new SharedCallbackData( - msg => this.UnregisterCallback(msg.Id), + this.UnregisterCallback, this.loggerFactory.CreateLogger(), this.clientMessagingOptions.ResponseTimeout, this.clientMessagingOptions.CancelRequestOnTimeout, @@ -375,9 +375,9 @@ public void ReceiveResponse(Message response) } } - private void UnregisterCallback(CorrelationId id) + private void UnregisterCallback(CallbackData callback) { - callbacks.TryRemove(id, out _); + callbacks.TryRemove(KeyValuePair.Create(callback.Message.Id, callback)); } private void ConstructorReset() diff --git a/src/Orleans.Core/Runtime/SharedCallbackData.cs b/src/Orleans.Core/Runtime/SharedCallbackData.cs index aef64491fb..3b95366439 100644 --- a/src/Orleans.Core/Runtime/SharedCallbackData.cs +++ b/src/Orleans.Core/Runtime/SharedCallbackData.cs @@ -6,13 +6,13 @@ namespace Orleans.Runtime; internal sealed class SharedCallbackData { - public readonly Action Unregister; + public readonly Action Unregister; public readonly ILogger Logger; private TimeSpan _responseTimeout; public long ResponseTimeoutStopwatchTicks; public SharedCallbackData( - Action unregister, + Action unregister, ILogger logger, TimeSpan responseTimeout, bool cancelOnTimeout, diff --git a/src/Orleans.Runtime/Core/CallbackRegistry.cs b/src/Orleans.Runtime/Core/CallbackRegistry.cs new file mode 100644 index 0000000000..7b8e104c5b --- /dev/null +++ b/src/Orleans.Runtime/Core/CallbackRegistry.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Orleans.Runtime; + +internal sealed class CallbackRegistry +{ + private readonly ConcurrentDictionary<(GrainId, CorrelationId), CallbackData> _callbacks = new(); + + internal int Count => _callbacks.Count; + + public bool TryAdd(CallbackData callback) + => _callbacks.TryAdd((callback.Message.SendingGrain, callback.Message.Id), callback); + + public bool TryCompleteResponse(Message response) + { + if (response.ResponseTarget is CallbackData directCallback) + { + var completed = directCallback.TryDoCallback(response); + TryRemove(directCallback); + return completed; + } + + if (_callbacks.TryRemove((response.TargetGrain, response.Id), out var callback)) + { + return callback.TryDoCallback(response); + } + + return false; + } + + public bool TryGetResponseCallback(Message response, [NotNullWhen(true)] out CallbackData? callback) + { + if (response.ResponseTarget is CallbackData directCallback) + { + if (!directCallback.IsCompleted) + { + callback = directCallback; + return true; + } + + callback = null; + return false; + } + + return _callbacks.TryGetValue((response.TargetGrain, response.Id), out callback); + } + + public bool TryRemove(CallbackData callback) + => _callbacks.TryRemove(KeyValuePair.Create( + (callback.Message.SendingGrain, callback.Message.Id), + callback)); + + public int CountWhere(TState state, Func predicate) + { + var result = 0; + foreach (var callback in _callbacks.Values) + { + if (predicate(callback, state)) + { + result++; + } + } + + return result; + } + + public void ForEach(TState state, Action action) + { + foreach (var callback in _callbacks.Values) + { + action(callback, state); + } + } +} diff --git a/src/Orleans.Runtime/Core/InsideRuntimeClient.cs b/src/Orleans.Runtime/Core/InsideRuntimeClient.cs index d605549c37..f80f389c2d 100644 --- a/src/Orleans.Runtime/Core/InsideRuntimeClient.cs +++ b/src/Orleans.Runtime/Core/InsideRuntimeClient.cs @@ -31,7 +31,7 @@ internal sealed partial class InsideRuntimeClient : IRuntimeClient, ILifecyclePa private readonly ILogger invokeExceptionLogger; private readonly ILoggerFactory loggerFactory; private readonly SiloMessagingOptions messagingOptions; - private readonly ConcurrentDictionary<(GrainId, CorrelationId), CallbackData> callbacks; + private readonly CallbackRegistry callbacks; private readonly InterfaceToImplementationMappingCache interfaceToImplementationMapping; private readonly SharedCallbackData sharedCallbackData; private readonly SharedCallbackData systemSharedCallbackData; @@ -74,7 +74,7 @@ public InsideRuntimeClient( this._applicationRequestInstruments = new(orleansInstruments); this.ServiceProvider = serviceProvider; this.MySilo = siloDetails.SiloAddress; - this.callbacks = new ConcurrentDictionary<(GrainId, CorrelationId), CallbackData>(); + this.callbacks = new CallbackRegistry(); this.messageFactory = messageFactory; this.ConcreteGrainFactory = new GrainFactory(this, referenceActivator, interfaceIdResolver, interfaceToTypeResolver); this.logger = loggerFactory.CreateLogger(); @@ -88,7 +88,7 @@ public InsideRuntimeClient( var callbackDataLogger = loggerFactory.CreateLogger(); this.sharedCallbackData = new SharedCallbackData( - msg => this.UnregisterCallback(msg.SendingGrain, msg.Id), + this.UnregisterCallback, callbackDataLogger, this.messagingOptions.ResponseTimeout, this.messagingOptions.CancelRequestOnTimeout, @@ -96,7 +96,7 @@ public InsideRuntimeClient( cancellationManager: null!); this.systemSharedCallbackData = new SharedCallbackData( - msg => this.UnregisterCallback(msg.SendingGrain, msg.Id), + this.UnregisterCallback, callbackDataLogger, this.messagingOptions.SystemResponseTimeout, cancelOnTimeout: false, @@ -195,7 +195,12 @@ public void SendRequest( return; } - callbacks.TryAdd((message.SendingGrain, message.Id), callbackData); + if (!callbacks.TryAdd(callbackData)) + { + throw new InvalidOperationException($"A callback with id '{message.Id}' is already registered."); + } + + message.ResponseTarget = callbackData; callbackData.SubscribeForCancellation(cancellationToken); } else @@ -236,9 +241,9 @@ public void SendResponse(Message request, Response response) /// /// UnRegister a callback. /// - private void UnregisterCallback(GrainId grainId, CorrelationId correlationId) + private void UnregisterCallback(CallbackData callback) { - callbacks.TryRemove((grainId, correlationId), out _); + callbacks.TryRemove(callback); } public void SniffIncomingMessage(Message message) @@ -468,13 +473,8 @@ public void ReceiveResponse(Message message) private void ProcessResponseCallback(Message message) { - if (callbacks.TryRemove((message.TargetGrain, message.Id), out var callbackData)) - { - // IMPORTANT: we do not schedule the response callback via the scheduler, since the only thing it does - // is to resolve/break the resolver. The continuations/waits that are based on this resolution will be scheduled as work items. - callbackData.DoCallback(message); - } - else + // The callback completes inline, while continuations are scheduled as work items. + if (!callbacks.TryCompleteResponse(message)) { LogDebugNoCallbackForResponse(this.logger, message); } @@ -483,7 +483,7 @@ private void ProcessResponseCallback(Message message) private void ProcessStatusResponse(Message message) { var status = (StatusResponse)message.BodyObject!; - callbacks.TryGetValue((message.TargetGrain, message.Id), out var callback); + callbacks.TryGetResponseCallback(message, out var callback); var request = callback?.Message; if (request is not null) { @@ -566,7 +566,7 @@ private async Task OnRuntimeInitializeStop(CancellationToken tc) private void BreakOutstandingMessages() { - foreach (var (_, callback) in callbacks) + callbacks.ForEach(this, static (callback, self) => { try { @@ -574,9 +574,9 @@ private void BreakOutstandingMessages() } catch (Exception exception) { - LogWarningWhileProcessingCallbackExpiry(this.logger, exception); + LogWarningWhileProcessingCallbackExpiry(self.logger, exception); } - } + }); } private Task OnRuntimeInitializeStart(CancellationToken tc) @@ -600,13 +600,13 @@ override public string ToString() public void BreakOutstandingMessagesToSilo(SiloAddress deadSilo) { - foreach (var callback in callbacks) + callbacks.ForEach(deadSilo, static (callback, deadSilo) => { - if (deadSilo.Equals(callback.Value.Message.TargetSilo)) + if (deadSilo.Equals(callback.Message.TargetSilo)) { - callback.Value.OnTargetSiloFail(); + callback.OnTargetSiloFail(); } - } + }); } public void Participate(ISiloLifecycle lifecycle) @@ -616,7 +616,9 @@ public void Participate(ISiloLifecycle lifecycle) } public int GetRunningRequestsCount(GrainInterfaceType grainInterfaceType) - => this.callbacks.Count(c => c.Value.Message.InterfaceType == grainInterfaceType); + => this.callbacks.CountWhere( + grainInterfaceType, + static (callback, grainInterfaceType) => callback.Message.InterfaceType == grainInterfaceType); private async Task MonitorCallbackExpiry() { @@ -625,18 +627,18 @@ private async Task MonitorCallbackExpiry() try { var currentStopwatchTicks = ValueStopwatch.GetTimestamp(); - foreach (var (_, callback) in callbacks) + callbacks.ForEach(currentStopwatchTicks, static (callback, currentStopwatchTicks) => { if (callback.IsCompleted) { - continue; + return; } if (callback.IsExpired(currentStopwatchTicks)) { callback.OnTimeout(); } - } + }); } catch (Exception ex) { diff --git a/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs b/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs index 02cc2c67e5..f1badcaaeb 100644 --- a/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs +++ b/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs @@ -160,6 +160,33 @@ private Message RoundTripMessage(Message message) return deserializedMessage!; } + [TestSuite("BVT")] + [TestProvider("None")] + [Fact, TestCategory("BVT"), TestCategory("Serialization")] + public void Message_ResponseTarget_IsProcessLocal() + { + var target = new object(); + var request = messageFactory.CreateMessage(body: null, InvokeMethodOptions.None); + request.ResponseTarget = target; + + var response = messageFactory.CreateResponseMessage(request); + var rejection = messageFactory.CreateRejectionResponse( + request, + Message.RejectionTypes.Transient, + "Rejected"); + var status = messageFactory.CreateDiagnosticResponseMessage( + request, + isExecuting: true, + isWaiting: false, + diagnostics: []); + var deserializedResponse = RoundTripMessage(response); + + Assert.Same(target, response.ResponseTarget); + Assert.Same(target, rejection.ResponseTarget); + Assert.Same(target, status.ResponseTarget); + Assert.Null(deserializedResponse.ResponseTarget); + } + [TestSuite("Functional")] [TestProvider("None")] [Theory, TestCategory("Functional"), TestCategory("Serialization")] diff --git a/test/Orleans.Runtime.Tests/CallbackDataTests.cs b/test/Orleans.Runtime.Tests/CallbackDataTests.cs index dedcc46b75..609f31974c 100644 --- a/test/Orleans.Runtime.Tests/CallbackDataTests.cs +++ b/test/Orleans.Runtime.Tests/CallbackDataTests.cs @@ -68,7 +68,7 @@ private static WeakReference CreateCompletedCallback(CancellationToken cancellat private static CallbackData CreateCallback( IResponseCompletionSource completion, - Action unregister, + Action unregister, ApplicationRequestInstruments instruments) { var shared = new SharedCallbackData( diff --git a/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs b/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs new file mode 100644 index 0000000000..ef2a8ff1fa --- /dev/null +++ b/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs @@ -0,0 +1,245 @@ +using System.Diagnostics.Metrics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Orleans.Runtime; +using Orleans.Serialization.Invocation; +using Xunit; + +namespace Tester; + +[TestSuite("BVT")] +[TestProvider("None")] +[TestCategory("BVT")] +public class CallbackRegistryTests +{ + [Fact] + public void TryCompleteResponse_DirectTarget_CompletesExactCallbackAndCleansFallback() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var completion = new TestResponseCompletionSource(); + var callback = CreateCallback(registry, completion, CreateRequest(1), serviceProvider); + Assert.True(registry.TryAdd(callback)); + var response = CreateResponse(callback.Message, callback); + + Assert.True(registry.TryCompleteResponse(response)); + + Assert.Same(Response.Completed, completion.Response); + Assert.Equal(1, completion.CompletionCount); + Assert.Equal(0, registry.Count); + } + + [Fact] + public void TryCompleteResponse_SerializedResponse_UsesFallbackLookup() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var completion = new TestResponseCompletionSource(); + var callback = CreateCallback(registry, completion, CreateRequest(2), serviceProvider); + Assert.True(registry.TryAdd(callback)); + var response = CreateResponse(callback.Message, responseTarget: null); + + Assert.True(registry.TryCompleteResponse(response)); + + Assert.Same(Response.Completed, completion.Response); + Assert.Equal(1, completion.CompletionCount); + Assert.Equal(0, registry.Count); + } + + [Fact] + public void TryCompleteResponse_DirectRejection_CompletesExactCallback() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var completion = new TestResponseCompletionSource(); + var callback = CreateCallback(registry, completion, CreateRequest(7), serviceProvider); + Assert.True(registry.TryAdd(callback)); + var response = CreateResponse(callback.Message, callback); + response.Result = Message.ResponseTypes.Rejection; + response.BodyObject = new RejectionResponse + { + RejectionType = Message.RejectionTypes.Transient, + RejectionInfo = "Rejected", + }; + + Assert.True(registry.TryCompleteResponse(response)); + + Assert.IsType(completion.Response.Exception); + Assert.Equal(1, completion.CompletionCount); + Assert.Equal(0, registry.Count); + } + + [Fact] + public void TryGetResponseCallback_DirectStatus_UsesExactCallback() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var callback = CreateCallback( + registry, + new TestResponseCompletionSource(), + CreateRequest(3), + serviceProvider); + Assert.True(registry.TryAdd(callback)); + var status = CreateResponse(callback.Message, callback); + status.Result = Message.ResponseTypes.Status; + status.BodyObject = new StatusResponse(true, false, []); + + Assert.True(registry.TryGetResponseCallback(status, out var result)); + + Assert.Same(callback, result); + callback.OnHostShutdown(); + } + + [Fact] + public void TryCompleteResponse_StaleDirectTarget_DoesNotRemoveReplacement() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var firstCompletion = new TestResponseCompletionSource(); + var first = CreateCallback(registry, firstCompletion, CreateRequest(4), serviceProvider); + Assert.True(registry.TryAdd(first)); + first.OnTimeout(); + + var replacementCompletion = new TestResponseCompletionSource(); + var replacement = CreateCallback(registry, replacementCompletion, CreateRequest(4), serviceProvider); + Assert.True(registry.TryAdd(replacement)); + var staleResponse = CreateResponse(first.Message, first); + + Assert.False(registry.TryCompleteResponse(staleResponse)); + Assert.Equal(1, registry.Count); + Assert.True(registry.TryCompleteResponse(CreateResponse(replacement.Message, responseTarget: null))); + + Assert.IsType(firstCompletion.Response.Exception); + Assert.Same(Response.Completed, replacementCompletion.Response); + Assert.Equal(1, firstCompletion.CompletionCount); + Assert.Equal(1, replacementCompletion.CompletionCount); + Assert.Equal(0, registry.Count); + } + + [Theory] + [InlineData(TerminalRace.Timeout)] + [InlineData(TerminalRace.TargetFailure)] + [InlineData(TerminalRace.Shutdown)] + public void TryCompleteResponse_TerminalRace_CompletesExactlyOnce(TerminalRace race) + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var completion = new TestResponseCompletionSource(); + var callback = CreateCallback(registry, completion, CreateRequest(5), serviceProvider); + Assert.True(registry.TryAdd(callback)); + var response = CreateResponse(callback.Message, callback); + + Parallel.Invoke( + () => CompleteTerminal(callback, race), + () => registry.TryCompleteResponse(response)); + + Assert.Equal(1, completion.CompletionCount); + Assert.Equal(0, registry.Count); + } + + [Fact] + public void TryCompleteResponse_CancellationRace_CompletesExactlyOnce() + { + using var serviceProvider = CreateServiceProvider(); + using var cancellation = new CancellationTokenSource(); + var registry = new CallbackRegistry(); + var completion = new TestResponseCompletionSource(); + var callback = CreateCallback(registry, completion, CreateRequest(6), serviceProvider); + Assert.True(registry.TryAdd(callback)); + callback.SubscribeForCancellation(cancellation.Token); + var response = CreateResponse(callback.Message, callback); + + Parallel.Invoke( + cancellation.Cancel, + () => registry.TryCompleteResponse(response)); + + Assert.Equal(1, completion.CompletionCount); + Assert.Equal(0, registry.Count); + } + + private static void CompleteTerminal(CallbackData callback, TerminalRace race) + { + switch (race) + { + case TerminalRace.Timeout: + callback.OnTimeout(); + break; + case TerminalRace.TargetFailure: + callback.OnTargetSiloFail(); + break; + case TerminalRace.Shutdown: + callback.OnHostShutdown(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(race)); + } + } + + private static CallbackData CreateCallback( + CallbackRegistry registry, + IResponseCompletionSource completion, + Message request, + IServiceProvider serviceProvider) + { + var shared = new SharedCallbackData( + callback => registry.TryRemove(callback), + NullLogger.Instance, + responseTimeout: TimeSpan.FromMinutes(1), + cancelOnTimeout: false, + waitForCancellationAcknowledgement: false, + cancellationManager: null); + var instruments = new ApplicationRequestInstruments( + new OrleansInstruments(serviceProvider.GetRequiredService())); + return new CallbackData(shared, completion, request, instruments); + } + + private static Message CreateRequest(long id) => new() + { + Id = new CorrelationId(id), + SendingGrain = GrainId.Create("callback-caller", "1"), + TargetGrain = GrainId.Create("callback-target", "1"), + }; + + private static Message CreateResponse(Message request, CallbackData? responseTarget) => new() + { + Direction = Message.Directions.Response, + Result = Message.ResponseTypes.Success, + Id = request.Id, + TargetGrain = request.SendingGrain, + SendingGrain = request.TargetGrain, + BodyObject = Response.Completed, + ResponseTarget = responseTarget, + }; + + private static ServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddMetrics(); + return services.BuildServiceProvider(); + } + + public enum TerminalRace + { + Timeout, + TargetFailure, + Shutdown, + } + + private sealed class TestResponseCompletionSource : IResponseCompletionSource + { + private Response? _response; + private int _completionCount; + + public Response Response => Volatile.Read(ref _response)!; + + public int CompletionCount => Volatile.Read(ref _completionCount); + + public void Complete(Response value) + { + Interlocked.Increment(ref _completionCount); + Interlocked.CompareExchange(ref _response, value, null); + } + + public void Complete() => Complete(Response.Completed); + } +} From e43f46003da5133fbc8711e0d1df604d10ecf2d4 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 12:21:38 -0700 Subject: [PATCH 09/10] perf(runtime): stripe callback fallback lookup --- .../Messaging/StripedCallbackDictionary.cs | 214 ++++++++++++++++++ src/Orleans.Runtime/Core/CallbackRegistry.cs | 48 ++-- .../Core/InsideRuntimeClient.cs | 6 +- .../CallbackRegistryTests.cs | 54 ++++- .../StripedCallbackDictionaryTests.cs | 134 +++++++++++ 5 files changed, 422 insertions(+), 34 deletions(-) create mode 100644 src/Orleans.Core/Messaging/StripedCallbackDictionary.cs create mode 100644 test/Orleans.Runtime.Tests/StripedCallbackDictionaryTests.cs diff --git a/src/Orleans.Core/Messaging/StripedCallbackDictionary.cs b/src/Orleans.Core/Messaging/StripedCallbackDictionary.cs new file mode 100644 index 0000000000..eb08b359d1 --- /dev/null +++ b/src/Orleans.Core/Messaging/StripedCallbackDictionary.cs @@ -0,0 +1,214 @@ +#nullable enable +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Orleans.Runtime; + +internal sealed class StripedCallbackDictionary + where TValue : notnull +{ + private const int StripeBits = 7; + private const ulong HashFactor = 11_400_714_819_323_198_485; + public const int StripeCount = 1 << StripeBits; + private readonly Stripe[] _stripes = CreateStripes(); + private int _isClosed; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetStripeIndex(CorrelationId correlationId) + => (int)(unchecked((ulong)correlationId.ToInt64() * HashFactor) >> (64 - StripeBits)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(CorrelationId id, TValue value) + => TryAdd(id, value, out _); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(CorrelationId id, TValue value, out bool isClosed) + { + var stripe = GetStripe(id); + lock (stripe.Lock) + { + if (Volatile.Read(ref _isClosed) != 0) + { + isClosed = true; + return false; + } + + isClosed = false; + return stripe.Dictionary.TryAdd(id, value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(CorrelationId id, [NotNullWhen(true)] out TValue? value) + { + var stripe = GetStripe(id); + lock (stripe.Lock) + { + return stripe.Dictionary.TryGetValue(id, out value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryRemove(CorrelationId id, [NotNullWhen(true)] out TValue? value) + { + var stripe = GetStripe(id); + lock (stripe.Lock) + { + return stripe.Dictionary.Remove(id, out value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryRemove(CorrelationId id, TValue value) + { + var stripe = GetStripe(id); + lock (stripe.Lock) + { + if (!stripe.Dictionary.TryGetValue(id, out var current) + || !EqualityComparer.Default.Equals(current, value)) + { + return false; + } + + return stripe.Dictionary.Remove(id); + } + } + + public int Count => CountLocked(0); + + public void Close() + { + Volatile.Write(ref _isClosed, 1); + CloseLocked(0); + } + + public int CountWhere(TState state, Func predicate) + => CountWhereLocked(0, state, predicate); + + public void ForEach(TState state, Action action) + { + foreach (var stripe in _stripes) + { + TValue[]? snapshot = null; + var snapshotCount = 0; + try + { + lock (stripe.Lock) + { + if (stripe.Dictionary.Count == 0) + { + continue; + } + + snapshot = ArrayPool.Shared.Rent(stripe.Dictionary.Count); + foreach (var value in stripe.Dictionary.Values) + { + snapshot[snapshotCount++] = value; + } + } + + for (var index = 0; index < snapshotCount; index++) + { + action(snapshot[index], state); + } + } + finally + { + if (snapshot is not null) + { + ArrayPool.Shared.Return( + snapshot, + clearArray: RuntimeHelpers.IsReferenceOrContainsReferences()); + } + } + } + } + + private static Stripe[] CreateStripes() + { + var result = new Stripe[StripeCount]; + for (var index = 0; index < result.Length; index++) + { + result[index] = new Stripe(); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Stripe GetStripe(CorrelationId id) => _stripes[GetStripeIndex(id)]; + + private int CountLocked(int stripeIndex) + { + if (stripeIndex < _stripes.Length) + { + lock (_stripes[stripeIndex].Lock) + { + return CountLocked(stripeIndex + 1); + } + } + + var result = 0; + foreach (var stripe in _stripes) + { + result += stripe.Dictionary.Count; + } + + return result; + } + + private void CloseLocked(int stripeIndex) + { + if (stripeIndex >= _stripes.Length) + { + return; + } + + lock (_stripes[stripeIndex].Lock) + { + CloseLocked(stripeIndex + 1); + } + } + + private int CountWhereLocked( + int stripeIndex, + TState state, + Func predicate) + { + if (stripeIndex < _stripes.Length) + { + lock (_stripes[stripeIndex].Lock) + { + return CountWhereLocked(stripeIndex + 1, state, predicate); + } + } + + var result = 0; + foreach (var stripe in _stripes) + { + foreach (var value in stripe.Dictionary.Values) + { + if (predicate(value, state)) + { + result++; + } + } + } + + return result; + } + + private sealed class Stripe + { +#if NET9_0_OR_GREATER + public readonly System.Threading.Lock Lock = new(); +#else + public readonly object Lock = new(); +#endif + public readonly Dictionary Dictionary = new(); + } +} diff --git a/src/Orleans.Runtime/Core/CallbackRegistry.cs b/src/Orleans.Runtime/Core/CallbackRegistry.cs index 7b8e104c5b..1d02808110 100644 --- a/src/Orleans.Runtime/Core/CallbackRegistry.cs +++ b/src/Orleans.Runtime/Core/CallbackRegistry.cs @@ -1,18 +1,31 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Orleans.Runtime; internal sealed class CallbackRegistry { - private readonly ConcurrentDictionary<(GrainId, CorrelationId), CallbackData> _callbacks = new(); + // MessageFactory is a singleton and assigns host-unique correlation ids. + private readonly StripedCallbackDictionary _callbacks = new(); internal int Count => _callbacks.Count; - public bool TryAdd(CallbackData callback) - => _callbacks.TryAdd((callback.Message.SendingGrain, callback.Message.Id), callback); + public bool TryRegister(CallbackData callback) + { + if (_callbacks.TryAdd(callback.Message.Id, callback, out var isClosed)) + { + return true; + } + + if (isClosed) + { + return false; + } + + throw new InvalidOperationException($"A callback with id '{callback.Message.Id}' is already registered."); + } + + public void Close() => _callbacks.Close(); public bool TryCompleteResponse(Message response) { @@ -23,7 +36,7 @@ public bool TryCompleteResponse(Message response) return completed; } - if (_callbacks.TryRemove((response.TargetGrain, response.Id), out var callback)) + if (_callbacks.TryRemove(response.Id, out var callback)) { return callback.TryDoCallback(response); } @@ -45,33 +58,20 @@ public bool TryGetResponseCallback(Message response, [NotNullWhen(true)] out Cal return false; } - return _callbacks.TryGetValue((response.TargetGrain, response.Id), out callback); + return _callbacks.TryGetValue(response.Id, out callback); } public bool TryRemove(CallbackData callback) - => _callbacks.TryRemove(KeyValuePair.Create( - (callback.Message.SendingGrain, callback.Message.Id), - callback)); + => _callbacks.TryRemove(callback.Message.Id, callback); public int CountWhere(TState state, Func predicate) { - var result = 0; - foreach (var callback in _callbacks.Values) - { - if (predicate(callback, state)) - { - result++; - } - } - - return result; + return _callbacks.CountWhere(state, predicate); } public void ForEach(TState state, Action action) { - foreach (var callback in _callbacks.Values) - { - action(callback, state); - } + _callbacks.ForEach(state, action); } + } diff --git a/src/Orleans.Runtime/Core/InsideRuntimeClient.cs b/src/Orleans.Runtime/Core/InsideRuntimeClient.cs index f80f389c2d..d9cbe0feaf 100644 --- a/src/Orleans.Runtime/Core/InsideRuntimeClient.cs +++ b/src/Orleans.Runtime/Core/InsideRuntimeClient.cs @@ -195,9 +195,10 @@ public void SendRequest( return; } - if (!callbacks.TryAdd(callbackData)) + if (!callbacks.TryRegister(callbackData)) { - throw new InvalidOperationException($"A callback with id '{message.Id}' is already registered."); + callbackData.OnHostShutdown(); + return; } message.ResponseTarget = callbackData; @@ -551,6 +552,7 @@ private async Task OnRuntimeInitializeStop(CancellationToken tc) { Volatile.Write(ref _isStopping, 1); this.callbackTimer.Dispose(); + callbacks.Close(); // Once the silo is shutting down it can no longer receive responses, so any requests which // are still outstanding will never complete. Fault them now so that in-flight grain calls // observe a terminal result instead of hanging forever, which would otherwise deadlock grain diff --git a/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs b/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs index ef2a8ff1fa..496f79e75f 100644 --- a/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs +++ b/test/Orleans.Runtime.Tests/CallbackRegistryTests.cs @@ -19,7 +19,7 @@ public void TryCompleteResponse_DirectTarget_CompletesExactCallbackAndCleansFall var registry = new CallbackRegistry(); var completion = new TestResponseCompletionSource(); var callback = CreateCallback(registry, completion, CreateRequest(1), serviceProvider); - Assert.True(registry.TryAdd(callback)); + Assert.True(registry.TryRegister(callback)); var response = CreateResponse(callback.Message, callback); Assert.True(registry.TryCompleteResponse(response)); @@ -36,7 +36,7 @@ public void TryCompleteResponse_SerializedResponse_UsesFallbackLookup() var registry = new CallbackRegistry(); var completion = new TestResponseCompletionSource(); var callback = CreateCallback(registry, completion, CreateRequest(2), serviceProvider); - Assert.True(registry.TryAdd(callback)); + Assert.True(registry.TryRegister(callback)); var response = CreateResponse(callback.Message, responseTarget: null); Assert.True(registry.TryCompleteResponse(response)); @@ -53,7 +53,7 @@ public void TryCompleteResponse_DirectRejection_CompletesExactCallback() var registry = new CallbackRegistry(); var completion = new TestResponseCompletionSource(); var callback = CreateCallback(registry, completion, CreateRequest(7), serviceProvider); - Assert.True(registry.TryAdd(callback)); + Assert.True(registry.TryRegister(callback)); var response = CreateResponse(callback.Message, callback); response.Result = Message.ResponseTypes.Rejection; response.BodyObject = new RejectionResponse @@ -79,7 +79,7 @@ public void TryGetResponseCallback_DirectStatus_UsesExactCallback() new TestResponseCompletionSource(), CreateRequest(3), serviceProvider); - Assert.True(registry.TryAdd(callback)); + Assert.True(registry.TryRegister(callback)); var status = CreateResponse(callback.Message, callback); status.Result = Message.ResponseTypes.Status; status.BodyObject = new StatusResponse(true, false, []); @@ -97,12 +97,12 @@ public void TryCompleteResponse_StaleDirectTarget_DoesNotRemoveReplacement() var registry = new CallbackRegistry(); var firstCompletion = new TestResponseCompletionSource(); var first = CreateCallback(registry, firstCompletion, CreateRequest(4), serviceProvider); - Assert.True(registry.TryAdd(first)); + Assert.True(registry.TryRegister(first)); first.OnTimeout(); var replacementCompletion = new TestResponseCompletionSource(); var replacement = CreateCallback(registry, replacementCompletion, CreateRequest(4), serviceProvider); - Assert.True(registry.TryAdd(replacement)); + Assert.True(registry.TryRegister(replacement)); var staleResponse = CreateResponse(first.Message, first); Assert.False(registry.TryCompleteResponse(staleResponse)); @@ -126,7 +126,7 @@ public void TryCompleteResponse_TerminalRace_CompletesExactlyOnce(TerminalRace r var registry = new CallbackRegistry(); var completion = new TestResponseCompletionSource(); var callback = CreateCallback(registry, completion, CreateRequest(5), serviceProvider); - Assert.True(registry.TryAdd(callback)); + Assert.True(registry.TryRegister(callback)); var response = CreateResponse(callback.Message, callback); Parallel.Invoke( @@ -145,7 +145,7 @@ public void TryCompleteResponse_CancellationRace_CompletesExactlyOnce() var registry = new CallbackRegistry(); var completion = new TestResponseCompletionSource(); var callback = CreateCallback(registry, completion, CreateRequest(6), serviceProvider); - Assert.True(registry.TryAdd(callback)); + Assert.True(registry.TryRegister(callback)); callback.SubscribeForCancellation(cancellation.Token); var response = CreateResponse(callback.Message, callback); @@ -157,6 +157,44 @@ public void TryCompleteResponse_CancellationRace_CompletesExactlyOnce() Assert.Equal(0, registry.Count); } + [Fact] + public void TryRegister_DuplicateCorrelationIdAcrossSenders_Throws() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + var first = CreateCallback(registry, new TestResponseCompletionSource(), CreateRequest(8), serviceProvider); + Assert.True(registry.TryRegister(first)); + var duplicateRequest = CreateRequest(8); + duplicateRequest.SendingGrain = GrainId.Create("callback-caller", "2"); + var duplicate = CreateCallback( + registry, + new TestResponseCompletionSource(), + duplicateRequest, + serviceProvider); + + Assert.Throws(() => registry.TryRegister(duplicate)); + + first.OnHostShutdown(); + Assert.Equal(0, registry.Count); + } + + [Fact] + public void TryRegister_AfterClose_DoesNotPublishCallback() + { + using var serviceProvider = CreateServiceProvider(); + var registry = new CallbackRegistry(); + registry.Close(); + var callback = CreateCallback( + registry, + new TestResponseCompletionSource(), + CreateRequest(9), + serviceProvider); + + Assert.False(registry.TryRegister(callback)); + + Assert.Equal(0, registry.Count); + } + private static void CompleteTerminal(CallbackData callback, TerminalRace race) { switch (race) diff --git a/test/Orleans.Runtime.Tests/StripedCallbackDictionaryTests.cs b/test/Orleans.Runtime.Tests/StripedCallbackDictionaryTests.cs new file mode 100644 index 0000000000..02acaa2c6e --- /dev/null +++ b/test/Orleans.Runtime.Tests/StripedCallbackDictionaryTests.cs @@ -0,0 +1,134 @@ +using Orleans.Runtime; +using Xunit; + +namespace Tester; + +[TestSuite("BVT")] +[TestProvider("None")] +[TestCategory("BVT")] +public class StripedCallbackDictionaryTests +{ + private static readonly Action EmptyVisitor = static (_, _) => { }; + private static readonly Func MatchValue = static (value, expected) => value == expected; + + [Fact] + public void GetStripeIndex_OverflowAndStride_DistributesCorrelationIds() + { + var start = long.MaxValue - (StripedCallbackDictionary.StripeCount / 2); + var consecutiveStripes = Enumerable.Range(0, StripedCallbackDictionary.StripeCount) + .Select(offset => new CorrelationId(unchecked(start + offset))) + .Select(StripedCallbackDictionary.GetStripeIndex) + .Distinct() + .Count(); + var stridedStripes = Enumerable.Range(0, StripedCallbackDictionary.StripeCount) + .Select(offset => new CorrelationId(offset * StripedCallbackDictionary.StripeCount)) + .Select(StripedCallbackDictionary.GetStripeIndex) + .Distinct() + .Count(); + + Assert.True(consecutiveStripes > StripedCallbackDictionary.StripeCount / 2); + Assert.True(stridedStripes > StripedCallbackDictionary.StripeCount / 2); + } + + [Fact] + public void AddGetAndRemove_ValueIdentityIsPreserved() + { + var dictionary = new StripedCallbackDictionary(); + var id = new CorrelationId(42); + var value = new object(); + + Assert.True(dictionary.TryAdd(id, value)); + Assert.False(dictionary.TryAdd(id, new object())); + Assert.True(dictionary.TryGetValue(id, out var found)); + Assert.Same(value, found); + Assert.False(dictionary.TryRemove(id, new object())); + Assert.True(dictionary.TryRemove(id, value)); + Assert.False(dictionary.TryGetValue(id, out _)); + } + + [Fact] + public void ConcurrentOperations_CountAndValuesRemainExact() + { + var dictionary = new StripedCallbackDictionary(); + + Parallel.For(0, 10_000, index => + { + var id = new CorrelationId(index); + Assert.True(dictionary.TryAdd(id, index)); + Assert.True(dictionary.TryGetValue(id, out var value)); + Assert.Equal(index, value); + }); + + Assert.Equal(10_000, dictionary.Count); + Assert.Equal(10_000, dictionary.CountWhere(0, static (value, _) => value >= 0)); + + Parallel.For(0, 10_000, index => + { + Assert.True(dictionary.TryRemove(new CorrelationId(index), out var value)); + Assert.Equal(index, value); + }); + + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void ForEach_SnapshotAllowsValuesToRemoveThemselves() + { + var dictionary = new StripedCallbackDictionary(); + for (var index = 0; index < 32; index++) + { + Assert.True(dictionary.TryAdd(new CorrelationId(index), index)); + } + + dictionary.ForEach(dictionary, static (value, dictionary) => + { + Assert.True(dictionary.TryRemove(new CorrelationId(value), out var removed)); + Assert.Equal(value, removed); + }); + + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void ForEach_EmptyDictionary_DoesNotAllocate() + { + var dictionary = new StripedCallbackDictionary(); + dictionary.ForEach((object?)null, EmptyVisitor); + + var allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); + dictionary.ForEach((object?)null, EmptyVisitor); + var allocated = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore; + + Assert.Equal(0, allocated); + } + + [Fact] + public void CountWhere_StatefulPredicate_DoesNotAllocate() + { + var dictionary = new StripedCallbackDictionary(); + Assert.True(dictionary.TryAdd(new CorrelationId(42), 42)); + Assert.Equal(1, dictionary.CountWhere(42, MatchValue)); + + var allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); + var count = dictionary.CountWhere(42, MatchValue); + var allocated = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore; + + Assert.Equal(1, count); + Assert.Equal(0, allocated); + } + + [Fact] + public void Close_ClosesAdmissionAndRetainsPublishedValues() + { + var dictionary = new StripedCallbackDictionary(); + Assert.True(dictionary.TryAdd(new CorrelationId(1), 1)); + + dictionary.Close(); + + Assert.False(dictionary.TryAdd(new CorrelationId(2), 2, out var isClosed)); + Assert.True(isClosed); + Assert.Equal(1, dictionary.Count); + Assert.True(dictionary.TryRemove(new CorrelationId(1), out var value)); + Assert.Equal(1, value); + } +} From 88ed24754faf0b6e2a3fb06dff7f0dac706a0378 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 13:24:15 -0700 Subject: [PATCH 10/10] test(runtime): cover serialized callback fallback --- .../Serialization/MessageSerializerTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs b/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs index f1badcaaeb..91d060f926 100644 --- a/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs +++ b/test/Orleans.Core.Tests/Serialization/MessageSerializerTests.cs @@ -179,12 +179,16 @@ public void Message_ResponseTarget_IsProcessLocal() isExecuting: true, isWaiting: false, diagnostics: []); + var deserializedRequest = RoundTripMessage(request); var deserializedResponse = RoundTripMessage(response); + var responseToDeserializedRequest = messageFactory.CreateResponseMessage(deserializedRequest); Assert.Same(target, response.ResponseTarget); Assert.Same(target, rejection.ResponseTarget); Assert.Same(target, status.ResponseTarget); + Assert.Null(deserializedRequest.ResponseTarget); Assert.Null(deserializedResponse.ResponseTarget); + Assert.Null(responseToDeserializedRequest.ResponseTarget); } [TestSuite("Functional")]