dotnet/orleans

Cannot activate the grain after clearing its state.

scalalang2 opened this issue · 5 comments

Environment

  • Persistent Storage Provider : DynamoDB
  • Framework Version : Orleans 8.2.0

Background

The ClearStateAsync funcction doesn't remove the entry from storage by default.
Instead it only sets the state value to null,

If ReadStateAsync() is invoked when the value is null,
then it occurs an error from Serializer. : Insufficient data present in buffer.

  • ReadStateAsync is called when trying to activate the same grain
    • the serializer cannot read the value cleared by ClearStateAsync

How to reproduce

public class TestGrain : Grain, ITestGrain
{
    private readonly IPersistentState<TestState> state;

    public TestGrain([PersistentState("Test")] IPersistentState<TestState> state)
    {
        this.state = state;
    }

    public async Task Test()
    {
        // Calling this method after clearing the state will trigger an error.
        await this.state.ReadStateAsync();

        if (this.state.Activated == false)
        {
            this.state.State.Activated = true;
            await this.state.WriteStateAsync();
        }
        else
        {
            await this.state.ClearStateAsync();
        }
    }
}

Logs

System.InvalidOperationException: Insufficient data present in buffer.
   at Orleans.Serialization.Buffers.Reader`1.ThrowInsufficientData() in /_/src/Orleans.Serialization/Buffers/Reader.cs:line 741
   at Orleans.Serialization.Buffers.Reader`1.MoveNext() in /_/src/Orleans.Serialization/Buffers/Reader.cs:line 602
   at Orleans.Serialization.Buffers.Reader`1.ReadByteSlow(Reader`1& reader) in /_/src/Orleans.Serialization/Buffers/Reader.cs:line 643
   at Orleans.Serialization.Serializer.Deserialize[T](ReadOnlySpan`1 source) in /_/src/Orleans.Serialization/Serializer.cs:line 423
   at Orleans.Serialization.Serializer.Deserialize[T](ReadOnlyMemory`1 source) in /_/src/Orleans.Serialization/Serializer.cs:line 466
   at Orleans.Storage.OrleansGrainStorageSerializer.Deserialize[T](BinaryData input) in /_/src/Orleans.Core/Providers/StorageSerializer/OrleansGrainStateSerializer.cs:line 34
   at Orleans.Storage.GrainStorageSerializerExtensions.Deserialize[T](IGrainStorageSerializer serializer, ReadOnlyMemory`1 input) in /_/src/Orleans.Core/Providers/IGrainStorageSerializer.cs:line 43
   at Orleans.Storage.DynamoDBGrainStorage.ConvertFromStorageFormat[T](GrainStateRecord entity) in /_/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs:line 330
2024-09-30 14:17:38 [ERR] Error from storage provider DynamoDBGrainStorage.Test during ReadStateAsync for grain test/Testnull - Orleans.Storage.DynamoDBGrainStorage

I had also investigated the AzureTableStorage code
it seems that same error should be triggerred since it can be empty.

Even if this is an intentional behavior
it needs to be fixed because it's currently causing issues with the Streaming functionality.

My Suggestion.

T dataValue = default;
try
{
  if(entity.State.Length > 0)
      dataValue = this.options.GrainStorageSerializer.Deserialize<T>(entity.State);
}
catch (Exception exc)
{
   // handle errors.
}

@ReubenBond If you agree with the solution described above comment, I'll take this job.

@scalalang2 looks good to me, but we should make the if condition also check for null.
At the call site, we should add an else to the if (record != null) check to re-initialize State to a new instance by calling IActivator.Create<T>(). This will involve:

  • Resolving IActivatorProvider from the IServiceProvider in the constructor and storing it in a field
  • Calling _activatorProvider.GetActivator<T>().Create() in ReadStateAsync() (when record is null after a successful read) and ClearStateAsync() after successfully clearing state.

How does that sound?

@ReubenBond Thanks for reply,
I understood that you suggested making changes as follows, Did I understand correctly?

DynamoDBGrainStorage.ReadStateAsync

if (record != null)
{
    var loadedState = ConvertFromStorageFormat<T>(record);
    grainState.RecordExists = loadedState != null;
    grainState.State = loadedState ?? Activator.CreateInstance<T>();
    grainState.ETag = record.ETag.ToString();
}
else
{
    grainState.State = _activatorProvider.GetActivator<T>().Create()
}

DynamoDBGrainStorage.ClearStateAsync

if (this.options.DeleteStateOnClear)
{
    // codes
}
else
{
    // codes
}

grainState.State = _activatorProvider.GetActivator<T>().Create()

Additionally, I'd like to put the issue I've noticed with Streaming Functioanlity.
Once PubSubRendezvousGrain Grain clears its state, it couldn't reactivate in subsequent operations.
-> this would be solved after changing the code as above.