/* * Copyright 2012-2016, the original author or authors. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.flipkart.flux.controller; import akka.actor.ActorSystem; import akka.actor.Props; import akka.testkit.TestActorRef; import com.fasterxml.jackson.databind.ObjectMapper; import com.flipkart.flux.MockActorRef; import com.flipkart.flux.api.EventData; import com.flipkart.flux.api.EventDefinition; import com.flipkart.flux.api.ExecutionUpdateData; import com.flipkart.flux.api.core.TaskExecutionMessage; import com.flipkart.flux.dao.iface.AuditDAO; import com.flipkart.flux.dao.iface.EventsDAO; import com.flipkart.flux.dao.iface.StateMachinesDAO; import com.flipkart.flux.dao.iface.StatesDAO; import com.flipkart.flux.domain.*; import com.flipkart.flux.impl.message.TaskAndEvents; import com.flipkart.flux.impl.task.registry.RouterRegistry; import com.flipkart.flux.metrics.iface.MetricsClient; import com.flipkart.flux.representation.ClientElbPersistenceService; import com.flipkart.flux.task.redriver.RedriverRegistry; import com.flipkart.flux.taskDispatcher.ExecutionNodeTaskDispatcher; import com.flipkart.flux.util.TestUtils; import com.typesafe.config.ConfigFactory; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import javax.ws.rs.core.Response; import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class WorkFlowExecutionControllerTest { @Mock StateMachinesDAO stateMachinesDAO; @Mock EventsDAO eventsDAO; @Mock StatesDAO statesDAO; @Mock AuditDAO auditDAO; @Mock private RouterRegistry routerRegistry; @Mock private ExecutionNodeTaskDispatcher executionNodeTaskDispatcher; @Mock private RedriverRegistry redriverRegistry; @Mock private MetricsClient metricsClient; @Mock private ClientElbPersistenceService clientElbPersistenceService; TestActorRef<MockActorRef> mockActor; private WorkFlowExecutionController workFlowExecutionController; private ObjectMapper objectMapper; ActorSystem actorSystem; @Before public void setUp() throws Exception { Thread.sleep(1000); workFlowExecutionController = new WorkFlowExecutionController(eventsDAO, stateMachinesDAO, statesDAO, auditDAO, executionNodeTaskDispatcher, redriverRegistry, metricsClient, clientElbPersistenceService); when(stateMachinesDAO.findById(anyString())).thenReturn(TestUtils.getStandardTestMachineWithId()); when(clientElbPersistenceService.findByIdClientElb(anyString())).thenReturn("http://localhost:9997"); actorSystem = ActorSystem.create("testActorSystem",ConfigFactory.load("testAkkaActorSystem")); mockActor = TestActorRef.create(actorSystem, Props.create(MockActorRef.class)); when(routerRegistry.getRouter(anyString())).thenReturn(mockActor); objectMapper = new ObjectMapper(); } @After public void tearDown() throws Exception { actorSystem.terminate(); } @Test public void testEventPost_shouldForwardToTaskDispatcher() throws Exception { final EventData testEventData = new EventData("event0", "java.lang.String", "42", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event0")).thenReturn(new Event("event0", "java.lang.String", Event.EventStatus.pending, "standard-machine", null, null)); EventData[] expectedEvents = new EventData[]{new EventData("event0", "java.lang.String", "42", "runtime")}; when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event0")); when(executionNodeTaskDispatcher.forwardExecutionMessage(anyString(), anyObject())).thenReturn(Response.Status.ACCEPTED.getStatusCode()); workFlowExecutionController.postEvent(testEventData, "standard-machine"); State state = stateMachinesDAO.findById("standard-machine").getStates().stream().filter((s) -> s.getId() == 4L).findFirst().orElse(null); TaskExecutionMessage msg = new TaskExecutionMessage(); msg.setRouterName(WorkFlowExecutionController.getRouterName(state.getTask())); msg.setAkkaMessage(new TaskAndEvents(state.getName(), state.getTask(), state.getId(), expectedEvents, state.getStateMachineId(), "test_state_machine", state.getOutputEvent(), state.getRetryCount())); verify(executionNodeTaskDispatcher, times(1)).forwardExecutionMessage("http://localhost:9997" + "/api/execution", msg); verifyNoMoreInteractions(executionNodeTaskDispatcher); } @Test public void testEventPost_shouldNotFetchEventDataFromDBIfStateIsDependantOnSingleEvent() throws Exception { final EventData testEventData = new EventData("event1", "foo", "someStringData", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event1")).thenReturn(new Event("event1", "foo", Event.EventStatus.pending, "1", null, null)); when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event1")); workFlowExecutionController.postEvent(testEventData, "standard-machine"); // As states 2 and 3 dependant on single event there should be no more interactions with eventDAO to fetch event data verify(eventsDAO, times(0)).findByEventNamesAndSMId("standard-machine", Collections.singletonList("event1")); } @Test public void testEventPost_taskRedriveDelay() throws Exception { final EventData testEventData1 = new EventData("event1", "java.lang.String", "42", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event1")).thenReturn(new Event("event1", "java.lang.String", Event.EventStatus.pending, "1", null, null)); EventData[] expectedEvents1 = new EventData[]{new EventData("event1", "java.lang.String", "42", "runtime")}; when(eventsDAO.findByEventNamesAndSMId("standard-machine", Collections.singletonList("event1"))).thenReturn(Arrays.asList(expectedEvents1)); when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event1")); workFlowExecutionController.postEvent(testEventData1, "standard-machine"); final EventData testEventData0 = new EventData("event0", "java.lang.String", "42", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event0")).thenReturn(new Event("event0", "java.lang.String", Event.EventStatus.pending, "1", null, null)); EventData[] expectedEvents0 = new EventData[]{new EventData("event0", "java.lang.String", "42", "runtime")}; when(eventsDAO.findByEventNamesAndSMId("standard-machine", Collections.singletonList("event0"))).thenReturn(Arrays.asList(expectedEvents0)); when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event0")); workFlowExecutionController.postEvent(testEventData0, "standard-machine"); // give time to execute Thread.sleep(2000); verify(redriverRegistry).registerTask(2L, "standard-machine", 32800); //state with id 2 has 3 retries and 100ms timeout verify(redriverRegistry).registerTask(4L, "standard-machine", 8400); //state with id 4 has 1 retries and 100ms timeout } @Test public void testEventPost_shouldNotSendExecuteTaskIfItIsAlreadyCompleted() throws Exception { final EventData testEventData = new EventData("event0", "java.lang.String", "42", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event0")).thenReturn(new Event("event0", "java.lang.String", Event.EventStatus.pending, "1", null, null)); EventData[] expectedEvents = new EventData[]{new EventData("event0", "java.lang.String", "42", "runtime")}; when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event0")); when(executionNodeTaskDispatcher.forwardExecutionMessage(anyString(), anyObject())).thenReturn(Response.Status.ACCEPTED.getStatusCode()); workFlowExecutionController.postEvent(testEventData, "standard-machine"); State state = stateMachinesDAO.findById("standard-machine").getStates().stream().filter((s) -> s.getId() == 4L).findFirst().orElse(null); state.setStatus(Status.completed); //post the event again, this should not send msg to router for execution workFlowExecutionController.postEvent(testEventData, "standard-machine"); verify(executionNodeTaskDispatcher, times(1)).forwardExecutionMessage(anyString(), anyObject()); verifyNoMoreInteractions(executionNodeTaskDispatcher); } @Test public void testEventPost_shouldSendExecuteTaskIfItIsNotCompleted() throws Exception { final EventData testEventData = new EventData("event0", "java.lang.String", "42", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event0")).thenReturn(new Event("event0", "java.lang.String", Event.EventStatus.pending, "1", null, null)); EventData[] expectedEvents = new EventData[]{new EventData("event0", "java.lang.String", "42", "runtime")}; when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event0")); when(executionNodeTaskDispatcher.forwardExecutionMessage(anyString(), anyObject())).thenReturn(Response.Status.ACCEPTED.getStatusCode()); workFlowExecutionController.postEvent(testEventData, "standard-machine"); StateMachine stateMachine = stateMachinesDAO.findById("standard-machine"); State state = stateMachinesDAO.findById("standard-machine").getStates().stream().filter((s) -> s.getId() == 4L).findFirst().orElse(null); state.setStatus(Status.errored); //post the event again, this should send msg to router again for execution workFlowExecutionController.postEvent(testEventData, "standard-machine"); verify(executionNodeTaskDispatcher, times(2)).forwardExecutionMessage(anyString(), anyObject()); verifyNoMoreInteractions(executionNodeTaskDispatcher); } @Test public void testEventPost_shouldNotSendExecuteTaskIfItIsCancelled() throws Exception { final EventData testEventData = new EventData("event0", "java.lang.String", "42", "runtime"); when(eventsDAO.findBySMIdAndName("standard-machine", "event0")).thenReturn(new Event("event0", "java.lang.String", Event.EventStatus.pending, "1", null, null)); EventData[] expectedEvents = new EventData[]{testEventData}; when(eventsDAO.findTriggeredOrCancelledEventsNamesBySMId("standard-machine")).thenReturn(Collections.singletonList("event0")); when(executionNodeTaskDispatcher.forwardExecutionMessage(anyString(), anyObject())).thenReturn(Response.Status.ACCEPTED.getStatusCode()); workFlowExecutionController.postEvent(testEventData, "standard-machine"); StateMachine stateMachine = stateMachinesDAO.findById("standard-machine"); State state = stateMachine.getStates().stream().filter((s) -> s.getId() == 4L).findFirst().orElse(null); state.setStatus(Status.cancelled); //post the event again, this should not send msg to router for execution workFlowExecutionController.postEvent(testEventData, "standard-machine"); // Dispatcher Thread should only forward the task for Execution only once. verify(executionNodeTaskDispatcher, times(1)).forwardExecutionMessage(anyString(), anyObject()); } @Test public void testCancelPath_shouldCancelPathTillJoinNode() throws Exception { //setup the below state machine, and do cancel with event3, that should cancel till state3 // // state1 --------(event1)---------> state2 --------(event2)--------------------> state3 // | ^ // | | // | ---(event3)--> state5 --(event4)--- (event6) // | | | | // |----(event1)---> state4 -- |---> state7 ---- // |---(event3)--> state6 --(event5)---| // HashMap<String, Event.EventStatus> eventStatusHashMap = new HashMap<String, Event.EventStatus>() {{ put("event1", Event.EventStatus.triggered); put("event2", Event.EventStatus.triggered); put("event3", Event.EventStatus.pending); put("event4", Event.EventStatus.pending); put("event5", Event.EventStatus.pending); }}; String outputEvent1 = null; String outputEvent2 = null; String outputEvent3 = null; String outputEvent4 = null; String outputEvent5 = null; String outputEvent6 = null; try { outputEvent1 = objectMapper.writeValueAsString(new EventDefinition("event1", "SomeEvent.class")); outputEvent2 = objectMapper.writeValueAsString(new EventDefinition("event2", "SomeEvent.class")); outputEvent3 = objectMapper.writeValueAsString(new EventDefinition("event3", "SomeEvent.class")); outputEvent4 = objectMapper.writeValueAsString(new EventDefinition("event4", "SomeEvent.class")); outputEvent5 = objectMapper.writeValueAsString(new EventDefinition("event5", "SomeEvent.class")); outputEvent6 = objectMapper.writeValueAsString(new EventDefinition("event6", "SomeEvent.class")); } catch (Exception e) { e.printStackTrace(); } State state1 = new State(1L, "state1", null, null, null, null, new ArrayList<>(), 0L, 1000L, outputEvent1, Status.initialized, null, 0L, "state-machine-cancel-path", 1L); State state2 = new State(1L, "state2", null, null, null, null, new ArrayList<String>() {{ add("event1"); }}, 0L, 1000L, outputEvent2, Status.initialized, null, 0L, "state-machine-cancel-path", 2L); State state3 = new State(1L, "state3", null, null, null, null, new ArrayList<String>() {{ add("event2"); add("event6"); }}, 0L, 1000L, null, Status.initialized, null, 0L, "state-machine-cancel-path", 3L); State state4 = new State(1L, "state4", null, null, null, null, new ArrayList<String>() {{ add("event1"); }}, 0L, 1000L, outputEvent3, Status.initialized, null, 0L, "state-machine-cancel-path", 4L); State state5 = new State(1L, "state5", null, null, null, null, new ArrayList<String>() {{ add("event3"); }}, 0L, 1000L, outputEvent4, Status.initialized, null, 0L, "state-machine-cancel-path", 5L); State state6 = new State(1L, "state6", null, null, null, null, new ArrayList<String>() {{ add("event3"); }}, 0L, 1000L, outputEvent5, Status.initialized, null, 0L, "state-machine-cancel-path", 6L); State state7 = new State(1L, "state7", null, null, null, null, new ArrayList<String>() {{ add("event4"); add("event5"); }}, 0L, 1000L, outputEvent6, Status.initialized, null, 0L, "state-machine-cancel-path", 7L); Set<State> states = new HashSet<State>() {{ add(state1); add(state2); add(state3); add(state4); add(state5); add(state6); add(state7); }}; StateMachine stateMachine = new StateMachine("state-machine-cancel-path", 1L, "state_machine_1", null, states, "client_elb_id_1"); EventData testEventData = new EventData("event3", null, null, "runtime", true); when(eventsDAO.getAllEventsNameAndStatus("state-machine-cancel-path", true)).thenReturn(eventStatusHashMap); when(stateMachinesDAO.findById("state-machine-cancel-path")).thenReturn(stateMachine); // invoke cancel Set<State> executableStates = workFlowExecutionController.cancelPath(stateMachine.getId(), testEventData); assertThat(executableStates.size()).isEqualTo(1); assertThat(executableStates.contains("state3")); verify(eventsDAO).markEventAsCancelled("state-machine-cancel-path", "event3"); verify(eventsDAO).markEventAsCancelled("state-machine-cancel-path", "event4"); verify(eventsDAO).markEventAsCancelled("state-machine-cancel-path", "event5"); verify(eventsDAO).markEventAsCancelled("state-machine-cancel-path", "event6"); verify(statesDAO).updateStatus("state-machine-cancel-path", 5L, Status.cancelled); verify(statesDAO).updateStatus("state-machine-cancel-path", 6L, Status.cancelled); verify(statesDAO).updateStatus("state-machine-cancel-path", 7L, Status.cancelled); } @Test public void testUpdateTaskStatusBehaviourWhenTaskStatusIsUpdatedToRunning() { workFlowExecutionController.updateTaskStatus("random-state-machine", 1L, new ExecutionUpdateData("random-state-machine", "someStateMachine", "someTask", 1L, com.flipkart.flux.api.Status.running, 0, 1, "", false)); verify(statesDAO).updateStatus("random-state-machine", 1L, Status.running); verify(auditDAO).create("random-state-machine", new AuditRecord("random-state-machine", 1L, 1L, Status.running, null , "")); verifyNoMoreInteractions(redriverRegistry); } @Test public void testUpdateTaskStatusBehaviourWhenTaskStatusIsUpdatedToCompleted() { workFlowExecutionController.updateTaskStatus("random-state-machine", 1L, new ExecutionUpdateData("random-state-machine", "someStateMachine", "someTask", 1L, com.flipkart.flux.api.Status.completed, 0, 1, "", true)); verify(statesDAO).updateStatus("random-state-machine", 1L, Status.completed); verify(auditDAO).create("random-state-machine", new AuditRecord("random-state-machine", 1L, 1L, Status.completed, null , "")); verify(redriverRegistry).deRegisterTask("random-state-machine",1L ); } }