dotnet/orleans · critical · InvalidOperationException

Azure Table journal header state does not include a generati

Error message

Azure Table journal header state does not include a generation.

What it means

Thrown during AzureTableJournalStorage.AppendAsync when the cached header provider state has no Generation value. The generation identifies the current set of data rows; without it the append cannot name its rows correctly. A missing generation means the header was loaded in an inconsistent or partial state and recovery is required before appending.

Source

Thrown at src/Azure/Orleans.Journaling.AzureStorage/AzureTableJournalStorage.cs:255

        // Appends are written as one entity group transaction, so validate its limits before touching storage.
        ThrowIfBatchTooLarge(value.Length);
        var startTimestamp = Stopwatch.GetTimestamp();
        var succeeded = false;

        try
        {
            for (var attempt = 0; ; attempt++)
            {
                // Ensure local state has the current header ETag and manifest before making a conditional write.
                if (!HeaderExists)
                {
                    await EnsureHeaderAsync(cancellationToken).ConfigureAwait(false);
                }

                var expectedETag = _headerETag;
                var expectedProviderState = _headerProviderState;
                var generation = expectedProviderState.Generation
                    ?? throw new InvalidOperationException("Azure Table journal header state does not include a generation.");
                var entities = CreateDataEntities(value, generation, firstSequence: expectedProviderState.RowCount);
                var newProviderState = expectedProviderState with
                {
                    RowCount = checked(expectedProviderState.RowCount + entities.Count),
                    Length = checked(expectedProviderState.Length + value.Length),
                    AppendRowCount = checked(expectedProviderState.AppendRowCount + entities.Count),
                    AppendLength = checked(expectedProviderState.AppendLength + value.Length),
                };

                // Guard the whole batch with the last observed header ETag so appends fail if the
                // journal changed since this instance recovered it.
                var actions = new List<TableTransactionAction>(entities.Count + 1)
                {
                    new(TableTransactionActionType.UpdateMerge, CreateHeaderCountsPatch(newProviderState), expectedETag),
                };
                foreach (var entity in entities)
                {
                    actions.Add(new(TableTransactionActionType.Add, entity));

View on GitHub (pinned to fca799fa70)

Solutions

  1. Trigger a full recovery/compaction to rewrite the header with a valid generation.
  2. If migrating schemas, write a one-time migration that backfills the Generation property on existing headers.
  3. Inspect the header entity in Azure Table storage and verify the GenerationPropertyName ('Generation') is set.
Defensive patterns

Strategy: try-catch

Try / catch

try
{
    await storage.AppendAsync(value, ct);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("does not include a generation"))
{
    logger.LogWarning(ex, "Header missing generation; triggering compaction.");
    await storage.CompactAsync(ct);
}

Prevention

When it happens

Trigger: Calling AppendAsync after EnsureHeaderAsync loaded a header whose ProviderState.Generation is null. This can happen if the header entity was written by an incompatible version, corrupted, or partially merged by a concurrent writer.

Common situations: Schema migration where an older header lacks the Generation property; concurrent replace that merged a header without generation; a bug in custom header serialization; reading a header written by a different (incompatible) journal implementation.

Related errors


AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13). Data as JSON: /api/errors/525be67b304fbec4. Report an issue: GitHub.