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

ObjectResolver sometimes throws InvalidCastException #2315

Open
scottfavre opened this issue Jan 16, 2025 · 2 comments
Open

ObjectResolver sometimes throws InvalidCastException #2315

scottfavre opened this issue Jan 16, 2025 · 2 comments
Labels

Comments

@scottfavre
Copy link

Describe the bug

We sometimes see bizarre exceptions when trying to build custom class maps like this:

System.InvalidCastException: Unable to cast object of type 'CsvHelper.Configuration.MemberMap`2[SomeModelClass,System.Int32]' to type 'CsvHelper.Configuration.MemberMap`2[SomeOtherModelClass,System.DateOnly]'.

Once a process starts throwing these exceptions it seems to be permanent and the only fix is to kill the process.

To Reproduce
Intermittent thread safety issues are very difficult to reliably reproduce.

Expected behavior

ObjectResolver appears to have inconsistent locking in the default configuration.

The default instance uses an object local to the static constructor for locking:

static ObjectResolver()
{
var objectCreator = new ObjectCreator();
var locker = new object();
current = new ObjectResolver(type => true, (type, args) =>
{
lock (locker)
{
return objectCreator.CreateInstance(type, args);
}
});
}

But then later in the class objectCreator.CreateInstance is called without the locking:

public object Resolve(Type type, params object[] constructorArgs)
{
if (CanResolve(type))
{
return ResolveFunction(type, constructorArgs);
}
if (UseFallback)
{
return objectCreator.CreateInstance(type, constructorArgs);
}
throw new CsvHelperException($"Type '{type.FullName}' can't be resolved and fallback is turned off.");
}

And indeed ObjectCreator has a cache that is not thread safe:

private readonly Dictionary<int, Func<object[], object>> cache = new Dictionary<int, Func<object[], object>>();

Screenshots
n/a

Additional context
Our application often receives many requests in parallel so it is very possible for there to be multiple requests trying to build class maps for the first time at the same time.

@scottfavre scottfavre added the bug label Jan 16, 2025
@scottfavre scottfavre changed the title ObjectResolver is not threadsafe ObjectResolver sometimes throws InvalidCastException Jan 22, 2025
@scottfavre
Copy link
Author

I dug into trying to reproduce this today and after several maddening hours, I'm convinced that it is not a thread safety issue, since I was unable to write any code that hit the non-locked calls to ObjectCreator.

However, there is another possibility (which is even harder to reproduce, of course):

https://github.com/JoshClose/CsvHelper/blob/master/src/CsvHelper/ObjectCreator.cs#L65-L76

Here we're building the cache key with a hash code built up from the types of the target and its constructor arguments.

HashCode uses a random seed to make hashes different between runs:

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/HashCode.cs#L59

There is code to handle hash collisions in GetConstructorCacheKey, so it is possible that some processes use a seed that causes collisions.

This theory is consistent with:

  • the exception observed in the logs
  • that once a process is in this state it will always have the error
  • that it is very rare (we've only observed it a handful of times over many thousands of process lifecycles)
  • that it is very hard to reproduce (there's no good way to control or inspect the HashCode seed value)

@scottfavre
Copy link
Author

Simple testing with a trivial version of GetConstructorCacheKey that just returns a constant value reproduces the exception.

private static int GetConstructorCacheKey(Type type, Type[] args)
{
	return 1;
}

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

No branches or pull requests

1 participant