diff --git a/src/main/java/com/db/javaschool/tdd/babakov/homework/CycleFoundException.java b/src/main/java/com/db/javaschool/tdd/babakov/homework/CycleFoundException.java new file mode 100644 index 0000000..be06faf --- /dev/null +++ b/src/main/java/com/db/javaschool/tdd/babakov/homework/CycleFoundException.java @@ -0,0 +1,23 @@ +package com.db.javaschool.tdd.babakov.homework; + +public class CycleFoundException extends Exception { + protected CycleFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public CycleFoundException(Throwable cause) { + super(cause); + } + + public CycleFoundException(String message, Throwable cause) { + super(message, cause); + } + + public CycleFoundException(String message) { + super(message); + } + + public CycleFoundException() { + super(); + } +} diff --git a/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNode.java b/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNode.java new file mode 100644 index 0000000..4fe2608 --- /dev/null +++ b/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNode.java @@ -0,0 +1,8 @@ +package com.db.javaschool.tdd.babakov.homework; + +import java.util.Collection; + +public interface RunnableNode { + void run(); + Collection getChildNodes(); +} diff --git a/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutor.java b/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutor.java new file mode 100644 index 0000000..cce094b --- /dev/null +++ b/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutor.java @@ -0,0 +1,9 @@ +package com.db.javaschool.tdd.babakov.homework; + +import java.util.concurrent.ExecutionException; + +public interface RunnableNodesExecutor { + void execute(RunnableNode firstNode) throws CycleFoundException, ExecutionException, InterruptedException; + + +} diff --git a/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutorImpl.java b/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutorImpl.java new file mode 100644 index 0000000..b3b5ced --- /dev/null +++ b/src/main/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutorImpl.java @@ -0,0 +1,97 @@ +package com.db.javaschool.tdd.babakov.homework; + + +import java.util.*; +import java.util.concurrent.*; + +public class RunnableNodesExecutorImpl implements RunnableNodesExecutor { + + private class AdditionalInfoForRunningNode { + Collection dependentOnLatches = new ArrayList<>(); + Collection dependentOnNodes = new ArrayList<>(); + CountDownLatch ownLatch = new CountDownLatch(1); + } + + Map node2Info = new ConcurrentHashMap<>(); + + @Override + public void execute(RunnableNode firstNode) throws CycleFoundException, ExecutionException, InterruptedException { + prepareMapsAndCheckForCycle(firstNode); + + Deque deq = new ConcurrentLinkedDeque<>(); + deq.add(firstNode); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + while (!deq.isEmpty()) { + final RunnableNode currentNode = deq.pollFirst(); + + executor.submit(new Runnable() { + @Override + public void run() { + try { + AdditionalInfoForRunningNode info = node2Info.get(currentNode); + for (CountDownLatch latche : info.dependentOnLatches) { + latche.await(); + } + + currentNode.run(); + info.ownLatch.countDown(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + + if (currentNode.getChildNodes() != null) { + deq.addAll(currentNode.getChildNodes()); + } + } + executor.shutdown(); + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } + + private void prepareMapsAndCheckForCycle(RunnableNode firstNode) throws CycleFoundException { + Deque deq = new ConcurrentLinkedDeque<>(); + deq.add(firstNode); + + while (!deq.isEmpty()) { + RunnableNode currentNode = deq.pollFirst(); + AdditionalInfoForRunningNode info = createRecordIfAbsent(currentNode); + + if (currentNode == firstNode) { + info.ownLatch.countDown(); + } + + if (currentNode.getChildNodes() == null) { + continue; + } + + for (RunnableNode node : currentNode.getChildNodes()) { + AdditionalInfoForRunningNode cninfo = createRecordIfAbsent(node); + + cninfo.dependentOnLatches.add(info.ownLatch); + cninfo.dependentOnNodes.add(currentNode); + + cninfo.dependentOnNodes.addAll(info.dependentOnNodes); + + if (cninfo.dependentOnNodes.contains(node)) { + throw new CycleFoundException(); + } + + deq.add(node); + } + } + + + } + + private AdditionalInfoForRunningNode createRecordIfAbsent(RunnableNode node) { + AdditionalInfoForRunningNode info = node2Info.get(node); + if (info == null) { + info = new AdditionalInfoForRunningNode(); + node2Info.put(node, info); + } + return info; + } +} diff --git a/src/test/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutorImplTest.java b/src/test/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutorImplTest.java new file mode 100644 index 0000000..c911469 --- /dev/null +++ b/src/test/java/com/db/javaschool/tdd/babakov/homework/RunnableNodesExecutorImplTest.java @@ -0,0 +1,62 @@ +package com.db.javaschool.tdd.babakov.homework; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RunnableNodesExecutorImplTest { + RunnableNodesExecutor executor; + + @Before + public void init() { + executor = new RunnableNodesExecutorImpl(); + } + + + @Test(expected = CycleFoundException.class, timeout = 100) + public void testExecute_cyclicDependenciesTest() throws Exception { + RunnableNode cyclicTask = mock(RunnableNode.class); + RunnableNode cyclicSubtask = mock(RunnableNode.class); + when(cyclicTask.getChildNodes()).thenReturn(Collections.singletonList(cyclicSubtask)); + when(cyclicSubtask.getChildNodes()).thenReturn(Collections.singletonList(cyclicTask)); + executor.execute(cyclicTask); + } + + @Test(timeout = 100) + public void testExecute_onePossibleExecutionWay() throws Exception { + RunnableNode mainNode = mock(RunnableNode.class); + RunnableNode subNode = mock(RunnableNode.class); + when(mainNode.getChildNodes()).thenReturn(Collections.singletonList(subNode)); + executor.execute(mainNode); + + InOrder inOrder = inOrder(mainNode, subNode); + inOrder.verify(mainNode).run(); + inOrder.verify(subNode).run(); + } + + @Test(timeout = 100) + public void testExecute_fewPossibleExecutionWays() throws Exception { + RunnableNode mainNode = mock(RunnableNode.class); + RunnableNode subtask1 = mock(RunnableNode.class); + RunnableNode subtask2 = mock(RunnableNode.class); + when(mainNode.getChildNodes()).thenReturn(Arrays.asList(subtask1, subtask2)); + + executor.execute(mainNode); + + InOrder inOrder1 = inOrder(mainNode, subtask1); + inOrder1.verify(mainNode).run(); + inOrder1.verify(subtask1).run(); + + InOrder inOrder2 = inOrder(mainNode, subtask2); + inOrder2.verify(mainNode).run(); + inOrder2.verify(subtask2).run(); + } +} \ No newline at end of file