Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id 'dd-trace-java.module.instrumentation'
id 'dd-trace-java.jmh-conventions'
}

muzzle {
Expand All @@ -20,6 +21,7 @@ addTestSuiteForDir('lettuce62Test', 'test')

dependencies {
compileOnly group: 'io.lettuce', name: 'lettuce-core', version: '5.0.0.RELEASE'
jmh group: 'io.lettuce', name: 'lettuce-core', version: '5.0.0.RELEASE'

testImplementation group: 'com.redis.testcontainers', name: 'testcontainers-redis', version: '1.6.2'
testImplementation libs.testcontainers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package datadog.trace.instrumentation.lettuce5;

import io.lettuce.core.output.CommandOutput;
import io.lettuce.core.protocol.CommandArgs;
import io.lettuce.core.protocol.CommandType;
import io.lettuce.core.protocol.ProtocolKeyword;
import io.lettuce.core.protocol.RedisCommand;
import io.netty.buffer.ByteBuf;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

/**
* Compares the old {@code HashSet<String>} lookup in {@link
* LettuceInstrumentationUtil#expectsResponse(RedisCommand)} against the new {@code
* EnumSet<CommandType>} lookup. The win is lookup strategy (bit-set test vs. string hash/equals),
* not avoided allocation -- these {@code CommandType} names have no whitespace, so {@code
* toString().trim()} never allocates. {@code oldExpectsResponse} reproduces the removed
* implementation for comparison.
*
* <p>Split into two traffic shapes: {@code Miss} (ordinary data commands, ~100% of real traffic)
* and {@code Hit} (DEBUG/SHUTDOWN, rare but exercises the matching path).
*
* <pre>
* ./gradlew :dd-java-agent:instrumentation:lettuce:lettuce-5.0:jmh # add -prof gc
* </pre>
*/
@Fork(3)
@Warmup(iterations = 2, time = 5)
@Measurement(iterations = 5, time = 5)
@Threads(1)
public class LettuceCommandMatchingBenchmark {

/** Byte-for-byte reproduction of the {@code Set<String>}-based check this replaces. */
private static final String[] NON_INSTRUMENTING_COMMAND_WORDS =
new String[] {"SHUTDOWN", "DEBUG", "OOM", "SEGFAULT"};

private static final Set<String> NON_INSTRUMENTING_COMMANDS_OLD =
new HashSet<>(Arrays.asList(NON_INSTRUMENTING_COMMAND_WORDS));

private static boolean oldExpectsResponse(final RedisCommand command) {
String commandName = "Redis Command";
if (command != null && command.getType() != null) {
commandName = command.getType().toString().trim();
}
return !NON_INSTRUMENTING_COMMANDS_OLD.contains(commandName);
}

/** Minimal {@link RedisCommand} stub -- only {@link #getType()} is ever exercised here. */
private static final class FakeRedisCommand implements RedisCommand<Object, Object, Object> {
private final ProtocolKeyword type;

FakeRedisCommand(final ProtocolKeyword type) {
this.type = type;
}

@Override
public ProtocolKeyword getType() {
return type;
}

@Override
public CommandOutput<Object, Object, Object> getOutput() {
throw new UnsupportedOperationException();
}

@Override
public void complete() {
throw new UnsupportedOperationException();
}

@Override
public boolean completeExceptionally(final Throwable throwable) {
throw new UnsupportedOperationException();
}

@Override
public void cancel() {
throw new UnsupportedOperationException();
}

@Override
public CommandArgs<Object, Object> getArgs() {
throw new UnsupportedOperationException();
}

@Override
public void encode(final ByteBuf buf) {
throw new UnsupportedOperationException();
}

@Override
public boolean isCancelled() {
throw new UnsupportedOperationException();
}

@Override
public boolean isDone() {
throw new UnsupportedOperationException();
}

@Override
public void setOutput(final CommandOutput<Object, Object, Object> output) {
throw new UnsupportedOperationException();
}
}

// Representative production traffic: ordinary data commands, none of which ever match
// NON_INSTRUMENTING_COMMANDS.
private static final RedisCommand[] MISS_COMMANDS =
Arrays.stream(
new CommandType[] {
CommandType.GET,
CommandType.SET,
CommandType.EXISTS,
CommandType.EXPIRE,
CommandType.HSET,
CommandType.LPUSH,
CommandType.INCR,
})
.map(FakeRedisCommand::new)
.toArray(RedisCommand[]::new);

// Rare admin commands that always match NON_INSTRUMENTING_COMMANDS. Not representative of real
// traffic volume -- included only to exercise the hit path.
private static final RedisCommand[] HIT_COMMANDS =
Arrays.stream(new CommandType[] {CommandType.DEBUG, CommandType.SHUTDOWN})
.map(FakeRedisCommand::new)
.toArray(RedisCommand[]::new);

private abstract static class Cursor {
int index = 0;

abstract RedisCommand[] commands();

RedisCommand next() {
final RedisCommand[] commands = commands();
final int i = index;
index = (i + 1) % commands.length;
return commands[i];
}
}

@State(Scope.Thread)
public static class MissCursor extends Cursor {
@Override
RedisCommand[] commands() {
return MISS_COMMANDS;
}
}

@State(Scope.Thread)
public static class HitCursor extends Cursor {
@Override
RedisCommand[] commands() {
return HIT_COMMANDS;
}
}

@Benchmark
public boolean missOld(final MissCursor cursor) {
return oldExpectsResponse(cursor.next());
}

@Benchmark
public boolean missNew(final MissCursor cursor) {
return LettuceInstrumentationUtil.expectsResponse(cursor.next());
}

@Benchmark
public boolean hitOld(final HitCursor cursor) {
return oldExpectsResponse(cursor.next());
}

@Benchmark
public boolean hitNew(final HitCursor cursor) {
return LettuceInstrumentationUtil.expectsResponse(cursor.next());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ public void onConnection(final AgentSpan span, final RedisURI connection) {
}

public void onCommand(final AgentSpan span, final RedisCommand command) {
final String commandName = LettuceInstrumentationUtil.getCommandName(command);
span.setResourceName(LettuceInstrumentationUtil.getCommandResourceName(commandName));
span.setResourceName(LettuceInstrumentationUtil.getCommandResourceName(command));
}

public String resourceNameForConnection(final RedisURI redisURI) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
package datadog.trace.instrumentation.lettuce5;

import io.lettuce.core.protocol.CommandType;
import io.lettuce.core.protocol.ProtocolKeyword;
import io.lettuce.core.protocol.RedisCommand;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

public class LettuceInstrumentationUtil {

public static final String[] NON_INSTRUMENTING_COMMAND_WORDS =
new String[] {"SHUTDOWN", "DEBUG", "OOM", "SEGFAULT"};
// DEBUG covers `DEBUG OOM`/`DEBUG SEGFAULT`: Lettuce sends those as command type DEBUG with
// "OOM"/SEGFAULT as an argument, not as the command type.
public static final Set<CommandType> NON_INSTRUMENTING_COMMANDS =
EnumSet.of(CommandType.SHUTDOWN, CommandType.DEBUG);

public static final String[] AGENT_CRASHING_COMMANDS_WORDS =
new String[] {"CLIENT", "CLUSTER", "COMMAND", "CONFIG", "DEBUG", "SCRIPT"};
public static final Set<CommandType> AGENT_CRASHING_COMMANDS =
EnumSet.of(
CommandType.CLIENT,
CommandType.CLUSTER,
CommandType.COMMAND,
CommandType.CONFIG,
CommandType.DEBUG,
CommandType.SCRIPT);

public static final String AGENT_CRASHING_COMMAND_PREFIX = "COMMAND-NAME:";

public static final Set<String> nonInstrumentingCommands =
new HashSet<>(Arrays.asList(NON_INSTRUMENTING_COMMAND_WORDS));
// Fallback for custom (non-CommandType) ProtocolKeyword implementations.
private static final Set<String> NON_INSTRUMENTING_COMMAND_NAMES =
commandNames(NON_INSTRUMENTING_COMMANDS);

public static final Set<String> agentCrashingCommands =
new HashSet<>(Arrays.asList(AGENT_CRASHING_COMMANDS_WORDS));
private static final Set<String> AGENT_CRASHING_COMMAND_NAMES =
commandNames(AGENT_CRASHING_COMMANDS);

private static Set<String> commandNames(final Set<CommandType> commands) {
final Set<String> names = new HashSet<>();
for (final CommandType command : commands) {
names.add(command.toString());
}
return names;
}

/**
* Determines whether a redis command should finish its relevant span early (as soon as tags are
Expand All @@ -30,32 +49,41 @@ public class LettuceInstrumentationUtil {
* @return false if the span should finish early (the command will not have a return value)
*/
public static boolean expectsResponse(final RedisCommand command) {
final String commandName = LettuceInstrumentationUtil.getCommandName(command);
return !nonInstrumentingCommands.contains(commandName);
if (command == null) {
return true;
}
final ProtocolKeyword type = command.getType();
if (type == null) {
return true;
}
if (type instanceof CommandType) {
return !NON_INSTRUMENTING_COMMANDS.contains(type);
}
return !NON_INSTRUMENTING_COMMAND_NAMES.contains(type.toString().trim());
}

// Workaround to keep trace agent from crashing
// Currently the commands in AGENT_CRASHING_COMMANDS_WORDS will crash the trace agent and
// traces with these commands as the resource name will not be processed by the trace agent
// https://github.com/DataDog/datadog-trace-agent/blob/master/quantizer/redis.go#L18 has
// list of commands that will currently fail at the trace agent level.

/**
* Workaround to keep trace agent from crashing Currently the commands in
* AGENT_CRASHING_COMMANDS_WORDS will crash the trace agent and traces with these commands as the
* resource name will not be processed by the trace agent
* Workaround to keep trace agent from crashing Currently the commands in AGENT_CRASHING_COMMANDS
* will crash the trace agent and traces with these commands as the resource name will not be
* processed by the trace agent
* https://github.com/DataDog/datadog-trace-agent/blob/master/quantizer/redis.go#L18 has list of
* commands that will currently fail at the trace agent level.
*
* @param actualCommandName the actual redis command
* @param command the lettuce RedisCommand object
* @return the redis command with a prefix if it is a command that will crash the trace agent,
* otherwise, the original command is returned.
*/
public static String getCommandResourceName(final String actualCommandName) {
if (agentCrashingCommands.contains(actualCommandName)) {
return AGENT_CRASHING_COMMAND_PREFIX + actualCommandName;
public static String getCommandResourceName(final RedisCommand command) {
final String commandName = getCommandName(command);
final ProtocolKeyword type = command == null ? null : command.getType();
final boolean crashesAgent =
type instanceof CommandType
? AGENT_CRASHING_COMMANDS.contains(type)
: type != null && AGENT_CRASHING_COMMAND_NAMES.contains(commandName);
if (crashesAgent) {
return AGENT_CRASHING_COMMAND_PREFIX + commandName;
}
return actualCommandName;
return commandName;
}

/**
Expand Down