Repository navigation
AOP proxies: release targets and pop the exposed proxy when the call throws - #391
Merged
Merged
Conversation
…throws TargetAopProxyMethodBuilder emitted GetTarget ... ReleaseTarget without a try/finally, so a throwing advised method leaked pooled targets (the pool eventually ran dry and blocked). The invocation is now wrapped in an IL try/finally. BaseCompositionAopProxy.Equals/GetHashCode/ToString get the same treatment in C#. AbstractAutoProxyCreator.CreateProxy borrowed a target it never used or released; the borrow is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGkLQuAuUjvRjrU1AcTsmk Signed-off-by: Marko Lahma <marko.lahma@gmail.com>
AbstractAopProxyMethodBuilder pushed the proxy on the AopContext stack when ExposeProxy was set and popped it at the end of the method, outside any finally, so every call that threw left a stale entry on the thread's stack. The method body now runs in an IL try whose finally pops the proxy. The push moves to the end of BeginMethod (after the local initialisation, before the try), and ExposeProxy is read once into a local so the pop mirrors the push. The target release (inner finally) runs before the pop (outer finally). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGkLQuAuUjvRjrU1AcTsmk Signed-off-by: Marko Lahma <marko.lahma@gmail.com>
…er docs) Add tests for the inheritance-based proxy with ExposeProxy and a throwing method, and for generic methods (get/release balance and AopContext pop) on composition and decorator proxies. Document the try/finally contract of GenerateMethod/GenerateMethodLogic/EndMethod, and that AbstractAopProxyTypeBuilder.PushTarget borrows without releasing. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGkLQuAuUjvRjrU1AcTsmk Signed-off-by: Marko Lahma <marko.lahma@gmail.com>
lahma
force-pushed
the
4x/aop-release-target
branch
from
November 3, 2026 22:49
0fc74cb to
3aadb40
Compare
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.
AOP proxies didn't always undo what they set up when the advised call threw. This fixes both cases.
Pooled and borrowed targets.
TargetAopProxyMethodBuilder, used by both composition and decorator proxies) calledReleaseTargetwithout afinally. A throwing call left aSimplePoolTargetSourceobject checked out, so a small pool ran dry and blocked.try { … } finally { TargetSource.ReleaseTarget(target) }.ref/outresults go through locals, and the singleretcomes after the block.BaseCompositionAopProxy.Equals,GetHashCodeandToStringborrowed and released the target the same way. They now usetry/finally.AbstractAutoProxyCreator.CreateProxycalledtargetSource.GetTarget()into a local that nothing read. For a pooled source that object was never returned. For a prototype or lazy source it created or initialised a target too early. The borrow is gone.The exposed proxy.
ExposeProxyset,BeginMethodpushed the proxy onto the thread'sAopContextstack andEndMethodpopped it, again without afinally. Each throwing call left one entry behind, soAopContext.CurrentProxycould return the wrong proxy.GenerateMethod's outerfinallyfor the composition, decorator, inheritance and introduction builders. The push happens just before thetry.ExposeProxyis read once per call instead of twice, so the pop always matches the push.finallyblocks unwind in reverse order of acquisition: the target is released first, then the proxy is popped.Compatibility. Public signatures don't change, but the protected contract of the public IL builders does, for anyone who writes their own:
GenerateMethodLogicnow runs inside protected regions. It must leave the stack empty and not emitret.EndMethodno longer pops.The XML docs state this. There is no BreakingChanges entry, because the change only affects custom IL builders. The happy path costs the same or less per call.
Tests.
ReleaseTargetTestscovers composition and decorator proxies, each with and without an interceptor:intand struct returns,ref/outparameters and generic methods;SimplePoolTargetSourcewithMaxSize = 1;Equals/GetHashCode/ToString;AopContextbalance after throwing calls, including nested proxies and introduced methods.InheritanceAopProxyTestsgets an exposed-proxy case.AutoProxyTargetSourceBorrowTestschecks that proxy creation borrows nothing.FormatUsingDefaults.spring-reviewerchecked the IL block structure, stack balance, branches inside the regions and the hot path. It accepted the change with follow-ups, which are in the last commit.Found during the analysis of #239; that leak itself was fixed in #347.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LGkLQuAuUjvRjrU1AcTsmk