Run license validation off the construction path (fixes #4640)#4645
Open
jbogard wants to merge 1 commit into
Open
Run license validation off the construction path (fixes #4640)#4645jbogard wants to merge 1 commit into
jbogard wants to merge 1 commit into
Conversation
MapperConfiguration validated the license key synchronously in its constructor via Task.Run(() => handler.ValidateTokenAsync(...)).GetResult(). Under a lazily-built DI singleton (e.g. Lamar) that runs while the container holds its singleton-build lock, and Task.Run needs a free thread-pool thread to complete. During a cold start that takes immediate traffic the pool is saturated (every request thread is parked waiting on that same lock), so the queued validation work can never be scheduled — the lock holder blocks forever and the whole app convoys behind it. A production dump on 16.1.1 in #4640 shows exactly this: one lock owner, 524 waiters, CPU idle. The validated license is logging-only: LicenseValidator.Validate just emits log messages and gates no mapping behavior, and the result has exactly one reader (this call). So validation need not be synchronous and can move off the construction path entirely: - MapperConfiguration: offload validation+logging to a dedicated LongRunning thread (Task.Factory.StartNew + TaskScheduler.Default) and return immediately. The constructor never blocks under the DI lock, so it can't deadlock regardless of pool state. The body is wrapped in try/catch so a faulted fire-and-forget task can't surface as an unobserved exception. - LicenseAccessor.ValidateKey: drop the Task.Run wrapper now that validation always runs on that dedicated background thread (no SynchronizationContext, and local JWT validation completes synchronously, so no pool dependency). Adds a regression test asserting validation logs on a different thread than the constructor's (fails on the old synchronous behavior, passes here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a startup deadlock scenario (#4640) caused by synchronous license validation during MapperConfiguration construction under DI singleton-build locks, by moving license validation/logging off the construction path.
Changes:
- Offloads license validation from
MapperConfigurationconstruction to a backgroundLongRunningtask. - Simplifies
LicenseAccessor.ValidateKeyby removing theTask.Run(...).GetResult()wrapper. - Adds a regression test asserting that license validation logging occurs on a different thread than the constructor.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/AutoMapper/Configuration/MapperConfiguration.cs | Starts license validation on a dedicated background task instead of running synchronously during configuration construction. |
| src/AutoMapper/Licensing/LicenseAccessor.cs | Removes Task.Run wrapper around JWT validation now that validation is performed off the construction path. |
| src/UnitTests/Licensing/LicenseValidationBackgroundTests.cs | Adds a regression test ensuring license validation logs are emitted from a non-construction thread. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+18
to
+20
| var provider = new ThreadCapturingLoggerProvider(); | ||
| var factory = new LoggerFactory(); | ||
| factory.AddProvider(provider); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4640 — license validation can deadlock the whole app under cold-start thread-pool starvation.
Problem
MapperConfiguration's constructor validated the license key synchronously:Under a DI container that builds
IMapper/MapperConfigurationas a lazy singleton (e.g. Lamar), that construction runs while the container holds its singleton-build lock.Task.Runqueues work to the thread pool and.GetResult()blocks the calling thread until it completes. During a cold start that takes immediate traffic, the pool is saturated — every request thread is parked waiting on that same singleton lock — so the queued validation work can never be scheduled. The lock holder blocks forever and the whole process convoys behind it. The 16.1.1 production dump in #4640 pins it exactly: 1 lock owner, 524 waiters, CPU idle (~12%).This is a follow-up to #4612 / #4613. The earlier
.Result→Task.Run(...).GetResult()change dodged theSynchronizationContextdeadlock but requires a free pool thread — the new failure mode. The synchronousJsonWebTokenHandler.ValidateTokenis, in 8.x, itselfValidateTokenAsync(...).ConfigureAwait(false).GetAwaiter().GetResult()(same hazard) and is[Obsolete], so it's not a fix either.Key insight
The validated license is logging-only —
LicenseValidator.Validatejust emits log messages and gates no mapping behavior, and the result has exactly one reader (that one call). So validation needn't be synchronous and can move off the construction path entirely.Fix
MapperConfiguration— offload validation + logging to a dedicatedLongRunningthread (Task.Factory.StartNew+TaskScheduler.Default) and return immediately. The constructor never blocks under the DI build lock, so it can't deadlock regardless of pool state. The body is wrapped in try/catch so a faulted fire-and-forget task can't surface as an unobserved exception.LicenseAccessor.ValidateKey— drop theTask.Runwrapper now that validation always runs on that dedicated background thread (noSynchronizationContext, and local JWT validation completes synchronously, so no pool dependency).Behavior change
License log lines (valid / missing / expired) now appear shortly after startup rather than synchronously during the first config build. A process that builds a config and exits immediately may not emit them. No functional impact — mapping never depends on the license.
Tests
LicenseValidationBackgroundTestsasserts validation logs on a different thread than the constructor's. Verified it fails on the old synchronous behavior and passes here.TreatWarningsAsErrors.Follow-up (not in this PR)
MediatR ships the same
LicenseAccessorsync-over-async validation and almost certainly has the identical deadlock — to be ported separately.🤖 Generated with Claude Code