Skip to content

Commit

Permalink
feat: make launcher options configurable via env
Browse files Browse the repository at this point in the history
For deployment on cloud environments, it is beneficial to provide the
launcher options via environment variables instead of command line
arguments. This change supports reading options either from command line
or environment variable.

The `withEnvironment` method is a newly introduced helper method which
is useful when certain code should be executed in a context that exports
a set of environment variables.
  • Loading branch information
s4heid committed Apr 7, 2021
1 parent 1a80e51 commit f51886d
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 66 deletions.
60 changes: 36 additions & 24 deletions src/main/java/io/neonbee/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -129,37 +130,48 @@ protected static void executePreProcessors(List<LauncherPreProcessor> preProcess
static NeonBeeOptions parseOptions(CommandLine commandLine) {
NeonBeeOptions.Mutable neonBeeOptions = new NeonBeeOptions.Mutable();

Optional.ofNullable(commandLine.<String>getOptionValue(WORKING_DIR.getName()))
.or(() -> Optional.of("./working_dir/"))
getLauncherOptionStringValue(commandLine, WORKING_DIR).or(() -> Optional.of("./working_dir/"))
.ifPresent(cwd -> neonBeeOptions.setWorkingDirectory(Paths.get(cwd)));

Optional.ofNullable(commandLine.<String>getOptionValue(INSTANCE_NAME.getName()))
.ifPresent(neonBeeOptions::setInstanceName);

Optional.ofNullable(commandLine.<Integer>getOptionValue(EVENT_LOOP_POOL_SIZE.getName()))
getLauncherOptionStringValue(commandLine, INSTANCE_NAME).ifPresent(neonBeeOptions::setInstanceName);
getLauncherOptionIntegerValue(commandLine, EVENT_LOOP_POOL_SIZE)
.ifPresent(neonBeeOptions::setEventLoopPoolSize);
getLauncherOptionIntegerValue(commandLine, WORKER_POOL_SIZE).ifPresent(neonBeeOptions::setWorkerPoolSize);
getLauncherOptionIntegerValue(commandLine, CLUSTER_PORT).ifPresent(neonBeeOptions::setClusterPort);
getLauncherOptionStringValue(commandLine, CLUSTER_CONFIG).ifPresent(neonBeeOptions::setClusterConfigResource);
getLauncherOptionIntegerValue(commandLine, SERVER_VERTICLE_PORT)
.ifPresent(neonBeeOptions::setServerVerticlePort);
getLauncherOptionStringValue(commandLine, ACTIVE_PROFILES).ifPresent(neonBeeOptions::setActiveProfileValues);
getLauncherOptionStringValue(commandLine, TIMEZONE_ID).ifPresent(neonBeeOptions::setTimeZoneId);

Optional.ofNullable(commandLine.<Integer>getOptionValue(WORKER_POOL_SIZE.getName()))
.ifPresent(neonBeeOptions::setWorkerPoolSize);

neonBeeOptions.setIgnoreClassPath(commandLine.isFlagEnabled(IGNORE_CLASS_PATH_FLAG.getName()));
neonBeeOptions.setDisableJobScheduling(commandLine.isFlagEnabled(DISABLE_JOB_SCHEDULING_FLAG.getName()));
neonBeeOptions.setIgnoreClassPath(getLauncherOptionBooleanValue(commandLine, IGNORE_CLASS_PATH_FLAG));
neonBeeOptions.setDisableJobScheduling(getLauncherOptionBooleanValue(commandLine, DISABLE_JOB_SCHEDULING_FLAG));
neonBeeOptions.setClustered(getLauncherOptionBooleanValue(commandLine, CLUSTERED));

neonBeeOptions.setClustered(commandLine.isFlagEnabled(CLUSTERED.getName()));
Optional.ofNullable(commandLine.<Integer>getOptionValue(CLUSTER_PORT.getName()))
.ifPresent(neonBeeOptions::setClusterPort);
Optional.ofNullable(commandLine.<String>getOptionValue(CLUSTER_CONFIG.getName()))
.ifPresent(neonBeeOptions::setClusterConfigResource);
return neonBeeOptions;
}

Optional.ofNullable(commandLine.<Integer>getOptionValue(SERVER_VERTICLE_PORT.getName()))
.ifPresent(neonBeeOptions::setServerVerticlePort);
@VisibleForTesting
static boolean getLauncherOptionBooleanValue(CommandLine commandLine, Option option) {
if (commandLine.isFlagEnabled(option.getName())) {
return true;
}
return Optional.ofNullable(System.getenv(transformToEnvName(option.getLongName()))).map(Boolean::parseBoolean)
.orElse(false);
}

Optional.ofNullable(commandLine.<String>getOptionValue(ACTIVE_PROFILES.getName()))
.ifPresent(neonBeeOptions::setActiveProfileValues);
@VisibleForTesting
static Optional<Integer> getLauncherOptionIntegerValue(CommandLine commandLine, Option option) {
return Optional.ofNullable(commandLine.<Integer>getOptionValue(option.getName())).or(() -> Optional
.ofNullable(System.getenv(transformToEnvName(option.getLongName()))).map(Integer::parseInt));
}

Optional.ofNullable(commandLine.<String>getOptionValue(TIMEZONE_ID.getName()))
.ifPresent(neonBeeOptions::setTimeZoneId);
@VisibleForTesting
static Optional<String> getLauncherOptionStringValue(CommandLine commandLine, Option option) {
return Optional.ofNullable(commandLine.<String>getOptionValue(option.getName()))
.or(() -> Optional.ofNullable(System.getenv(transformToEnvName(option.getLongName()))));
}

return neonBeeOptions;
private static String transformToEnvName(String longName) {
return "NEONBEE_" + longName.replaceAll("-", "_").toUpperCase(Locale.ROOT);
}
}
97 changes: 55 additions & 42 deletions src/test/java/io/neonbee/LauncherTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import static io.neonbee.Launcher.INTERFACE;
import static io.neonbee.Launcher.parseOptions;
import static io.neonbee.test.helper.FileSystemHelper.createTempDirectory;
import static io.neonbee.test.helper.SystemHelper.withEnvironment;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -26,9 +28,12 @@ class LauncherTest {

private String[] args;

private static String workDir;

@BeforeAll
static void setUp() throws IOException {
tempDirPath = createTempDirectory();
workDir = tempDirPath.toAbsolutePath().toString();
}

@AfterAll
Expand All @@ -37,77 +42,61 @@ static void tearDown() {
}

@Test
@DisplayName("should throw error, if working directory value is not passed")
@DisplayName("should throw an error, if working directory value is not passed")
void throwErrorIfWorkingDirValueIsEmpty() {
args = new String[] { "-cwd" };
MissingValueException exception =
assertThrows(MissingValueException.class, () -> parseOptions(INTERFACE.parse(List.of(args))));
MissingValueException exception = assertThrows(MissingValueException.class, this::parseOptionsArgs);
assertThat(exception.getMessage()).isEqualTo("The option 'working-directory' requires a value");
}

@Test
@DisplayName("should throw error, if instance-name is empty")
@DisplayName("should throw an error, if instance-name is empty")
void throwErrorIfInstanceNameIsEmpty() {
args = new String[] { "-cwd", tempDirPath.toAbsolutePath().toString(), "-name", "" };
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> parseOptions(INTERFACE.parse(List.of(args))));
args = new String[] { "-cwd", workDir, "-name", "" };
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, this::parseOptionsArgs);
assertThat(exception.getMessage()).isEqualTo("instanceName must not be empty");
}

@Test
@DisplayName("should use passed instance-name")
void usePassedInstanceName() {
args = new String[] { "-cwd", tempDirPath.toAbsolutePath().toString(), "-name", "Hodor" };
NeonBeeOptions neonBeeOptions = parseOptions(INTERFACE.parse(List.of(args)));
assertThat(neonBeeOptions.getInstanceName()).isEqualTo("Hodor");
}

@Test
@DisplayName("should throw error, if the passed value is other than integer for worker pool size")
void validateWorkerPoolSizeValue() {
args = new String[] { "-cwd", tempDirPath.toAbsolutePath().toString(), "-name", "Hodor", "-wps", "hodor" };
InvalidValueException exception =
assertThrows(InvalidValueException.class, () -> parseOptions(INTERFACE.parse(List.of(args))));
args = new String[] { "-cwd", workDir, "-name", "Hodor", "-wps", "hodor" };
InvalidValueException exception = assertThrows(InvalidValueException.class, this::parseOptionsArgs);
assertThat(exception.getMessage()).isEqualTo("The value 'hodor' is not accepted by 'worker-pool-size'");
}

@Test
@DisplayName("should throw error, if the passed value is other than integer for event loop pool size")
void validateEventLoopPoolSizeValue() {
args = new String[] { "-cwd", tempDirPath.toAbsolutePath().toString(), "-name", "Hodor", "-elps", "hodor" };
InvalidValueException exception =
assertThrows(InvalidValueException.class, () -> parseOptions(INTERFACE.parse(List.of(args))));
args = new String[] { "-cwd", workDir, "-name", "Hodor", "-elps", "hodor" };
InvalidValueException exception = assertThrows(InvalidValueException.class, this::parseOptionsArgs);
assertThat(exception.getMessage()).isEqualTo("The value 'hodor' is not accepted by 'event-loop-pool-size'");
}

@Test
@DisplayName("should generate expected neonbee options")
void testExpectedNeonBeeOptions() {
args = new String[] { "-cwd", tempDirPath.toAbsolutePath().toString(), "-name", "Hodor", "-wps", "2", "-elps",
"2", "-no-cp", "-no-jobs", "-svp", "9000" };
NeonBeeOptions neonBeeOptions = parseOptions(INTERFACE.parse(List.of(args)));
assertThat(neonBeeOptions.getInstanceName()).isEqualTo("Hodor");
assertThat(neonBeeOptions.getWorkerPoolSize()).isEqualTo(2);
assertThat(neonBeeOptions.getEventLoopPoolSize()).isEqualTo(2);
assertThat(neonBeeOptions.shouldIgnoreClassPath()).isTrue();
assertThat(neonBeeOptions.shouldDisableJobScheduling()).isTrue();
assertThat(neonBeeOptions.getServerVerticlePort()).isEqualTo(9000);
void testExpectedNeonBeeOptions() throws Exception {
args = new String[] { "-cwd", workDir, "-name", "Hodor", "-wps", "2", "-elps", "2", "-no-cp", "-no-jobs",
"-svp", "9000" };
assertNeonBeeOptions();

args = new String[] {};
neonBeeOptions = parseOptions(INTERFACE.parse(List.of(args)));
assertThat(neonBeeOptions.getServerVerticlePort()).isNull();
Map<String, String> envMap = Map.of("NEONBEE_WORKING_DIR", workDir, "NEONBEE_INSTANCE_NAME", "Hodor",
"NEONBEE_WORKER_POOL_SIZE", "2", "NEONBEE_EVENT_LOOP_POOL_SIZE", "2", "NEONBEE_IGNORE_CLASS_PATH",
"true", "NEONBEE_DISABLE_JOB_SCHEDULING", "true", "NEONBEE_SERVER_VERTICLE_PORT", "9000");
withEnvironment(envMap, this::assertNeonBeeOptions);
}

@Test
@DisplayName("should generate expected clustered neonbee options")
void testExpectedClusterNeonBeeOptions() {
args = new String[] { "-cwd", tempDirPath.toAbsolutePath().toString(), "-cl", "-cc", "hazelcast-local.xml",
"-clp", "10000" };
NeonBeeOptions neonBeeOptions = parseOptions(INTERFACE.parse(List.of(args)));
assertThat(neonBeeOptions.getClusterPort()).isEqualTo(10000);
assertThat(neonBeeOptions.isClustered()).isTrue();
assertThat(neonBeeOptions.getClusterConfig()).isInstanceOf(ClasspathXmlConfig.class);
ClasspathXmlConfig xmlConfig = (ClasspathXmlConfig) neonBeeOptions.getClusterConfig();
assertThat(xmlConfig.getNetworkConfig().getPort()).isEqualTo(20000);
void testExpectedClusterNeonBeeOptions() throws Exception {
args = new String[] { "-cwd", workDir, "-cl", "-cc", "hazelcast-local.xml", "-clp", "10000" };
assertClusteredOptions();

args = new String[] {};
Map<String, String> envMap = Map.of("NEONBEE_WORKING_DIR", workDir, "NEONBEE_CLUSTERED", "true",
"NEONBEE_CLUSTER_CONFIG", "hazelcast-local.xml", "NEONBEE_CLUSTER_PORT", "10000");
withEnvironment(envMap, this::assertClusteredOptions);
}

@Test
Expand All @@ -131,4 +120,28 @@ public boolean isPreProcessorExecuted() {
return preProcessorExecuted;
}
}

private void assertNeonBeeOptions() {
NeonBeeOptions neonBeeOptions = parseOptionsArgs();
assertThat(neonBeeOptions.getInstanceName()).isEqualTo("Hodor");
assertThat(neonBeeOptions.getWorkerPoolSize()).isEqualTo(2);
assertThat(neonBeeOptions.getEventLoopPoolSize()).isEqualTo(2);
assertThat(neonBeeOptions.shouldIgnoreClassPath()).isTrue();
assertThat(neonBeeOptions.shouldDisableJobScheduling()).isTrue();
assertThat(neonBeeOptions.getServerVerticlePort()).isEqualTo(9000);
}

private void assertClusteredOptions() {
NeonBeeOptions neonBeeOptions = parseOptionsArgs();
assertThat(neonBeeOptions.getClusterPort()).isEqualTo(10000);
assertThat(neonBeeOptions.isClustered()).isTrue();
assertThat(neonBeeOptions.getClusterConfig()).isInstanceOf(ClasspathXmlConfig.class);

ClasspathXmlConfig xmlConfig = (ClasspathXmlConfig) neonBeeOptions.getClusterConfig();
assertThat(xmlConfig.getNetworkConfig().getPort()).isEqualTo(20000);
}

private NeonBeeOptions parseOptionsArgs() {
return parseOptions(INTERFACE.parse(List.of(args)));
}
}
15 changes: 15 additions & 0 deletions src/test/java/io/neonbee/test/helper/SystemHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ public static void setEnvironment(Map<String, String> newEnvironment) throws Exc
modifiableEnvironment.putAll(newEnvironment);
}

/**
* This method replaces the JVM wide copy of the system environment with the passed environment map, executes the
* passed runnable, and resets the environment variables to the original value again.
*
* @param newEnvironment The new environment map
* @param runnable The code that should be executed within the context of passed environment variables
* @throws Exception Could not change environment
*/
public static void withEnvironment(Map<String, String> newEnvironment, Runnable runnable) throws Exception {
Map<String, String> oldEnvironment = Map.copyOf(System.getenv());
setEnvironment(newEnvironment);
runnable.run();
SystemHelper.setEnvironment(oldEnvironment);
}

private SystemHelper() {
// Utils class no need to instantiate
}
Expand Down

0 comments on commit f51886d

Please sign in to comment.