Skip to content

Commit ac7d418

Browse files
committed
Support environment variables in performance tool
Environment variables can override command line arguments. Fixes #37
1 parent 582d56c commit ac7d418

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed

src/docs/asciidoc/performance-tool.adoc

+25
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,31 @@ This can be useful to inspect threads if the tool seems blocked.
477477
The endpoint can then be declared in a Prometheus instance to scrape the metrics.
478478
* `--monitoring-port`: set the port to use for the web server.
479479

480+
===== Using Environment Variables as Options
481+
482+
Environment variables can sometimes be easier to work with than command line options.
483+
This is especially true when using a manifest file for configuration (with Docker Compose or Kubernetes) and the number of options used grows.
484+
485+
The performance tool automatically uses environment variables that match the snake case version of its long options.
486+
E.g. it automatically picks up the value of the `BATCH_SIZE` environment variable for the `--batch-size` option, but only if the environment variable is defined.
487+
488+
You can list the environment variables that the tool picks up with the following command:
489+
490+
----
491+
java -jar stream-perf-test.jar --environment-variables
492+
----
493+
494+
The short version of the option is `-env`.
495+
496+
To avoid collisions with environment variables that already exist, it is possible to specify a prefix for the environment variables that the tool looks up.
497+
This prefix is defined with the `RABBITMQ_STREAM_PERF_TEST_ENV_PREFIX` environment variable, e.g.:
498+
499+
----
500+
RABBITMQ_STREAM_PERF_TEST_ENV_PREFIX="STREAM_PERF_TEST_"
501+
----
502+
503+
With `RABBITMQ_STREAM_PERF_TEST_ENV_PREFIX="STREAM_PERF_TEST_"` defined, the tool looks for the `STREAM_PERF_TEST_BATCH_SIZE` environment variable, not `BATCH_SIZE`.
504+
480505
===== Logging
481506

482507
The performance tool binary uses Logback with an internal configuration file.

src/main/java/com/rabbitmq/stream/perf/StreamPerfTest.java

+52-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
1414
package com.rabbitmq.stream.perf;
1515

16+
import static com.rabbitmq.stream.perf.Utils.ENVIRONMENT_VARIABLE_LOOKUP;
17+
import static com.rabbitmq.stream.perf.Utils.ENVIRONMENT_VARIABLE_PREFIX;
18+
import static com.rabbitmq.stream.perf.Utils.OPTION_TO_ENVIRONMENT_VARIABLE;
1619
import static java.lang.String.format;
1720

1821
import com.google.common.util.concurrent.RateLimiter;
@@ -74,6 +77,7 @@
7477
import java.util.concurrent.atomic.AtomicInteger;
7578
import java.util.concurrent.atomic.AtomicLong;
7679
import java.util.function.BiFunction;
80+
import java.util.function.Function;
7781
import java.util.function.Supplier;
7882
import java.util.stream.Collectors;
7983
import java.util.stream.IntStream;
@@ -82,6 +86,7 @@
8286
import org.slf4j.Logger;
8387
import org.slf4j.LoggerFactory;
8488
import picocli.CommandLine;
89+
import picocli.CommandLine.Model.CommandSpec;
8590

8691
@CommandLine.Command(
8792
name = "stream-perf-test",
@@ -329,6 +334,12 @@ public class StreamPerfTest implements Callable<Integer> {
329334
defaultValue = "8080")
330335
private int monitoringPort;
331336

337+
@CommandLine.Option(
338+
names = {"--environment-variables", "-env"},
339+
description = "show usage with environment variables",
340+
defaultValue = "false")
341+
private boolean environmentVariables;
342+
332343
private MetricsCollector metricsCollector;
333344
private PerformanceMetrics performanceMetrics;
334345
private List<Monitoring> monitorings;
@@ -422,10 +433,9 @@ private static boolean isTls(Collection<String> uris) {
422433

423434
@Override
424435
public Integer call() throws Exception {
425-
if (this.version) {
426-
versionInformation(System.out);
427-
System.exit(0);
428-
}
436+
maybeDisplayVersion();
437+
maybeDisplayEnvironmentVariablesHelp();
438+
overridePropertiesWithEnvironmentVariables();
429439

430440
Codec codec = createCodec(this.codecClass);
431441

@@ -808,6 +818,44 @@ public Integer call() throws Exception {
808818
return 0;
809819
}
810820

821+
private void overridePropertiesWithEnvironmentVariables() throws Exception {
822+
Function<String, String> optionToEnvMappings =
823+
OPTION_TO_ENVIRONMENT_VARIABLE
824+
.andThen(ENVIRONMENT_VARIABLE_PREFIX)
825+
.andThen(ENVIRONMENT_VARIABLE_LOOKUP);
826+
827+
Utils.assignValuesToCommand(this, optionToEnvMappings);
828+
this.monitorings.forEach(
829+
command -> {
830+
try {
831+
Utils.assignValuesToCommand(command, optionToEnvMappings);
832+
} catch (Exception e) {
833+
LOGGER.warn(
834+
"Error while trying to assign environment variables to command {}",
835+
command.getClass());
836+
}
837+
});
838+
}
839+
840+
private void maybeDisplayEnvironmentVariablesHelp() {
841+
if (this.environmentVariables) {
842+
Collection<Object> commands = new ArrayList<>(this.monitorings.size() + 1);
843+
commands.add(this);
844+
commands.addAll(this.monitorings);
845+
CommandSpec commandSpec = Utils.buildCommandSpec(commands.toArray());
846+
CommandLine commandLine = new CommandLine(commandSpec);
847+
CommandLine.usage(commandLine, System.out);
848+
System.exit(0);
849+
}
850+
}
851+
852+
private void maybeDisplayVersion() {
853+
if (this.version) {
854+
versionInformation(System.out);
855+
System.exit(0);
856+
}
857+
}
858+
811859
private ShutdownService.CloseCallback closeStep(
812860
String message, ShutdownService.CloseCallback callback) {
813861
return new CloseCallback() {

src/main/java/com/rabbitmq/stream/perf/Utils.java

+112
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.rabbitmq.stream.StreamCreator.LeaderLocator;
1919
import com.rabbitmq.stream.compression.Compression;
2020
import com.sun.management.OperatingSystemMXBean;
21+
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.Field;
2123
import java.lang.reflect.Method;
2224
import java.security.cert.X509Certificate;
2325
import java.text.CharacterIterator;
@@ -27,8 +29,11 @@
2729
import java.time.format.DateTimeFormatter;
2830
import java.time.format.DateTimeParseException;
2931
import java.time.temporal.TemporalAccessor;
32+
import java.util.ArrayList;
3033
import java.util.Arrays;
34+
import java.util.Collection;
3135
import java.util.Collections;
36+
import java.util.Comparator;
3237
import java.util.HashMap;
3338
import java.util.List;
3439
import java.util.Locale;
@@ -38,6 +43,7 @@
3843
import java.util.concurrent.ThreadFactory;
3944
import java.util.concurrent.atomic.AtomicLong;
4045
import java.util.function.BiFunction;
46+
import java.util.function.Function;
4147
import java.util.function.LongSupplier;
4248
import java.util.stream.Collectors;
4349
import java.util.stream.IntStream;
@@ -47,11 +53,38 @@
4753
import org.slf4j.Logger;
4854
import org.slf4j.LoggerFactory;
4955
import picocli.CommandLine;
56+
import picocli.CommandLine.Command;
5057
import picocli.CommandLine.ITypeConverter;
58+
import picocli.CommandLine.Model.CommandSpec;
59+
import picocli.CommandLine.Model.OptionSpec;
60+
import picocli.CommandLine.Option;
5161

5262
class Utils {
5363

5464
static final X509TrustManager TRUST_EVERYTHING_TRUST_MANAGER = new TrustEverythingTrustManager();
65+
static final Function<String, String> OPTION_TO_ENVIRONMENT_VARIABLE =
66+
option -> {
67+
if (option.startsWith("--")) {
68+
return option.replace("--", "").replace('-', '_').toUpperCase(Locale.ENGLISH);
69+
} else if (option.startsWith("-")) {
70+
return option.substring(1).replace('-', '_').toUpperCase(Locale.ENGLISH);
71+
} else {
72+
return option.replace('-', '_').toUpperCase(Locale.ENGLISH);
73+
}
74+
};
75+
static final Function<String, String> ENVIRONMENT_VARIABLE_PREFIX =
76+
name -> {
77+
String prefix = System.getenv("RABBITMQ_STREAM_PERF_TEST_ENV_PREFIX");
78+
if (prefix == null || prefix.trim().isEmpty()) {
79+
return name;
80+
}
81+
if (prefix.endsWith("_")) {
82+
return prefix + name;
83+
} else {
84+
return prefix + "_" + name;
85+
}
86+
};
87+
static final Function<String, String> ENVIRONMENT_VARIABLE_LOOKUP = name -> System.getenv(name);
5588
private static final LongSupplier TOTAL_MEMORY_SIZE_SUPPLIER;
5689
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
5790
private static final String RANGE_SEPARATOR_1 = "-";
@@ -166,6 +199,85 @@ private static void throwConversionException(String format, String... arguments)
166199
throw new CommandLine.TypeConversionException(String.format(format, (Object[]) arguments));
167200
}
168201

202+
static void assignValuesToCommand(Object command, Function<String, String> optionMapping)
203+
throws Exception {
204+
LOGGER.debug("Assigning values to command {}", command.getClass());
205+
Collection<String> arguments = new ArrayList<>();
206+
Collection<Field> fieldsToAssign = new ArrayList<>();
207+
for (Field field : command.getClass().getDeclaredFields()) {
208+
Option option = field.getAnnotation(Option.class);
209+
if (option == null) {
210+
LOGGER.debug("No option annotation for field {}", field.getName());
211+
continue;
212+
}
213+
String longOption =
214+
Arrays.stream(option.names())
215+
.sorted(Comparator.comparingInt(String::length).reversed())
216+
.findFirst()
217+
.get();
218+
LOGGER.debug("Looking up new value for option {}", longOption);
219+
String newValue = optionMapping.apply(longOption);
220+
221+
LOGGER.debug(
222+
"New value found for option {} (field {}): {}", longOption, field.getName(), newValue);
223+
if (newValue == null) {
224+
continue;
225+
}
226+
fieldsToAssign.add(field);
227+
if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
228+
if (Boolean.parseBoolean(newValue)) {
229+
arguments.add(longOption);
230+
}
231+
} else {
232+
arguments.add(longOption + " " + newValue);
233+
}
234+
}
235+
if (fieldsToAssign.size() > 0) {
236+
Constructor<?> defaultConstructor = command.getClass().getConstructor();
237+
Object commandBuffer = defaultConstructor.newInstance();
238+
String argumentsLine = String.join(" ", arguments);
239+
LOGGER.debug("Arguments line with extra values: {}", argumentsLine);
240+
String[] args = argumentsLine.split(" ");
241+
commandBuffer = CommandLine.populateCommand(commandBuffer, args);
242+
for (Field field : fieldsToAssign) {
243+
field.setAccessible(true);
244+
field.set(command, field.get(commandBuffer));
245+
}
246+
}
247+
}
248+
249+
static CommandSpec buildCommandSpec(Object... commands) {
250+
Object mainCommand = commands[0];
251+
Command commandAnnotation = mainCommand.getClass().getAnnotation(Command.class);
252+
CommandSpec spec = CommandSpec.create();
253+
spec.name(commandAnnotation.name());
254+
spec.mixinStandardHelpOptions(commandAnnotation.mixinStandardHelpOptions());
255+
for (Object command : commands) {
256+
for (Field f : command.getClass().getDeclaredFields()) {
257+
Option annotation = f.getAnnotation(Option.class);
258+
if (annotation == null) {
259+
continue;
260+
}
261+
String name =
262+
Arrays.stream(annotation.names())
263+
.sorted(Comparator.comparingInt(String::length).reversed())
264+
.findFirst()
265+
.map(OPTION_TO_ENVIRONMENT_VARIABLE::apply)
266+
.get();
267+
spec.addOption(
268+
OptionSpec.builder(name)
269+
.type(f.getType())
270+
.description(annotation.description())
271+
.paramLabel("<" + name.replace("_", "-") + ">")
272+
.defaultValue(annotation.defaultValue())
273+
.showDefaultValue(annotation.showDefaultValue())
274+
.build());
275+
}
276+
}
277+
278+
return spec;
279+
}
280+
169281
static class ByteCapacityTypeConverter implements CommandLine.ITypeConverter<ByteCapacity> {
170282

171283
@Override

src/test/java/com/rabbitmq/stream/perf/UtilsTest.java

+59
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
import com.rabbitmq.stream.perf.Utils.SniServerNamesConverter;
2929
import java.util.Arrays;
3030
import java.util.Collections;
31+
import java.util.HashMap;
3132
import java.util.List;
3233
import java.util.Locale;
34+
import java.util.Map;
3335
import java.util.Random;
3436
import java.util.UUID;
3537
import java.util.function.BiFunction;
@@ -44,6 +46,9 @@
4446
import org.junit.jupiter.params.provider.MethodSource;
4547
import org.junit.jupiter.params.provider.ValueSource;
4648
import picocli.CommandLine;
49+
import picocli.CommandLine.Command;
50+
import picocli.CommandLine.Model.CommandSpec;
51+
import picocli.CommandLine.Option;
4752
import picocli.CommandLine.TypeConversionException;
4853

4954
public class UtilsTest {
@@ -263,4 +268,58 @@ void streamsSpecifyOnlyOneStreamWhenStreamCountIsSpecified() {
263268
assertThatThrownBy(() -> Utils.streams("2", Arrays.asList("stream1", "stream2")))
264269
.isInstanceOf(IllegalArgumentException.class);
265270
}
271+
272+
@Test
273+
void assignValuesToCommand() throws Exception {
274+
TestCommand command = new TestCommand();
275+
Map<String, String> mappings = new HashMap<>();
276+
mappings.put("aaa", "42"); // takes only long options
277+
mappings.put("b", "true");
278+
mappings.put("offset", "first");
279+
Utils.assignValuesToCommand(command, option -> mappings.get(option));
280+
assertThat(command.a).isEqualTo(42);
281+
assertThat(command.b).isTrue();
282+
assertThat(command.c).isFalse();
283+
assertThat(command.offsetSpecification).isEqualTo(OffsetSpecification.first());
284+
}
285+
286+
@Test
287+
void buildCommandSpec() {
288+
CommandSpec spec = Utils.buildCommandSpec(new TestCommand());
289+
assertThat(spec.optionsMap()).hasSize(4).containsKeys("AAA", "B", "C", "OFFSET");
290+
}
291+
292+
@ParameterizedTest
293+
@CsvSource({
294+
"--uris,URIS",
295+
"--stream-count,STREAM_COUNT",
296+
"-sc,SC",
297+
"--sub-entry-size, SUB_ENTRY_SIZE",
298+
"-ses,SES"
299+
})
300+
void optionToEnvironmentVariable(String option, String envVariable) {
301+
assertThat(Utils.OPTION_TO_ENVIRONMENT_VARIABLE.apply(option)).isEqualTo(envVariable);
302+
}
303+
304+
@Command(name = "test-command")
305+
static class TestCommand {
306+
@Option(
307+
names = {"aaa", "a"},
308+
defaultValue = "10")
309+
private int a = 10;
310+
311+
@Option(names = "b", defaultValue = "false")
312+
private boolean b = false;
313+
314+
@Option(names = "c", defaultValue = "false")
315+
private boolean c = false;
316+
317+
@CommandLine.Option(
318+
names = {"offset"},
319+
defaultValue = "next",
320+
converter = Utils.OffsetSpecificationTypeConverter.class)
321+
private OffsetSpecification offsetSpecification = OffsetSpecification.next();
322+
323+
public TestCommand() {}
324+
}
266325
}

0 commit comments

Comments
 (0)