OrleansContrib/Orleans.Providers.MongoDB

Exceptions because the provider can't deserialize datetimes.

ErroneousFatality opened this issue · 8 comments

I'm using the latest version of this library from Nuget, v2.4: https://www.nuget.org/packages/Orleans.Providers.MongoDB/

How can I deserialize DateTimes properly?

What exception do you get?

Exception:

fail: Orleans.Runtime.ReminderService.LocalReminderService[102915]
      Could not deliver reminder tick for [RoundGrain_WakeMeUpInside, GrainReference:*grn/4D69D0BD/400625bf15bb63a8c712e9b338fbb18b030000004d69d0bd-0x53CDE135, 00:01:00, 2019-06-24 17:44:17.856 GMT, 5058680b-a0e4-4da1-9cfd-93dfa509d7e4, 36, Ticking], next 25.06.2019. 13.47.17.
Orleans.Runtime.OrleansException: Error from storage provider MongoGrainStorage.Grains.RoundGrain during ReadState for grain Type=Grains.RoundGrain Pk=*grn/4D69D0BD/400625bf15bb63a8c712e9b338fbb18b030000004d69d0bd-0x53CDE135 Id=GrainReference:*grn/4D69D0BD/38fbb18b Error=

Exc level 0: Newtonsoft.Json.JsonReaderException: Could not convert string to DateTime: 2019-06-24T19.43.17+02:00. Path 'RoundCreationTime'.
   at Newtonsoft.Json.JsonReader.ReadDateTimeString(String s)
   at Newtonsoft.Json.JsonReader.ReadAsDateTime()
   at Newtonsoft.Json.JsonReader.ReadForType(JsonContract contract, Boolean hasConverter)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Populate(JsonReader reader, Object target)
   at Newtonsoft.Json.JsonSerializer.PopulateInternal(JsonReader reader, Object target)
   at Orleans.Providers.MongoDB.StorageProviders.Serializers.JsonGrainStateSerializer.Deserialize(IGrainState grainState, JObject entityData)
   at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.<>c__DisplayClass16_0.<<ReadStateAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.<>c__DisplayClass20_0.<<DoAndLog>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.DoAndLog[T](String actionName, Func`1 action)
   at Orleans.Core.StateStorageBridge`1.ReadStateAsync() in D:\build\agent\_work\7\s\src\Orleans.Runtime\Storage\StateStorageBridge.cs:line 68 ---> Newtonsoft.Json.JsonReaderException: Could not convert string to DateTime: 2019-06-24T19.43.17+02:00. Path 'RoundCreationTime'.
   at Newtonsoft.Json.JsonReader.ReadDateTimeString(String s)
   at Newtonsoft.Json.JsonReader.ReadAsDateTime()
   at Newtonsoft.Json.JsonReader.ReadForType(JsonContract contract, Boolean hasConverter)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Populate(JsonReader reader, Object target)
   at Newtonsoft.Json.JsonSerializer.PopulateInternal(JsonReader reader, Object target)
   at Orleans.Providers.MongoDB.StorageProviders.Serializers.JsonGrainStateSerializer.Deserialize(IGrainState grainState, JObject entityData)
   at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.<>c__DisplayClass16_0.<<ReadStateAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.<>c__DisplayClass20_0.<<DoAndLog>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Orleans.Providers.MongoDB.StorageProviders.MongoGrainStorage.DoAndLog[T](String actionName, Func`1 action)
   at Orleans.Core.StateStorageBridge`1.ReadStateAsync() in D:\build\agent\_work\7\s\src\Orleans.Runtime\Storage\StateStorageBridge.cs:line 68
   --- End of inner exception stack trace ---
   at Orleans.Core.StateStorageBridge`1.ReadStateAsync() in D:\build\agent\_work\7\s\src\Orleans.Runtime\Storage\StateStorageBridge.cs:line 80
   at Orleans.LifecycleSubject.WrapExecution(CancellationToken ct, Func`2 action) in D:\build\agent\_work\7\s\src\Orleans.Core\Lifecycle\LifecycleSubject.cs:line 115
   at Orleans.LifecycleSubject.OnStart(CancellationToken ct) in D:\build\agent\_work\7\s\src\Orleans.Core\Lifecycle\LifecycleSubject.cs:line 55
   at Orleans.Runtime.Catalog.CallGrainActivate(ActivationData activation, Dictionary`2 requestContextData) in D:\build\agent\_work\7\s\src\Orleans.Runtime\Catalog\Catalog.cs:line 1170
   at Orleans.Runtime.Scheduler.AsyncClosureWorkItem.Execute() in D:\build\agent\_work\7\s\src\Orleans.Runtime\Scheduler\ClosureWorkItem.cs:line 63
   at Orleans.Runtime.Catalog.InitActivation(ActivationData activation, String grainType, String genericArguments, Dictionary`2 requestContextData) in D:\build\agent\_work\7\s\src\Orleans.Runtime\Catalog\Catalog.cs:line 556
   at Orleans.Runtime.ReminderService.LocalReminderService.LocalReminderData.OnTimerTick(AverageTimeSpanStatistic tardinessStat, ILogger Logger) in D:\build\agent\_work\7\s\src\Orleans.Runtime\ReminderService\LocalReminderService.cs:line 583

State class:

public class RoundState
    {
        public IGameStrategy GameStrategy { get; set; }

        public RoundStatus Status { get; set; }
        public ICollection<ISlipGrain> RoundSlips { get; set; }
        public ICollection<int> RoundNumbers { get; set; }
        
        public DateTime RoundCreationTime { get; set; }
        public DateTime SubmissionsEndTime { get; set; }
        public DateTime NumbersGenerationTime { get; set; }
        public DateTime FinalizationTime { get; set; }

        public Guid GameStreamId { get; set; }
    }

Are you trying to use reminders? What is your setup?

Yes I am using reminders.
I'm not sure which setup you are refering to.

In the example have a look at INewsReminderGrain and confirm you are doing something similar.

Yeah, my RoundGrains (max 20 active at a time) register a Reminder on a 1minute interval to reactivate them, in case they get deactivated, because they have some Timer logic that needs to be done in their 20min lifecycle.

@SebastianStehle Any thoughts, please? I am not that accustomed to reminders.

We ran into DateTime serialization/deserialization problem as well. The culprit in our case seems to be MongoGrainStorage.ToBson (public static BsonValue ToBson(this JToken source))
The case for datetime looks like this:

case JTokenType.Date:
  object obj = ((JValue) source).Value;
  switch (obj)
  {
	case DateTime dateTime:
	  return (BsonValue) dateTime.ToString("yyyy-MM-ddTHH:mm:ssK");
	case DateTimeOffset dateTimeOffset:
	  return (BsonValue) dateTimeOffset.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK");
	default:
	  return (BsonValue) obj.ToString();
  }

The ToString calls do not specify the format provider therefore the time separator (:) will get replaced by the current culture's DateTimeFormatInfo.TimeSeparator. This cannot be configured even though the silo host builder configuration extension method would suggest this to be the case:

.AddMongoDBGrainStorage("YourStoreName", options =>
	{
		options.ConfigureJsonSerializerSettings = settings => settings.Culture = CultureInfo.InvariantCulture;
		options.DatabaseName = "YourDatabaseName";
		options.CollectionPrefix = "YourPrefgix";
	})

Setting the culture has no effect on serializing DateTime values.