dotnet / dotnet/orleans

RegisterAsStreamProducer failed: `fullkey` field of `StreamId` is null

Open
#9,224 3 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.