Make GenericScope proxy locking conditional on ScopeCache - #1730
Open
akenra wants to merge 1 commit into
Open
Conversation
Scoped proxies created by GenericScope guard every invocation with a read lock keyed by the bean name, and destruction takes the matching write lock. Those locks are shared by all instances using the same GenericScope even when the configured ScopeCache confines instances to a single thread, causing heavy contention for thread-local scopes (spring-cloudgh-630). Introduce ScopeCache.requiresLocking() (default true), have ThreadLocalScopeCache return false, and skip proxy locking when the cache does not require it. Also replace the bean-name String monitor inside BeanLifecycleWrapper with the wrapper instance so independent scope instances no longer contend on shared strings during lazy creation and destruction. Default behavior for StandardScopeCache and RefreshScope is unchanged. Fixes spring-cloudgh-630 Signed-off-by: akenra <37288280+akenra@users.noreply.github.com>
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.
Context
GenericScopeguards every proxied invocation with a read lock keyed by the bean name, anddestroy()takes the matching write lock. Those locks live in a single map on the scope instance, so all scope instances sharing oneGenericScopecontend - even when the configuredScopeCacheconfines instances to a single thread (e.g.ThreadLocalScopeCache). For message-listener-style usage (one thread-local scope per consumer thread), a single instance's teardown write-locks the bean name for every thread.The analysis in #630 was endorsed by @dsyer back in 2020:
Six years later - here is that pull request. Nothing was ever merged in between; the locking code is unchanged since the original 2017 commit referenced in the issue.
What this PR does
ScopeCache.requiresLocking()(default boolean, returnstrue) documenting the contract: caches whose instances are confined to a single thread returnfalse. Concurrency semantics belong to the cache implementation - the reporter's instinct - without contorting the storage-only contract into an invocation-guard.ThreadLocalScopeCacheoverrides it tofalse, andGenericScope'sLockedScopedProxyFactoryBeanskips read-lock acquisition entirely when the cache does not require locking. The UndeclaredThrowableException when using @RefreshScope in @Service with checked Exceptions #349UndeclaredThrowableExceptionunwrap is preserved on both paths; destruction-side write locks are intentionally left untouched (uncontended when no readers exist, keeps the diff minimal).BeanLifecycleWrappersynchronized onthis.name- a shared, typically-interned String - so independent scope instances creating same-named beans serialized against each other during lazy creation and destruction. The monitor is now the wrapper instance itself, which is exactly the state being guarded. Single-wrapper-per-name-per-cache holds via the existingputIfAbsentsemantics of both in-tree caches.Backward compatibility
StandardScopeCache(and thereforeRefreshScope) never overriderequiresLocking(), so every refresh path keeps identical locking. All existingRefreshScope*Testspass unmodified.ScopeCacheimplementations.setScopeCache(...)with a non-default cache today; the affected surface is custom scopes like the reporter's.Testing
GenericScopeLockingTests(first direct test coverage this area has had): contract defaults, proxy invocation consults locks forStandardScopeCache/ skips forThreadLocalScopeCache, and cross-instance creation of same-named beans proceeds without blocking. Tests went red before the fix (compile gap + behavioral failures on the oldGenericScope) and green after.spring-cloud-contextsuite: 232 tests, no failures from this change. (One pre-existing, unrelated error inEncryptorFactoryTests.testWithRsaPrivateKeyreproduces identically on cleanmain; it is environment-related PEM parsing and untouched here.)Fixes #630
Notes for reviewers
BeanLifecycleWrappermonitor-granularity fix into a separate commit/PR if preferred.callbackfield written outside synchronization, and a potential NPE indestroy(name)if the lock map lacks an entry.