You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Task.Run(...).GetAwaiter().GetResult() sync-over-async added in #4613 (shipped in 16.1.1) does not eliminate the license-validation deadlock. Under a cold-start thread-pool-starvation scenario it makes the deadlock more likely, because the validation now needs a free pool thread to complete while the calling thread blocks on it under the DI singleton-build lock. We captured a full production memory dump on 16.1.1 showing the whole app convoyed behind that one lock with CPU idle.
#4612 reported AutoMapper.dll getting locked during Web Deploy on 16.0.0. You traced it to sync-over-async in LicenseAccessor and switched to Task.Run in #4613, shipped in 16.1.1. This issue is that the Task.Run change reduced the problem but didn't eliminate it. Since #4612 is closed and locked, I'm filing a new one for the same root cause.
The current 16.1.1 implementation in src/AutoMapper/Licensing/LicenseAccessor.cs:
with Current => _license ??= Initialize(); and Initialize() calling ValidateKey during the first MapperConfiguration construction.
Expected behavior
A cold-starting instance with a valid commercial license takes traffic without wedging, regardless of how much concurrent load arrives before the MapperConfiguration singleton is built.
Actual behavior
Under a deploy that cold-starts every instance simultaneously into immediate live traffic, all instances wedge at once: CPU sits idle (~12%) while every request thread is parked, waiting on the singleton-build lock held by the one thread blocked inside ValidateKey. The instances only recover after a long delay (slow pool thread injection) or an instance recycle.
Why Task.Run(...).GetResult() still deadlocks under pool starvation
Every inbound request resolves IMapper, forcing the lazy singleton build of MapperConfiguration on first hit under Lamar's singleton-build lock.
That build calls LicenseAccessor.ValidateKey, which does Task.Run(() => handler.ValidateTokenAsync(...)).GetAwaiter().GetResult().
Task.Runqueues the validation work onto the thread pool and .GetResult()blocks the calling thread until that work item completes, all while the calling thread holds the singleton-build lock.
During cold start the pool is already saturated by the inbound request burst (every request is parked needing IMapper), with no idle threads and only slow hill-climbing thread injection. The queued Task.Run work item therefore can't be scheduled.
The lock holder blocks indefinitely, and every other request convoys behind the singleton lock. The whole process stalls.
The Task.Run form is actually worse than a plain in-place .GetResult() here. It introduces a dependency on pool-thread availability for the very work whose result is blocking a pool thread, which is a thread-pool-starvation deadlock. The blocking call sits under a process-wide singleton lock, so it takes the whole app down rather than one request.
Stack trace (verbatim from clrstack, lock holder, top-down)
One thread is building the singleton (blocked in ValidateKey), and 524 are waiting on its lock.
Requested fix
License validation appears to be local JWT validation with no network I/O, so it shouldn't need to be async at all on this path. Any of these would address it:
Use the synchronous validation path.JsonWebTokenHandler has a synchronous ValidateToken. You noted in AutoMapper 16.0.0 – Web Deploy sometimes fails with locked AutoMapper.dll #4612 it's marked obsolete, but for local-only validation a synchronous call removes the sync-over-async hazard entirely and is preferable to Task.Run(...).GetResult(), which can't make forward progress under pool starvation.
Move validation off the construction / singleton-lock path. Validate lazily or deferred so it never runs the blocking wait while holding the DI singleton-build lock.
Document this footgun and a recommended warm-up. Forcing an eager build is already possible (resolve IMapper at startup before the app takes traffic, which is the workaround we used). The gap is guidance: nothing warns that the default lazy-singleton build runs license validation under the DI container's build lock, or recommends warming it on startup when you're on a lazy-resolving container like Lamar. A short note in the licensing/registration docs would save the next team this dump.
On reproduction
This is a cold-start + immediate-load race: it does not reproduce when the singleton is built quietly with pool threads to spare, so we don't have a minimal deterministic Gist. What we do have is the full 16.1.1 production dump above, which pins the mechanism. I can share more from the dump (SOS output, the full clrstack -all). And if you put a candidate fix in a pre-release, we can run it in our environment and report back whether the cold-start wedge still happens, since our deploys reproduce it reliably.
TL;DR
The
Task.Run(...).GetAwaiter().GetResult()sync-over-async added in #4613 (shipped in 16.1.1) does not eliminate the license-validation deadlock. Under a cold-start thread-pool-starvation scenario it makes the deadlock more likely, because the validation now needs a free pool thread to complete while the calling thread blocks on it under the DI singleton-build lock. We captured a full production memory dump on 16.1.1 showing the whole app convoyed behind that one lock with CPU idle.Environment
cfg.LicenseKeyw3wp)IMapper/MapperConfigurationregistered as singletons built lazily on first resolveMapperConfigurationconstructionRelationship to #4612 / #4613
#4612 reported
AutoMapper.dllgetting locked during Web Deploy on 16.0.0. You traced it to sync-over-async inLicenseAccessorand switched toTask.Runin #4613, shipped in 16.1.1. This issue is that theTask.Runchange reduced the problem but didn't eliminate it. Since #4612 is closed and locked, I'm filing a new one for the same root cause.The current 16.1.1 implementation in
src/AutoMapper/Licensing/LicenseAccessor.cs:with
Current => _license ??= Initialize();andInitialize()callingValidateKeyduring the firstMapperConfigurationconstruction.Expected behavior
A cold-starting instance with a valid commercial license takes traffic without wedging, regardless of how much concurrent load arrives before the
MapperConfigurationsingleton is built.Actual behavior
Under a deploy that cold-starts every instance simultaneously into immediate live traffic, all instances wedge at once: CPU sits idle (~12%) while every request thread is parked, waiting on the singleton-build lock held by the one thread blocked inside
ValidateKey. The instances only recover after a long delay (slow pool thread injection) or an instance recycle.Why
Task.Run(...).GetResult()still deadlocks under pool starvationIMapper, forcing the lazy singleton build ofMapperConfigurationon first hit under Lamar's singleton-build lock.LicenseAccessor.ValidateKey, which doesTask.Run(() => handler.ValidateTokenAsync(...)).GetAwaiter().GetResult().Task.Runqueues the validation work onto the thread pool and.GetResult()blocks the calling thread until that work item completes, all while the calling thread holds the singleton-build lock.IMapper), with no idle threads and only slow hill-climbing thread injection. The queuedTask.Runwork item therefore can't be scheduled.The
Task.Runform is actually worse than a plain in-place.GetResult()here. It introduces a dependency on pool-thread availability for the very work whose result is blocking a pool thread, which is a thread-pool-starvation deadlock. The blocking call sits under a process-wide singleton lock, so it takes the whole app down rather than one request.Stack trace (verbatim from
clrstack, lock holder, top-down)The other ~524 threads are all blocked one level up in
Lamar...SingletonResolver.Resolve/Lamar.IoC.Scope.GetInstance, waiting on the same lock.Dump evidence (1.4 GB
w3wpfull dump, 16.1.1)Lamar...SingletonResolver: 525Lamar.IoC.Scope.GetInstance: 525<AddAutoMapperClasses>lambda: 526LicenseAccessor: 3,ValidateKey: 1,Monitor.Wait: 4One thread is building the singleton (blocked in
ValidateKey), and 524 are waiting on its lock.Requested fix
License validation appears to be local JWT validation with no network I/O, so it shouldn't need to be async at all on this path. Any of these would address it:
JsonWebTokenHandlerhas a synchronousValidateToken. You noted in AutoMapper 16.0.0 – Web Deploy sometimes fails with locked AutoMapper.dll #4612 it's marked obsolete, but for local-only validation a synchronous call removes the sync-over-async hazard entirely and is preferable toTask.Run(...).GetResult(), which can't make forward progress under pool starvation.IMapperat startup before the app takes traffic, which is the workaround we used). The gap is guidance: nothing warns that the default lazy-singleton build runs license validation under the DI container's build lock, or recommends warming it on startup when you're on a lazy-resolving container like Lamar. A short note in the licensing/registration docs would save the next team this dump.On reproduction
This is a cold-start + immediate-load race: it does not reproduce when the singleton is built quietly with pool threads to spare, so we don't have a minimal deterministic Gist. What we do have is the full 16.1.1 production dump above, which pins the mechanism. I can share more from the dump (SOS output, the full
clrstack -all). And if you put a candidate fix in a pre-release, we can run it in our environment and report back whether the cold-start wedge still happens, since our deploys reproduce it reliably.