{"record":{"id":"cec75650ad9112a0","repo":"dotnet/orleans","slug":"azure-table-journal-header-belongs-to-journal-id","errorCode":null,"errorMessage":"Azure Table journal header belongs to journal id \"{journalId ?? \"<missing>\"}\", not \"{_journalId.Value}\". Ensure that configured partition keys are unique.","messagePattern":"Azure Table journal header belongs to journal id \"(.+?)\", not \"(.+?)\"\\. Ensure that configured partition keys are unique\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"critical","filePath":"src/Azure/Orleans.Journaling.AzureStorage/AzureTableJournalStorage.cs","lineNumber":1058,"sourceCode":"    }\n\n    private IJournalMetadata CreateJournalMetadata(ETag eTag, TableEntity entity)\n    {\n        ValidateHeaderJournalId(entity);\n        return new JournalMetadata(\n            NormalizeFormat(entity.GetString(FormatPropertyName)),\n            eTag == default ? null : eTag.ToString(),\n            DeserializeCallerMetadata(entity.GetString(MetadataPropertyName)));\n    }\n\n    private void ValidateHeaderJournalId(TableEntity entity)\n    {\n        var journalId = entity.GetString(JournalIdPropertyName);\n        // Legacy headers did not store the canonical journal id. They remain addressable through the\n        // configured partition mapping and are backfilled by the next append, replace, or metadata update.\n        if (journalId is not null && !string.Equals(journalId, _journalId.Value, StringComparison.Ordinal))\n        {\n            throw new InvalidOperationException(\n                $\"Azure Table journal header belongs to journal id \\\"{journalId ?? \"<missing>\"}\\\", not \\\"{_journalId.Value}\\\". \" +\n                \"Ensure that configured partition keys are unique.\");\n        }\n    }\n\n    private static string? NormalizeFormat(string? format) => format is { Length: > 0 } ? format : null;\n\n    private static string SerializeCallerMetadata(IReadOnlyDictionary<string, string>? metadata)\n        => metadata is { Count: > 0 } ? JsonSerializer.Serialize(metadata) : \"{}\";\n\n    private static Dictionary<string, string> DeserializeCallerMetadata(string? json)\n    {\n        if (json is not { Length: > 0 })\n        {\n            return new Dictionary<string, string>(StringComparer.Ordinal);\n        }\n\n        Dictionary<string, string>? parsed;","sourceCodeStart":1040,"sourceCodeEnd":1076,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/Azure/Orleans.Journaling.AzureStorage/AzureTableJournalStorage.cs#L1040-L1076","documentation":"The header row's JournalId property does not match the journal id this storage instance was constructed with. ValidateHeaderJournalId (AzureTableJournalStorage.cs:1051) allows a missing JournalId (legacy headers are backfilled), but a present-but-different value means two distinct journals are mapping to the same Azure Table partition key — a collision that would mix or overwrite unrelated data.","triggerScenarios":"GetPartitionKey maps two different JournalId values to the same partition key; the header read from that partition was written by a different journal and already carries its own JournalId; thrown at AzureTableJournalStorage.cs:1056-1061.","commonSituations":"A custom GetPartitionKey delegate (AzureTableJournalStorageOptions.GetPartitionKey) returns a constant or colliding key for multiple journals; two silos/apps share a table with overlapping partition-key ranges; a partition-key scheme that drops distinguishing parts of the JournalId.","solutions":["Ensure GetPartitionKey produces a unique partition key per JournalId (the default percent-encodes JournalId.Value — keep it or make your mapper injective).","If the collision already happened, move one journal's data to a separate table or delete the conflicting partition after confirming ownership.","Do not reuse partition keys across journals that share a table."],"exampleFix":"// before — collides for many journals\noptions.GetPartitionKey = _ => \"journal\";\n\n// after — unique per journal id\noptions.GetPartitionKey = jid => Uri.EscapeDataString(jid.Value);","handlingStrategy":"validation","validationCode":"// Verify the partition-key mapper is injective for the journals in use\nvar keys = journals.Select(options.GetPartitionKeyForJournal).ToList();\nif (keys.Distinct(StringComparer.Ordinal).Count() != keys.Count)\n    throw new InvalidOperationException(\"Partition key collision detected across journals\");","typeGuard":"static bool IsPartitionKeyUniquePerJournal(\n    Func<JournalId, string> mapper, IReadOnlyList<JournalId> ids)\n    => ids.Select(mapper).Distinct(StringComparer.Ordinal).Count() == ids.Count;","tryCatchPattern":null,"preventionTips":["Keep the default percent-encoding partition-key mapper or prove your mapper is injective.","Do not share partition keys across journals.","Use separate tables for unrelated journal sets."],"tags":["azure-table","partition-collision","configuration","journal-id"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}