diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java index 5906d1792b..ad53c76c1b 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java @@ -16,6 +16,7 @@ package com.alibaba.csp.sentinel.slotchain; import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.slots.block.flow.PrototypeSlotWrapper; /** * @author qinan.qn @@ -50,8 +51,9 @@ public void addFirst(AbstractLinkedProcessorSlot protocolProcessor) { @Override public void addLast(AbstractLinkedProcessorSlot protocolProcessor) { - end.setNext(protocolProcessor); - end = protocolProcessor; + PrototypeSlotWrapper processor = new PrototypeSlotWrapper(protocolProcessor); + end.setNext(processor); + end = processor; } /** diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/PrototypeSlotWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/PrototypeSlotWrapper.java new file mode 100644 index 0000000000..82f7381807 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/PrototypeSlotWrapper.java @@ -0,0 +1,39 @@ +package com.alibaba.csp.sentinel.slots.block.flow; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * @author joyce + * @date 2026/5/14 + * @since 1.8.5 + **/ +public class PrototypeSlotWrapper extends AbstractLinkedProcessorSlot { + + private final AbstractLinkedProcessorSlot delegate; + + public PrototypeSlotWrapper(AbstractLinkedProcessorSlot delegate) { + this.delegate = delegate; + } + + @Override + public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable { + this.delegate.fireEntry(context, resourceWrapper, obj, count, prioritized, args); + } + + @Override + public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + this.delegate.fireExit(context, resourceWrapper, count, args); + } + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, Object param, int count, boolean prioritized, Object... args) throws Throwable { + delegate.entry(context, resourceWrapper, param, count, prioritized, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + delegate.exit(context, resourceWrapper, count, args); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/ProcessorSlotChainTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/ProcessorSlotChainTest.java new file mode 100644 index 0000000000..deede3e8d9 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/ProcessorSlotChainTest.java @@ -0,0 +1,76 @@ +package com.alibaba.csp.sentinel.slots; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.DefaultProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import java.lang.reflect.Field; + +/** + * @author joyce + * @date 2026/5/14 + */ +public class ProcessorSlotChainTest { + + @Test + public void testBeforeFix_nextOfSingletonIsShared() throws Exception { + DefaultSlot singleton = new DefaultSlot(); + DefaultProcessorSlotChain chain1 = createLegacyChain(); + DefaultProcessorSlotChain chain2 = createLegacyChain(); + + DefaultSlot prototypeA1 = new DefaultSlot(); + chain1.addLast(singleton); + chain1.addLast(prototypeA1); + + DefaultSlot prototypeA2 = new DefaultSlot(); + chain2.addLast(singleton); + chain2.addLast(prototypeA2); + + Assert.assertEquals(chain1.getNext().getNext(), chain2.getNext().getNext()); + } + @Test + public void testAfterFix_eachChainHasItsOwnWrapper() { + DefaultSlot singleton = new DefaultSlot(); + DefaultProcessorSlotChain chain1 = new DefaultProcessorSlotChain(); + DefaultProcessorSlotChain chain2 = new DefaultProcessorSlotChain(); + + chain1.addLast(singleton); + chain1.addLast(new DefaultSlot()); + + chain2.addLast(singleton); + chain2.addLast(new DefaultSlot()); + Assert.assertNotEquals(chain1.getNext().getNext(), chain2.getNext().getNext()); + } + + private DefaultProcessorSlotChain createLegacyChain() throws Exception { + DefaultProcessorSlotChain chain = Mockito.spy(new DefaultProcessorSlotChain()); + Field endField = DefaultProcessorSlotChain.class.getDeclaredField("end"); + endField.setAccessible(true); + + Mockito.doAnswer(invocation -> { + AbstractLinkedProcessorSlot slot = invocation.getArgument(0); + AbstractLinkedProcessorSlot end = + (AbstractLinkedProcessorSlot) endField.get(chain); + end.setNext(slot); + endField.set(chain, slot); + return null; + }).when(chain).addLast(Mockito.any(AbstractLinkedProcessorSlot.class)); + + return chain; + } + + private static class DefaultSlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, + boolean prioritized, Object... args) throws Throwable { + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + } + } +}