Deserialization smell
bboyle1234 opened this issue · 1 comments
bboyle1234 commented
At the moment I'm investigating this code and exception hit during deserialization during grain re-activation.
public interface IResourceLockGrain : IGrainWithStringKey {
Task<ResourceLock> GetResourseLock(string key, string requesterInformation, TimeStamp expiry);
}
public class ResourceLock {
public string Key;
public string RequesterInformation;
public TimeStamp Expiry;
}
[Serializable]
public class ResourceLockedException : Exception {
public readonly ResourceLock ResourceLock;
public ResourceLockedException(ResourceLock resourceLock) : base($"Resource '{resourceLock.Key}' is already locked by '{resourceLock.RequesterInformation}' until '{resourceLock.Expiry.AsUtc():yyyy-MM-dd HH:mm:ss}'.") {
ResourceLock = resourceLock;
}
protected ResourceLockedException(SerializationInfo info, StreamingContext context) : base(info, context) {
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
if (info == null) throw new ArgumentNullException("info");
info.AddValue(nameof(ResourceLock), ResourceLock);
base.GetObjectData(info, context);
}
}
public class ResourceLockGrain : Grain<Dictionary<string, ResourceLock>>, IResourceLockGrain {
public override async Task OnActivateAsync() {
await Cleanup();
await base.OnActivateAsync();
RegisterTimer(_ => Cleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
async Task Cleanup() {
var now = TimeStamp.Now;
var keysToRemove = State.Where(kvp => kvp.Value.Expiry < now).Select(kvp => kvp.Key).ToList();
if (keysToRemove.Count > 0) {
foreach (var key in keysToRemove)
State.Remove(key);
await WriteStateAsync();
}
}
public async Task<ResourceLock> GetResourseLock(string key, string requesterInformation, TimeStamp expiry) {
if (State.TryGetValue(key, out var resourceLock)) {
if (resourceLock.Expiry > TimeStamp.Now)
throw new ResourceLockedException(resourceLock);
return resourceLock;
}
resourceLock = new ResourceLock {
Key = key,
RequesterInformation = requesterInformation,
Expiry = expiry,
};
State[key] = resourceLock;
await WriteStateAsync();
return resourceLock;
}
}
Mongo storage:
{
"_id": "GrainReference=0000000000000000000000000000000006000000025df238+NadexAccountListViewGrain",
"_etag": "87219c32-1406-4cde-996c-00094b661675",
"_doc": {
"__id": "324",
"__type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[Apex.Grains.ResourceLock, Apex.Grains]], System.Private.CoreLib"
}
}
Exception:
Orleans.Runtime.OrleansException: Error from storage provider MongoGrainStorage.Apex.Grains.ResourceLockGrain during ReadState for grain Type=Apex.Grains.ResourceLockGrain Pk=*grn/25DF238/0000000000000000000000000000000006000000025df238+NadexAccountListViewGrain-0x7607CBC0 Id=GrainReference:*grn/25DF238/00000000+NadexAccountListViewGrain Error=
Newtonsoft.Json.JsonSerializationException: Error converting value "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[Apex.Grains.ResourceLock, Apex.Grains]], System.Private.CoreLib" to type 'Apex.Grains.ResourceLock'. Path '$type'.
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Populate(JsonReader reader, Object target)
at Newtonsoft.Json.JsonSerializer.PopulateInternal(JsonReader reader, Object target)
at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.<>c__DisplayClass16_0.<<ReadStateAsync>b__0>d.MoveNext()
bboyle1234 commented
"Fixed" the issue by replacing the grain code as follows. The grain state type is now a wrapper around the original dictionary.
public class ResourceLockGrain : Grain<ResourceLockGrain.MyState>, IResourceLockGrain {
public override async Task OnActivateAsync() {
await Cleanup();
await base.OnActivateAsync();
RegisterTimer(_ => Cleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
async Task Cleanup() {
var now = TimeStamp.Now;
var keysToRemove = State.Locks.Where(kvp => kvp.Value.Expiry < now).Select(kvp => kvp.Key).ToList();
if (keysToRemove.Count > 0) {
foreach (var key in keysToRemove)
State.Locks.Remove(key);
await WriteStateAsync();
}
}
public async Task<ResourceLock> GetResourceLock(string key, string requesterInformation, TimeStamp expiry) {
if (State.Locks.TryGetValue(key, out var resourceLock)) {
if (resourceLock.Expiry > TimeStamp.Now)
throw new ResourceLockedException(resourceLock);
return resourceLock;
}
resourceLock = new ResourceLock {
Key = key,
RequesterInformation = requesterInformation,
Expiry = expiry,
};
State.Locks[key] = resourceLock;
await WriteStateAsync();
return resourceLock;
}
public class MyState {
public Dictionary<string, ResourceLock> Locks = new Dictionary<string, ResourceLock>();
}
}
I'm using custom serializer that was later merged into the mongodb code base:
public class ApexGrainStateSerializer : IGrainStateSerializer {
private readonly JsonSerializer serializer;
public ApexGrainStateSerializer(ITypeResolver typeResolver, IGrainFactory grainFactory)
: this(JsonSerializer.Create(OrleansJsonSerializer.GetDefaultSerializerSettings(typeResolver, grainFactory))) {
}
protected ApexGrainStateSerializer(JsonSerializer serializer) {
serializer.NullValueHandling = NullValueHandling.Include;
serializer.DefaultValueHandling = DefaultValueHandling.Populate;
this.serializer = serializer;
}
public void Deserialize(IGrainState grainState, JObject entityData) {
var jsonReader = new JTokenReader(entityData);
serializer.Populate(jsonReader, grainState.State);
}
public JObject Serialize(IGrainState grainState) {
return JObject.FromObject(grainState.State, serializer);
}
}