dotnet/orleans · error · InvalidOperationException
Cached Event Hub message is missing its offset.
Error message
Cached Event Hub message is missing its offset.
What it means
EventHubDataAdapter.GetOffset throws InvalidOperationException when SegmentBuilder.ReadNextString returns null while reading the offset from a CachedMessage's Segment. The offset is the Event Hub message's OffsetString — a string identifier for the event's position in the partition. A null return means the segment data is corrupted, was not properly encoded, or the segment has been partially overwritten. This method is called during cache purge (OnPurge) to update the checkpointer with the last purged offset.
Source
Thrown at src/Azure/Orleans.Streaming.EventHubs/Providers/Streams/EventHub/EventHubDataAdapter.cs:90
};
}
public virtual StreamPosition GetStreamPosition(string partition, EventData queueMessage)
{
StreamId streamId = this.GetStreamIdentity(queueMessage);
StreamSequenceToken token =
new EventHubSequenceTokenV2(queueMessage.OffsetString, queueMessage.SequenceNumber, 0);
return new StreamPosition(streamId, token);
}
/// <summary>
/// Get offset from cached message. Left to derived class, as only it knows how to get this from the cached message.
/// </summary>
public virtual string GetOffset(CachedMessage lastItemPurged)
{
int readOffset = 0;
return SegmentBuilder.ReadNextString(lastItemPurged.Segment, ref readOffset)
?? throw new InvalidOperationException("Cached Event Hub message is missing its offset.");
}
/// <summary>
/// Get the Event Hub partition key to use for a stream.
/// </summary>
/// <param name="streamId">The stream Guid.</param>
/// <returns>The partition key to use for the stream.</returns>
public virtual string GetPartitionKey(StreamId streamId) => streamId.GetKeyAsString();
/// <summary>
/// Get the <see cref="IStreamIdentity"/> for an event message.
/// </summary>
/// <param name="queueMessage">The event message.</param>
/// <returns>The stream identity.</returns>
public virtual StreamId GetStreamIdentity(EventData queueMessage)
{
string streamKey = queueMessage.PartitionKey;
string? streamNamespace = queueMessage.GetStreamNamespaceProperty();View on GitHub (pinned to fca799fa70)
Solutions
- If using a custom EventHubDataAdapter subclass, ensure EncodeMessageIntoSegment and GetOffset agree on the segment layout — the first field written must be the offset string.
- Clear the cache and checkpoint state (Azure Blob Storage lease/checkpoint blob) after upgrading Orleans if the CachedMessage segment format changed, to avoid loading stale incompatible messages.
- If the error recurs, check for a custom IEventHubDataAdapter or a custom IEventHubQueueCache that may alter segment encoding.
- Inspect the EventHubDataAdapter.EncodeMessageIntoSegment to confirm offset is the first SegmentBuilder.Append call, matching the read order in GetOffset.
Example fix
// before — custom data adapter overrides encoding but forgets offset
public class CustomDataAdapter : EventHubDataAdapter
{
protected override ArraySegment<byte> EncodeMessageIntoSegment(
EventData msg, Func<int, ArraySegment<byte>> getSegment)
{
// offset NOT written first — GetOffset will fail
...
}
}
// after — keep offset as first field, or override GetOffset public class CustomDataAdapter : EventHubDataAdapter
{
protected override ArraySegment<byte> EncodeMessageIntoSegment(
EventData msg, Func<int, ArraySegment<byte>> getSegment)
{
// write offset FIRST to match base GetOffset
...
}
// or override GetOffset to match your layout
public override string GetOffset(CachedMessage lastItemPurged)
{
int readOffset = yourCustomOffsetPosition;
return SegmentBuilder.ReadNextString(lastItemPurged.Segment, ref readOffset)
?? throw new InvalidOperationException("offset missing");
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// If using a custom data adapter, validate segment encoding matches GetOffset: // Ensure offset is the FIRST field written in EncodeMessageIntoSegment.
Try / catch
// When handling cache purge in custom infrastructure:
try
{
var offset = dataAdapter.GetOffset(lastItemPurged);
checkpointer.Update(offset, DateTime.UtcNow, CancellationToken.None);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("missing its offset"))
{
logger.LogError(ex, "Cached message segment is corrupted; skipping checkpoint update");
// Do not rethrow in purge path — one bad message should not crash the silo
} Prevention
- If subclassing EventHubDataAdapter, ensure EncodeMessageIntoSegment writes the offset as the first field, matching the base GetOffset read order.
- After upgrading Orleans, clear checkpoint and cache state if the CachedMessage segment format changed.
- Test custom data adapter implementations with round-trip encode/decode to verify segment consistency.
When it happens
Trigger: During cache eviction when OnPurge calls dataAdapter.GetOffset(lastItemPurged) and the CachedMessage's Segment does not contain a valid encoded string at the expected position. This can happen if EncodeMessageIntoSegment wrote data in a different order than GetOffset reads it, if the segment buffer was corrupted or reused, or if a custom IEventHubDataAdapter subclass overrides encoding but not GetOffset (or vice versa).
Common situations: A custom EventHubDataAdapter subclass that overrides EncodeMessageIntoSegment to change the segment layout but does not also override GetOffset to match; memory corruption from a buffer pool bug where a FixedSizeBuffer segment is overwritten; a version mismatch where cached messages from an older Orleans version are loaded by a newer data adapter with a different segment format; extremely rare race conditions in the eviction strategy.
Related errors
- One or more errors occurred.
- Message size is too big. MessageSize: {size}
- This adapter only supports read
- Storage state corrupted: no record for committed state v{thi
- Invalid {nameof(AdoNetStreamOptions)} values for ADO.NET Str
AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13).
Data as JSON: /api/errors/ff125e70286fb357.
Report an issue: GitHub.