OrleansContrib/Orleans.Providers.MongoDB

Deserialization smell

bboyle1234 opened this issue · 1 comments

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()

"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);
        }
    }