Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot activate the grain after clearing its state. #9154

Open
scalalang2 opened this issue Sep 30, 2024 · 5 comments
Open

Cannot activate the grain after clearing its state. #9154

scalalang2 opened this issue Sep 30, 2024 · 5 comments

Comments

@scalalang2
Copy link

scalalang2 commented Sep 30, 2024

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
@scalalang2
Copy link
Author

scalalang2 commented Sep 30, 2024

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.

@scalalang2
Copy link
Author

scalalang2 commented Sep 30, 2024

My Suggestion.

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

@scalalang2
Copy link
Author

scalalang2 commented Oct 1, 2024

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

@ReubenBond
Copy link
Member

@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?

@scalalang2
Copy link
Author

scalalang2 commented Oct 2, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants