RegisterAsStreamProducer failed: `fullkey` field of `StreamId` is null
- Dominant language
- C#
- Stars
- 10.9k
- Forks
- 2.1k
- Avg merge
- 13h 56m
- Merged PRs (30d)
- 351
Description
I have implemented a custom PersistentStreamProvider by following the example of the Azure Queue Provider, and while the core logic seems to be working fine, I am getting the following exception:
```
fail: Orleans.Streams.MY_STREAM_PROVIDER[103317]
RegisterAsStreamProducer failed
System.ArgumentNullException: Value cannot be null. (Parameter 'bytes')
at System.ArgumentNullException.Throw(String paramName)
at System.Text.Encoding.GetCharCount(Byte[] bytes)
at Orleans.Runtime.StreamId.System.ISpanFormattable.TryFormat(Span`1 destination, Int32& charsWritten, ReadOnlySpan`1 format, IFormatProvider provider) in /_/src/Orleans.Streaming/StreamId.cs:line 183
at System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted[T](T value)
at Orleans.Runtime.QualifiedStreamId.ToString() in /_/src/Orleans.Streaming/InternalStreamId.cs:line 50
at Orleans.Streams.GrainBasedPubSubRuntime.GetRendezvousGrain(QualifiedStreamId streamId) in /_/src/Orleans.Streaming/PubSub/GrainBasedPubSubRuntime.cs:line 62
at Orleans.Streams.GrainBasedPubSubRuntime.RegisterProducer(QualifiedStreamId streamId, GrainId streamProducer) in /_/src/Orleans.Streaming/PubSub/GrainBasedPubSubRuntime.cs:line 20
at Orleans.Streams.StreamPubSubImpl.RegisterProducer(QualifiedStreamId streamId, GrainId streamProducer) in /_/src/Orleans.Streaming/PubSub/StreamPubSubImpl.cs:line 33
at Orleans.Streams.PersistentStreamPullingAgent.PubsubRegisterProducer(IStreamPubSub pubSub, QualifiedStreamId streamId, GrainId meAsStreamProducer, ILogger logger) in /_/src/Orleans.Streaming/PersistentStreams/PersistentStreamPullingAgent.cs:line 802
```
Apparently the `fullkey` field of `StreamId` is null, while it is supposed to be serialized by the native serializer.
This exception occurs just after `IList IQueueAdapterReceiver.GetQueueMessagesAsync(maxCount)` returns the list of `IBatchContainers`. As a result none of the consumers receive any message and the rest of the pipeline is not executed (`IQueueAdapterReceiver.MessagesDeliveredAsync(messages)` is not called etc...).
I have verified that the messages batches are correctly serialized and deserialized. Upon deserialization their content looks like `StreamId:SERVERS_STREAM/ef3fc375fd2b490e96c20859d23a24fb,Context:,SequenceToken:[EventSequenceToken: SeqNum=3, EventIndex=0]`
So the StreamId is at least set on the batch, however what has caught my attention is that the Events list is `null` upon deserialization. It is not even empty, it is null. And I have confirmed that it does contain events when it is serialized. Here is the IBatchContainer implementation (in f#):
```fsharp
[]
[]
type MyBatchContainer
[]
(
streamId: StreamId,
events: List,
requestContext: Dictionary,
realSequenceToken: EventSequenceToken
) =
[]
let mutable sequenceToken: EventSequenceToken = null
do
throwIfNull events
sequenceToken <- realSequenceToken
new(streamId: StreamId, events: List, requestContext: Dictionary) =
MyBatchContainer(streamId, events, requestContext, null)
member this.RealSequenceToken
with get () = sequenceToken
and set (token: EventSequenceToken) = sequenceToken <- token
[]
member val StreamId = streamId with get, set
[]
member val Events = events with get, set
[]
member val RequestContext = requestContext with get, set
interface IBatchContainer with
member this.GetEvents<'T>() =
events
|> Seq.filter (fun x -> x :? 'T)
|> Seq.map (fun x -> x :?> 'T)
|> Seq.mapi (fun i e ->
Tuple.Create<'T, StreamSequenceToken>(e, sequenceToken.CreateSequenceTokenForEvent(i)))
member this.ImportRequestContext() =
match requestContext <> null with
| true ->
RequestContextExtensions.Import(requestContext)
true
| false -> false
member this.StreamId = streamId
member this.SequenceToken: StreamSequenceToken = sequenceToken
```
And here is the generated code, which looks fine to me:
```csharp
public sealed class Copier_MyBatchContainer : global::Orleans.Serialization.Cloning.IDeepCopier, global::Orleans.Serialization.Cloning.IBaseCopier
{
private readonly global::Orleans.Serialization.Activators.IActivator _activator;
private readonly global::Orleans.Serialization.Codecs.ListCopier _copier0;
private readonly global::Orleans.Serialization.Codecs.DictionaryCopier _copier1;
[global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public global::Corp.MyBatchContainer DeepCopy(global::Corp.MyBatchContainer original, global::Orleans.Serialization.Cloning.CopyContext context)
{
if (context.TryGetCopy(original, out global::Corp.MyBatchContainer existing))
return existing;
if (original.GetType() != typeof(global::Corp.MyBatchContainer))
return context.DeepCopy(original);
var result = _activator.Create();
context.RecordCopy(original, result);
DeepCopy(original, result, context);
return result;
}
public Copier_MyBatchContainer(global::Orleans.Serialization.Activators.IActivator _activator, global::Orleans.Serialization.Serializers.ICodecProvider codecProvider)
{
this._activator = OrleansGeneratedCodeHelper.UnwrapService(this, _activator);
_copier0 = OrleansGeneratedCodeHelper.GetService>(this, codecProvider);
_copier1 = OrleansGeneratedCodeHelper.GetService>(this, codecProvider);
}
[global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public void DeepCopy(global::Corp.MyBatchContainer input, global::Corp.MyBatchContainer output, global::Orleans.Serialization.Cloning.CopyContext context)
{
output.StreamId = input.StreamId;
output.Events = _copier0.DeepCopy(input.Events, context);
output.RequestContext = _copier1.DeepCopy(input.RequestContext, context);
}
}
```
I serialize the messages using `Orleans.Serialization.Serializer.SerializeToArray(batchMessage)`, and I have confirmed that the new native serializer is being used (ie. the one relying on the `GenerateSerializer` attributes)
What is particularly painful to me is that since `RegisterAsStreamProducer` is invoked by the Orleans framework, I have no idea where it gets the StreamId from, and therefore where the source of the issue could be. My guess is that the problem with the empty events should be unrelated since events do not even contain a StreamId, but this is not very helpful.
And with the problem with slow debugging that will only be fixed in .NET 9, inspecting external code during a debugging session is essentially impossible (exceptions all over the place due to timeouts, debug symbols not loaded etc...).
Any pointer from someone familiar with the inner working of streams would be greatly appreciated. Just an educated guess on the root cause of the RegisterAsStreamProducer failed would already be massively helpful.
But since I do not even know where `Orleans.Streams.PersistentStreamPullingAgent.PubsubRegisterProducer(IStreamPubSub pubSub, QualifiedStreamId streamId, GrainId meAsStreamProducer, ILogger logger)` gets this streamId, investigating has been a very painful experience, I've been on it for close to 2 days already.
Contributor guide
Research direction
Start with Orleans.Streaming/StreamId.cs at line 183 and PersistentStreamPullingAgent.cs around line 802 to trace how the QualifiedStreamId is produced during producer registration. Compare the custom MyBatchContainer fields with its generated copier and the Serializer.SerializeToArray round trip, including the null Events result. Done means the deserialized stream data preserves the required values and producer registration proceeds to message delivery.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100