diff --git a/README.md b/README.md index 2a27aae..f6316c1 100644 --- a/README.md +++ b/README.md @@ -183,4 +183,11 @@ void Thread_safe_counter() { ```groovy testImplementation group: 'pl.amazingcode', name: 'threads-collider', version: "1.0.2" -``` \ No newline at end of file +``` + +### TODO + +- [ ] Add deadlock detection for ThreadCollider +- [ ] Update javadocs for timeout methods for ThreadsCollider +- [ ] Check negative times method arguments +- [ ] Check processors count diff --git a/src/main/java/pl/amazingcode/threadscollider/exceptions/UnfinishedThreads.java b/src/main/java/pl/amazingcode/threadscollider/exceptions/UnfinishedThreads.java index 1d8f60f..fe4eb1d 100644 --- a/src/main/java/pl/amazingcode/threadscollider/exceptions/UnfinishedThreads.java +++ b/src/main/java/pl/amazingcode/threadscollider/exceptions/UnfinishedThreads.java @@ -9,6 +9,11 @@ import java.lang.management.ThreadMXBean; import java.util.concurrent.TimeUnit; +/** + * Exception thrown when {@link pl.amazingcode.threadscollider.single.ThreadsCollider} or {@link + * pl.amazingcode.threadscollider.multi.MultiThreadsCollider} has not finished threads within the + * specified timeout. + */ public final class UnfinishedThreads extends RuntimeException { private static final String MESSAGE = @@ -25,6 +30,13 @@ private UnfinishedThreads(String message) { super(message); } + /** + * Creates new instance of {@link UnfinishedThreads}. + * + * @param timeout - threads timeout + * @param timeUnit - timeout time unit + * @return {@link UnfinishedThreads} + */ public static UnfinishedThreads becauseTimeoutExceeded(long timeout, TimeUnit timeUnit) { return new UnfinishedThreads(format(MESSAGE, timeout, timeUnit, threadDump())); diff --git a/src/main/java/pl/amazingcode/threadscollider/multi/Action.java b/src/main/java/pl/amazingcode/threadscollider/multi/Action.java new file mode 100644 index 0000000..4a31fe8 --- /dev/null +++ b/src/main/java/pl/amazingcode/threadscollider/multi/Action.java @@ -0,0 +1,37 @@ +package pl.amazingcode.threadscollider.multi; + +import java.util.Optional; + +class Action { + + private final Runnable runnable; + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private final Optional actionName; + + private Action(Runnable runnable, Optional actionName) { + + this.runnable = runnable; + this.actionName = actionName; + } + + static Action of(Runnable runnable, String actionName) { + + return new Action(runnable, Optional.ofNullable(actionName)); + } + + static Action of(Runnable runnable) { + + return new Action(runnable, Optional.empty()); + } + + Runnable runnable() { + + return runnable; + } + + Optional actionName() { + + return actionName; + } +} diff --git a/src/main/java/pl/amazingcode/threadscollider/multi/MandatoryActionBuilder.java b/src/main/java/pl/amazingcode/threadscollider/multi/MandatoryActionBuilder.java index 15a6aa1..37e1d76 100644 --- a/src/main/java/pl/amazingcode/threadscollider/multi/MandatoryActionBuilder.java +++ b/src/main/java/pl/amazingcode/threadscollider/multi/MandatoryActionBuilder.java @@ -10,4 +10,13 @@ public interface MandatoryActionBuilder { * @return {@link TimesBuilder} */ TimesBuilder withAction(Runnable action); + + /** + * Set action to be executed. + * + * @param action action to be executed + * @param actionName description of action used when reporting deadlocked threads. + * @return {@link TimesBuilder} + */ + TimesBuilder withAction(Runnable action, String actionName); } diff --git a/src/main/java/pl/amazingcode/threadscollider/multi/MultiOptionalBuilder.java b/src/main/java/pl/amazingcode/threadscollider/multi/MultiOptionalBuilder.java index 19f2c3e..0446fe2 100644 --- a/src/main/java/pl/amazingcode/threadscollider/multi/MultiOptionalBuilder.java +++ b/src/main/java/pl/amazingcode/threadscollider/multi/MultiOptionalBuilder.java @@ -20,7 +20,9 @@ public interface MultiOptionalBuilder { * same value as timeout for {@link MultiThreadsCollider#collide()}. * * @param timeout - await termination timeout for executor service used by {@link - * ThreadsCollider}. + * MultiThreadsCollider} and for running threads. In case of presence of running threads + * timeout, total {@link MultiThreadsCollider} timeout is sum of executor service timeout and + * running threads timeout (timeout * 2). * @return {@link MultiTimeUnitBuilder} */ MultiTimeUnitBuilder withAwaitTerminationTimeout(long timeout); diff --git a/src/main/java/pl/amazingcode/threadscollider/multi/MultiThreadsCollider.java b/src/main/java/pl/amazingcode/threadscollider/multi/MultiThreadsCollider.java index 0900b54..3e8ab25 100644 --- a/src/main/java/pl/amazingcode/threadscollider/multi/MultiThreadsCollider.java +++ b/src/main/java/pl/amazingcode/threadscollider/multi/MultiThreadsCollider.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -27,7 +28,7 @@ public final class MultiThreadsCollider implements AutoCloseable { return thread; }; - private final List runnables; + private final List runnables; private final List times; private final ExecutorService executor; private final int threadsCount; @@ -39,7 +40,7 @@ public final class MultiThreadsCollider implements AutoCloseable { private final Consumer threadsExceptionsConsumer; private MultiThreadsCollider( - List runnables, + List runnables, List times, int threadsCount, long timeout, @@ -67,14 +68,14 @@ private MultiThreadsCollider( public void collide() { try { - Iterator runnableIterator = runnables.iterator(); + Iterator runnableIterator = runnables.iterator(); Iterator timesIterator = times.iterator(); while (runnableIterator.hasNext()) { - Runnable runnable = runnableIterator.next(); + Action action = runnableIterator.next(); int times = timesIterator.next(); for (int i = 0; i < times; i++) { - executor.execute(() -> decorate(runnable)); + executor.execute(() -> decorate(action)); } } @@ -92,9 +93,11 @@ public void collide() { } } - private void decorate(Runnable runnable) { + private void decorate(Action action) { try { + setThreadName(action.actionName()); + startedThreadsCount.incrementAndGet(); while (startedThreadsCount.get() < threadsCount) @@ -103,7 +106,7 @@ private void decorate(Runnable runnable) { while (spinLock.get()) ; - runnable.run(); + action.runnable().run(); } catch (Exception exception) { consumeException(exception); } finally { @@ -111,6 +114,14 @@ private void decorate(Runnable runnable) { } } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private void setThreadName(Optional actionName) { + + actionName + .map(name -> Thread.currentThread().getName() + " [" + name + "]") + .ifPresent(Thread.currentThread()::setName); + } + /** Shuts down the executor service and waits for all threads to finish by given timeout. */ @Override public void close() { @@ -137,7 +148,7 @@ public static class MultiThreadsColliderBuilder MultiTimeUnitBuilder, MultiOptionalBuilder { - private final List runnables; + private final List runnables; private final List times; private long timeout = DEFAULT_TIMEOUT; private TimeUnit timeUnit = DEFAULT_TIME_UNIT; @@ -162,7 +173,14 @@ public static MandatoryActionBuilder multiThreadsCollider() { @Override public TimesBuilder withAction(Runnable runnable) { - this.runnables.add(runnable); + this.runnables.add(Action.of(runnable)); + return this; + } + + @Override + public TimesBuilder withAction(Runnable runnable, String actionName) { + + this.runnables.add(Action.of(runnable, actionName)); return this; } diff --git a/src/main/java/pl/amazingcode/threadscollider/multi/OptionalActionBuilder.java b/src/main/java/pl/amazingcode/threadscollider/multi/OptionalActionBuilder.java index bfc73c2..b0a11b9 100644 --- a/src/main/java/pl/amazingcode/threadscollider/multi/OptionalActionBuilder.java +++ b/src/main/java/pl/amazingcode/threadscollider/multi/OptionalActionBuilder.java @@ -13,6 +13,15 @@ public interface OptionalActionBuilder { */ TimesBuilder withAction(Runnable action); + /** + * Sets action to be executed by {@link MultiThreadsCollider}. + * + * @param action action to be executed by {@link MultiThreadsCollider} + * @param actionName description of action used when reporting deadlocked threads. + * @return {@link TimesBuilder} + */ + TimesBuilder withAction(Runnable action, String actionName); + /** * Sets await termination timeout for executor service used by {@link MultiThreadsCollider}. * diff --git a/src/test/java/pl/amazingcode/threadscollider/multi/Deadlock_Scenarios.java b/src/test/java/pl/amazingcode/threadscollider/multi/Deadlock_Scenarios.java index a9cb90f..2d2a72e 100644 --- a/src/test/java/pl/amazingcode/threadscollider/multi/Deadlock_Scenarios.java +++ b/src/test/java/pl/amazingcode/threadscollider/multi/Deadlock_Scenarios.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.RepeatedTest; @@ -43,6 +44,7 @@ void setUp() { list2 = new ArrayList<>(); } + @Disabled @RepeatedTest(10) void Detect_deadlock() { // Given @@ -51,9 +53,9 @@ void Detect_deadlock() { // When try (MultiThreadsCollider collider = multiThreadsCollider() - .withAction(() -> update1(list1, list2)) + .withAction(() -> update1(list1, list2), "update1") .times(ACTION_THREADS_COUNT) - .withAction(() -> update2(list2, list1)) + .withAction(() -> update2(list2, list1), "update2") .times(ACTION_THREADS_COUNT) .withThreadsExceptionsConsumer(exceptions::add) .withAwaitTerminationTimeout(100)