diff --git a/CHANGES.md b/CHANGES.md index b5bcc6ad61..9a400d92d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,10 @@ Release Notes. `Statement.RETURN_GENERATED_KEYS` or column names (apache/skywalking#14138). * Fix the `NullPointerException` thrown by the Lettuce plugin and the lost trace context of reactive commands when Lettuce tracing is enabled, and when a reactive command is cancelled before it is written (apache/skywalking#14178). +* Trace the commands that Lettuce writes as a batch, such as `StatefulConnection#dispatch(Collection)`, with one + `Lettuce/BATCH_WRITE` span. Stop the Lettuce plugin from starting a trace of its own for every reactive command + created without an active trace, and from creating a span that is never finished for a command that is already + cancelled or timed out when it is written. All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/267?closed=1) diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/RedisReactiveCreatePublisherMethodInterceptorV5.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/RedisReactiveCreatePublisherMethodInterceptorV5.java index 12ddea18f7..a9585bb492 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/RedisReactiveCreatePublisherMethodInterceptorV5.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/RedisReactiveCreatePublisherMethodInterceptorV5.java @@ -39,7 +39,7 @@ * *

If the Reactor Context does not already contain a snapshot, this interceptor * captures the current active context and writes it into the subscriber context - * as a fallback propagation mechanism.

+ * as a fallback propagation mechanism. Nothing is captured when no context is active.

*/ public class RedisReactiveCreatePublisherMethodInterceptorV5 implements InstanceMethodsAroundInterceptorV2 { @@ -56,6 +56,12 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA if (!(ret instanceof Mono) && !(ret instanceof Flux)) { return ret; } + // Without an active trace there is nothing to capture, and a local span would start a trace of its own. + // A snapshot put into the Reactor Context by another plugin, such as spring-webflux, is still continued + // when the command is written. + if (!ContextManager.isActive()) { + return ret; + } final AbstractSpan localSpan = ContextManager.createLocalSpan("Lettuce/Reactive/" + method.getName()); localSpan.setComponent(ComponentsDefine.LETTUCE); SpanLayer.asCache(localSpan); diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/define/RedisChannelWriterInstrumentationV5.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/define/RedisChannelWriterInstrumentationV5.java index bf3e52f79c..1aa9b55abf 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/define/RedisChannelWriterInstrumentationV5.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-5.x-6.4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v5/define/RedisChannelWriterInstrumentationV5.java @@ -18,6 +18,7 @@ package org.apache.skywalking.apm.plugin.lettuce.v5.define; +import java.util.Collection; import java.util.Collections; import java.util.List; import net.bytebuddy.description.method.MethodDescription; @@ -50,8 +51,8 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { new InstanceMethodsInterceptPoint() { @Override public ElementMatcher getMethodsMatcher() { - return named("write").and(takesArgument(0, named("io.lettuce.core.protocol.RedisCommand")).or( - takesArgument(0, List.class))); + return named("write").and(takesArgument(0, named("io.lettuce.core.protocol.RedisCommand")) + .or(takesArgument(0, Collection.class))); } @Override diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/RedisReactiveCreatePublisherMethodInterceptorV65.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/RedisReactiveCreatePublisherMethodInterceptorV65.java index 36320b5cd1..043d42cfdb 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/RedisReactiveCreatePublisherMethodInterceptorV65.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/RedisReactiveCreatePublisherMethodInterceptorV65.java @@ -38,7 +38,7 @@ * *

If the Reactor Context does not already contain a snapshot, this interceptor * captures the current active context and writes it into the subscriber context - * as a fallback propagation mechanism.

+ * as a fallback propagation mechanism. Nothing is captured when no context is active.

*/ public class RedisReactiveCreatePublisherMethodInterceptorV65 implements InstanceMethodsAroundInterceptorV2 { @@ -55,6 +55,12 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA if (!(ret instanceof Mono) && !(ret instanceof Flux)) { return ret; } + // Without an active trace there is nothing to capture, and a local span would start a trace of its own. + // A snapshot put into the Reactor Context by another plugin, such as spring-webflux, is still continued + // when the command is written. + if (!ContextManager.isActive()) { + return ret; + } final AbstractSpan localSpan = ContextManager.createLocalSpan("Lettuce/Reactive/" + method.getName()); localSpan.setComponent(ComponentsDefine.LETTUCE); diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/define/RedisChannelWriterInstrumentationV65.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/define/RedisChannelWriterInstrumentationV65.java index 5e10c43561..de771ae7f9 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/define/RedisChannelWriterInstrumentationV65.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-6.5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/lettuce/v65/define/RedisChannelWriterInstrumentationV65.java @@ -18,6 +18,7 @@ package org.apache.skywalking.apm.plugin.lettuce.v65.define; +import java.util.Collection; import java.util.Collections; import java.util.List; import net.bytebuddy.description.method.MethodDescription; @@ -50,8 +51,8 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { new InstanceMethodsInterceptPoint() { @Override public ElementMatcher getMethodsMatcher() { - return named("write").and(takesArgument(0, named("io.lettuce.core.protocol.RedisCommand")).or( - takesArgument(0, List.class))); + return named("write").and(takesArgument(0, named("io.lettuce.core.protocol.RedisCommand")) + .or(takesArgument(0, Collection.class))); } @Override diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptor.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptor.java index 19f9ab2a5e..d34a809995 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptor.java @@ -37,7 +37,6 @@ import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.Collection; -import java.util.List; import java.util.Optional; @SuppressWarnings("unchecked") @@ -51,10 +50,17 @@ public class RedisChannelWriterInterceptor implements InstanceMethodsAroundInter @Override public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) { String peer = (String) objInst.getSkyWalkingDynamicField(); - RedisCommand spanCarrierCommand = getSpanCarrierCommand(allArguments[0]); + RedisCommand writtenCommand = getWrittenCommand(allArguments[0]); + RedisCommand spanCarrierCommand = getSpanCarrierCommand(writtenCommand); if (spanCarrierCommand == null) { return; } + // A command that is already done, such as one cancelled or timed out while its write was delayed, is dropped + // by the CommandHandler, or gets no terminal call that reaches the span carrier, so its span would never be + // finished. + if (isDone(writtenCommand, spanCarrierCommand)) { + return; + } EnhancedInstance enhancedCommand = (EnhancedInstance) spanCarrierCommand; RedisCommandEnhanceInfo redisCommandEnhanceInfo = (RedisCommandEnhanceInfo) enhancedCommand.getSkyWalkingDynamicField(); @@ -64,10 +70,11 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr } // command has been handle by another channel writer (cluster or sentinel case) - if (redisCommandEnhanceInfo.getSpan() != null) { + AbstractSpan existingSpan = redisCommandEnhanceInfo.getSpan(); + if (existingSpan != null) { //set peer in last channel writer (delegate) if (peer != null) { - redisCommandEnhanceInfo.getSpan().setPeer(peer); + existingSpan.setPeer(peer); } return; } @@ -112,6 +119,17 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr } enhancedCommand.setSkyWalkingDynamicField(redisCommandEnhanceInfo.setSpan(span)); + + // The command may have been cancelled or timed out before the span was set, when its terminal interceptor + // had no span to finish. + if (isDone(writtenCommand, spanCarrierCommand)) { + AbstractSpan unfinishedSpan = redisCommandEnhanceInfo.takeSpan(); + if (unfinishedSpan != null) { + enhancedCommand.setSkyWalkingDynamicField(null); + unfinishedSpan.errorOccurred(); + unfinishedSpan.asyncFinish(); + } + } } private String getArgsKey(RedisCommand redisCommand) { @@ -141,12 +159,14 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA @Override public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { RedisCommand redisCommand = getSpanCarrierCommand(allArguments[0]); - if (redisCommand instanceof EnhancedInstance && ((EnhancedInstance) redisCommand).getSkyWalkingDynamicField() != null) { + if (redisCommand instanceof EnhancedInstance) { EnhancedInstance enhancedRedisCommand = (EnhancedInstance) redisCommand; - AbstractSpan abstractSpan = ((RedisCommandEnhanceInfo) enhancedRedisCommand.getSkyWalkingDynamicField()).getSpan(); - enhancedRedisCommand.setSkyWalkingDynamicField(null); - abstractSpan.log(t); - abstractSpan.asyncFinish(); + AbstractSpan abstractSpan = RedisCommandEnhanceInfo.takeSpan(enhancedRedisCommand); + if (abstractSpan != null) { + enhancedRedisCommand.setSkyWalkingDynamicField(null); + abstractSpan.log(t); + abstractSpan.asyncFinish(); + } } } @@ -154,21 +174,41 @@ protected String getCommandName(ProtocolKeyword protocol) { return "UNKNOWN"; } - static RedisCommand getSpanCarrierCommand(Object o) { - RedisCommand command = null; + /** + * @return the written command. For a batch write, it is the last command that is not done, which carries the span + * of the whole batch, because a done command is dropped while the others are still sent. A command of the batch + * that is cancelled while the batch is being written can still leave it without a span, or with two. + */ + private static RedisCommand getWrittenCommand(Object o) { if (o instanceof RedisCommand) { - command = (RedisCommand) o; - } else if (o instanceof List) { - List list = (List) o; - command = list.isEmpty() ? null : (RedisCommand) list.get(list.size() - 1); - } else if (o instanceof Collection) { - Collection> redisCommands = (Collection>) o; - RedisCommand last = null; - for (RedisCommand redisCommand : redisCommands) { - last = redisCommand; + return (RedisCommand) o; + } + if (!(o instanceof Collection)) { + return null; + } + RedisCommand lastCommand = null; + RedisCommand lastCommandNotDone = null; + boolean anyCommandNotDone = false; + for (RedisCommand redisCommand : (Collection>) o) { + lastCommand = redisCommand; + if (redisCommand == null || !isDone(redisCommand, getSpanCarrierCommand(redisCommand))) { + lastCommandNotDone = redisCommand; + anyCommandNotDone = true; } - command = last; } + return anyCommandNotDone ? lastCommandNotDone : lastCommand; + } + + /** + * The CommandHandler checks the written command, while a wrapper of the span carrier, such as an AsyncCommand + * whose cancellation cancels the carrier first, or a ClusterCommand, can look not done after the carrier is. + */ + private static boolean isDone(RedisCommand writtenCommand, RedisCommand spanCarrierCommand) { + return writtenCommand.isDone() || spanCarrierCommand != null && spanCarrierCommand.isDone(); + } + + static RedisCommand getSpanCarrierCommand(Object o) { + RedisCommand command = getWrittenCommand(o); if (command instanceof DecoratedCommand) { while (command instanceof DecoratedCommand) { DecoratedCommand wrapper = (DecoratedCommand) command; diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCancelMethodInterceptor.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCancelMethodInterceptor.java index fd29897ca2..9bbc7962cd 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCancelMethodInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCancelMethodInterceptor.java @@ -36,7 +36,7 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr @Override public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) { - AbstractSpan span = RedisCommandEnhanceInfo.getSpan(objInst); + AbstractSpan span = RedisCommandEnhanceInfo.takeSpan(objInst); if (span != null) { span.errorOccurred(); span.tag(new StringTag(CANCEL_SIGNAL_TAG), COMMAND_CANCEL_VALUE); diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteExceptionallyMethodInterceptor.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteExceptionallyMethodInterceptor.java index d00c950ef0..4cf608844b 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteExceptionallyMethodInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteExceptionallyMethodInterceptor.java @@ -33,7 +33,7 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr @Override public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) { - AbstractSpan span = RedisCommandEnhanceInfo.getSpan(objInst); + AbstractSpan span = RedisCommandEnhanceInfo.takeSpan(objInst); if (span != null) { Throwable t = (Throwable) allArguments[0]; span.log(t); diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteMethodInterceptor.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteMethodInterceptor.java index 9ea73190b0..76fcc888e9 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteMethodInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandCompleteMethodInterceptor.java @@ -33,7 +33,7 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr @Override public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) { - AbstractSpan span = RedisCommandEnhanceInfo.getSpan(objInst); + AbstractSpan span = RedisCommandEnhanceInfo.takeSpan(objInst); if (span != null) { span.asyncFinish(); objInst.setSkyWalkingDynamicField(null); diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandEnhanceInfo.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandEnhanceInfo.java index e797d31692..e4b147eb8e 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandEnhanceInfo.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/main/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisCommandEnhanceInfo.java @@ -18,6 +18,7 @@ package org.apache.skywalking.apm.plugin.lettuce.common; +import java.util.concurrent.atomic.AtomicReference; import org.apache.skywalking.apm.agent.core.context.ContextSnapshot; import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; @@ -37,11 +38,11 @@ */ class RedisCommandEnhanceInfo { - private AbstractSpan span; + private final AtomicReference span = new AtomicReference<>(); private ContextSnapshot snapshot; public AbstractSpan getSpan() { - return span; + return span.get(); } /** @@ -53,8 +54,22 @@ static AbstractSpan getSpan(EnhancedInstance command) { return enhanceInfo == null ? null : enhanceInfo.getSpan(); } + /** + * @return the span of the command, which the caller must finish, or null if the command has no span or another + * caller has taken it. A command can be cancelled while its write is traced, so more than one caller may try to + * finish the span, and only the first one gets it. + */ + static AbstractSpan takeSpan(EnhancedInstance command) { + RedisCommandEnhanceInfo enhanceInfo = (RedisCommandEnhanceInfo) command.getSkyWalkingDynamicField(); + return enhanceInfo == null ? null : enhanceInfo.takeSpan(); + } + + AbstractSpan takeSpan() { + return span.getAndSet(null); + } + public RedisCommandEnhanceInfo setSpan(AbstractSpan span) { - this.span = span; + this.span.set(span); return this; } diff --git a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/test/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptorTest.java b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/test/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptorTest.java index db63ef9cf4..91ae4b4218 100644 --- a/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/test/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptorTest.java +++ b/apm-sniffer/apm-sdk-plugin/lettuce-plugins/lettuce-common/src/test/java/org/apache/skywalking/apm/plugin/lettuce/common/RedisChannelWriterInterceptorTest.java @@ -111,6 +111,20 @@ public void setSkyWalkingDynamicField(Object value) { } } + private static class CancelledWhileSpanIsCreatedCommand extends MockRedisCommand { + public CancelledWhileSpanIsCreatedCommand(ProtocolKeyword type, CommandOutput output) { + super(type, output); + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + super.setSkyWalkingDynamicField(value); + if (value != null) { + cancel(); + } + } + } + @Before public void setUp() { LettucePluginConfig.Plugin.Lettuce.TRACE_REDIS_PARAMETERS = true; @@ -141,6 +155,45 @@ public void testInterceptor() { assertThat(SpanHelper.getPeer(spans.get(0)), is(PEER)); } + @Test + public void testDoneCommandHasNoSpan() { + MockRedisCommand redisCommand = new MockRedisCommand<>(CommandType.SET, null); + redisCommand.cancel(); + interceptor.beforeMethod(mockRedisChannelWriterInstance, null, new Object[]{redisCommand}, null, null); + + assertNull(redisCommand.getSkyWalkingDynamicField()); + assertThat(segmentStorage.getTraceSegments().size(), is(0)); + } + + @Test + public void testCommandCancelledWhileSpanIsCreatedHasItsSpanFinished() { + CancelledWhileSpanIsCreatedCommand redisCommand = new CancelledWhileSpanIsCreatedCommand<>(CommandType.SET, null); + // The cancellation interceptor found no span, and the command is done by the time the writer has set the span. + new RedisCommandCancelMethodInterceptor().afterMethod(redisCommand, null, null, null, null); + interceptor.beforeMethod(mockRedisChannelWriterInstance, null, new Object[]{redisCommand}, null, null); + + assertNull(redisCommand.getSkyWalkingDynamicField()); + assertThat(segmentStorage.getTraceSegments().size(), is(1)); + List spans = SegmentHelper.getSpans(segmentStorage.getTraceSegments().get(0)); + assertThat(spans.size(), is(1)); + assertThat(SpanHelper.getErrorOccurred(spans.get(0)), is(true)); + } + + @Test + public void testSpanIsTakenByOneTerminalInterceptor() { + MockRedisCommand redisCommand = new MockRedisCommand<>(CommandType.SET, null); + interceptor.beforeMethod(mockRedisChannelWriterInstance, null, new Object[]{redisCommand}, null, null); + Object enhanceInfo = redisCommand.getSkyWalkingDynamicField(); + + redisCommandCompleteMethodInterceptor.afterMethod(redisCommand, null, null, null, null); + // A concurrent cancellation read the enhance info before the completion cleared it. + redisCommand.setSkyWalkingDynamicField(enhanceInfo); + new RedisCommandCancelMethodInterceptor().afterMethod(redisCommand, null, null, null, null); + + assertThat(segmentStorage.getTraceSegments().size(), is(1)); + assertThat(SpanHelper.getErrorOccurred(SegmentHelper.getSpans(segmentStorage.getTraceSegments().get(0)).get(0)), is(false)); + } + @Test public void testGetSpanCarrierCommand() throws Exception { Command command = new Command<>(CommandType.SET, null, null); @@ -159,6 +212,13 @@ public void testGetSpanCarrierCommand() throws Exception { RedisCommand last = (RedisCommand) getSpanCarrierCommandMethod.invoke(interceptor, list); assertEquals(command, last); + Command cancelled = new Command<>(CommandType.SET, null, null); + cancelled.cancel(); + list.add(cancelled); + RedisCommand lastNotDone = + (RedisCommand) getSpanCarrierCommandMethod.invoke(interceptor, list); + assertEquals(command, lastNotDone); + list.remove(cancelled); RedisCommand nullValue1 = (RedisCommand) getSpanCarrierCommandMethod.invoke(interceptor, (Object) null); assertNull(nullValue1); diff --git a/test/plugin/scenarios/lettuce-6.5.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/lettuce-6.5.x-scenario/config/expectedData.yaml index dbeb6b6138..9ba6c04dda 100644 --- a/test/plugin/scenarios/lettuce-6.5.x-scenario/config/expectedData.yaml +++ b/test/plugin/scenarios/lettuce-6.5.x-scenario/config/expectedData.yaml @@ -189,6 +189,20 @@ segmentItems: spanId: 7 isError: false spanType: Local + - operationName: Lettuce/BATCH_WRITE + parentSpanId: 0 + spanId: 9 + spanLayer: Cache + startTime: not null + endTime: not null + componentId: 57 + isError: false + spanType: Exit + peer: not null + skipAnalysis: false + tags: + - { key: cache.type, value: Redis } + - { key: cache.cmd, value: BATCH_WRITE } - operationName: GET:/lettuce-6.5.x-scenario/case/lettuce-case parentSpanId: -1 spanId: 0 diff --git a/test/plugin/scenarios/lettuce-6.5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java b/test/plugin/scenarios/lettuce-6.5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java index d2cc201025..45984070e0 100644 --- a/test/plugin/scenarios/lettuce-6.5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java +++ b/test/plugin/scenarios/lettuce-6.5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java @@ -25,7 +25,14 @@ import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.reactive.RedisReactiveCommands; import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.output.StatusOutput; +import io.lettuce.core.protocol.AsyncCommand; +import io.lettuce.core.protocol.Command; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandType; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Value; @@ -72,7 +79,13 @@ public String lettuceCase() { .thenReturn("Success"); result.block(); - + + // Commands dispatched as a batch are written to the channel and flushed together. + AsyncCommand batchSet0 = setCommand("key2", "value2"); + AsyncCommand batchSet1 = setCommand("key3", "value3"); + connection0.dispatch(Arrays.asList(batchSet0, batchSet1)); + LettuceFutures.awaitAll(5, TimeUnit.SECONDS, batchSet0, batchSet1); + connection0.close(); connection1.close(); connection2.close(); @@ -80,6 +93,11 @@ public String lettuceCase() { return "Success"; } + private static AsyncCommand setCommand(String key, String value) { + CommandArgs args = new CommandArgs<>(StringCodec.UTF8).addKey(key).addValue(value); + return new AsyncCommand<>(new Command<>(CommandType.SET, new StatusOutput<>(StringCodec.UTF8), args)); + } + @RequestMapping("/healthCheck") @ResponseBody public String healthCheck() { diff --git a/test/plugin/scenarios/lettuce-scenario/config/expectedData.yaml b/test/plugin/scenarios/lettuce-scenario/config/expectedData.yaml index 884191cfe3..d2831cb893 100644 --- a/test/plugin/scenarios/lettuce-scenario/config/expectedData.yaml +++ b/test/plugin/scenarios/lettuce-scenario/config/expectedData.yaml @@ -189,6 +189,20 @@ segmentItems: spanId: 7 isError: false spanType: Local + - operationName: Lettuce/BATCH_WRITE + parentSpanId: 0 + spanId: 9 + spanLayer: Cache + startTime: not null + endTime: not null + componentId: 57 + isError: false + spanType: Exit + peer: not null + skipAnalysis: false + tags: + - { key: cache.type, value: Redis } + - { key: cache.cmd, value: BATCH_WRITE } - operationName: GET:/lettuce-scenario/case/lettuce-case parentSpanId: -1 spanId: 0 diff --git a/test/plugin/scenarios/lettuce-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java b/test/plugin/scenarios/lettuce-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java index d2cc201025..45984070e0 100644 --- a/test/plugin/scenarios/lettuce-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java +++ b/test/plugin/scenarios/lettuce-scenario/src/main/java/org/apache/skywalking/apm/testcase/lettuce/controller/LettuceController.java @@ -25,7 +25,14 @@ import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.reactive.RedisReactiveCommands; import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.output.StatusOutput; +import io.lettuce.core.protocol.AsyncCommand; +import io.lettuce.core.protocol.Command; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandType; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Value; @@ -72,7 +79,13 @@ public String lettuceCase() { .thenReturn("Success"); result.block(); - + + // Commands dispatched as a batch are written to the channel and flushed together. + AsyncCommand batchSet0 = setCommand("key2", "value2"); + AsyncCommand batchSet1 = setCommand("key3", "value3"); + connection0.dispatch(Arrays.asList(batchSet0, batchSet1)); + LettuceFutures.awaitAll(5, TimeUnit.SECONDS, batchSet0, batchSet1); + connection0.close(); connection1.close(); connection2.close(); @@ -80,6 +93,11 @@ public String lettuceCase() { return "Success"; } + private static AsyncCommand setCommand(String key, String value) { + CommandArgs args = new CommandArgs<>(StringCodec.UTF8).addKey(key).addValue(value); + return new AsyncCommand<>(new Command<>(CommandType.SET, new StatusOutput<>(StringCodec.UTF8), args)); + } + @RequestMapping("/healthCheck") @ResponseBody public String healthCheck() { diff --git a/test/plugin/scenarios/lettuce-webflux-5x-scenario/config/expectedData.yaml b/test/plugin/scenarios/lettuce-webflux-5x-scenario/config/expectedData.yaml index cdc04a6242..8a26690266 100644 --- a/test/plugin/scenarios/lettuce-webflux-5x-scenario/config/expectedData.yaml +++ b/test/plugin/scenarios/lettuce-webflux-5x-scenario/config/expectedData.yaml @@ -15,7 +15,9 @@ # limitations under the License. segmentItems: - serviceName: lettuce-webflux-5x-scenario - segmentSize: nq 0 + # The health check, the entry, and one segment per Redis command. Creating a reactive command must not + # start a trace of its own. + segmentSize: 5 segments: - segmentId: not null spans: diff --git a/test/plugin/scenarios/lettuce-webflux-6x-scenario/config/expectedData.yaml b/test/plugin/scenarios/lettuce-webflux-6x-scenario/config/expectedData.yaml index e5857aa054..2cc30665a8 100644 --- a/test/plugin/scenarios/lettuce-webflux-6x-scenario/config/expectedData.yaml +++ b/test/plugin/scenarios/lettuce-webflux-6x-scenario/config/expectedData.yaml @@ -15,7 +15,9 @@ # limitations under the License. segmentItems: - serviceName: lettuce-webflux-6x-scenario - segmentSize: nq 0 + # The health check, the entry, and one segment per Redis command. Creating a reactive command must not + # start a trace of its own. + segmentSize: 5 segments: - segmentId: not null spans: