diff --git a/src/main/java/spark/FilterImpl.java b/src/main/java/spark/FilterImpl.java index a138fd6726..2a5b29115f 100644 --- a/src/main/java/spark/FilterImpl.java +++ b/src/main/java/spark/FilterImpl.java @@ -27,7 +27,7 @@ */ public abstract class FilterImpl implements Filter, Wrapper { - static final String DEFAULT_ACCEPT_TYPE = "*/*"; + public static final String DEFAULT_ACCEPT_TYPE = "*/*"; private String path; private String acceptType; @@ -63,7 +63,7 @@ static FilterImpl create(final String path, final Filter filter) { * @param filter the filter * @return the wrapped route */ - static FilterImpl create(final String path, String acceptType, final Filter filter) { + public static FilterImpl create(final String path, String acceptType, final Filter filter) { if (acceptType == null) { acceptType = DEFAULT_ACCEPT_TYPE; } diff --git a/src/main/java/spark/RouteImpl.java b/src/main/java/spark/RouteImpl.java index 543dfefc15..1f102d23a1 100644 --- a/src/main/java/spark/RouteImpl.java +++ b/src/main/java/spark/RouteImpl.java @@ -26,7 +26,7 @@ * @author Per Wendel */ public abstract class RouteImpl implements Route, Wrapper { - static final String DEFAULT_ACCEPT_TYPE = "*/*"; + public static final String DEFAULT_ACCEPT_TYPE = "*/*"; private String path; private String acceptType; diff --git a/src/main/java/spark/route/Routes.java b/src/main/java/spark/route/Routes.java index 518d050314..2478c06b74 100644 --- a/src/main/java/spark/route/Routes.java +++ b/src/main/java/spark/route/Routes.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import spark.FilterImpl; import spark.RouteImpl; @@ -49,7 +50,7 @@ public static Routes create() { * Constructor */ protected Routes() { - routes = new ArrayList<>(); + routes = new CopyOnWriteArrayList<>(); } /** diff --git a/src/test/java/spark/route/RoutesConcurrencyTest.java b/src/test/java/spark/route/RoutesConcurrencyTest.java new file mode 100644 index 0000000000..2fd97086a2 --- /dev/null +++ b/src/test/java/spark/route/RoutesConcurrencyTest.java @@ -0,0 +1,103 @@ +package spark.route; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; + +import spark.FilterImpl; +import spark.RouteImpl; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +public class RoutesConcurrencyTest { + private final AtomicInteger numberOfSuccessfulIterations = new AtomicInteger(); + private final AtomicInteger numberOfFailedIterations = new AtomicInteger(); + private ExecutorService executorService; + + @Rule + public final ErrorCollector collector = new ErrorCollector(); + + private static final int NUMBER_OF_ITERATIONS = 10_000; + private static final int NUMBER_OF_THREADS = 2; + private static final int NUMBER_OF_TASKS = NUMBER_OF_THREADS; + + private static final String ROUTE_PATH_PREFIX = "/route/"; + private static final String FILTER_PATH_PREFIX = "/filter/"; + + @Before + public void setup() { + numberOfSuccessfulIterations.set(0); + numberOfFailedIterations.set(0); + } + + @After + public void teardown() { + if (executorService != null && !executorService.isShutdown()) { + try { + executorService.shutdownNow(); + if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { + System.err.println("Executor service did not terminate."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + @Test + public void classShouldBeThreadSafe() throws Exception { + executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS); + List> tasks = partitionIterationsIntoTasks(); + List> futureResults = executorService.invokeAll(tasks); + executorService.shutdown(); + for (Future futureResult : futureResults) { + futureResult.get(); + collector.checkThat(futureResult.isDone(), is(true)); + } + collector.checkThat(numberOfSuccessfulIterations.intValue(), equalTo(NUMBER_OF_ITERATIONS)); + collector.checkThat(numberOfFailedIterations.intValue(), equalTo(0)); + } + + private List> partitionIterationsIntoTasks() { + final List> tasks = new ArrayList<>(); + final Routes routes = Routes.create(); + final int numberOfIterationsPerTask = NUMBER_OF_ITERATIONS / NUMBER_OF_TASKS; + for (int taskIndex = 0; taskIndex < NUMBER_OF_TASKS; taskIndex++) { + final int fromIteration = numberOfIterationsPerTask * taskIndex; + final int toIteration = numberOfIterationsPerTask * (taskIndex + 1); + tasks.add(() -> { + for (int iterationIndex = fromIteration; iterationIndex < toIteration; iterationIndex++) { + try { + String routePath = ROUTE_PATH_PREFIX + iterationIndex; + String filterPath = FILTER_PATH_PREFIX + iterationIndex; + routes.add(HttpMethod.get, RouteImpl.create(routePath, RouteImpl.DEFAULT_ACCEPT_TYPE, null)); + routes.add(HttpMethod.get, FilterImpl.create(filterPath, FilterImpl.DEFAULT_ACCEPT_TYPE, null)); + routes.find(HttpMethod.get, routePath, RouteImpl.DEFAULT_ACCEPT_TYPE); + routes.findMultiple(HttpMethod.get, filterPath, FilterImpl.DEFAULT_ACCEPT_TYPE); + routes.findAll(); + routes.remove(routePath, HttpMethod.get.toString()); + routes.remove(filterPath); + routes.clear(); + numberOfSuccessfulIterations.getAndIncrement(); + } catch (Exception e) { + numberOfFailedIterations.getAndIncrement(); + } + } + return null; + }); + } + return tasks; + } +}