Skip to content

Commit

Permalink
Make newScheduledExecutor public and move to StandardLifecycles
Browse files Browse the repository at this point in the history
* Move newScheduledExecutor from MonitoredJobs to StandardLifecycles
* Update the javadocs in StandardLifecycles since it is no longer just
  about setting up lifecycle listeners
* Since MonitoredJobsTest and StandardLifecyclesTest had the same code
  to shut down the ScheduledExecutorService, moved that code into a
  new shared test utility, TestExecutors, which ensures the executor
  is gracefully shut down once the test is finished with it

Closes #109
  • Loading branch information
sleberknight authored and chrisrohr committed Mar 23, 2021
1 parent 2c7cd4e commit af4176c
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.base.KiwiStrings.f;
import static org.kiwiproject.dropwizard.util.lifecycle.StandardLifecycles.newScheduledExecutor;

import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.setup.Environment;
Expand All @@ -21,7 +22,6 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Pattern;

/**
* A set of utilities to assist in setting up MonitoredJobs with health checks.
Expand All @@ -36,9 +36,6 @@ public class MonitoredJobs {
@VisibleForTesting
static final Set<String> JOBS = new HashSet<>();

private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
private static final String EMPTY_STRING = "";

/**
* Create a new {@link MonitoredJob}, setup the {@link MonitoredJobHealthCheck} and schedule the job on the given
* {@link Environment}, with the given name, schedule and runnable.
Expand Down Expand Up @@ -283,22 +280,4 @@ public MonitoredJob registerJob() {
}
}

/**
* Create a new {@link ScheduledExecutorService} whose lifecycle is managed by Dropwizard.
*
* @param env the Dropwizard environment
* @param name the name of the executor (whitespace will be removed e.g. "My Executor" will become "MyExecutor")
* @return a new ScheduledExecutorService instance attached to the lifecycle of the given Dropwizard environment
* @see Environment#lifecycle()
* @see io.dropwizard.lifecycle.setup.LifecycleEnvironment#scheduledExecutorService(String)
*/
private static ScheduledExecutorService newScheduledExecutor(Environment env, String name) {
checkArgumentNotNull(env, "Dropwizard Environment must not be null");
checkArgumentNotBlank(name, "name must not be blank");

var safeName = f("Scheduled-{}-%d", WHITESPACE_PATTERN.matcher(name).replaceAll(EMPTY_STRING));
return env.lifecycle()
.scheduledExecutorService(safeName, true)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.kiwiproject.dropwizard.util.lifecycle;

import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.base.KiwiStrings.f;

import io.dropwizard.setup.Environment;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -9,20 +13,26 @@
import org.kiwiproject.registry.server.RegistryService;

import java.time.Instant;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Pattern;

/**
* Utility to setup up some Dropwizard and Jetty lifecycle listeners
* Contains some "standard" static utilities related to Dropwizard and Jetty lifecycles.
*/
@UtilityClass
@Slf4j
public class StandardLifecycles {

private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
private static final String EMPTY_STRING = "";

/**
* Logs the server status
* Logs the given server status. For example you can use this to log the successful startup of a service in
* a consistent manner.
*
* @param status the status of the server
* @implNote This is logging at WARN level so that even in a production-like environment where logging levels may be set to WARN, the server
* startup will always be logged.
* @implNote This is logging at WARN level so that even in a production-like environment where logging levels
* may be set to WARN, the server startup will always be logged.
*/
public static void logServiceStatusWarningWithStatus(String status) {
LOG.warn("==========================================================================");
Expand All @@ -31,11 +41,13 @@ public static void logServiceStatusWarningWithStatus(String status) {
}

/**
* Adds lifecycle listeners to the Dropwizard environment that will register and unregister the service at startup and shutdown, respectively
* Adds lifecycle listeners to the Dropwizard environment that will register and unregister the service at
* startup and shutdown, respectively.
*
* @param registryService a pre-built {@link RegistryService} that will connect to a service discovery for registration/unregistration
* @param serviceInfo the metadata about the service (i.e. name, version, etc)
* @param environment the Dropwizard environment
* @param registryService a pre-built {@link RegistryService} that will connect to a service discovery for
* registration/un-registration
* @param serviceInfo the metadata about the service (i.e. name, version, etc)
* @param environment the Dropwizard environment
*/
public static void addRegistryLifecycleListeners(RegistryService registryService,
ServiceInfo serviceInfo,
Expand Down Expand Up @@ -64,8 +76,8 @@ public static void addServerConnectorLoggingLifecycleListener(Environment enviro
/**
* Adds a lifecycle listener that logs the current process id on startup.
*
* @param processId the process id or null if unable to find it
* @param environment the Dropwizard environment
* @param processId the process id or null if unable to find it
* @param environment the Dropwizard environment
*/
public static void addProcessIdLoggingLifecycleListener(Long processId, Environment environment) {
environment.lifecycle().addServerLifecycleListener(new ProcessIdLoggingServerLifecycleListener(processId));
Expand All @@ -74,10 +86,29 @@ public static void addProcessIdLoggingLifecycleListener(Long processId, Environm
/**
* Adds a lifecycle listener to print out the status of the server with configured ports at startup.
*
* @param serviceInfo the metadata about the service (i.e. name, version, etc)
* @param environment the Dropwizard environment
* @param serviceInfo the metadata about the service (i.e. name, version, etc)
* @param environment the Dropwizard environment
*/
public static void addServiceRunningLifecycleListener(ServiceInfo serviceInfo, Environment environment) {
environment.lifecycle().addServerLifecycleListener(new ServerStatusServerLifecycleListener(serviceInfo));
}

/**
* Create a new {@link ScheduledExecutorService} whose lifecycle is managed by Dropwizard.
*
* @param env the Dropwizard environment
* @param name the name of the executor (whitespace will be removed e.g. "My Executor" will become "MyExecutor")
* @return a new ScheduledExecutorService instance attached to the lifecycle of the given Dropwizard environment
* @see Environment#lifecycle()
* @see io.dropwizard.lifecycle.setup.LifecycleEnvironment#scheduledExecutorService(String)
*/
public static ScheduledExecutorService newScheduledExecutor(Environment env, String name) {
checkArgumentNotNull(env, "Dropwizard Environment must not be null");
checkArgumentNotBlank(name, "name must not be blank");

var safeName = f("Scheduled-{}-%d", WHITESPACE_PATTERN.matcher(name).replaceAll(EMPTY_STRING));
return env.lifecycle()
.scheduledExecutorService(safeName, true)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.kiwiproject.dropwizard.util.concurrent;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

@Slf4j
public class TestExecutors {

/**
* Perform actions using the given executor inside the Consumer, then shut the executor service down and
* await termination (with timeout).
*/
public static void use(ScheduledExecutorService executor, Consumer<ScheduledExecutorService> consumer)
throws InterruptedException {

try {
consumer.accept(executor);
} finally {
executor.shutdown();
var terminatedOk = executor.awaitTermination(500, TimeUnit.MILLISECONDS);
LOG.info("terminated OK within timeout period? {}", terminatedOk);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.kiwiproject.base.KiwiEnvironment;
import org.kiwiproject.dropwizard.util.concurrent.TestExecutors;
import org.kiwiproject.dropwizard.util.config.JobSchedule;
import org.kiwiproject.dropwizard.util.health.MonitoredJobHealthCheck;
import org.kiwiproject.test.dropwizard.mockito.DropwizardMockitoMocks;
Expand Down Expand Up @@ -260,13 +261,12 @@ private boolean executedMoreThanFiveMinutesAgo(MonitoredJob job) {
void whenUsingBuilderWithCustomExecutor() throws InterruptedException {
var count = new AtomicInteger();
Runnable countingTask = count::incrementAndGet;
var executor = Executors.newSingleThreadScheduledExecutor();
var fastSchedule = JobSchedule.builder()
.initialDelay(Duration.seconds(0))
.intervalDelay(Duration.seconds(1))
.build();
TestExecutors.use(Executors.newSingleThreadScheduledExecutor(), executor -> {
var fastSchedule = JobSchedule.builder()
.initialDelay(Duration.seconds(0))
.intervalDelay(Duration.seconds(1))
.build();

try {
var job = MonitoredJobs.builder()
.name("FastJob1234")
.task(countingTask)
Expand All @@ -280,12 +280,9 @@ void whenUsingBuilderWithCustomExecutor() throws InterruptedException {
assertThat(job.getLastSuccess()).hasValueGreaterThan(Instant.now().minusSeconds(2).toEpochMilli());
assertThat(job.getLastFailure()).hasValue(0);
assertThat(job.getFailureCount()).hasValue(0);

assertHealthCheckWasRegistered(job);
} finally {
executor.shutdown();
var terminatedOk = executor.awaitTermination(100, TimeUnit.MILLISECONDS);
LOG.info("terminatedOk? {}", terminatedOk);
}
});
}

private void assertAndVerifyJob(MonitoredJob job) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package org.kiwiproject.dropwizard.util.lifecycle;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.codahale.metrics.NoopMetricRegistry;
import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
import io.dropwizard.setup.Environment;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.kiwiproject.dropwizard.util.concurrent.TestExecutors;
import org.kiwiproject.registry.config.ServiceInfo;
import org.kiwiproject.registry.management.dropwizard.RegistrationLifecycleListener;
import org.kiwiproject.registry.server.RegistryService;
import org.kiwiproject.test.dropwizard.mockito.DropwizardMockitoMocks;

import java.util.concurrent.ScheduledExecutorService;

@DisplayName("StandardLifecycles")
class StandardLifecyclesTest {

Expand Down Expand Up @@ -76,4 +85,34 @@ void shouldAddProcessIdLoggingLifecycleListener() {

}

@Nested
class NewScheduledExecutor {

@Test
void shouldRequireEnvironment() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StandardLifecycles.newScheduledExecutor(null, "My Executor"));
}

@Test
void shouldRequireName() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StandardLifecycles.newScheduledExecutor(mock(Environment.class), ""));
}

@Test
void shouldBuildScheduledExecutorService() throws InterruptedException {
var lifecycleEnvironment = new LifecycleEnvironment(new NoopMetricRegistry());
var environment = mock(Environment.class);
when(environment.lifecycle()).thenReturn(lifecycleEnvironment);
var name = "My Custom Executor";

TestExecutors.use(StandardLifecycles.newScheduledExecutor(environment, name), executor -> {
assertThat(executor).isInstanceOf(ScheduledExecutorService.class);
assertThat(executor.isShutdown()).isFalse();
assertThat(executor.isTerminated()).isFalse();
});
}
}

}

0 comments on commit af4176c

Please sign in to comment.