JsConfig.Reset() inconsistencies

We had some strange issues with a test that was failing with serialization errors once in a while. It would fail only when multiple tests where run, when run stand-alone it would never fail.
I spend some time hunting this down, and it turns out to ultimately be the result of JsConfig.Reset() not always resetting static state consistently. (We are stopping and starting AppHost, and the apphost Dispose() calls JsConfig.Reset()).

What happens is that JsConfig.Reset() loops through types it has seen to reset the config for each of these types in the order it has seen them. The reset triggers JsonReader<T>.Refresh() to recreate itā€™s ReadFn. The first problem is that this happens while the setup for the other types are not all reset yet, so it may still pick up some customization for underlying properties which are not reset yet. The first problem is that it creates the ReadFn instead of just clearing it, which means later customization to underlying properties is not picked up.

All this being static it persist over multiple test in a test run which means that depending on test order a test may leave a ReadFn in an inconsistent state after it disposed an AppHost.

The following test demonstrates this behavior. It has two tests, which are expected to succeed, but fail. It also has a test which succeeds when run individually but fails when all tests are run.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using ServiceStack;
using ServiceStack.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace JsConfigTests
{
    public class SomeObject
    {
        public CustomField Value { get; set; } = new CustomField(new byte[0]);
    }

    public class CustomField
    {
        private readonly String val;

        public String Value
        {
            get { return val; }
        }
        
        public CustomField(byte[] val)
        {
            this.val = val.FromUtf8Bytes();
        }
    }
    
    [TestCaseOrderer("JsConfigTests.AlphabeticalOrderer", "JsConfigTests")]
    public class JsConfigTest
    {
        public static void SetupSerialization()
        {
            JsConfig.ThrowOnError = true;
            JsConfig<CustomField>.DeSerializeFn = x => new CustomField(Encoding.UTF8.GetBytes(x));
        }

        // This test should succeed, but doesn't because JsConfig.Reset() leaves an inconsistent state.
        // It breaks because the Reset() created a new ReadFn for SomeObject while the custom serializer for 
        // CustomField was still in place.
        [Fact]
        public void B_FailAfterReset()
        {
            // Test Json
            var str = "{\"value\":\"Test\"}";

            // Configure serialization
            SetupSerialization();

            // Prove we can read the thing correctly
            SomeObject obj;
            obj = str.FromJson<SomeObject>();
            Assert.NotNull(obj);
            Assert.Equal("Test", obj.Value.Value);

            // Dispose the AppHost, this will clear custom serialization functions and call JsConfig<T>.Refresh()
            JsConfig.Reset();
            JsConfig.ThrowOnError = true;

            // Because custom serialization is cleared we expect serialization to now fail.
            Assert.Throws<SerializationException>(() => str.FromJson<SomeObject>());
        }
        
        // In this case the ReadFn for SomeObject is reset correctly due to different ordering. However, because a new 
        // ReadFn is created, instead of it just being cleared, subsequent custom serialization setup is not picked up,
        // because that only resets the ReadFn for CustomField, not the one for SomeObject.
        [Fact]
        public void A_FailAfterSecondSetup()
        {
            // Test Json
            var str = "{\"value\":\"Test\"}";

            // Configure serialization
            SetupSerialization();

            // Make sure to hit CustomField first, this ensures it's also reset first.
            var field = "LALA".FromJson<CustomField>();
            Assert.NotNull(field);
            Assert.Equal("LALA", field.Value);
            
            // Prove we can read the thing correctly
            SomeObject obj;
            obj = str.FromJson<SomeObject>();
            Assert.NotNull(obj);
            Assert.Equal("Test", obj.Value.Value);

            // Dispose the AppHost, this will clear custom serialization functions and call JsConfig<T>.Refresh()
            JsConfig.Reset();
            JsConfig.ThrowOnError = true;

            // Because custom serialization is cleared we expect
            Assert.Throws<SerializationException>(() => str.FromJson<SomeObject>());

            // Now we set the custom deserialize function again, expecting serialization to work again.
            SetupSerialization();
            
            // But it doesn't work. Setting the deserialization function refreshes the config for Custom, but 
            // the config for Thing still has the default function.
            obj = str.FromJson<SomeObject>();
            Assert.NotNull(obj);
            Assert.Equal("Test", obj.Value.Value);
        }

        // This is the most basic test, it runs fine when run stand-alone. But when running multiple tests it fails
        // when a previous test leaves an incorrect ReadFn for SomeObject.
        [Fact]
        public void C_Succeed()
        {
            // Test Json
            var str = "{\"value\":\"Test\"}";

            // Configure serialization
            SetupSerialization();

            // Prove we can read the thing correctly
            SomeObject obj;
            obj = str.FromJson<SomeObject>();
            Assert.NotNull(obj);
            Assert.Equal("Test", obj.Value.Value);
        }
    }

    public class AlphabeticalOrderer : ITestCaseOrderer
    {
        public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
        {
            return testCases.OrderBy(x => x.DisplayName);
        }
    }
}

Solving this may not be trivial, because the statics canā€™t be deleted to completely reset things. I think somehow invalidating ReadFn in JsonReader<T> instead of recreating it would solve the error scenarioā€™s Iā€™ve seen, but Iā€™m not sure if that would create new issues.

P.s. At least for us this only affects testing, not production, and Iā€™ve got a workaround for that already. So itā€™s not really urgent for us.

JsConfig.Reset() can only reset static field caches, i.e. it canā€™t reset static type fields generic type initializers which are only fired once per App Domain. But this API shouldnā€™t be used at runtime so itā€™s only useful for tests, if you do run into an issue with dirty state that canā€™t be reset between tests use a different Type to isolate it.

Using a different type is not really an option (it happens on NodaTime types we use). We also use the same AppHost implementation, which we Dispose and restart between tests specifically to ensure stuff works within the config of that AppHost.

But looking at this: https://github.com/ServiceStack/ServiceStack.Text/blob/d4239f7c133ab2c2bbbd7bd75c3b5b5c26b67307/src/ServiceStack.Text/Json/JsonReader.Generic.cs#L74

It seems to me (but I havenā€™t tested it yet) that setting ReadFn to null there so it lazily gets created once needed would fix this. Would you consider a change like that? If so, I can see if I can get that to workā€¦

Itā€™s not ThreadSafe to assign it outside of a static initializer at runtime, it would need to be a different API. First try using a local modified copy of ServiceStack.Text to find out what additional APIs youā€™d need and Iā€™ll look at adding them if they donā€™t impact the default runtime behavior.

Ok, I see the problem with thread safety. Iā€™ll take a look and see if I can come up with something.

I found a case where JsConfig.Reset() misses a a type when it only has a custom DeserializeFn set. In this case that type doesnā€™t get reset. I created a test for that scenario here:

In the same commit (here) I fix that by adding everything that Json/JsvReader/Writer sees to the unique types list. That fixes the test, but itā€™s a rather blunt fix, thereā€™s probably a better way to fix it.

I think this bug put me on the wrong path a bit, as I was testing with just a DeserializeFn. When that failed I assumed it had to do with the order in which JsConfig.Reset() handled different types, but that seems incorrect. With this one bug fixed I can no longer find a scenario where JsConfig.Reset() does leave previous customizations in place.

1 Like

cool can you submit a Pull Request for this and Iā€™ll merge + deploy.

Sure:

FYI if needed this change is now available from v5.12.1 thatā€™s now available on MyGet.

Ok, Iā€™ll check it out.
Thanks!