Skip to content

HttpClientFactory prevent throwing if ILogger couldn't be created#90503

Merged
CarnaViire merged 6 commits into
dotnet:mainfrom
CarnaViire:hcf-stop-timer
Aug 14, 2023
Merged

HttpClientFactory prevent throwing if ILogger couldn't be created#90503
CarnaViire merged 6 commits into
dotnet:mainfrom
CarnaViire:hcf-stop-timer

Conversation

@CarnaViire

Copy link
Copy Markdown
Member

Fixes #90386

@ghost

ghost commented Aug 13, 2023

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #90386

Author: CarnaViire
Assignees: -
Labels:

area-Extensions-HttpClientFactory

Milestone: -

Comment thread src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs Outdated
Comment thread src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs Outdated
{
logger = loggerLazy.Value;
}
catch { } // not throwing in logs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This eats all exceptions? What was being done before?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before, the ILogger was created during DefaultHttpClientFactory constructor, so if any exception happens, DefaultHttpClientFactory instance was not created at all. Now, the instance is already up and running.

My understanding is that logging should never throw (especially given that the logging could be turned off for this logger category in the settings... but it will be applied only within the logging infra). The only time it could throw is somewhere around the startup when everything gets created (how it was before). Unfortunately, due to circular dependency (in e.g. OTel) we cannot do this in constructor anymore (see #89032).

My initial attempt to fix this was to go away from ILogger and substitute it with EventSource, but this would be a breaking change (even though these logs seem to be internal diagnostic debug logs).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(now I believe it is also needed in case the factory was never disposed...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should only eat the documented exceptions for Lazy<T>.Value which are:

I'm not sure any of those would actually throw, so perhaps the only likely exception would be null access?

@CarnaViire CarnaViire Aug 14, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual reported one is ObjectDisposedException; though, it could be any exception thrown by e.g. a 3rd party ILoggerProvider/ILoggerFactory/failing to create ILogger instance because of some other problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me then that we should only eat ObjectDisposedException, as that's the one we know is safe to be eaten.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically I can only swallow ObjectDisposedException, that would fix the reported issue. But if we ever stumble upon some other ILogger creation exception, this will become a regression, since it would crash the process where it was not crashing before.

@CarnaViire CarnaViire Aug 14, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I meant not that the current PR would introduce a regression, but #89531 already did)

Comment thread src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs Outdated
@CarnaViire

Copy link
Copy Markdown
Member Author

PTAL @stephentoub

_expiredHandlers.Enqueue(expired);

Log.HandlerExpired(_logger.Value, active.Name, active.Lifetime);
Log.HandlerExpired(_logger, active.Name, active.Lifetime);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear on why making the handler implement IDisposable impacts where we initialize the logger. Can you elaborate on that?

@CarnaViire CarnaViire Aug 14, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not handler, but the factory... So the story is:

When the application is shutting down, ServiceProvider gets disposed. (It will also dispose all IDisposable singletons alongside).

Now, the Logger has lazy initialization (since #89032), that would happen on the first access to it. The first access happens when (upd.: this is when I realize I've been stopping the wrong timer - it should have been expiryTimer, not cleanupTimer 🤦‍♀️) ExpiryTimer_Tick is being called the first time, meaning the first of created handlers has just expired (ExpiryTimer_Tick is called from a timer attached to the cached HttpMessageHandler instance).

So if the application is shutting down around the handler expiry, what happened was that:

  1. The ServiceProvider gets disposed by the app
  2. The expiry callback still fires, because no one has stopped it, and tries to log
  3. It accesses Lazy, which accesses ServiceProvider to get ILoggerFactory from it
  4. ServiceProvider throws ObjectDisposedException

Now re me realizing it was the wrong timer. Making the factory disposable theoretically would still make the behavior a little bit better, because if _dispoised is set already, ExpiryTimer_Tick will just return. But the proper fix would be to go over all cached handlers and stop their respective timers.

Given the time left before ZBB, I am considering to actually remove all of the changes and leave only swallowing exceptions on ILogger creation. All the timers would still fire, but they were firing before.

Now, I can just only ever swallow ObjectDisposedException, that would fix the reported issue. But if we ever stumble upon some other ILogger creation exception, this will become a regression, since it would crash the process where it was not crashing before.

Thoughts @stephentoub ?

@CarnaViire CarnaViire changed the title HttpClientFactory stop cleanup timer on dispose HttpClientFactory prevent throwing if ILogger couldn't be created Aug 14, 2023
{
logger = loggerLazy.Value;
}
catch { } // not throwing in logs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume at this point there's nowhere we can reasonably log the exception, since it was the retrieval of the logger in the first place that failed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly :(

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ObjectDisposedException intermittently crashes test process from DefaultHttpClientFactory.ExpiryTimer_Tick

6 participants