{"record":{"id":"84e5d736a4044df9","repo":"dotnet/orleans","slug":"the-grain-did-not-reactivate-after-deactivateonidl","errorCode":null,"errorMessage":"The grain did not reactivate after DeactivateOnIdle.","messagePattern":"The grain did not reactivate after DeactivateOnIdle\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"samples/JournalingAzureBlobJson/JournalingAzureBlobJson/Program.cs","lineNumber":107,"sourceCode":"        Console.WriteLine();\n        Console.WriteLine($\"Raw JSONL blob contents from {settings.ContainerName}/{walBlobName}:\");\n        Console.WriteLine(await DownloadBlobAsText(blob));\n\n        await host.StopAsync();\n        return 0;\n    }\n\n    private static async Task<string> DownloadBlobAsText(AppendBlobClient blob)\n    {\n        var result = await blob.DownloadContentAsync();\n        return Encoding.UTF8.GetString(result.Value.Content.ToArray());\n    }\n\n    private static void ValidateRecovery(JournaledSampleSummary written, JournaledSampleSummary recovered)\n    {\n        if (written.ActivationId == recovered.ActivationId)\n        {\n            throw new InvalidOperationException(\"The grain did not reactivate after DeactivateOnIdle.\");\n        }\n\n        EnsureEqual(\"inventory\", written.Inventory, recovered.Inventory, InventoryEntryEquals);\n        EnsureEqual(\"events\", written.Events, recovered.Events, JournalEventEquals);\n        EnsureEqual(\"work queue\", written.WorkQueue, recovered.WorkQueue, WorkItemEquals);\n        EnsureEqual(\"tags\", written.Tags, recovered.Tags, static (left, right) => string.Equals(left, right, StringComparison.Ordinal));\n        EnsureEqual(\"balance\", written.Balance, recovered.Balance, AccountBalanceEquals);\n        EnsureEqual(\"profile\", written.Profile, recovered.Profile, ProfileStateEquals);\n        EnsureEqual(\"completion status\", written.CompletionStatus, recovered.CompletionStatus, static (left, right) => left == right);\n        EnsureEqual(\"receipt\", written.Receipt, recovered.Receipt, static (left, right) => left == right);\n    }\n\n    private static void EnsureEqual<T>(string name, IReadOnlyList<T> written, IReadOnlyList<T> recovered, Func<T, T, bool> equals)\n    {\n        if (written.Count != recovered.Count)\n        {\n            throw new InvalidOperationException($\"Recovered {name} count does not match. Written: {written.Count}, recovered: {recovered.Count}.\");\n        }","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/samples/JournalingAzureBlobJson/JournalingAzureBlobJson/Program.cs#L89-L125","documentation":"An InvalidOperationException thrown by ValidateRecovery in the JournalingAzureBlobJson sample after the journaled grain is deactivated and re-read. The check compares the ActivationId written before deactivation with the one recovered after; if they are equal, the grain was not actually reactivated (DeactivateOnIdle did not produce a new activation), so the recovery test is invalid. This is a self-check that the durability round-trip exercised a fresh activation.","triggerScenarios":"grain.Deactivate() (which calls DeactivateOnIdle) is followed by only a short delay (500ms), then GetSummary() returns the same ActivationId — meaning the old activation was reused or not yet torn down before the summary was captured.","commonSituations":"The 500ms delay is too short on a slow machine so the activation hasn't been collected yet. Deactivation timing in Orleans is best-effort; under load the activation may persist longer. A grain-activation cache/call-interceptor returning the cached summary.","solutions":["Increase the post-deactivation delay or poll until a new ActivationId is observed before validating recovery.","Ensure DeactivateOnIdle actually completes — call a method that forces reactivation or wait for the deactivation callback.","Confirm no other call is keeping the activation alive (which would cancel idle deactivation)."],"exampleFix":"// before\nawait grain.Deactivate();\nawait Task.Delay(TimeSpan.FromMilliseconds(500));\nvar recovered = await grain.GetSummary();\n\n// after (poll for a fresh activation)\nawait grain.Deactivate();\nvar writtenId = written.ActivationId;\nGuid recoveredId;\nJournaledSampleSummary recovered;\ndo {\n    await Task.Delay(TimeSpan.FromMilliseconds(250));\n    recovered = await grain.GetSummary();\n    recoveredId = recovered.ActivationId;\n} while (recoveredId == writtenId);","handlingStrategy":"validation","validationCode":"var writtenId = written.ActivationId;\nJournaledSampleSummary recovered;\ndo {\n    await Task.Delay(TimeSpan.FromMilliseconds(250));\n    recovered = await grain.GetSummary();\n} while (recovered.ActivationId == writtenId);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Wait long enough (or poll) for the activation to be torn down and recreated after DeactivateOnIdle.","Avoid keeping the activation alive with concurrent calls during deactivation.","Treat this exception as a test-harness timing bug, not a production failure."],"tags":["orleans","journaling","durability","test-assertion","grain-lifecycle"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}