/* * Copyright © 2015-2016 Cask Data, Inc. * * 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 co.cask.cdap.internal.app.services.http.handlers; import co.cask.cdap.AppWithSchedule; import co.cask.cdap.AppWithStreamSizeSchedule; import co.cask.cdap.AppWithWorkflow; import co.cask.cdap.ConcurrentWorkflowApp; import co.cask.cdap.ConditionalWorkflowApp; import co.cask.cdap.PauseResumeWorklowApp; import co.cask.cdap.WorkflowAppWithErrorRuns; import co.cask.cdap.WorkflowAppWithFork; import co.cask.cdap.WorkflowAppWithLocalDatasets; import co.cask.cdap.WorkflowAppWithScopedParameters; import co.cask.cdap.WorkflowFailureInForkApp; import co.cask.cdap.WorkflowTokenTestPutApp; import co.cask.cdap.api.schedule.ScheduleSpecification; import co.cask.cdap.api.workflow.WorkflowActionNode; import co.cask.cdap.api.workflow.WorkflowActionSpecification; import co.cask.cdap.api.workflow.WorkflowToken; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.utils.Tasks; import co.cask.cdap.gateway.handlers.WorkflowHttpHandler; import co.cask.cdap.internal.app.services.http.AppFabricTestBase; import co.cask.cdap.proto.DatasetSpecificationSummary; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.ProgramRunStatus; import co.cask.cdap.proto.ProgramStatus; import co.cask.cdap.proto.ProgramType; import co.cask.cdap.proto.RunRecord; import co.cask.cdap.proto.ScheduledRuntime; import co.cask.cdap.proto.StreamProperties; import co.cask.cdap.proto.WorkflowNodeStateDetail; import co.cask.cdap.proto.WorkflowTokenDetail; import co.cask.cdap.proto.WorkflowTokenNodeDetail; import co.cask.cdap.proto.codec.ScheduleSpecificationCodec; import co.cask.cdap.proto.codec.WorkflowActionSpecificationCodec; import co.cask.cdap.proto.codec.WorkflowTokenDetailCodec; import co.cask.cdap.proto.codec.WorkflowTokenNodeDetailCodec; import co.cask.cdap.proto.id.Ids; import co.cask.cdap.proto.id.ProgramId; import co.cask.cdap.test.XSlowTests; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import org.apache.http.HttpResponse; import org.apache.http.util.EntityUtils; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import org.junit.experimental.categories.Category; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.lang.reflect.Type; import java.nio.file.Files; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * Tests for {@link WorkflowHttpHandler} */ public class WorkflowHttpHandlerTest extends AppFabricTestBase { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(ScheduleSpecification.class, new ScheduleSpecificationCodec()) .registerTypeAdapter(WorkflowActionSpecification.class, new WorkflowActionSpecificationCodec()) .registerTypeAdapter(WorkflowTokenDetail.class, new WorkflowTokenDetailCodec()) .registerTypeAdapter(WorkflowTokenNodeDetail.class, new WorkflowTokenNodeDetailCodec()) .create(); private static final Type LIST_WORKFLOWACTIONNODE_TYPE = new TypeToken<List<WorkflowActionNode>>() { }.getType(); private static final Type MAP_STRING_TO_WORKFLOWNODESTATEDETAIL_TYPE = new TypeToken<Map<String, WorkflowNodeStateDetail>>() { }.getType(); private static final Type MAP_STRING_TO_DATASETSPECIFICATIONSUMMARY_TYPE = new TypeToken<Map<String, DatasetSpecificationSummary>>() { }.getType(); private void verifyRunningProgramCount(final Id.Program program, final String runId, final int expected) throws Exception { Tasks.waitFor(expected, new Callable<Integer>() { @Override public Integer call() throws Exception { return runningProgramCount(program, runId); } }, 10, TimeUnit.SECONDS); } private Integer runningProgramCount(Id.Program program, String runId) throws Exception { String path = String.format("apps/%s/workflows/%s/runs/%s/current", program.getApplicationId(), program.getId(), runId); HttpResponse response = doGet(getVersionedAPIPath(path, program.getNamespaceId())); if (response.getStatusLine().getStatusCode() == 200) { String json = EntityUtils.toString(response.getEntity()); List<WorkflowActionNode> output = GSON.fromJson(json, LIST_WORKFLOWACTIONNODE_TYPE); return output.size(); } return null; } private void suspendWorkflow(Id.Program program, String runId, int expectedStatusCode) throws Exception { String path = String.format("apps/%s/workflows/%s/runs/%s/suspend", program.getApplicationId(), program.getId(), runId); HttpResponse response = doPost(getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, program.getNamespaceId())); Assert.assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode()); } private void setAndTestRuntimeArgs(Id.Program programId, Map<String, String> args) throws Exception { HttpResponse response; String argString = GSON.toJson(args, new TypeToken<Map<String, String>>() { }.getType()); String path = String.format("apps/%s/workflows/%s/runtimeargs", programId.getApplicationId(), programId.getId()); String versionedRuntimeArgsUrl = getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, programId.getNamespaceId()); response = doPut(versionedRuntimeArgsUrl, argString); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); response = doGet(versionedRuntimeArgsUrl); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); String responseEntity = EntityUtils.toString(response.getEntity()); Map<String, String> argsRead = GSON.fromJson(responseEntity, new TypeToken<Map<String, String>>() { }.getType()); Assert.assertEquals(args.size(), argsRead.size()); } /** * Tries to resume a Workflow and expect the call completed with the status. */ private void resumeWorkflow(Id.Program program, String runId, int expectedStatusCode) throws Exception { String path = String.format("apps/%s/workflows/%s/runs/%s/resume", program.getApplicationId(), program.getId(), runId); HttpResponse response = doPost(getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, program.getNamespaceId())); Assert.assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode()); } private HttpResponse getWorkflowCurrentStatus(Id.Program program, String runId) throws Exception { String currentUrl = String.format("apps/%s/workflows/%s/runs/%s/current", program.getApplicationId(), program.getId(), runId); String versionedUrl = getVersionedAPIPath(currentUrl, Constants.Gateway.API_VERSION_3_TOKEN, program.getNamespaceId()); return doGet(versionedUrl); } private String createInput(String folderName) throws IOException { File inputDir = tmpFolder.newFolder(folderName); File inputFile = new File(inputDir.getPath() + "/words.txt"); try (BufferedWriter writer = Files.newBufferedWriter(inputFile.toPath(), Charsets.UTF_8)) { writer.write("this text has"); writer.newLine(); writer.write("two words text inside"); } return inputDir.getAbsolutePath(); } /** * Returns a list of {@link ScheduledRuntime}. * * @param program the program id * @param next if true, fetch the list of future run times. If false, fetch the list of past run times. */ private List<ScheduledRuntime> getScheduledRunTime(Id.Program program, boolean next) throws Exception { String nextRunTimeUrl = String.format("apps/%s/workflows/%s/%sruntime", program.getApplicationId(), program.getId(), next ? "next" : "previous"); String versionedUrl = getVersionedAPIPath(nextRunTimeUrl, Constants.Gateway.API_VERSION_3_TOKEN, program.getNamespaceId()); HttpResponse response = doGet(versionedUrl); return readResponse(response, new TypeToken<List<ScheduledRuntime>>() { }.getType()); } private String getLocalDatasetPath(ProgramId workflowId, String runId) { String path = String.format("apps/%s/workflows/%s/runs/%s/localdatasets", workflowId.getApplication(), workflowId.getProgram(), runId); return getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespace()); } private Map<String, DatasetSpecificationSummary> getWorkflowLocalDatasets(ProgramId workflowId, String runId) throws Exception { HttpResponse response = doGet(getLocalDatasetPath(workflowId, runId)); return readResponse(response, MAP_STRING_TO_DATASETSPECIFICATIONSUMMARY_TYPE); } private void deleteWorkflowLocalDatasets(ProgramId workflowId, String runId) throws Exception { doDelete(getLocalDatasetPath(workflowId, runId)); } @Test public void testLocalDatasetDeletion() throws Exception { String keyValueTableType = "co.cask.cdap.api.dataset.lib.KeyValueTable"; String filesetType = "co.cask.cdap.api.dataset.lib.FileSet"; Map<String, String> keyValueTableProperties = ImmutableMap.of("foo", "bar"); Map<String, String> filesetProperties = ImmutableMap.of("anotherFoo", "anotherBar"); HttpResponse response = deploy(WorkflowAppWithLocalDatasets.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); File waitFile = new File(tmpFolder.newFolder() + "/wait.file"); File doneFile = new File(tmpFolder.newFolder() + "/done.file"); ProgramId workflowId = new ProgramId(TEST_NAMESPACE2, WorkflowAppWithLocalDatasets.NAME, ProgramType.WORKFLOW, WorkflowAppWithLocalDatasets.WORKFLOW_NAME); startProgram(workflowId.toId(), ImmutableMap.of("wait.file", waitFile.getAbsolutePath(), "done.file", doneFile.getAbsolutePath(), "dataset.*.keep.local", "true")); while (!waitFile.exists()) { TimeUnit.MILLISECONDS.sleep(50); } String runId = getRunIdOfRunningProgram(workflowId.toId()); doneFile.createNewFile(); waitState(workflowId.toId(), ProgramStatus.STOPPED.name()); Map<String, DatasetSpecificationSummary> localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId); Assert.assertEquals(2, localDatasetSummaries.size()); DatasetSpecificationSummary keyValueTableSummary = new DatasetSpecificationSummary("MyTable." + runId, keyValueTableType, keyValueTableProperties); Assert.assertEquals(keyValueTableSummary, localDatasetSummaries.get("MyTable")); DatasetSpecificationSummary filesetSummary = new DatasetSpecificationSummary("MyFile." + runId, filesetType, filesetProperties); Assert.assertEquals(filesetSummary, localDatasetSummaries.get("MyFile")); deleteWorkflowLocalDatasets(workflowId, runId); localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId); Assert.assertEquals(0, localDatasetSummaries.size()); waitFile = new File(tmpFolder.newFolder() + "/wait.file"); doneFile = new File(tmpFolder.newFolder() + "/done.file"); startProgram(workflowId.toId(), ImmutableMap.of("wait.file", waitFile.getAbsolutePath(), "done.file", doneFile.getAbsolutePath(), "dataset.MyTable.keep.local", "true")); while (!waitFile.exists()) { TimeUnit.MILLISECONDS.sleep(50); } runId = getRunIdOfRunningProgram(workflowId.toId()); doneFile.createNewFile(); waitState(workflowId.toId(), ProgramStatus.STOPPED.name()); localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId); Assert.assertEquals(1, localDatasetSummaries.size()); keyValueTableSummary = new DatasetSpecificationSummary("MyTable." + runId, keyValueTableType, keyValueTableProperties); Assert.assertEquals(keyValueTableSummary, localDatasetSummaries.get("MyTable")); deleteWorkflowLocalDatasets(workflowId, runId); localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId); Assert.assertEquals(0, localDatasetSummaries.size()); } @Test public void testWorkflowPauseResume() throws Exception { String pauseResumeWorkflowApp = "PauseResumeWorkflowApp"; String pauseResumeWorkflow = "PauseResumeWorkflow"; // Files used to synchronize between this test and workflow execution File firstSimpleActionFile = new File(tmpFolder.newFolder() + "/firstsimpleaction.file"); File firstSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/firstsimpleaction.file.done"); File forkedSimpleActionFile = new File(tmpFolder.newFolder() + "/forkedsimpleaction.file"); File forkedSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/forkedsimpleaction.file.done"); File anotherForkedSimpleActionFile = new File(tmpFolder.newFolder() + "/anotherforkedsimpleaction.file"); File anotherForkedSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/anotherforkedsimpleaction.file.done"); File lastSimpleActionFile = new File(tmpFolder.newFolder() + "/lastsimpleaction.file"); File lastSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/lastsimpleaction.file.done"); HttpResponse response = deploy(PauseResumeWorklowApp.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Id.Program programId = Id.Program.from(TEST_NAMESPACE2, pauseResumeWorkflowApp, ProgramType.WORKFLOW, pauseResumeWorkflow); Map<String, String> runtimeArguments = Maps.newHashMap(); runtimeArguments.put("first.simple.action.file", firstSimpleActionFile.getAbsolutePath()); runtimeArguments.put("first.simple.action.donefile", firstSimpleActionDoneFile.getAbsolutePath()); runtimeArguments.put("forked.simple.action.file", forkedSimpleActionFile.getAbsolutePath()); runtimeArguments.put("forked.simple.action.donefile", forkedSimpleActionDoneFile.getAbsolutePath()); runtimeArguments.put("anotherforked.simple.action.file", anotherForkedSimpleActionFile.getAbsolutePath()); runtimeArguments.put("anotherforked.simple.action.donefile", anotherForkedSimpleActionDoneFile.getAbsolutePath()); runtimeArguments.put("last.simple.action.file", lastSimpleActionFile.getAbsolutePath()); runtimeArguments.put("last.simple.action.donefile", lastSimpleActionDoneFile.getAbsolutePath()); setAndTestRuntimeArgs(programId, runtimeArguments); // Start the Workflow startProgram(programId, 200); // Workflow should be running waitState(programId, ProgramStatus.RUNNING.name()); // Get runid for the running Workflow String runId = getRunIdOfRunningProgram(programId); while (!firstSimpleActionFile.exists()) { TimeUnit.MILLISECONDS.sleep(50); } // Only one Workflow node should be running verifyRunningProgramCount(programId, runId, 1); // Suspend the Workflow suspendWorkflow(programId, runId, 200); // Workflow status hould be SUSPENDED waitState(programId, ProgramStatus.STOPPED.name()); // Meta store information for this Workflow should reflect suspended run verifyProgramRuns(programId, "suspended"); // Suspending the already suspended Workflow should give CONFLICT suspendWorkflow(programId, runId, 409); // Signal the FirstSimpleAction in the Workflow to continue Assert.assertTrue(firstSimpleActionDoneFile.createNewFile()); // Even if the Workflow is suspended, currently executing action will complete and currently running nodes // should be zero verifyRunningProgramCount(programId, runId, 0); // Verify that Workflow is still suspended verifyProgramRuns(programId, "suspended"); // Resume the execution of the Workflow resumeWorkflow(programId, runId, 200); // Workflow should be running waitState(programId, ProgramStatus.RUNNING.name()); verifyProgramRuns(programId, "running"); // Resume on already running Workflow should give conflict resumeWorkflow(programId, runId, 409); // Wait till fork execution in the Workflow starts while (!(forkedSimpleActionFile.exists() && anotherForkedSimpleActionFile.exists())) { TimeUnit.MILLISECONDS.sleep(50); } // Workflow should have 2 nodes running because of the fork verifyRunningProgramCount(programId, runId, 2); // Suspend the Workflow suspendWorkflow(programId, runId, 200); // Status of the Workflow should be suspended waitState(programId, ProgramStatus.STOPPED.name()); // Store should reflect the suspended status of the Workflow verifyProgramRuns(programId, "suspended"); // Allow currently executing actions to complete Assert.assertTrue(forkedSimpleActionDoneFile.createNewFile()); Assert.assertTrue(anotherForkedSimpleActionDoneFile.createNewFile()); // Workflow should have zero actions running verifyRunningProgramCount(programId, runId, 0); verifyProgramRuns(programId, "suspended"); Assert.assertTrue(!lastSimpleActionFile.exists()); resumeWorkflow(programId, runId, 200); waitState(programId, ProgramStatus.RUNNING.name()); while (!lastSimpleActionFile.exists()) { TimeUnit.SECONDS.sleep(1); } verifyRunningProgramCount(programId, runId, 1); Assert.assertTrue(lastSimpleActionDoneFile.createNewFile()); verifyProgramRuns(programId, "completed"); waitState(programId, ProgramStatus.STOPPED.name()); suspendWorkflow(programId, runId, 404); resumeWorkflow(programId, runId, 404); } @Category(XSlowTests.class) @Test public void testMultipleWorkflowInstances() throws Exception { String appWithConcurrentWorkflow = ConcurrentWorkflowApp.class.getSimpleName(); // Files used to synchronize between this test and workflow execution File tempDir = tmpFolder.newFolder(appWithConcurrentWorkflow); File run1File = new File(tempDir, "concurrentRun1.file"); File run2File = new File(tempDir, "concurrentRun2.file"); File run1DoneFile = new File(tempDir, "concurrentRun1.done"); File run2DoneFile = new File(tempDir, "concurrentRun2.done"); // create app in default namespace so that v2 and v3 api can be tested in the same test String defaultNamespace = Id.Namespace.DEFAULT.getId(); HttpResponse response = deploy(ConcurrentWorkflowApp.class, Constants.Gateway.API_VERSION_3_TOKEN, defaultNamespace); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Id.Program programId = Id.Program.from(Id.Namespace.DEFAULT, appWithConcurrentWorkflow, ProgramType.WORKFLOW, ConcurrentWorkflowApp.ConcurrentWorkflow.class.getSimpleName()); // start run 1 startProgram(programId, ImmutableMap.of(ConcurrentWorkflowApp.FILE_TO_CREATE_ARG, run1File.getAbsolutePath(), ConcurrentWorkflowApp.DONE_FILE_ARG, run1DoneFile.getAbsolutePath()), 200); // start run 2 startProgram(programId, ImmutableMap.of(ConcurrentWorkflowApp.FILE_TO_CREATE_ARG, run2File.getAbsolutePath(), ConcurrentWorkflowApp.DONE_FILE_ARG, run2DoneFile.getAbsolutePath()), 200); while (!(run1File.exists() && run2File.exists())) { TimeUnit.MILLISECONDS.sleep(50); } verifyMultipleConcurrentRuns(programId); Assert.assertTrue(run1DoneFile.createNewFile()); Assert.assertTrue(run2DoneFile.createNewFile()); waitState(programId, ProgramStatus.STOPPED.name()); // delete the application deleteApp(programId.getApplication(), 200, 60, TimeUnit.SECONDS); } private void verifyMultipleConcurrentRuns(Id.Program workflowId) throws Exception { verifyProgramRuns(workflowId, ProgramRunStatus.RUNNING.name(), 1); List<RunRecord> historyRuns = getProgramRuns(workflowId, "running"); Assert.assertEquals(2, historyRuns.size()); HttpResponse response = getWorkflowCurrentStatus(workflowId, historyRuns.get(0).getPid()); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); String json = EntityUtils.toString(response.getEntity()); List<WorkflowActionNode> nodes = GSON.fromJson(json, LIST_WORKFLOWACTIONNODE_TYPE); Assert.assertEquals(1, nodes.size()); Assert.assertEquals(ConcurrentWorkflowApp.SimpleAction.class.getSimpleName(), nodes.get(0).getProgram().getProgramName()); response = getWorkflowCurrentStatus(workflowId, historyRuns.get(1).getPid()); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); json = EntityUtils.toString(response.getEntity()); nodes = GSON.fromJson(json, LIST_WORKFLOWACTIONNODE_TYPE); Assert.assertEquals(1, nodes.size()); Assert.assertEquals(ConcurrentWorkflowApp.SimpleAction.class.getSimpleName(), nodes.get(0).getProgram().getProgramName()); } private void verifyFileExists(final List<File> fileList) throws Exception { Tasks.waitFor(true, new Callable<Boolean>() { @Override public Boolean call() throws Exception { for (File file : fileList) { if (!file.exists()) { return false; } } return true; } }, 180, TimeUnit.SECONDS); } @Test public void testWorkflowForkApp() throws Exception { File directory = tmpFolder.newFolder(); Map<String, String> runtimeArgs = new HashMap<>(); // Files used to synchronize between this test and workflow execution File firstFile = new File(directory, "first.file"); File firstDoneFile = new File(directory, "first.done"); runtimeArgs.put("first.file", firstFile.getAbsolutePath()); runtimeArgs.put("first.donefile", firstDoneFile.getAbsolutePath()); File branch1File = new File(directory, "branch1.file"); File branch1DoneFile = new File(directory, "branch1.done"); runtimeArgs.put("branch1.file", branch1File.getAbsolutePath()); runtimeArgs.put("branch1.donefile", branch1DoneFile.getAbsolutePath()); File branch2File = new File(directory, "branch2.file"); File branch2DoneFile = new File(directory, "branch2.done"); runtimeArgs.put("branch2.file", branch2File.getAbsolutePath()); runtimeArgs.put("branch2.donefile", branch2DoneFile.getAbsolutePath()); HttpResponse response = deploy(WorkflowAppWithFork.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Id.Program programId = Id.Program.from( TEST_NAMESPACE2, WorkflowAppWithFork.class.getSimpleName(), ProgramType.WORKFLOW, WorkflowAppWithFork.WorkflowWithFork.class.getSimpleName()); setAndTestRuntimeArgs(programId, runtimeArgs); // Start a Workflow startProgram(programId); // Workflow should be running waitState(programId, ProgramStatus.RUNNING.name()); // Get the runId for the currently running Workflow String runId = getRunIdOfRunningProgram(programId); // Wait till first action in the Workflow starts executing verifyFileExists(Lists.newArrayList(firstFile)); verifyRunningProgramCount(programId, runId, 1); // Stop the Workflow stopProgram(programId); // Workflow run record should be marked 'killed' verifyProgramRuns(programId, "killed"); // Delete the asset created in the previous run Assert.assertTrue(firstFile.delete()); // Start the Workflow again startProgram(programId); // Workflow should be running waitState(programId, ProgramStatus.RUNNING.name()); // Get the runId for the currently running Workflow String newRunId = getRunIdOfRunningProgram(programId); Assert.assertTrue( String.format("Expected a new runId to be generated after starting the workflow for the second time, but " + "found old runId '%s' = new runId '%s'", runId, newRunId), !runId.equals(newRunId)); // Store the new RunId runId = newRunId; // Wait till first action in the Workflow starts executing verifyFileExists(Lists.newArrayList(firstFile)); verifyRunningProgramCount(programId, runId, 1); // Signal the first action to continue Assert.assertTrue(firstDoneFile.createNewFile()); // Wait till fork in the Workflow starts executing verifyFileExists(Lists.newArrayList(branch1File, branch2File)); // Two actions should be running in Workflow as a part of the fork verifyRunningProgramCount(programId, runId, 2); // Stop the program while in fork stopProgram(programId, 200); // Wait till the program stop waitState(programId, ProgramStatus.STOPPED.name()); // Current endpoint would return 404 response = getWorkflowCurrentStatus(programId, runId); Assert.assertEquals(404, response.getStatusLine().getStatusCode()); // Now there should be 2 RunRecord with status killed verifyProgramRuns(programId, "killed", 1); // Delete the assets generated in the previous run Assert.assertTrue(firstFile.delete()); Assert.assertTrue(firstDoneFile.delete()); Assert.assertTrue(branch1File.delete()); Assert.assertTrue(branch2File.delete()); // Restart the run again startProgram(programId); // Wait till the Workflow is running waitState(programId, ProgramStatus.RUNNING.name()); // Store the new RunRecord for the currently running run runId = getRunIdOfRunningProgram(programId); // Wait till first action in the Workflow starts executing verifyFileExists(Lists.newArrayList(firstFile)); verifyRunningProgramCount(programId, runId, 1); // Signal the first action to continue Assert.assertTrue(firstDoneFile.createNewFile()); // Wait till fork in the Workflow starts executing verifyFileExists(Lists.newArrayList(branch1File, branch2File)); // Two actions should be running in Workflow as a part of the fork verifyRunningProgramCount(programId, runId, 2); // Signal the Workflow that execution can be continued Assert.assertTrue(branch1DoneFile.createNewFile()); Assert.assertTrue(branch2DoneFile.createNewFile()); // Workflow should now have one completed run verifyProgramRuns(programId, "completed"); } private String getRunIdOfRunningProgram(final Id.Program programId) throws Exception { Tasks.waitFor(1, new Callable<Integer>() { @Override public Integer call() throws Exception { return getProgramRuns(programId, "running").size(); } }, 5, TimeUnit.SECONDS); List<RunRecord> historyRuns = getProgramRuns(programId, "running"); Assert.assertEquals(1, historyRuns.size()); RunRecord record = historyRuns.get(0); return record.getPid(); } private Map<String, WorkflowNodeStateDetail> getWorkflowNodeStates(ProgramId workflowId, String runId) throws Exception { String path = String.format("apps/%s/workflows/%s/runs/%s/nodes/state", workflowId.getApplication(), workflowId.getProgram(), runId); path = getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespace()); HttpResponse response = doGet(path); return readResponse(response, MAP_STRING_TO_WORKFLOWNODESTATEDETAIL_TYPE); } @Category(XSlowTests.class) @Test public void testWorkflowScopedArguments() throws Exception { String workflowRunIdProperty = "workflowrunid"; HttpResponse response = deploy(WorkflowAppWithScopedParameters.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); ProgramId programId = Ids.namespace(TEST_NAMESPACE2).app(WorkflowAppWithScopedParameters.APP_NAME) .workflow(WorkflowAppWithScopedParameters.ONE_WORKFLOW); Map<String, String> runtimeArguments = Maps.newHashMap(); runtimeArguments.put("debug", "true"); runtimeArguments.put("mapreduce.*.debug", "false"); runtimeArguments.put("mapreduce.OneMR.debug", "true"); runtimeArguments.put("input.path", createInput("ProgramInput")); runtimeArguments.put("mapreduce.OneMR.input.path", createInput("OneMRInput")); runtimeArguments.put("mapreduce.OneMR.logical.start.time", "1234567890000"); runtimeArguments.put("mapreduce.AnotherMR.input.path", createInput("AnotherMRInput")); runtimeArguments.put("spark.*.input.path", createInput("SparkInput")); runtimeArguments.put("output.path", new File(tmpFolder.newFolder(), "ProgramOutput").getAbsolutePath()); runtimeArguments.put("mapreduce.OneMR.output.path", new File(tmpFolder.newFolder(), "OneMROutput").getAbsolutePath()); runtimeArguments.put("spark.AnotherSpark.output.path", new File(tmpFolder.newFolder(), "AnotherSparkOutput").getAbsolutePath()); runtimeArguments.put("mapreduce.*.processing.time", "1HR"); runtimeArguments.put("dataset.Purchase.cache.seconds", "30"); runtimeArguments.put("dataset.UserProfile.schema.property", "constant"); runtimeArguments.put("dataset.unknown.dataset", "false"); runtimeArguments.put("dataset.*.read.timeout", "60"); setAndTestRuntimeArgs(programId.toId(), runtimeArguments); // Start the workflow startProgram(programId.toId()); waitState(programId.toId(), ProgramStatus.RUNNING.name()); // Wait until we have a run record verifyProgramRuns(programId.toId(), "running"); List<RunRecord> workflowHistoryRuns = getProgramRuns(programId.toId(), "running"); String workflowRunId = workflowHistoryRuns.get(0).getPid(); Id.Program mr1ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME, ProgramType.MAPREDUCE, WorkflowAppWithScopedParameters.ONE_MR); waitState(mr1ProgramId, ProgramStatus.RUNNING.name()); List<RunRecord> oneMRHistoryRuns = getProgramRuns(mr1ProgramId, "running"); String expectedMessage = String.format("Cannot stop the program '%s' started by the Workflow run '%s'. " + "Please stop the Workflow.", new Id.Run(mr1ProgramId, oneMRHistoryRuns.get(0).getPid()), workflowRunId); stopProgram(mr1ProgramId, oneMRHistoryRuns.get(0).getPid(), 400, expectedMessage); verifyProgramRuns(programId.toId(), "completed"); workflowHistoryRuns = getProgramRuns(programId.toId(), "completed"); oneMRHistoryRuns = getProgramRuns(mr1ProgramId, "completed"); Id.Program mr2ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME, ProgramType.MAPREDUCE, WorkflowAppWithScopedParameters.ANOTHER_MR); List<RunRecord> anotherMRHistoryRuns = getProgramRuns(mr2ProgramId, "completed"); Id.Program spark1ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME, ProgramType.SPARK, WorkflowAppWithScopedParameters.ONE_SPARK); List<RunRecord> oneSparkHistoryRuns = getProgramRuns(spark1ProgramId, "completed"); Id.Program spark2ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME, ProgramType.SPARK, WorkflowAppWithScopedParameters.ANOTHER_SPARK); List<RunRecord> anotherSparkHistoryRuns = getProgramRuns(spark2ProgramId, "completed"); Assert.assertEquals(1, workflowHistoryRuns.size()); Assert.assertEquals(1, oneMRHistoryRuns.size()); Assert.assertEquals(1, anotherMRHistoryRuns.size()); Assert.assertEquals(1, oneSparkHistoryRuns.size()); Assert.assertEquals(1, anotherSparkHistoryRuns.size()); Map<String, String> workflowRunRecordProperties = workflowHistoryRuns.get(0).getProperties(); Map<String, String> oneMRRunRecordProperties = oneMRHistoryRuns.get(0).getProperties(); Map<String, String> anotherMRRunRecordProperties = anotherMRHistoryRuns.get(0).getProperties(); Map<String, String> oneSparkRunRecordProperties = oneSparkHistoryRuns.get(0).getProperties(); Map<String, String> anotherSparkRunRecordProperties = anotherSparkHistoryRuns.get(0).getProperties(); Assert.assertNotNull(oneMRRunRecordProperties.get(workflowRunIdProperty)); Assert.assertEquals(workflowHistoryRuns.get(0).getPid(), oneMRRunRecordProperties.get(workflowRunIdProperty)); Assert.assertNotNull(anotherMRRunRecordProperties.get(workflowRunIdProperty)); Assert.assertEquals(workflowHistoryRuns.get(0).getPid(), anotherMRRunRecordProperties.get(workflowRunIdProperty)); Assert.assertNotNull(oneSparkRunRecordProperties.get(workflowRunIdProperty)); Assert.assertEquals(workflowHistoryRuns.get(0).getPid(), oneSparkRunRecordProperties.get(workflowRunIdProperty)); Assert.assertNotNull(anotherSparkRunRecordProperties.get(workflowRunIdProperty)); Assert.assertEquals(workflowHistoryRuns.get(0).getPid(), anotherSparkRunRecordProperties.get(workflowRunIdProperty)); Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ONE_MR), oneMRHistoryRuns.get(0).getPid()); Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ONE_SPARK), oneSparkHistoryRuns.get(0).getPid()); Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ANOTHER_MR), anotherMRHistoryRuns.get(0).getPid()); Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ANOTHER_SPARK), anotherSparkHistoryRuns.get(0).getPid()); // Get Workflow node states Map<String, WorkflowNodeStateDetail> nodeStates = getWorkflowNodeStates(programId, workflowHistoryRuns.get(0).getPid()); Assert.assertNotNull(nodeStates); Assert.assertEquals(5, nodeStates.size()); WorkflowNodeStateDetail mrNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ONE_MR); Assert.assertNotNull(mrNodeState); Assert.assertEquals(WorkflowAppWithScopedParameters.ONE_MR, mrNodeState.getNodeId()); Assert.assertEquals(oneMRHistoryRuns.get(0).getPid(), mrNodeState.getRunId()); mrNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ANOTHER_MR); Assert.assertNotNull(mrNodeState); Assert.assertEquals(WorkflowAppWithScopedParameters.ANOTHER_MR, mrNodeState.getNodeId()); Assert.assertEquals(anotherMRHistoryRuns.get(0).getPid(), mrNodeState.getRunId()); WorkflowNodeStateDetail sparkNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ONE_SPARK); Assert.assertNotNull(sparkNodeState); Assert.assertEquals(WorkflowAppWithScopedParameters.ONE_SPARK, sparkNodeState.getNodeId()); Assert.assertEquals(oneSparkHistoryRuns.get(0).getPid(), sparkNodeState.getRunId()); sparkNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ANOTHER_SPARK); Assert.assertNotNull(sparkNodeState); Assert.assertEquals(WorkflowAppWithScopedParameters.ANOTHER_SPARK, sparkNodeState.getNodeId()); Assert.assertEquals(anotherSparkHistoryRuns.get(0).getPid(), sparkNodeState.getRunId()); WorkflowNodeStateDetail oneActionNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ONE_ACTION); Assert.assertNotNull(oneActionNodeState); Assert.assertEquals(WorkflowAppWithScopedParameters.ONE_ACTION, oneActionNodeState.getNodeId()); } @Ignore @Test public void testWorkflowSchedules() throws Exception { // Steps for the test: // 1. Deploy the app // 2. Verify the schedules // 3. Verify the history after waiting a while // 4. Suspend the schedule // 5. Verify there are no runs after the suspend by looking at the history // 6. Resume the schedule // 7. Verify there are runs after the resume by looking at the history String appName = "AppWithSchedule"; String workflowName = "SampleWorkflow"; String sampleSchedule = "SampleSchedule"; // deploy app with schedule in namespace 2 HttpResponse response = deploy(AppWithSchedule.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Id.Program programId = Id.Program.from(TEST_NAMESPACE2, appName, ProgramType.WORKFLOW, workflowName); Map<String, String> runtimeArguments = ImmutableMap.of("someKey", "someWorkflowValue", "workflowKey", "workflowValue"); setAndTestRuntimeArgs(programId, runtimeArguments); // get schedules List<ScheduleSpecification> schedules = getSchedules(TEST_NAMESPACE2, appName, workflowName); Assert.assertEquals(1, schedules.size()); String scheduleName = schedules.get(0).getSchedule().getName(); Assert.assertFalse(scheduleName.isEmpty()); // TODO [CDAP-2327] Sagar Investigate why following check fails sometimes. Mostly test case issue. // List<ScheduledRuntime> previousRuntimes = getScheduledRunTime(programId, scheduleName, "previousruntime"); // Assert.assertTrue(previousRuntimes.size() == 0); long current = System.currentTimeMillis(); // Resume the schedule Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, sampleSchedule)); // Check schedule status assertSchedule(programId, scheduleName, true, 30, TimeUnit.SECONDS); List<ScheduledRuntime> runtimes = getScheduledRunTime(programId, true); String id = runtimes.get(0).getId(); Assert.assertTrue(String.format("Expected schedule id '%s' to contain schedule name '%s'", id, scheduleName), id.contains(scheduleName)); Long nextRunTime = runtimes.get(0).getTime(); Assert.assertTrue(String.format("Expected nextRuntime '%s' to be greater than current runtime '%s'", nextRunTime, current), nextRunTime > current); // Verify that at least one program is completed verifyProgramRuns(programId, "completed"); // Suspend the schedule Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName)); // check paused state assertSchedule(programId, scheduleName, false, 30, TimeUnit.SECONDS); // check that there were at least 1 previous runs List<ScheduledRuntime> previousRuntimes = getScheduledRunTime(programId, false); int numRuns = previousRuntimes.size(); Assert.assertTrue(String.format("After sleeping for two seconds, the schedule should have at least triggered " + "once, but found %s previous runs", numRuns), numRuns >= 1); // Verify no program running verifyNoRunWithStatus(programId, "running"); // get number of completed runs after schedule is suspended int workflowRuns = getProgramRuns(programId, "completed").size(); // verify that resuming the suspended schedule again has expected behavior (spawns new runs) Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, scheduleName)); //check scheduled state assertSchedule(programId, scheduleName, true, 30, TimeUnit.SECONDS); // Verify that the program ran after the schedule was resumed verifyProgramRuns(programId, "completed", workflowRuns); // Suspend the schedule Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName)); //check paused state assertSchedule(programId, scheduleName, false, 30, TimeUnit.SECONDS); //Check status of a non existing schedule try { assertSchedule(programId, "invalid", true, 2, TimeUnit.SECONDS); Assert.fail(); } catch (Exception e) { // expected } //Schedule operations using invalid namespace try { assertSchedule(Id.Program.from(TEST_NAMESPACE1, appName, ProgramType.WORKFLOW, workflowName), scheduleName, true, 2, TimeUnit.SECONDS); Assert.fail(); } catch (Exception e) { // expected } Assert.assertEquals(404, suspendSchedule(TEST_NAMESPACE1, appName, scheduleName)); Assert.assertEquals(404, resumeSchedule(TEST_NAMESPACE1, appName, scheduleName)); verifyNoRunWithStatus(programId, "running"); deleteApp(Id.Application.from(TEST_NAMESPACE2, AppWithSchedule.class.getSimpleName()), 200); } @Test public void testStreamSizeSchedules() throws Exception { // Steps for the test: // 1. Deploy the app // 2. Verify the schedules // 3. Ingest data in the stream // 4. Verify the history after waiting a while // 5. Suspend the schedule // 6. Ingest data in the stream // 7. Verify there are no runs after the suspend by looking at the history // 8. Resume the schedule // 9. Verify there are runs after the resume by looking at the history String appName = "AppWithStreamSizeSchedule"; String sampleSchedule1 = "SampleSchedule1"; String sampleSchedule2 = "SampleSchedule2"; String workflowName = "SampleWorkflow"; String streamName = "stream"; Id.Program programId = Id.Program.from(TEST_NAMESPACE2, appName, ProgramType.WORKFLOW, workflowName); StringBuilder longStringBuilder = new StringBuilder(); for (int i = 0; i < 10000; i++) { longStringBuilder.append("dddddddddd"); } String longString = longStringBuilder.toString(); // deploy app with schedule in namespace 2 HttpResponse response = deploy(AppWithStreamSizeSchedule.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, sampleSchedule1)); Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, sampleSchedule2)); // get schedules List<ScheduleSpecification> schedules = getSchedules(TEST_NAMESPACE2, appName, workflowName); Assert.assertEquals(2, schedules.size()); String scheduleName1 = schedules.get(0).getSchedule().getName(); String scheduleName2 = schedules.get(1).getSchedule().getName(); Assert.assertNotNull(scheduleName1); Assert.assertFalse(scheduleName1.isEmpty()); // Change notification threshold for stream response = doPut(String.format("/v3/namespaces/%s/streams/%s/properties", TEST_NAMESPACE2, streamName), "{'notification.threshold.mb': 1}"); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); response = doGet(String.format("/v3/namespaces/%s/streams/%s", TEST_NAMESPACE2, streamName)); String json = EntityUtils.toString(response.getEntity()); StreamProperties properties = new Gson().fromJson(json, StreamProperties.class); Assert.assertEquals(1, properties.getNotificationThresholdMB().intValue()); // Ingest over 1MB of data in stream for (int i = 0; i < 12; ++i) { response = doPost(String.format("/v3/namespaces/%s/streams/%s", TEST_NAMESPACE2, streamName), longString); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); } // Only schedule 1 should get executed verifyProgramRuns(programId, "completed"); //Check schedule status assertSchedule(programId, scheduleName1, true, 30, TimeUnit.SECONDS); assertSchedule(programId, scheduleName2, true, 30, TimeUnit.SECONDS); Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName1)); Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName2)); //check paused state assertSchedule(programId, scheduleName1, false, 30, TimeUnit.SECONDS); assertSchedule(programId, scheduleName2, false, 30, TimeUnit.SECONDS); int workflowRuns = getProgramRuns(programId, "completed").size(); // Should still be one Assert.assertEquals(1, workflowRuns); // Sleep for some time and verify there are no more scheduled jobs after the suspend. for (int i = 0; i < 12; ++i) { response = doPost(String.format("/v3/namespaces/%s/streams/%s", TEST_NAMESPACE2, streamName), longString); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); } TimeUnit.SECONDS.sleep(5); int workflowRunsAfterSuspend = getProgramRuns(programId, "completed").size(); Assert.assertEquals(workflowRuns, workflowRunsAfterSuspend); Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, scheduleName1)); assertRunHistory(programId, "completed", workflowRunsAfterSuspend, 60, TimeUnit.SECONDS); //check scheduled state assertSchedule(programId, scheduleName1, true, 30, TimeUnit.SECONDS); //Check status of a non existing schedule try { assertSchedule(programId, "invalid", true, 2, TimeUnit.SECONDS); Assert.fail(); } catch (Exception e) { // expected } Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName1)); //check paused state assertSchedule(programId, scheduleName1, false, 30, TimeUnit.SECONDS); //Schedule operations using invalid namespace try { assertSchedule(Id.Program.from(TEST_NAMESPACE1, appName, ProgramType.WORKFLOW, workflowName), scheduleName1, true, 2, TimeUnit.SECONDS); Assert.fail(); } catch (Exception e) { // expected } Assert.assertEquals(404, suspendSchedule(TEST_NAMESPACE1, appName, scheduleName1)); Assert.assertEquals(404, resumeSchedule(TEST_NAMESPACE1, appName, scheduleName1)); TimeUnit.SECONDS.sleep(2); //wait till any running jobs just before suspend call completes. } @Test public void testWorkflowRuns() throws Exception { String appName = "WorkflowAppWithErrorRuns"; String workflowName = "WorkflowWithErrorRuns"; HttpResponse response = deploy(WorkflowAppWithErrorRuns.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Id.Program programId = Id.Program.from(TEST_NAMESPACE2, appName, ProgramType.WORKFLOW, workflowName); // Test the "KILLED" state of the Workflow. File instance1File = new File(tmpFolder.newFolder() + "/instance1.file"); File instance2File = new File(tmpFolder.newFolder() + "/instance2.file"); File doneFile = new File(tmpFolder.newFolder() + "/done.file"); // Start the first Workflow run. Map<String, String> propertyMap = ImmutableMap.of("simple.action.file", instance1File.getAbsolutePath(), "simple.action.donefile", doneFile.getAbsolutePath()); startProgram(programId, propertyMap); // Start another Workflow run. propertyMap = ImmutableMap.of("simple.action.file", instance2File.getAbsolutePath(), "simple.action.donefile", doneFile.getAbsolutePath()); startProgram(programId, propertyMap); // Wait till the execution of actions in both Workflow runs is started. while (!(instance1File.exists() && instance2File.exists())) { TimeUnit.MILLISECONDS.sleep(50); } // Verify that there are two runs of the Workflow currently running. List<RunRecord> historyRuns = getProgramRuns(programId, "running"); Assert.assertEquals(2, historyRuns.size()); // Stop both Workflow runs. String runId = historyRuns.get(0).getPid(); stopProgram(programId, runId, 200); runId = historyRuns.get(1).getPid(); stopProgram(programId, runId, 200); // Verify both runs should be marked "KILLED". verifyProgramRuns(programId, "killed", 1); // Test the "COMPLETE" state of the Workflow. File instanceFile = new File(tmpFolder.newFolder() + "/instance.file"); propertyMap = ImmutableMap.of("simple.action.file", instanceFile.getAbsolutePath(), "simple.action.donefile", doneFile.getAbsolutePath()); startProgram(programId, propertyMap); while (!instanceFile.exists()) { TimeUnit.MILLISECONDS.sleep(50); } // Verify that currently only one run of the Workflow should be running. historyRuns = getProgramRuns(programId, "running"); Assert.assertEquals(1, historyRuns.size()); Assert.assertTrue(doneFile.createNewFile()); // Verify that Workflow should move to "COMPLETED" state. verifyProgramRuns(programId, "completed"); // Test the "FAILED" state of the program. propertyMap = ImmutableMap.of("ThrowError", "true"); startProgram(programId, propertyMap); // Verify that the Workflow should be marked as "FAILED". verifyProgramRuns(programId, "failed"); } private String createConditionInput(String folderName, int numGoodRecords, int numBadRecords) throws IOException { File inputDir = tmpFolder.newFolder(folderName); File inputFile = new File(inputDir.getPath() + "/data.txt"); try (BufferedWriter writer = Files.newBufferedWriter(inputFile.toPath(), Charsets.UTF_8)) { // dummy good records containing ":" separated fields for (int i = 0; i < numGoodRecords; i++) { writer.write("Afname:ALname:A:B"); writer.newLine(); } // dummy bad records in which fields are not separated by ":" for (int i = 0; i < numBadRecords; i++) { writer.write("Afname ALname A B"); writer.newLine(); } } return inputDir.getAbsolutePath(); } @Category(XSlowTests.class) @Test public void testWorkflowCondition() throws Exception { String conditionalWorkflowApp = "ConditionalWorkflowApp"; String conditionalWorkflow = "ConditionalWorkflow"; HttpResponse response = deploy(ConditionalWorkflowApp.class, Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE2); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); Id.Program programId = Id.Program.from(TEST_NAMESPACE2, conditionalWorkflowApp, ProgramType.WORKFLOW, conditionalWorkflow); Map<String, String> runtimeArguments = Maps.newHashMap(); // Files used to synchronize between this test and workflow execution File ifForkOneActionFile = new File(tmpFolder.newFolder() + "/iffork_one.file"); File ifForkOneActionDoneFile = new File(tmpFolder.newFolder() + "/iffork_one.file.done"); runtimeArguments.put("iffork_one.simple.action.file", ifForkOneActionFile.getAbsolutePath()); runtimeArguments.put("iffork_one.simple.action.donefile", ifForkOneActionDoneFile.getAbsolutePath()); File ifForkAnotherActionFile = new File(tmpFolder.newFolder() + "/iffork_another.file"); File ifForkAnotherActionDoneFile = new File(tmpFolder.newFolder() + "/iffork_another.file.done"); runtimeArguments.put("iffork_another.simple.action.file", ifForkAnotherActionFile.getAbsolutePath()); runtimeArguments.put("iffork_another.simple.action.donefile", ifForkAnotherActionDoneFile.getAbsolutePath()); File elseForkOneActionFile = new File(tmpFolder.newFolder() + "/elsefork_one.file"); File elseForkOneActionDoneFile = new File(tmpFolder.newFolder() + "/elsefork_one.file.done"); runtimeArguments.put("elsefork_one.simple.action.file", elseForkOneActionFile.getAbsolutePath()); runtimeArguments.put("elsefork_one.simple.action.donefile", elseForkOneActionDoneFile.getAbsolutePath()); File elseForkAnotherActionFile = new File(tmpFolder.newFolder() + "/elsefork_another.file"); File elseForkAnotherActionDoneFile = new File(tmpFolder.newFolder() + "/elsefork_another.file.done"); runtimeArguments.put("elsefork_another.simple.action.file", elseForkAnotherActionFile.getAbsolutePath()); runtimeArguments.put("elsefork_another.simple.action.donefile", elseForkAnotherActionDoneFile.getAbsolutePath()); File elseForkThirdActionFile = new File(tmpFolder.newFolder() + "/elsefork_third.file"); File elseForkThirdActionDoneFile = new File(tmpFolder.newFolder() + "/elsefork_third.file.done"); runtimeArguments.put("elsefork_third.simple.action.file", elseForkThirdActionFile.getAbsolutePath()); runtimeArguments.put("elsefork_third.simple.action.donefile", elseForkThirdActionDoneFile.getAbsolutePath()); // create input data in which number of good records are lesser than the number of bad records runtimeArguments.put("inputPath", createConditionInput("ConditionProgramInput", 2, 12)); runtimeArguments.put("outputPath", new File(tmpFolder.newFolder(), "ConditionProgramOutput").getAbsolutePath()); setAndTestRuntimeArgs(programId, runtimeArguments); // Start the workflow startProgram(programId); // Since the number of good records are lesser than the number of bad records, // 'else' branch of the condition will get executed. // Wait till the execution of the fork on the else branch starts while (!(elseForkOneActionFile.exists() && elseForkAnotherActionFile.exists() && elseForkThirdActionFile.exists())) { TimeUnit.MILLISECONDS.sleep(50); } // Get running program run String runId = getRunIdOfRunningProgram(programId); // Since the fork on the else branch of condition has 3 parallel branches // there should be 3 programs currently running verifyRunningProgramCount(programId, runId, 3); // Signal the Workflow to continue Assert.assertTrue(elseForkOneActionDoneFile.createNewFile()); Assert.assertTrue(elseForkAnotherActionDoneFile.createNewFile()); Assert.assertTrue(elseForkThirdActionDoneFile.createNewFile()); verifyProgramRuns(programId, "completed"); List<RunRecord> workflowHistoryRuns = getProgramRuns(programId, "completed"); Id.Program recordVerifierProgramId = Id.Program.from(TEST_NAMESPACE2, conditionalWorkflowApp, ProgramType.MAPREDUCE, "RecordVerifier"); List<RunRecord> recordVerifierRuns = getProgramRuns(recordVerifierProgramId, "completed"); Id.Program wordCountProgramId = Id.Program.from(TEST_NAMESPACE2, conditionalWorkflowApp, ProgramType.MAPREDUCE, "ClassicWordCount"); List<RunRecord> wordCountRuns = getProgramRuns(wordCountProgramId, "completed"); Assert.assertEquals(1, workflowHistoryRuns.size()); Assert.assertEquals(1, recordVerifierRuns.size()); Assert.assertEquals(0, wordCountRuns.size()); // create input data in which number of good records are greater than the number of bad records runtimeArguments.put("inputPath", createConditionInput("AnotherConditionProgramInput", 10, 2)); runtimeArguments.put("mapreduce.RecordVerifier.outputPath", new File(tmpFolder.newFolder(), "ConditionProgramOutput").getAbsolutePath()); runtimeArguments.put("mapreduce.ClassicWordCount.outputPath", new File(tmpFolder.newFolder(), "ConditionProgramOutput").getAbsolutePath()); setAndTestRuntimeArgs(programId, runtimeArguments); // Start the workflow startProgram(programId); // Since the number of good records are greater than the number of bad records, // 'if' branch of the condition will get executed. // Wait till the execution of the fork on the if branch starts while (!(ifForkOneActionFile.exists() && ifForkAnotherActionFile.exists())) { TimeUnit.MILLISECONDS.sleep(50); } // Get running program run runId = getRunIdOfRunningProgram(programId); // Since the fork on the if branch of the condition has 2 parallel branches // there should be 2 programs currently running verifyRunningProgramCount(programId, runId, 2); // Signal the Workflow to continue Assert.assertTrue(ifForkOneActionDoneFile.createNewFile()); Assert.assertTrue(ifForkAnotherActionDoneFile.createNewFile()); verifyProgramRuns(programId, "completed", 1); workflowHistoryRuns = getProgramRuns(programId, "completed"); recordVerifierRuns = getProgramRuns(recordVerifierProgramId, "completed"); wordCountRuns = getProgramRuns(wordCountProgramId, "completed"); Assert.assertEquals(2, workflowHistoryRuns.size()); Assert.assertEquals(2, recordVerifierRuns.size()); Assert.assertEquals(1, wordCountRuns.size()); } @Test @SuppressWarnings("ConstantConditions") public void testWorkflowToken() throws Exception { Assert.assertEquals(200, deploy(AppWithWorkflow.class).getStatusLine().getStatusCode()); Id.Application appId = Id.Application.from(Id.Namespace.DEFAULT, AppWithWorkflow.NAME); final Id.Workflow workflowId = Id.Workflow.from(appId, AppWithWorkflow.SampleWorkflow.NAME); String outputPath = new File(tmpFolder.newFolder(), "output").getAbsolutePath(); startProgram(workflowId, ImmutableMap.of("inputPath", createInput("input"), "outputPath", outputPath)); Tasks.waitFor(1, new Callable<Integer>() { @Override public Integer call() throws Exception { return getProgramRuns(workflowId, ProgramRunStatus.COMPLETED.name()).size(); } }, 60, TimeUnit.SECONDS); List<RunRecord> programRuns = getProgramRuns(workflowId, ProgramRunStatus.COMPLETED.name()); Assert.assertEquals(1, programRuns.size()); RunRecord runRecord = programRuns.get(0); String pid = runRecord.getPid(); // Verify entire worfklow token WorkflowTokenDetail workflowTokenDetail = getWorkflowToken(workflowId, pid, null, null); List<WorkflowTokenDetail.NodeValueDetail> nodeValueDetails = workflowTokenDetail.getTokenData().get(AppWithWorkflow.DummyAction.TOKEN_KEY); Assert.assertEquals(2, nodeValueDetails.size()); Assert.assertEquals(AppWithWorkflow.SampleWorkflow.FIRST_ACTION, nodeValueDetails.get(0).getNode()); Assert.assertEquals(AppWithWorkflow.SampleWorkflow.SECOND_ACTION, nodeValueDetails.get(1).getNode()); Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(0).getValue()); Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(1).getValue()); // Verify entire workflow token by passing in the scope and key in the request workflowTokenDetail = getWorkflowToken(workflowId, pid, WorkflowToken.Scope.USER, AppWithWorkflow.DummyAction.TOKEN_KEY); nodeValueDetails = workflowTokenDetail.getTokenData().get(AppWithWorkflow.DummyAction.TOKEN_KEY); Assert.assertEquals(2, nodeValueDetails.size()); Assert.assertEquals(AppWithWorkflow.SampleWorkflow.FIRST_ACTION, nodeValueDetails.get(0).getNode()); Assert.assertEquals(AppWithWorkflow.SampleWorkflow.SECOND_ACTION, nodeValueDetails.get(1).getNode()); Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(0).getValue()); Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(1).getValue()); // Get workflow level tokens WorkflowTokenNodeDetail nodeDetail = getWorkflowToken(workflowId, pid, AppWithWorkflow.SampleWorkflow.NAME, WorkflowToken.Scope.USER, null); Map<String, String> tokenData = nodeDetail.getTokenDataAtNode(); Assert.assertEquals(2, tokenData.size()); Assert.assertEquals(AppWithWorkflow.SampleWorkflow.INITIALIZE_TOKEN_VALUE, tokenData.get(AppWithWorkflow.SampleWorkflow.INITIALIZE_TOKEN_KEY)); Assert.assertEquals(AppWithWorkflow.SampleWorkflow.DESTROY_TOKEN_SUCCESS_VALUE, tokenData.get(AppWithWorkflow.SampleWorkflow.DESTROY_TOKEN_KEY)); // Verify workflow token at a given node WorkflowTokenNodeDetail tokenAtNode = getWorkflowToken(workflowId, pid, AppWithWorkflow.SampleWorkflow.FIRST_ACTION, null, null); Map<String, String> tokenDataAtNode = tokenAtNode.getTokenDataAtNode(); Assert.assertEquals(1, tokenDataAtNode.size()); Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, tokenDataAtNode.get(AppWithWorkflow.DummyAction.TOKEN_KEY)); // Verify workflow token at a given node by passing in a scope and a key tokenAtNode = getWorkflowToken(workflowId, pid, AppWithWorkflow.SampleWorkflow.FIRST_ACTION, WorkflowToken.Scope.USER, AppWithWorkflow.DummyAction.TOKEN_KEY); tokenDataAtNode = tokenAtNode.getTokenDataAtNode(); Assert.assertEquals(1, tokenDataAtNode.size()); Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, tokenDataAtNode.get(AppWithWorkflow.DummyAction.TOKEN_KEY)); } private WorkflowTokenDetail getWorkflowToken(Id.Workflow workflowId, String runId, @Nullable WorkflowToken.Scope scope, @Nullable String key) throws Exception { String workflowTokenUrl = String.format("apps/%s/workflows/%s/runs/%s/token", workflowId.getApplicationId(), workflowId.getId(), runId); String versionedUrl = getVersionedAPIPath(appendScopeAndKeyToUrl(workflowTokenUrl, scope, key), Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespaceId()); HttpResponse response = doGet(versionedUrl); return readResponse(response, new TypeToken<WorkflowTokenDetail>() { }.getType(), GSON); } private WorkflowTokenNodeDetail getWorkflowToken(Id.Workflow workflowId, String runId, String nodeName, @Nullable WorkflowToken.Scope scope, @Nullable String key) throws Exception { String workflowTokenUrl = String.format("apps/%s/workflows/%s/runs/%s/nodes/%s/token", workflowId.getApplicationId(), workflowId.getId(), runId, nodeName); String versionedUrl = getVersionedAPIPath(appendScopeAndKeyToUrl(workflowTokenUrl, scope, key), Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespaceId()); HttpResponse response = doGet(versionedUrl); return readResponse(response, new TypeToken<WorkflowTokenNodeDetail>() { }.getType(), GSON); } private String appendScopeAndKeyToUrl(String workflowTokenUrl, @Nullable WorkflowToken.Scope scope, String key) { StringBuilder output = new StringBuilder(workflowTokenUrl); if (scope != null) { output.append(String.format("?scope=%s", scope.name())); if (key != null) { output.append(String.format("&key=%s", key)); } } else if (key != null) { output.append(String.format("?key=%s", key)); } return output.toString(); } private String createInputForRecordVerification(String folderName) throws IOException { File inputDir = tmpFolder.newFolder(folderName); File inputFile = new File(inputDir.getPath() + "/words.txt"); try (BufferedWriter writer = Files.newBufferedWriter(inputFile.toPath(), Charsets.UTF_8)) { writer.write("id1:value1"); writer.newLine(); writer.write("id2:value2"); writer.newLine(); writer.write("id3:value3"); } return inputDir.getAbsolutePath(); } @Test public void testWorkflowTokenPut() throws Exception { Assert.assertEquals(200, deploy(WorkflowTokenTestPutApp.class).getStatusLine().getStatusCode()); Id.Application appId = Id.Application.from(Id.Namespace.DEFAULT, WorkflowTokenTestPutApp.NAME); Id.Workflow workflowId = Id.Workflow.from(appId, WorkflowTokenTestPutApp.WorkflowTokenTestPut.NAME); Id.Program mapReduceId = Id.Program.from(appId, ProgramType.MAPREDUCE, WorkflowTokenTestPutApp.RecordCounter.NAME); Id.Program sparkId = Id.Program.from(appId, ProgramType.SPARK, WorkflowTokenTestPutApp.SparkTestApp.NAME); // Start program with inputPath and outputPath arguments. // This should succeed. The programs inside the workflow will attempt to write to the workflow token // from the Mapper's and Reducer's methods as well as from a Spark closure, and they will throw an exception // if that succeeds. // The MapReduce's beforeSubmit will record the workflow run id in the token, and the onFinish as well // as the mapper and the reducer will validate that they have the same workflow run id. String outputPath = new File(tmpFolder.newFolder(), "output").getAbsolutePath(); startProgram(workflowId, ImmutableMap.of("inputPath", createInputForRecordVerification("sixthInput"), "outputPath", outputPath)); waitState(workflowId, ProgramStatus.RUNNING.name()); waitState(workflowId, ProgramStatus.STOPPED.name()); // validate the completed workflow run and validate that it is the same as recorded in the token verifyProgramRuns(workflowId, "completed"); List<RunRecord> runs = getProgramRuns(workflowId, "completed"); Assert.assertEquals(1, runs.size()); String wfRunId = runs.get(0).getPid(); WorkflowTokenDetail tokenDetail = getWorkflowToken(workflowId, wfRunId, null, null); List<WorkflowTokenDetail.NodeValueDetail> details = tokenDetail.getTokenData().get("wf.runid"); Assert.assertEquals(1, details.size()); Assert.assertEquals(wfRunId, details.get(0).getValue()); // validate that none of the mapper, reducer or spark closure were able to write to the token for (String key : new String[] { "mapper.initialize.key", "map.key", "reducer.initialize.key", "reduce.key", "some.key" }) { Assert.assertFalse(tokenDetail.getTokenData().containsKey(key)); } List<RunRecord> sparkProgramRuns = getProgramRuns(sparkId, ProgramRunStatus.COMPLETED.name()); Assert.assertEquals(1, sparkProgramRuns.size()); } @Ignore @Test public void testWorkflowForkFailure() throws Exception { // Deploy an application containing workflow with fork. Fork executes MapReduce programs // 'FirstMapReduce' and 'SecondMapReduce' in parallel. Workflow is started with runtime argument // "mapreduce.SecondMapReduce.throw.exception", so that the MapReduce program 'SecondMapReduce' // fails. This causes the 'FirstMapReduce' program to get killed and Workflow is marked as failed. Assert.assertEquals(200, deploy(WorkflowFailureInForkApp.class).getStatusLine().getStatusCode()); Id.Application appId = Id.Application.from(Id.Namespace.DEFAULT, WorkflowFailureInForkApp.NAME); Id.Workflow workflowId = Id.Workflow.from(appId, WorkflowFailureInForkApp.WorkflowWithFailureInFork.NAME); Id.Program firstMRId = Id.Program.from(appId, ProgramType.MAPREDUCE, WorkflowFailureInForkApp.FIRST_MAPREDUCE_NAME); Id.Program secondMRId = Id.Program.from(appId, ProgramType.MAPREDUCE, WorkflowFailureInForkApp.SECOND_MAPREDUCE_NAME); String outputPath = new File(tmpFolder.newFolder(), "output").getAbsolutePath(); File fileToSync = new File(tmpFolder.newFolder() + "/sync.file"); File fileToWait = new File(tmpFolder.newFolder() + "/wait.file"); startProgram(workflowId, ImmutableMap.of("inputPath", createInput("testWorkflowForkFailureInput"), "outputPath", outputPath, "sync.file", fileToSync.getAbsolutePath(), "wait.file", fileToWait.getAbsolutePath(), "mapreduce." + WorkflowFailureInForkApp.SECOND_MAPREDUCE_NAME + ".throw.exception", "true")); waitState(workflowId, ProgramStatus.RUNNING.name()); waitState(workflowId, ProgramStatus.STOPPED.name()); verifyProgramRuns(workflowId, "failed"); List<RunRecord> mapReduceProgramRuns = getProgramRuns(firstMRId, ProgramRunStatus.KILLED.name()); Assert.assertEquals(1, mapReduceProgramRuns.size()); mapReduceProgramRuns = getProgramRuns(secondMRId, ProgramRunStatus.FAILED.name()); Assert.assertEquals(1, mapReduceProgramRuns.size()); } }