From d97b53dae894e68d79bfb3aa02de808b4524f6d0 Mon Sep 17 00:00:00 2001 From: Ankit Nanglia Date: Fri, 28 Jun 2019 18:25:48 +0530 Subject: [PATCH 01/12] Ability to abort running task/workflow (#52) Features: * API to terminate a task/ job * Do not enforce max execution time on a task Changes: * Ability to publish control messages from the scheduler to all the executors * Task handler contract changes to allow stopping a task * Ability to abort a running task/ workflow * Tests - Remove timer based approach to check the job and trigger completion --- .../com/cognitree/kronos/api/JobResource.java | 2 +- .../kronos/api/NamespaceResource.java | 2 +- .../cognitree/kronos/api/TaskResource.java | 68 +++++ .../kronos/api/WorkflowJobResource.java | 26 +- .../kronos/api/WorkflowResource.java | 2 +- .../api/WorkflowStatisticsResource.java | 2 +- .../kronos/api/WorkflowTriggerResource.java | 2 +- app/src/main/conf/queue.yaml | 3 +- ...Handler.java => MockAbortTaskHandler.java} | 38 +-- .../handlers/MockFailureTaskHandler.java | 23 +- .../handlers/MockSuccessTaskHandler.java | 25 +- .../kronos/queue/QueueServiceTest.java | 241 +++++++++++++++ .../com/cognitree/kronos/queue/QueueTest.java | 185 ----------- .../kronos/scheduler/JobServiceTest.java | 154 +++++----- .../scheduler/NamespaceServiceTest.java | 1 + .../kronos/scheduler/ServiceTest.java | 46 ++- .../kronos/scheduler/TaskServiceTest.java | 110 ++++--- .../kronos/scheduler/WorkflowServiceTest.java | 143 ++++----- .../scheduler/WorkflowTriggerServiceTest.java | 41 ++- .../cognitree/kronos/store/TaskStoreTest.java | 94 +++--- app/src/test/resources/executor.yaml | 2 +- app/src/test/resources/queue.yaml | 3 +- .../workflow-template-abort-tasks.yaml | 14 + .../cognitree/kronos}/ServiceException.java | 2 +- .../kronos/model/ControlMessage.java | 63 ++++ .../com/cognitree/kronos}/model/Messages.java | 15 +- .../java/com/cognitree/kronos/model/Task.java | 8 +- ...{TaskUpdate.java => TaskStatusUpdate.java} | 8 +- .../cognitree/kronos/queue/QueueConfig.java | 33 +- .../cognitree/kronos/queue/QueueService.java | 258 ++++++++++++++++ .../kronos/queue/consumer/Consumer.java | 11 +- .../kronos/queue/consumer/ConsumerConfig.java | 22 +- .../kronos/queue/consumer/RAMConsumer.java | 18 +- .../kronos/queue/producer/Producer.java | 21 +- .../kronos/queue/producer/ProducerConfig.java | 2 +- .../kronos/queue/producer/RAMProducer.java | 26 +- .../kronos/executor/ExecutorApp.java | 17 +- .../kronos/executor/ExecutorConfig.java | 3 +- .../kronos/executor/TaskExecutionService.java | 286 ++++++++++-------- .../handlers/ShellCommandHandler.java | 26 +- .../kronos/executor/handlers/TaskHandler.java | 24 +- .../executor/handlers/TaskHandlerConfig.java | 3 +- .../executor/TaskExecutorServiceTest.java | 75 ++--- .../executor/handlers/TestTaskHandler.java | 16 +- .../executor/handlers/TypeATaskHandler.java | 8 +- .../executor/handlers/TypeBTaskHandler.java | 8 +- executor/src/test/resources/executor.yaml | 2 +- executor/src/test/resources/queue.yaml | 5 +- .../scheduler/ConfigurationService.java | 51 ++-- .../kronos/scheduler/JobService.java | 24 ++ .../kronos/scheduler/MailService.java | 1 + .../kronos/scheduler/NamespaceService.java | 1 + .../kronos/scheduler/SchedulerApp.java | 37 ++- .../kronos/scheduler/SchedulerConfig.java | 3 +- .../kronos/scheduler/TaskProvider.java | 3 +- .../scheduler/TaskSchedulerService.java | 216 ++++++------- .../kronos/scheduler/TaskService.java | 73 +++-- .../scheduler/WorkflowSchedulerService.java | 1 + .../kronos/scheduler/WorkflowService.java | 11 +- .../scheduler/WorkflowTriggerService.java | 1 + .../scheduler/model/JobExecutionCounters.java | 67 ++++ ...unters.java => TaskExecutionCounters.java} | 14 +- .../kronos/scheduler/model/Workflow.java | 3 +- .../scheduler/model/WorkflowStatistics.java | 12 +- .../scheduler/store/impl/RAMStoreService.java | 1 - .../java/com.cognitree.kronos/TestUtil.java | 27 +- .../scheduler/ConfigurationServiceTest.java | 50 +-- scheduler/src/test/resources/queue.yaml | 3 +- scheduler/src/test/resources/scheduler.yaml | 1 - 69 files changed, 1800 insertions(+), 987 deletions(-) create mode 100644 api/src/main/java/com/cognitree/kronos/api/TaskResource.java rename app/src/test/java/com/cognitree/kronos/executor/handlers/{MockTaskHandler.java => MockAbortTaskHandler.java} (64%) create mode 100644 app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java delete mode 100644 app/src/test/java/com/cognitree/kronos/queue/QueueTest.java create mode 100644 app/src/test/resources/workflows/workflow-template-abort-tasks.yaml rename {scheduler/src/main/java/com/cognitree/kronos/scheduler => common/src/main/java/com/cognitree/kronos}/ServiceException.java (96%) create mode 100644 common/src/main/java/com/cognitree/kronos/model/ControlMessage.java rename {scheduler/src/main/java/com/cognitree/kronos/scheduler => common/src/main/java/com/cognitree/kronos}/model/Messages.java (51%) rename common/src/main/java/com/cognitree/kronos/model/{TaskUpdate.java => TaskStatusUpdate.java} (93%) create mode 100644 common/src/main/java/com/cognitree/kronos/queue/QueueService.java create mode 100644 scheduler/src/main/java/com/cognitree/kronos/scheduler/model/JobExecutionCounters.java rename scheduler/src/main/java/com/cognitree/kronos/scheduler/model/{ExecutionCounters.java => TaskExecutionCounters.java} (87%) diff --git a/api/src/main/java/com/cognitree/kronos/api/JobResource.java b/api/src/main/java/com/cognitree/kronos/api/JobResource.java index b308712..8f3e067 100644 --- a/api/src/main/java/com/cognitree/kronos/api/JobResource.java +++ b/api/src/main/java/com/cognitree/kronos/api/JobResource.java @@ -17,8 +17,8 @@ package com.cognitree.kronos.api; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.scheduler.JobService; -import com.cognitree.kronos.scheduler.ServiceException; import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.model.Job; import io.swagger.annotations.Api; diff --git a/api/src/main/java/com/cognitree/kronos/api/NamespaceResource.java b/api/src/main/java/com/cognitree/kronos/api/NamespaceResource.java index bf43b96..3e6b5e4 100644 --- a/api/src/main/java/com/cognitree/kronos/api/NamespaceResource.java +++ b/api/src/main/java/com/cognitree/kronos/api/NamespaceResource.java @@ -17,8 +17,8 @@ package com.cognitree.kronos.api; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.scheduler.NamespaceService; -import com.cognitree.kronos.scheduler.ServiceException; import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.NamespaceId; diff --git a/api/src/main/java/com/cognitree/kronos/api/TaskResource.java b/api/src/main/java/com/cognitree/kronos/api/TaskResource.java new file mode 100644 index 0000000..e9c22a5 --- /dev/null +++ b/api/src/main/java/com/cognitree/kronos/api/TaskResource.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.cognitree.kronos.api; + +import com.cognitree.kronos.ServiceException; +import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.scheduler.TaskService; +import com.cognitree.kronos.scheduler.ValidationException; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path("/workflows/{workflow}/jobs/{job}/tasks") +@Api(value = "tasks", description = "manage runtime instance of workflow - tasks") +public class TaskResource { + private static final Logger logger = LoggerFactory.getLogger(TaskResource.class); + private static final String ABORT_ACTION = "abort"; + + @POST + @Path("/{task}") + @ApiOperation(value = "Execute an action on a task. Supported actions - abort") + @Produces(MediaType.APPLICATION_JSON) + public Response performAction(@ApiParam(value = "workflow name", required = true) + @PathParam("workflow") String workflow, + @ApiParam(value = "job id", required = true) + @PathParam("job") String job, + @ApiParam(value = "task name", required = true) + @PathParam("task") String task, + @ApiParam(value = "action", allowableValues = ABORT_ACTION, required = true) + @QueryParam("action") String action, + @HeaderParam("namespace") String namespace) throws ServiceException, ValidationException { + logger.info("Received request to perform action {} on task {} part of job {}, workflow {}", + action, task, job, workflow); + if (ABORT_ACTION.equals(action)) { + TaskService.getService().abortTask(Task.build(namespace, task, job, workflow)); + return Response.status(Response.Status.OK).build(); + } else { + return Response.status(Response.Status.BAD_REQUEST).entity("invalid action " + action).build(); + } + } + +} diff --git a/api/src/main/java/com/cognitree/kronos/api/WorkflowJobResource.java b/api/src/main/java/com/cognitree/kronos/api/WorkflowJobResource.java index 9b7c487..dc07738 100644 --- a/api/src/main/java/com/cognitree/kronos/api/WorkflowJobResource.java +++ b/api/src/main/java/com/cognitree/kronos/api/WorkflowJobResource.java @@ -17,10 +17,10 @@ package com.cognitree.kronos.api; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.response.JobResponse; import com.cognitree.kronos.scheduler.JobService; -import com.cognitree.kronos.scheduler.ServiceException; import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.model.Job; import com.cognitree.kronos.scheduler.model.Job.Status; @@ -36,6 +36,7 @@ import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -56,6 +57,7 @@ public class WorkflowJobResource { private static final Logger logger = LoggerFactory.getLogger(WorkflowJobResource.class); private static final String DEFAULT_DAYS = "10"; + private static final String ABORT_ACTION = "abort"; @GET @ApiOperation(value = "Get all running or executed jobs for a workflow", response = Job.class, responseContainer = "List", @@ -135,6 +137,28 @@ public Response getJob(@ApiParam(value = "workflow name", required = true) return Response.status(OK).entity(JobResponse.create(job, tasks)).build(); } + @POST + @Path("/{id}") + @ApiOperation(value = "Execute an action on a job. Supported actions - abort") + @Produces(MediaType.APPLICATION_JSON) + public Response performAction(@ApiParam(value = "workflow name", required = true) + @PathParam("workflow") String workflow, + @ApiParam(value = "job id", required = true) + @PathParam("id") String job, + @ApiParam(value = "action", allowableValues = ABORT_ACTION, required = true) + @QueryParam("action") String action, + @HeaderParam("namespace") String namespace) throws ServiceException, ValidationException { + logger.info("Received request to perform action {} on job {}, workflow {}", + action, job, workflow); + if (ABORT_ACTION.equals(action)) { + JobService.getService().abortJob(Job.build(namespace, job, workflow)); + return Response.status(Response.Status.OK).build(); + } else { + return Response.status(Response.Status.BAD_REQUEST).entity("invalid action " + action).build(); + } + } + + private long timeInMillisBeforeDays(int numberOfDays) { final long currentTimeMillis = System.currentTimeMillis(); return numberOfDays == -1 ? 0 : currentTimeMillis - (currentTimeMillis % TimeUnit.DAYS.toMillis(1)) diff --git a/api/src/main/java/com/cognitree/kronos/api/WorkflowResource.java b/api/src/main/java/com/cognitree/kronos/api/WorkflowResource.java index f9863e0..867f035 100644 --- a/api/src/main/java/com/cognitree/kronos/api/WorkflowResource.java +++ b/api/src/main/java/com/cognitree/kronos/api/WorkflowResource.java @@ -17,7 +17,7 @@ package com.cognitree.kronos.api; -import com.cognitree.kronos.scheduler.ServiceException; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.WorkflowService; import com.cognitree.kronos.scheduler.model.Workflow; diff --git a/api/src/main/java/com/cognitree/kronos/api/WorkflowStatisticsResource.java b/api/src/main/java/com/cognitree/kronos/api/WorkflowStatisticsResource.java index 03f6b27..f8ca3af 100644 --- a/api/src/main/java/com/cognitree/kronos/api/WorkflowStatisticsResource.java +++ b/api/src/main/java/com/cognitree/kronos/api/WorkflowStatisticsResource.java @@ -17,7 +17,7 @@ package com.cognitree.kronos.api; -import com.cognitree.kronos.scheduler.ServiceException; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.WorkflowService; import com.cognitree.kronos.scheduler.model.WorkflowStatistics; diff --git a/api/src/main/java/com/cognitree/kronos/api/WorkflowTriggerResource.java b/api/src/main/java/com/cognitree/kronos/api/WorkflowTriggerResource.java index 487cb13..2b12fc4 100644 --- a/api/src/main/java/com/cognitree/kronos/api/WorkflowTriggerResource.java +++ b/api/src/main/java/com/cognitree/kronos/api/WorkflowTriggerResource.java @@ -17,7 +17,7 @@ package com.cognitree.kronos.api; -import com.cognitree.kronos.scheduler.ServiceException; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.WorkflowTriggerService; import com.cognitree.kronos.scheduler.model.WorkflowId; diff --git a/app/src/main/conf/queue.yaml b/app/src/main/conf/queue.yaml index b8c02a0..70fc6e8 100755 --- a/app/src/main/conf/queue.yaml +++ b/app/src/main/conf/queue.yaml @@ -2,6 +2,7 @@ producerConfig: producerClass: com.cognitree.kronos.queue.producer.RAMProducer consumerConfig: consumerClass: com.cognitree.kronos.queue.consumer.RAMConsumer - pollIntervalInMs: 100 taskStatusQueue: taskstatus configurationQueue: configurations +controlMessageQueue: controlmessages +pollIntervalInMs: 1000 diff --git a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockTaskHandler.java b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockAbortTaskHandler.java similarity index 64% rename from app/src/test/java/com/cognitree/kronos/executor/handlers/MockTaskHandler.java rename to app/src/test/java/com/cognitree/kronos/executor/handlers/MockAbortTaskHandler.java index b0a94b7..d53495d 100644 --- a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockTaskHandler.java +++ b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockAbortTaskHandler.java @@ -19,6 +19,7 @@ import com.cognitree.kronos.executor.model.TaskResult; import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskId; import com.fasterxml.jackson.databind.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,35 +28,38 @@ import java.util.Collections; import java.util.List; -public class MockTaskHandler implements TaskHandler { - private static final Logger logger = LoggerFactory.getLogger(MockTaskHandler.class); +public class MockAbortTaskHandler implements TaskHandler { + private static final Logger logger = LoggerFactory.getLogger(MockAbortTaskHandler.class); - private static final List tasks = Collections.synchronizedList(new ArrayList<>()); + private static final List tasks = Collections.synchronizedList(new ArrayList<>()); + private boolean abort = false; + private Task task; - public static boolean finishExecution(String name, String job, String namespace) { - return tasks.add(getTaskId(name, job, namespace)); - } - - private static String getTaskId(String name, String job, String namespace) { - return name + job + namespace; + public static boolean isHandled(TaskId taskId) { + return tasks.contains(taskId); } @Override - public void init(ObjectNode handlerConfig) { + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { - logger.info("Received request to handle task {}", task); - - while (!tasks.contains(getTaskId(task.getName(), task.getJob(), task.getNamespace()))) { + public TaskResult execute() { + logger.info("Received request to execute task {}", task); + tasks.add(task); + while (!abort) { try { Thread.sleep(50); - } catch (InterruptedException impossible) { - // impossible + } catch (InterruptedException e) { + logger.error("Thread has been interrupted"); } } - tasks.remove(task.getName()); return TaskResult.SUCCESS; } + + @Override + public void abort() { + abort = true; + } } diff --git a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockFailureTaskHandler.java b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockFailureTaskHandler.java index 5dcc24a..ebafe0e 100644 --- a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockFailureTaskHandler.java +++ b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockFailureTaskHandler.java @@ -19,31 +19,34 @@ import com.cognitree.kronos.executor.model.TaskResult; import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskId; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class MockFailureTaskHandler implements TaskHandler { - private static final List tasks = Collections.synchronizedList(new ArrayList<>()); + private static final Logger logger = LoggerFactory.getLogger(MockFailureTaskHandler.class); + private static final List tasks = Collections.synchronizedList(new ArrayList<>()); - public static boolean isHandled(String name, String job, String namespace) { - return tasks.contains(getTaskId(name, job, namespace)); - } + private Task task; - private static String getTaskId(String name, String job, String namespace) { - return name + job + namespace; + public static boolean isHandled(TaskId taskId) { + return tasks.contains(taskId); } @Override - public void init(ObjectNode handlerConfig) { - + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { - tasks.add(getTaskId(task.getName(), task.getJob(), task.getNamespace())); + public TaskResult execute() { + logger.info("Received request to execute task {}", task); + tasks.add(task.getIdentity()); return new TaskResult(false); } } diff --git a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockSuccessTaskHandler.java b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockSuccessTaskHandler.java index ea1e4e7..79e4c98 100644 --- a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockSuccessTaskHandler.java +++ b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockSuccessTaskHandler.java @@ -19,7 +19,10 @@ import com.cognitree.kronos.executor.model.TaskResult; import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskId; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; @@ -28,30 +31,30 @@ public class MockSuccessTaskHandler implements TaskHandler { public static final HashMap CONTEXT = new HashMap<>(); + private static final Logger logger = LoggerFactory.getLogger(MockSuccessTaskHandler.class); - private static final List tasks = Collections.synchronizedList(new ArrayList<>()); + private static final List tasks = Collections.synchronizedList(new ArrayList<>()); static { CONTEXT.put("valOne", 1234); CONTEXT.put("valTwo", "abcd"); } - public static boolean isHandled(String name, String job, String namespace) { - return tasks.contains(getTaskId(name, job, namespace)); - } + private Task task; - private static String getTaskId(String name, String job, String namespace) { - return name + job + namespace; + public static boolean isHandled(TaskId taskId) { + return tasks.contains(taskId); } @Override - public void init(ObjectNode handlerConfig) { - + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { - tasks.add(getTaskId(task.getName(), task.getJob(), task.getNamespace())); - return new TaskResult(true, null, CONTEXT); + public TaskResult execute() { + logger.info("Received request to execute task {}", task); + tasks.add(task.getIdentity()); + return new TaskResult(true, null, new HashMap<>(CONTEXT)); } } diff --git a/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java b/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java new file mode 100644 index 0000000..37f5c6d --- /dev/null +++ b/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.cognitree.kronos.queue; + +import com.cognitree.kronos.ServiceException; +import com.cognitree.kronos.model.ControlMessage; +import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskStatusUpdate; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +import static com.cognitree.kronos.model.Task.Status.FAILED; +import static com.cognitree.kronos.model.Task.Status.RUNNING; +import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; + +public class QueueServiceTest { + private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); + private static final String SERVICE_NAME = "queue-service"; + private static final String TASK_TYPE_A = "typeA"; + private static final String TASK_TYPE_B = "typeB"; + private static final int WAIT_FOR_NEXT_POLL = 2000; + + private static QueueService QUEUE_SERVICE; + + @BeforeClass + public static void init() throws IOException, ServiceException { + final InputStream queueConfigAsStream = + QueueServiceTest.class.getClassLoader().getResourceAsStream("queue.yaml"); + QueueConfig queueConfig = MAPPER.readValue(queueConfigAsStream, QueueConfig.class); + QUEUE_SERVICE = new QueueService(queueConfig, SERVICE_NAME); + QUEUE_SERVICE.init(); + QUEUE_SERVICE.start(); + // initial call so that the topics are created + QUEUE_SERVICE.consumeTask(TASK_TYPE_A, 1); + QUEUE_SERVICE.consumeTask(TASK_TYPE_B, 1); + QUEUE_SERVICE.consumeTaskStatusUpdates(); + QUEUE_SERVICE.consumeControlMessages(); + } + + @AfterClass + public static void destroy() { + QUEUE_SERVICE.stop(); + QUEUE_SERVICE.destroy(); + } + + /** + * Test unordered messages + * + * @throws ServiceException + */ + @Test + public void testSendAndConsumeTask() throws ServiceException { + Task taskAOne = createTask(TASK_TYPE_A); + QUEUE_SERVICE.send(taskAOne); + Task taskATwo = createTask(TASK_TYPE_A); + QUEUE_SERVICE.send(taskATwo); + Task taskAThree = createTask(TASK_TYPE_A); + QUEUE_SERVICE.send(taskAThree); + + Task taskBOne = createTask(TASK_TYPE_B); + QUEUE_SERVICE.send(taskBOne); + Task taskBTwo = createTask(TASK_TYPE_B); + QUEUE_SERVICE.send(taskBTwo); + Task taskBThree = createTask(TASK_TYPE_B); + QUEUE_SERVICE.send(taskBThree); + + ArrayList tasksB = new ArrayList<>(); + tasksB.add(taskBOne); + tasksB.add(taskBTwo); + tasksB.add(taskBThree); + + List tasksBConsumed = getTasks(TASK_TYPE_B, 3); + Assert.assertTrue("Records sent " + tasksB + " and records consumed" + tasksBConsumed + " do not match", + tasksBConsumed.size() == tasksB.size() && tasksBConsumed.containsAll(tasksB)); + + ArrayList tasksA = new ArrayList<>(); + tasksA.add(taskAOne); + tasksA.add(taskATwo); + tasksA.add(taskAThree); + + List tasksAConsumed = getTasks(TASK_TYPE_A, 3); + Assert.assertTrue("Records sent " + tasksA + " and records consumed" + tasksAConsumed + " do not match", + tasksAConsumed.size() == tasksA.size() && tasksAConsumed.containsAll(tasksA)); + } + + /** + * Test ordered messages + * + * @throws ServiceException + */ + @Test + public void testSendAndConsumeTaskStatusUpdates() throws ServiceException { + Task task = createTask(TASK_TYPE_A); + TaskStatusUpdate taskStatusUpdateOne = createTaskStatusUpdate(task, RUNNING); + QUEUE_SERVICE.send(taskStatusUpdateOne); + TaskStatusUpdate taskStatusUpdateTwo = createTaskStatusUpdate(task, FAILED); + QUEUE_SERVICE.send(taskStatusUpdateTwo); + TaskStatusUpdate taskStatusUpdateThree = createTaskStatusUpdate(task, SUCCESSFUL); + QUEUE_SERVICE.send(taskStatusUpdateThree); + + List taskStatusUpdates = getTaskStatusUpdate(); + Assert.assertFalse(taskStatusUpdates.isEmpty()); + Assert.assertEquals(taskStatusUpdateOne, taskStatusUpdates.get(0)); + Assert.assertEquals(taskStatusUpdateTwo, taskStatusUpdates.get(1)); + Assert.assertEquals(taskStatusUpdateThree, taskStatusUpdates.get(2)); + } + + @Test + public void testSendAndConsumeControlMessageUpdates() throws ServiceException { + Task task = createTask(TASK_TYPE_A); + ControlMessage controlMessageOne = createControlMessages(task, Task.Action.ABORT); + QUEUE_SERVICE.send(controlMessageOne); + ControlMessage controlMessageTwo = createControlMessages(task, Task.Action.ABORT); + QUEUE_SERVICE.send(controlMessageTwo); + ControlMessage controlMessageThree = createControlMessages(task, Task.Action.TIME_OUT); + QUEUE_SERVICE.send(controlMessageThree); + + ArrayList controlMessages = new ArrayList<>(); + controlMessages.add(controlMessageOne); + controlMessages.add(controlMessageTwo); + controlMessages.add(controlMessageThree); + + List controlMessagesConsumed = getControlMessages(); + Assert.assertFalse(controlMessagesConsumed.isEmpty()); + + Assert.assertTrue("Records sent " + controlMessages + " and records consumed" + controlMessagesConsumed + " do not match", + controlMessagesConsumed.size() == controlMessages.size() && controlMessagesConsumed.containsAll(controlMessages)); + } + + private List getControlMessages() throws ServiceException { + int count = 10; + while (count > 0) { + List controlMessages = QUEUE_SERVICE.consumeControlMessages(); + if (!controlMessages.isEmpty()) { + return controlMessages; + } + try { + count--; + Thread.sleep(WAIT_FOR_NEXT_POLL); + } catch (InterruptedException e) { + // + } + } + return Collections.emptyList(); + } + + + private List getTaskStatusUpdate() throws ServiceException { + int count = 10; + while (count > 0) { + List taskStatusUpdates = QUEUE_SERVICE.consumeTaskStatusUpdates(); + if (!taskStatusUpdates.isEmpty()) { + return taskStatusUpdates; + } + try { + count--; + Thread.sleep(WAIT_FOR_NEXT_POLL); + } catch (InterruptedException e) { + // + } + } + return Collections.emptyList(); + } + + private List getTasks(String taskType, int size) throws ServiceException { + int count = 10; + while (count > 0) { + List tasks = QUEUE_SERVICE.consumeTask(taskType, size); + if (!tasks.isEmpty()) { + return tasks; + } + try { + count--; + Thread.sleep(WAIT_FOR_NEXT_POLL); + } catch (InterruptedException e) { + // + } + } + return Collections.emptyList(); + } + + private Task createTask(String type) { + final Task task = new Task(); + task.setName(UUID.randomUUID().toString()); + task.setWorkflow(UUID.randomUUID().toString()); + task.setJob(UUID.randomUUID().toString()); + task.setType(type); + HashMap properties = new HashMap<>(); + properties.put("A", "A"); + properties.put("B", "B"); + task.setProperties(properties); + task.setCreatedAt(System.currentTimeMillis()); + return task; + } + + private TaskStatusUpdate createTaskStatusUpdate(Task task, Task.Status status) { + final TaskStatusUpdate statusUpdate = new TaskStatusUpdate(); + statusUpdate.setTaskId(task); + HashMap properties = new HashMap<>(); + properties.put("A", "A"); + properties.put("B", "B"); + statusUpdate.setContext(properties); + statusUpdate.setStatus(status); + return statusUpdate; + } + + private ControlMessage createControlMessages(Task task, Task.Action action) { + final ControlMessage controlMessage = new ControlMessage(); + controlMessage.setTask(task); + controlMessage.setAction(action); + return controlMessage; + } + +} diff --git a/app/src/test/java/com/cognitree/kronos/queue/QueueTest.java b/app/src/test/java/com/cognitree/kronos/queue/QueueTest.java deleted file mode 100644 index cd3852b..0000000 --- a/app/src/test/java/com/cognitree/kronos/queue/QueueTest.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.cognitree.kronos.queue; - -import com.cognitree.kronos.queue.consumer.Consumer; -import com.cognitree.kronos.queue.consumer.ConsumerConfig; -import com.cognitree.kronos.queue.producer.Producer; -import com.cognitree.kronos.queue.producer.ProducerConfig; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.io.InputStream; -import java.util.LinkedList; -import java.util.List; - -public class QueueTest { - - private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); - private static final String TOPIC_A = "topicA"; - private static final String TOPIC_B = "topicB"; - - private static Producer PRODUCER; - private static Consumer CONSUMER; - - @Before - public void init() throws Exception { - final InputStream queueConfigAsStream = - QueueTest.class.getClassLoader().getResourceAsStream("queue.yaml"); - QueueConfig queueConfig = MAPPER.readValue(queueConfigAsStream, QueueConfig.class); - initProducer(queueConfig.getProducerConfig()); - initConsumer(queueConfig.getConsumerConfig()); - Thread.sleep(2000); - } - - private void initProducer(ProducerConfig producerConfig) throws Exception { - PRODUCER = (Producer) Class.forName(producerConfig.getProducerClass()) - .getConstructor() - .newInstance(); - PRODUCER.init(producerConfig.getConfig()); - } - - private void initConsumer(ConsumerConfig consumerConfig) throws Exception { - CONSUMER = (Consumer) Class.forName(consumerConfig.getConsumerClass()) - .getConstructor() - .newInstance(); - CONSUMER.init(consumerConfig.getConfig()); - for (int i = 0; i < 2; i++) { // for kafka consumer to poll all the messages before we start the test - CONSUMER.poll(TOPIC_A); - CONSUMER.poll(TOPIC_B); - } - } - - @After - public void destroy() { - PRODUCER.close(); - CONSUMER.close(); - CONSUMER.destroy(); - } - - @Test - public void testProducerAndConsumerSingleTopic() throws InterruptedException { - LinkedList records = new LinkedList<>(); - records.add("record1"); - records.add("record2"); - records.add("record3"); - records.add("record4"); - records.forEach(record -> PRODUCER.send(TOPIC_A, record)); - Thread.sleep(2000); - - List recordsFromConsumer = CONSUMER.poll(TOPIC_A, 4); - for (int i = 0; i < 2; i++) { - recordsFromConsumer.addAll(CONSUMER.poll(TOPIC_A)); - Thread.sleep(500); - } - Assert.assertTrue("Records sent " + records + " and records consumed" + recordsFromConsumer + " do not match", - recordsFromConsumer.size() == records.size() && - recordsFromConsumer.containsAll(records) && records.containsAll(recordsFromConsumer)); - } - - @Test - public void testProducerAndConsumerMultipleTopic() throws InterruptedException { - LinkedList recordsForA = new LinkedList<>(); - recordsForA.add("record1"); - recordsForA.add("record2"); - recordsForA.add("record3"); - recordsForA.add("record4"); - recordsForA.forEach(record -> PRODUCER.send(TOPIC_A, record)); - - LinkedList recordsForB = new LinkedList<>(); - recordsForB.add("record1"); - recordsForB.add("record2"); - recordsForB.add("record3"); - recordsForB.add("record4"); - recordsForB.forEach(record -> PRODUCER.send(TOPIC_B, record)); - - Thread.sleep(500); - - List recordsFromConsumerTopicA = CONSUMER.poll(TOPIC_A, 4); - for (int i = 0; i < 2; i++) { - recordsFromConsumerTopicA.addAll(CONSUMER.poll(TOPIC_A)); - Thread.sleep(500); - } - Assert.assertTrue("Records sent " + recordsForA + " and records consumed" + recordsFromConsumerTopicA + " do not match", - recordsFromConsumerTopicA.size() == recordsForA.size() && - recordsFromConsumerTopicA.containsAll(recordsForA) && recordsForA.containsAll(recordsFromConsumerTopicA)); - - List recordsFromConsumerTopicB = CONSUMER.poll(TOPIC_B, 4); - for (int i = 0; i < 2; i++) { - recordsFromConsumerTopicB.addAll(CONSUMER.poll(TOPIC_B)); - Thread.sleep(500); - } - Assert.assertTrue("Records sent " + recordsForB + " and records consumed" + recordsFromConsumerTopicB + " do not match", - recordsFromConsumerTopicB.size() == recordsForB.size() && - recordsFromConsumerTopicB.containsAll(recordsForB) && recordsForB.containsAll(recordsFromConsumerTopicB)); - } - - @Test - public void testProducerAndConsumerSingleTopicInOrder() throws InterruptedException { - LinkedList records = new LinkedList<>(); - records.add("record1"); - records.add("record2"); - records.add("record3"); - records.add("record4"); - records.forEach(record -> PRODUCER.sendInOrder(TOPIC_A, record, "orderingKey")); - Thread.sleep(2000); - - List recordsFromConsumer = CONSUMER.poll(TOPIC_A, 4); - for (int i = 0; i < 2; i++) { - recordsFromConsumer.addAll(CONSUMER.poll(TOPIC_A)); - Thread.sleep(500); - } - Assert.assertEquals(records, recordsFromConsumer); - } - - @Test - public void testProducerAndConsumerMultipleTopicInOrder() throws InterruptedException { - LinkedList recordsForA = new LinkedList<>(); - recordsForA.add("record1"); - recordsForA.add("record2"); - recordsForA.add("record3"); - recordsForA.add("record4"); - recordsForA.forEach(record -> PRODUCER.sendInOrder(TOPIC_A, record, "orderingKey")); - Thread.sleep(2000); - - LinkedList recordsForB = new LinkedList<>(); - recordsForB.add("record1"); - recordsForB.add("record2"); - recordsForB.add("record3"); - recordsForB.add("record4"); - recordsForB.forEach(record -> PRODUCER.sendInOrder(TOPIC_B, record, "orderingKey")); - - List recordsFromConsumerTopicA = CONSUMER.poll(TOPIC_A, 4); - for (int i = 0; i < 2; i++) { - recordsFromConsumerTopicA.addAll(CONSUMER.poll(TOPIC_A)); - Thread.sleep(500); - } - Assert.assertEquals(recordsForA, recordsFromConsumerTopicA); - - List recordsFromConsumerTopicB = CONSUMER.poll(TOPIC_B, 4); - for (int i = 0; i < 2; i++) { - recordsFromConsumerTopicB.addAll(CONSUMER.poll(TOPIC_B)); - Thread.sleep(500); - } - Assert.assertEquals(recordsForB, recordsFromConsumerTopicB); - } -} diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java index 91d7cc2..7929342 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java @@ -17,34 +17,33 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.executor.handlers.MockAbortTaskHandler; import com.cognitree.kronos.executor.handlers.MockFailureTaskHandler; -import com.cognitree.kronos.executor.handlers.MockTaskHandler; +import com.cognitree.kronos.executor.handlers.MockSuccessTaskHandler; +import com.cognitree.kronos.model.Messages; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.scheduler.model.Job; -import com.cognitree.kronos.scheduler.model.Messages; +import com.cognitree.kronos.scheduler.model.JobId; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; import org.junit.Assert; import org.junit.Test; -import org.quartz.Scheduler; import java.util.List; +import java.util.UUID; import static com.cognitree.kronos.TestUtil.scheduleWorkflow; +import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; import static com.cognitree.kronos.TestUtil.waitForTriggerToComplete; public class JobServiceTest extends ServiceTest { @Test public void testGetAllJobsByNamespace() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for both the job to be triggered - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace()); @@ -58,15 +57,11 @@ public void testGetAllJobsByNamespace() throws Exception { @Test public void testGetAllJobsCreatedInLastNDays() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for both the job to be triggered - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace()); @@ -80,15 +75,11 @@ public void testGetAllJobsCreatedInLastNDays() throws Exception { @Test public void testGetAllJobsByWorkflow() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for both the job to be triggered - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace(), workflowTriggerOne.getWorkflow(), @@ -102,15 +93,11 @@ public void testGetAllJobsByWorkflow() throws Exception { @Test public void testGetAllJobsByWorkflowAndTrigger() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for both the job to be triggered - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace(), workflowTriggerOne.getWorkflow(), @@ -124,13 +111,9 @@ public void testGetAllJobsByWorkflowAndTrigger() throws Exception { @Test public void testGetJobTasks() throws Exception { - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for the job to be triggered - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), @@ -143,12 +126,15 @@ public void testGetJobTasks() throws Exception { switch (task.getName()) { case "taskOne": Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; case "taskTwo": Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; case "taskThree": Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; default: Assert.fail(); @@ -158,13 +144,9 @@ public void testGetJobTasks() throws Exception { @Test public void testGetJobTasksFailedDueToTimeout() throws Exception { - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template-timeout-tasks.yaml"); + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_TIMEOUT_TASKS_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for the job to be triggered - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), @@ -174,39 +156,32 @@ public void testGetJobTasksFailedDueToTimeout() throws Exception { final Job job = workflowOneJobs.get(0); final List tasks = jobService.getTasks(job); Assert.assertEquals(3, tasks.size()); - Task taskThree = null; for (Task task : tasks) { switch (task.getName()) { case "taskOne": Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; case "taskTwo": Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; case "taskThree": - taskThree = task; - Assert.assertEquals(Task.Status.FAILED, task.getStatus()); - Assert.assertEquals(Messages.TIMED_OUT, task.getStatusMessage()); + Assert.assertEquals(Task.Status.ABORTED, task.getStatus()); + Assert.assertEquals(Messages.TIMED_OUT_EXECUTING_TASK_MESSAGE, task.getStatusMessage()); + Assert.assertTrue(MockAbortTaskHandler.isHandled(task.getIdentity())); break; default: Assert.fail(); } } - Assert.assertNotNull(taskThree); - // Right now timed out tasks are not cleaned up on the executor side - // thus finishing the task execution manually - MockTaskHandler.finishExecution(taskThree.getName(), taskThree.getJob(), taskThree.getNamespace()); } @Test public void testGetJobTasksFailedDueToHandler() throws Exception { - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template-failed-handler.yaml"); + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_FAILED_HANDLER_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for the job to be triggered - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), @@ -216,38 +191,73 @@ public void testGetJobTasksFailedDueToHandler() throws Exception { final Job job = workflowOneJobs.get(0); final List tasks = jobService.getTasks(job); Assert.assertEquals(3, tasks.size()); - Task taskTwo = null; for (Task task : tasks) { switch (task.getName()) { case "taskOne": Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; case "taskTwo": - taskTwo = task; Assert.assertEquals(Task.Status.FAILED, task.getStatus()); Assert.assertEquals(3, task.getRetryCount()); + Assert.assertTrue(MockFailureTaskHandler.isHandled(task.getIdentity())); break; case "taskThree": Assert.assertEquals(Task.Status.SKIPPED, task.getStatus()); - Assert.assertEquals(Messages.FAILED_DEPENDEE_TASK, task.getStatusMessage()); + Assert.assertEquals(Messages.FAILED_DEPENDEE_TASK_MESSAGE, task.getStatusMessage()); + break; + default: + Assert.fail(); + } + } + } + + @Test(expected = ValidationException.class) + public void testAbortJobNotFound() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + waitForJobsToTriggerAndComplete(workflowTrigger); + + List jobs = JobService.getService().get(workflowTrigger.getNamespace()); + Assert.assertFalse(jobs.isEmpty()); + JobService.getService().abortJob(JobId.build(workflowTrigger.getNamespace(), + UUID.randomUUID().toString(), workflowTrigger.getWorkflow())); + } + + @Test + public void testAbortJob() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_ABORT_TASKS_YAML); + + waitForTriggerToComplete(workflowTrigger, WorkflowSchedulerService.getService().getScheduler()); + + List jobs = JobService.getService().get(workflowTrigger.getNamespace()); + JobService.getService().abortJob(jobs.get(0).getIdentity()); + + waitForJobsToTriggerAndComplete(workflowTrigger); + List tasks = TaskService.getService().get(workflowTrigger.getNamespace()); + for (Task tsk : tasks) { + switch (tsk.getName()) { + case "taskOne": + Assert.assertEquals(Task.Status.ABORTED, tsk.getStatus()); + Assert.assertTrue(MockAbortTaskHandler.isHandled(tsk.getIdentity())); + break; + case "taskTwo": + Assert.assertEquals(Task.Status.ABORTED, tsk.getStatus()); break; default: Assert.fail(); } } - Assert.assertNotNull(taskTwo); - Assert.assertTrue(MockFailureTaskHandler.isHandled(taskTwo.getName(), taskTwo.getJob(), taskTwo.getNamespace())); + + Assert.assertEquals(Job.Status.FAILED, + JobService.getService().get(workflowTrigger.getNamespace()).get(0).getStatus()); } + @Test public void testDeleteJob() throws Exception { - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for both the job to be triggered - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); JobService jobService = JobService.getService(); final List workflowOneJobs = jobService.get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/NamespaceServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/NamespaceServiceTest.java index 0c3eb16..4eb2b61 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/NamespaceServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/NamespaceServiceTest.java @@ -17,6 +17,7 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.scheduler.model.Namespace; import org.junit.Assert; import org.junit.Test; diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java index c6e551c..38e6cc0 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java @@ -17,39 +17,75 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.executor.ExecutorApp; +import com.cognitree.kronos.executor.ExecutorConfig; +import com.cognitree.kronos.queue.QueueService; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.Workflow; import com.cognitree.kronos.scheduler.store.NamespaceStore; import com.cognitree.kronos.scheduler.store.StoreService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.junit.AfterClass; import org.junit.BeforeClass; +import java.io.InputStream; +import java.util.ArrayList; import java.util.List; public class ServiceTest { + static final String WORKFLOW_TEMPLATE_YAML = "workflows/workflow-template.yaml"; + static final String WORKFLOW_TEMPLATE_TIMEOUT_TASKS_YAML = "workflows/workflow-template-timeout-tasks.yaml"; + static final String WORKFLOW_TEMPLATE_FAILED_HANDLER_YAML = "workflows/workflow-template-failed-handler.yaml"; + static final String INVALID_WORKFLOW_MISSING_TASKS_TEMPLATE_YAML = "workflows/invalid-workflow-missing-tasks-template.yaml"; + static final String INVALID_WORKFLOW_DISABLED_TASKS_TEMPLATE_YAML = "workflows/invalid-workflow-disabled-tasks-template.yaml"; + static final String WORKFLOW_TEMPLATE_ABORT_TASKS_YAML = "workflows/workflow-template-abort-tasks.yaml"; + static final String WORKFLOW_TEMPLATE_WITH_TASK_CONTEXT_YAML = "workflows/workflow-template-with-task-context.yaml"; + static final String WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML = "workflows/workflow-template-with-properties.yaml"; + static final String WORKFLOW_TEMPLATE_WITH_DUPLICATE_POLICY_YAML = "workflows/workflow-template-with-duplicate-policy.yaml"; + + private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); private static final SchedulerApp SCHEDULER_APP = new SchedulerApp(); private static final ExecutorApp EXECUTOR_APP = new ExecutorApp(); - - private static List existingNamespaces; + private static final List EXISTING_NAMESPACE = new ArrayList<>(); @BeforeClass public static void start() throws Exception { SCHEDULER_APP.start(); EXECUTOR_APP.start(); - existingNamespaces = NamespaceService.getService().get(); + EXISTING_NAMESPACE.addAll(NamespaceService.getService().get()); + createTopics(); + } + + private static void createTopics() throws ServiceException, java.io.IOException { + // initial call so that the topics are created + QueueService.getService(QueueService.SCHEDULER_QUEUE).consumeTaskStatusUpdates(); + QueueService.getService(QueueService.SCHEDULER_QUEUE).consumeControlMessages(); + final InputStream executorConfigAsStream = + ServiceTest.class.getClassLoader().getResourceAsStream("executor.yaml"); + ExecutorConfig executorConfig = MAPPER.readValue(executorConfigAsStream, ExecutorConfig.class); + executorConfig.getTaskHandlerConfig().forEach((type, taskHandlerConfig) -> { + try { + QueueService.getService(QueueService.EXECUTOR_QUEUE) + .consumeTask(type, 0); + } catch (ServiceException e) { + // do nothing + } + }); } @AfterClass public static void stop() throws Exception { List namespaces = NamespaceService.getService().get(); - namespaces.removeAll(existingNamespaces); + namespaces.removeAll(EXISTING_NAMESPACE); cleanupStore(namespaces); SCHEDULER_APP.stop(); EXECUTOR_APP.stop(); // cleanup queue - TaskSchedulerService.getService().getConsumer().destroy(); + QueueService.getService(QueueService.SCHEDULER_QUEUE).destroy(); + QueueService.getService(QueueService.EXECUTOR_QUEUE).destroy(); } private static void cleanupStore(List namespaces) throws Exception { diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java index 9c03ffe..9d92fdd 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java @@ -17,32 +17,33 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.executor.handlers.MockAbortTaskHandler; import com.cognitree.kronos.executor.handlers.MockSuccessTaskHandler; +import com.cognitree.kronos.model.Messages; import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskId; import com.cognitree.kronos.scheduler.model.Job; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; import org.junit.Assert; import org.junit.Test; -import org.quartz.Scheduler; import java.util.Collections; import java.util.List; +import java.util.UUID; import static com.cognitree.kronos.TestUtil.scheduleWorkflow; +import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; import static com.cognitree.kronos.TestUtil.waitForTriggerToComplete; public class TaskServiceTest extends ServiceTest { @Test public void testGetTasksByNamespace() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); TaskService taskService = TaskService.getService(); final List workflowOneTasks = taskService.get(workflowTriggerOne.getNamespace()); @@ -53,61 +54,93 @@ public void testGetTasksByNamespace() throws Exception { @Test public void testGetTasksByJob() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); JobService jobService = JobService.getService(); TaskService taskService = TaskService.getService(); final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace(), 0, System.currentTimeMillis()); Assert.assertEquals(1, workflowOneJobs.size()); Assert.assertNotNull(jobService.get(workflowOneJobs.get(0))); - final List workflowOneTasks = taskService.get(workflowTriggerOne.getNamespace(), workflowOneJobs.get(0).getId(), workflowOneJobs.get(0).getWorkflow() - ); + final List workflowOneTasks = taskService.get(workflowTriggerOne.getNamespace(), + workflowOneJobs.get(0).getId(), workflowOneJobs.get(0).getWorkflow()); Assert.assertEquals(3, workflowOneTasks.size()); final List workflowTwoJobs = jobService.get(workflowTriggerTwo.getNamespace(), 0, System.currentTimeMillis()); Assert.assertEquals(1, workflowTwoJobs.size()); Assert.assertNotNull(jobService.get(workflowTwoJobs.get(0))); - final List workflowTwoTasks = taskService.get(workflowTriggerTwo.getNamespace(), workflowTwoJobs.get(0).getId(), workflowTwoJobs.get(0).getWorkflow() - ); + final List workflowTwoTasks = taskService.get(workflowTriggerTwo.getNamespace(), + workflowTwoJobs.get(0).getId(), workflowTwoJobs.get(0).getWorkflow()); Assert.assertEquals(3, workflowTwoTasks.size()); } @Test public void testGetTasksByStatus() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); TaskService taskService = TaskService.getService(); - final List workflowOneTasks = taskService.get(workflowTriggerOne.getNamespace(), Collections.singletonList(Task.Status.SUCCESSFUL) - ); + final List workflowOneTasks = taskService.get(workflowTriggerOne.getNamespace(), + Collections.singletonList(Task.Status.SUCCESSFUL)); Assert.assertEquals(3, workflowOneTasks.size()); - final List workflowTwoTasks = taskService.get(workflowTriggerTwo.getNamespace(), Collections.singletonList(Task.Status.SUCCESSFUL) - ); + final List workflowTwoTasks = taskService.get(workflowTriggerTwo.getNamespace(), + Collections.singletonList(Task.Status.SUCCESSFUL)); Assert.assertEquals(3, workflowTwoTasks.size()); } + @Test(expected = ValidationException.class) + public void testAbortTasksNotFound() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + waitForJobsToTriggerAndComplete(workflowTrigger); + + List jobs = JobService.getService().get(workflowTrigger.getNamespace()); + Assert.assertFalse(jobs.isEmpty()); + TaskService.getService().abortTask(TaskId.build(workflowTrigger.getNamespace(), + UUID.randomUUID().toString(), jobs.get(0).getId(), workflowTrigger.getWorkflow())); + } + + @Test + public void testAbortTasks() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_ABORT_TASKS_YAML); + + waitForTriggerToComplete(workflowTrigger, WorkflowSchedulerService.getService().getScheduler()); + + TaskService taskService = TaskService.getService(); + final List workflowTasks = taskService.get(workflowTrigger.getNamespace()); + Assert.assertEquals(2, workflowTasks.size()); + Task task = workflowTasks.stream().filter(t -> t.getName().equals("taskOne")).findFirst().get(); + TaskService.getService().abortTask(task); + + waitForJobsToTriggerAndComplete(workflowTrigger); + List tasks = taskService.get(workflowTrigger.getNamespace()); + for (Task tsk : tasks) { + switch (tsk.getName()) { + case "taskOne": + Assert.assertEquals(Task.Status.ABORTED, tsk.getStatus()); + Assert.assertTrue(MockAbortTaskHandler.isHandled(tsk.getIdentity())); + break; + case "taskTwo": + Assert.assertEquals(Task.Status.SKIPPED, tsk.getStatus()); + Assert.assertEquals(Messages.ABORTED_DEPENDEE_TASK_MESSAGE, tsk.getStatusMessage()); + break; + default: + Assert.fail(); + } + } + } + @Test public void testDeleteTask() throws Exception { - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template.yaml"); + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); TaskService taskService = TaskService.getService(); final List workflowOneTasks = taskService.get(workflowTrigger.getNamespace()); @@ -121,12 +154,9 @@ public void testDeleteTask() throws Exception { @Test public void testTaskWithContextFromDependee() throws Exception { - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template-with-task-context.yaml"); + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_WITH_TASK_CONTEXT_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); TaskService taskService = TaskService.getService(); final List workflowTasks = taskService.get(workflowTrigger.getNamespace()); diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowServiceTest.java index cb98734..20c4c59 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowServiceTest.java @@ -17,16 +17,15 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.executor.handlers.MockSuccessTaskHandler; import com.cognitree.kronos.model.Task; -import com.cognitree.kronos.scheduler.model.Job; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.Workflow; import com.cognitree.kronos.scheduler.model.WorkflowStatistics; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; import org.junit.Assert; import org.junit.Test; -import org.quartz.Scheduler; import org.quartz.SchedulerException; import java.io.IOException; @@ -38,14 +37,14 @@ import static com.cognitree.kronos.TestUtil.createNamespace; import static com.cognitree.kronos.TestUtil.createWorkflow; import static com.cognitree.kronos.TestUtil.scheduleWorkflow; -import static com.cognitree.kronos.TestUtil.waitForTriggerToComplete; +import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; public class WorkflowServiceTest extends ServiceTest { @Test(expected = ValidationException.class) public void testAddWorkflowWithoutNamespace() throws ValidationException, ServiceException, IOException { final WorkflowService workflowService = WorkflowService.getService(); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), UUID.randomUUID().toString()); workflowService.add(workflow); Assert.fail(); @@ -53,7 +52,7 @@ public void testAddWorkflowWithoutNamespace() throws ValidationException, Servic @Test(expected = ValidationException.class) public void testAddInValidWorkflowMissingTasks() throws Exception { - Workflow invalidWorkflow = createWorkflow("workflows/invalid-workflow-missing-tasks-template.yaml", + Workflow invalidWorkflow = createWorkflow(INVALID_WORKFLOW_MISSING_TASKS_TEMPLATE_YAML, UUID.randomUUID().toString(), UUID.randomUUID().toString()); WorkflowService.getService().add(invalidWorkflow); Assert.fail(); @@ -61,7 +60,7 @@ public void testAddInValidWorkflowMissingTasks() throws Exception { @Test(expected = ValidationException.class) public void testAddInValidWorkflowDisabledTaskDependency() throws Exception { - Workflow invalidWorkflow = createWorkflow("workflows/invalid-workflow-disabled-tasks-template.yaml", + Workflow invalidWorkflow = createWorkflow(INVALID_WORKFLOW_DISABLED_TASKS_TEMPLATE_YAML, UUID.randomUUID().toString(), UUID.randomUUID().toString()); WorkflowService.getService().add(invalidWorkflow); Assert.fail(); @@ -73,7 +72,7 @@ public void testAddWorkflow() throws ServiceException, ValidationException, IOEx NamespaceService.getService().add(namespaceOne); final WorkflowService workflowService = WorkflowService.getService(); - final Workflow workflowOne = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowOne = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); workflowService.add(workflowOne); @@ -81,7 +80,7 @@ public void testAddWorkflow() throws ServiceException, ValidationException, IOEx Assert.assertNotNull(workflowOneFromDB); Assert.assertEquals(workflowOne, workflowOneFromDB); - final Workflow workflowTwo = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowTwo = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); workflowService.add(workflowTwo); @@ -92,7 +91,7 @@ public void testAddWorkflow() throws ServiceException, ValidationException, IOEx Namespace namespaceTwo = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceTwo); - final Workflow workflowThree = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowThree = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceTwo.getName()); workflowService.add(workflowThree); @@ -115,7 +114,7 @@ public void testReAddWorkflow() throws ServiceException, ValidationException, IO final NamespaceService namespaceService = NamespaceService.getService(); final Namespace namespace = createNamespace(UUID.randomUUID().toString()); namespaceService.add(namespace); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); WorkflowService.getService().add(workflow); WorkflowService.getService().add(workflow); @@ -123,18 +122,16 @@ public void testReAddWorkflow() throws ServiceException, ValidationException, IO } @Test - public void testUpdateWorkflow() throws ServiceException, ValidationException, SchedulerException, IOException { + public void testUpdateWorkflow() throws ServiceException, ValidationException, IOException { final NamespaceService namespaceService = NamespaceService.getService(); final Namespace namespace = createNamespace(UUID.randomUUID().toString()); namespaceService.add(namespace); final WorkflowService workflowService = WorkflowService.getService(); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", - UUID.randomUUID().toString(), namespace.getName()); + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); workflowService.add(workflow); - final Workflow updatedWorkflow = createWorkflow("workflows/workflow-template.yaml", - workflow.getName(), namespace.getName()); + final Workflow updatedWorkflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, workflow.getName(), namespace.getName()); Workflow.WorkflowTask workflowTaskFour = new Workflow.WorkflowTask(); workflowTaskFour.setName("taskFour"); workflowTaskFour.setType("typeSuccess"); @@ -150,8 +147,7 @@ public void testDeleteWorkflow() throws ServiceException, SchedulerException, Va final NamespaceService namespaceService = NamespaceService.getService(); final Namespace namespace = createNamespace(UUID.randomUUID().toString()); namespaceService.add(namespace); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", - UUID.randomUUID().toString(), namespace.getName()); + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); WorkflowService.getService().add(workflow); final WorkflowService workflowService = WorkflowService.getService(); workflowService.delete(workflow); @@ -164,19 +160,17 @@ public void testTaskWithContextFromWorkflow() throws Exception { workflowProps.put("valOne", 1234); workflowProps.put("valTwo", "abcd"); - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template-with-properties.yaml", + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML, workflowProps, null); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); + TaskService taskService = TaskService.getService(); final List workflowTasks = taskService.get(workflowTrigger.getNamespace()); Assert.assertEquals(3, workflowTasks.size()); for (Task workflowTask : workflowTasks) { - Assert.assertEquals(workflowTask.getContext(), MockSuccessTaskHandler.CONTEXT); + Assert.assertEquals(MockSuccessTaskHandler.CONTEXT, workflowTask.getContext()); if (workflowTask.getName().equals("taskTwo")) { Assert.assertEquals(1234, workflowTask.getProperties().get("keyB")); } @@ -190,81 +184,74 @@ public void testTaskWithContextFromWorkflow() throws Exception { @Test public void testGetWorkflowStatistics() throws Exception { - final WorkflowTrigger workflowTriggerOne = scheduleWorkflow("workflows/workflow-template.yaml"); - final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow("workflows/workflow-template-failed-handler.yaml"); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_FAILED_HANDLER_YAML); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - // wait for both the job to be triggered - waitForTriggerToComplete(workflowTriggerOne, scheduler); - waitForTriggerToComplete(workflowTriggerTwo, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); WorkflowStatistics workflowOneStatistics = WorkflowService.getService() .getStatistics(workflowTriggerOne.getNamespace(), workflowTriggerOne.getWorkflow(), 0, System.currentTimeMillis()); - Assert.assertEquals(workflowOneStatistics.getJobs().getTotal(), 1); - Assert.assertEquals(workflowOneStatistics.getJobs().getActive(), 0); - Assert.assertEquals(workflowOneStatistics.getJobs().getFailed(), 0); - Assert.assertEquals(workflowOneStatistics.getJobs().getSkipped(), 0); + Assert.assertEquals(1, workflowOneStatistics.getJobs().getTotal()); + Assert.assertEquals(0, workflowOneStatistics.getJobs().getActive()); + Assert.assertEquals(0, workflowOneStatistics.getJobs().getFailed()); Assert.assertEquals(workflowOneStatistics.getJobs().getSuccessful(), 1); - Assert.assertEquals(workflowOneStatistics.getTasks().getTotal(), 3); - Assert.assertEquals(workflowOneStatistics.getTasks().getActive(), 0); - Assert.assertEquals(workflowOneStatistics.getTasks().getFailed(), 0); - Assert.assertEquals(workflowOneStatistics.getTasks().getSkipped(), 0); - Assert.assertEquals(workflowOneStatistics.getTasks().getSuccessful(), 3); + Assert.assertEquals(3, workflowOneStatistics.getTasks().getTotal()); + Assert.assertEquals(0, workflowOneStatistics.getTasks().getActive()); + Assert.assertEquals(0, workflowOneStatistics.getTasks().getFailed()); + Assert.assertEquals(0, workflowOneStatistics.getTasks().getSkipped()); + Assert.assertEquals(3, workflowOneStatistics.getTasks().getSuccessful()); + Assert.assertEquals(0, workflowOneStatistics.getTasks().getAborted()); WorkflowStatistics workflowOneStatisticsByNs = WorkflowService.getService() .getStatistics(workflowTriggerOne.getNamespace(), 0, System.currentTimeMillis()); - Assert.assertEquals(workflowOneStatisticsByNs.getJobs().getTotal(), 1); - Assert.assertEquals(workflowOneStatisticsByNs.getJobs().getActive(), 0); - Assert.assertEquals(workflowOneStatisticsByNs.getJobs().getFailed(), 0); - Assert.assertEquals(workflowOneStatisticsByNs.getJobs().getSkipped(), 0); - Assert.assertEquals(workflowOneStatisticsByNs.getJobs().getSuccessful(), 1); - - Assert.assertEquals(workflowOneStatisticsByNs.getTasks().getTotal(), 3); - Assert.assertEquals(workflowOneStatisticsByNs.getTasks().getActive(), 0); - Assert.assertEquals(workflowOneStatisticsByNs.getTasks().getFailed(), 0); - Assert.assertEquals(workflowOneStatisticsByNs.getTasks().getSkipped(), 0); - Assert.assertEquals(workflowOneStatisticsByNs.getTasks().getSuccessful(), 3); - + Assert.assertEquals(1, workflowOneStatisticsByNs.getJobs().getTotal()); + Assert.assertEquals(0, workflowOneStatisticsByNs.getJobs().getActive()); + Assert.assertEquals(0, workflowOneStatisticsByNs.getJobs().getFailed()); + Assert.assertEquals(1, workflowOneStatisticsByNs.getJobs().getSuccessful()); + + Assert.assertEquals(3, workflowOneStatisticsByNs.getTasks().getTotal()); + Assert.assertEquals(0, workflowOneStatisticsByNs.getTasks().getActive()); + Assert.assertEquals(0, workflowOneStatisticsByNs.getTasks().getFailed()); + Assert.assertEquals(0, workflowOneStatisticsByNs.getTasks().getSkipped()); + Assert.assertEquals(3, workflowOneStatisticsByNs.getTasks().getSuccessful()); + Assert.assertEquals(0, workflowOneStatisticsByNs.getTasks().getAborted()); WorkflowStatistics workflowTwoStatistics = WorkflowService.getService() .getStatistics(workflowTriggerTwo.getNamespace(), workflowTriggerTwo.getWorkflow(), 0, System.currentTimeMillis()); - Assert.assertEquals(workflowTwoStatistics.getJobs().getTotal(), 1); - Assert.assertEquals(workflowTwoStatistics.getJobs().getActive(), 0); - Assert.assertEquals(workflowTwoStatistics.getJobs().getFailed(), 1); - Assert.assertEquals(workflowTwoStatistics.getJobs().getSkipped(), 0); - Assert.assertEquals(workflowTwoStatistics.getJobs().getSuccessful(), 0); - - Assert.assertEquals(workflowTwoStatistics.getTasks().getTotal(), 3); - Assert.assertEquals(workflowTwoStatistics.getTasks().getActive(), 0); - Assert.assertEquals(workflowTwoStatistics.getTasks().getFailed(), 1); - Assert.assertEquals(workflowTwoStatistics.getTasks().getSkipped(), 1); - Assert.assertEquals(workflowTwoStatistics.getTasks().getSuccessful(), 1); + Assert.assertEquals(1, workflowTwoStatistics.getJobs().getTotal()); + Assert.assertEquals(0, workflowTwoStatistics.getJobs().getActive()); + Assert.assertEquals(1, workflowTwoStatistics.getJobs().getFailed()); + Assert.assertEquals(0, workflowTwoStatistics.getJobs().getSuccessful()); + + Assert.assertEquals(3, workflowTwoStatistics.getTasks().getTotal()); + Assert.assertEquals(0, workflowTwoStatistics.getTasks().getActive()); + Assert.assertEquals(1, workflowTwoStatistics.getTasks().getFailed()); + Assert.assertEquals(1, workflowTwoStatistics.getTasks().getSkipped()); + Assert.assertEquals(1, workflowTwoStatistics.getTasks().getSuccessful()); + Assert.assertEquals(0, workflowTwoStatistics.getTasks().getAborted()); WorkflowStatistics workflowTwoStatisticsByNs = WorkflowService.getService() .getStatistics(workflowTriggerTwo.getNamespace(), 0, System.currentTimeMillis()); - Assert.assertEquals(workflowTwoStatisticsByNs.getJobs().getTotal(), 1); - Assert.assertEquals(workflowTwoStatisticsByNs.getJobs().getActive(), 0); - Assert.assertEquals(workflowTwoStatisticsByNs.getJobs().getFailed(), 1); - Assert.assertEquals(workflowTwoStatisticsByNs.getJobs().getSkipped(), 0); - Assert.assertEquals(workflowTwoStatisticsByNs.getJobs().getSuccessful(), 0); - - - Assert.assertEquals(workflowTwoStatisticsByNs.getTasks().getTotal(), 3); - Assert.assertEquals(workflowTwoStatisticsByNs.getTasks().getActive(), 0); - Assert.assertEquals(workflowTwoStatisticsByNs.getTasks().getFailed(), 1); - Assert.assertEquals(workflowTwoStatisticsByNs.getTasks().getSkipped(), 1); - Assert.assertEquals(workflowTwoStatisticsByNs.getTasks().getSuccessful(), 1); + Assert.assertEquals(1, workflowTwoStatisticsByNs.getJobs().getTotal()); + Assert.assertEquals(0, workflowTwoStatisticsByNs.getJobs().getActive()); + Assert.assertEquals(1, workflowTwoStatisticsByNs.getJobs().getFailed()); + Assert.assertEquals(0, workflowTwoStatisticsByNs.getJobs().getSuccessful()); + + Assert.assertEquals(3, workflowTwoStatisticsByNs.getTasks().getTotal()); + Assert.assertEquals(0, workflowTwoStatisticsByNs.getTasks().getActive()); + Assert.assertEquals(1, workflowTwoStatisticsByNs.getTasks().getFailed()); + Assert.assertEquals(1, workflowTwoStatisticsByNs.getTasks().getSkipped()); + Assert.assertEquals(1, workflowTwoStatisticsByNs.getTasks().getSuccessful()); + Assert.assertEquals(0, workflowTwoStatisticsByNs.getTasks().getAborted()); } @Test(expected = ValidationException.class) public void testDuplicatePolicyOfSameType() throws Exception { - scheduleWorkflow("workflows/workflow-template-with-duplicate-policy.yaml", - null, null); + scheduleWorkflow(WORKFLOW_TEMPLATE_WITH_DUPLICATE_POLICY_YAML, null, null); Assert.fail(); } @@ -272,7 +259,7 @@ public void testDuplicatePolicyOfSameType() throws Exception { public void testMissingWorkflowPropertiesShouldFail() throws Exception { HashMap workflowProps = new HashMap<>(); workflowProps.put("valOne", 1234); - scheduleWorkflow("workflows/workflow-template-with-properties.yaml", workflowProps, null); + scheduleWorkflow(WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML, workflowProps, null); Assert.fail(); } } diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowTriggerServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowTriggerServiceTest.java index 890401a..258e139 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowTriggerServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/WorkflowTriggerServiceTest.java @@ -29,7 +29,6 @@ import org.junit.Assert; import org.junit.Test; import org.quartz.DateBuilder; -import org.quartz.Scheduler; import java.util.HashMap; import java.util.List; @@ -39,7 +38,7 @@ import static com.cognitree.kronos.TestUtil.createWorkflow; import static com.cognitree.kronos.TestUtil.createWorkflowTrigger; import static com.cognitree.kronos.TestUtil.scheduleWorkflow; -import static com.cognitree.kronos.TestUtil.waitForTriggerToComplete; +import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; import static org.quartz.DailyTimeIntervalScheduleBuilder.ALL_DAYS_OF_THE_WEEK; public class WorkflowTriggerServiceTest extends ServiceTest { @@ -69,7 +68,7 @@ public void testAddWorkflowTrigger() throws Exception { Namespace namespaceOne = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceOne); - final Workflow workflowOne = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowOne = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); WorkflowService.getService().add(workflowOne); @@ -84,7 +83,7 @@ public void testAddWorkflowTrigger() throws Exception { Namespace namespaceTwo = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceTwo); - final Workflow workflowTwo = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowTwo = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceTwo.getName()); WorkflowService.getService().add(workflowTwo); @@ -101,7 +100,7 @@ public void testAddWorkflowTriggerWithSimpleSchedule() throws Exception { Namespace namespace = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespace); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); WorkflowService.getService().add(workflow); @@ -129,7 +128,7 @@ public void testAddWorkflowTriggerWithFixedSchedule() throws Exception { Namespace namespace = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespace); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); WorkflowService.getService().add(workflow); @@ -156,7 +155,7 @@ public void testAddWorkflowTriggerWithCalendarSchedule() throws Exception { Namespace namespace = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespace); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); WorkflowService.getService().add(workflow); @@ -184,7 +183,7 @@ public void testAddWorkflowTriggerWithDailyTimeSchedule() throws Exception { Namespace namespace = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespace); - final Workflow workflow = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflow = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespace.getName()); WorkflowService.getService().add(workflow); @@ -213,7 +212,7 @@ public void testGetAllWorkflowTrigger() throws Exception { Namespace namespaceOne = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceOne); - final Workflow workflowOne = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowOne = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); WorkflowService.getService().add(workflowOne); @@ -229,7 +228,7 @@ public void testGetAllWorkflowTrigger() throws Exception { Namespace namespaceTwo = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceTwo); - final Workflow workflowTwo = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowTwo = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceTwo.getName()); WorkflowService.getService().add(workflowTwo); @@ -249,7 +248,7 @@ public void testGetAllWorkflowTriggerByWorkflowName() throws Exception { Namespace namespaceOne = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceOne); - final Workflow workflowOne = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowOne = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); WorkflowService.getService().add(workflowOne); @@ -265,7 +264,7 @@ public void testGetAllWorkflowTriggerByWorkflowName() throws Exception { Namespace namespaceTwo = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceTwo); - final Workflow workflowTwo = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowTwo = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceTwo.getName()); WorkflowService.getService().add(workflowTwo); @@ -285,7 +284,7 @@ public void testReAddWorkflowTrigger() throws Exception { Namespace namespaceOne = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceOne); - final Workflow workflowOne = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowOne = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); WorkflowService.getService().add(workflowOne); @@ -302,7 +301,7 @@ public void testDeleteWorkflowTrigger() throws Exception { Namespace namespaceOne = createNamespace(UUID.randomUUID().toString()); NamespaceService.getService().add(namespaceOne); - final Workflow workflowOne = createWorkflow("workflows/workflow-template.yaml", + final Workflow workflowOne = createWorkflow(WORKFLOW_TEMPLATE_YAML, UUID.randomUUID().toString(), namespaceOne.getName()); WorkflowService.getService().add(workflowOne); @@ -326,13 +325,10 @@ public void testWorkflowTriggerPropertiesOne() throws Exception { triggerProps.put("valOne", 123456); triggerProps.put("valTwo", "abcdef"); - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template-with-properties.yaml", + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML, workflowProps, triggerProps); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); TaskService taskService = TaskService.getService(); final List workflowTasks = taskService.get(workflowTrigger.getNamespace()); @@ -357,13 +353,10 @@ public void testWorkflowTriggerPropertiesTwo() throws Exception { HashMap triggerProps = new HashMap<>(); triggerProps.put("valOne", 123456); - final WorkflowTrigger workflowTrigger = scheduleWorkflow("workflows/workflow-template-with-properties.yaml", + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML, workflowProps, triggerProps); - final Scheduler scheduler = WorkflowSchedulerService.getService().getScheduler(); - waitForTriggerToComplete(workflowTrigger, scheduler); - // wait for tasks status to be consumed from queue - Thread.sleep(5000); + waitForJobsToTriggerAndComplete(workflowTrigger); TaskService taskService = TaskService.getService(); final List workflowTasks = taskService.get(workflowTrigger.getNamespace()); diff --git a/app/src/test/java/com/cognitree/kronos/store/TaskStoreTest.java b/app/src/test/java/com/cognitree/kronos/store/TaskStoreTest.java index fe73824..5b30d56 100644 --- a/app/src/test/java/com/cognitree/kronos/store/TaskStoreTest.java +++ b/app/src/test/java/com/cognitree/kronos/store/TaskStoreTest.java @@ -40,7 +40,6 @@ import static com.cognitree.kronos.model.Task.Status.RUNNING; import static com.cognitree.kronos.model.Task.Status.SCHEDULED; import static com.cognitree.kronos.model.Task.Status.SKIPPED; -import static com.cognitree.kronos.model.Task.Status.SUBMITTED; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; import static com.cognitree.kronos.model.Task.Status.UP_FOR_RETRY; import static com.cognitree.kronos.model.Task.Status.WAITING; @@ -78,10 +77,9 @@ public void testStoreAndLoadTasks() throws StoreException { Assert.assertTrue(tasksByStatusCreated.contains(taskOne)); Assert.assertTrue(tasksByStatusCreated.contains(taskTwo)); - assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(RUNNING))); assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(WAITING))); assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(SCHEDULED))); - assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(SUBMITTED))); + assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(RUNNING))); assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(FAILED))); assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(SKIPPED))); assertEquals(Collections.emptyList(), taskStore.loadByStatus(namespace, Collections.singletonList(UP_FOR_RETRY))); @@ -93,15 +91,14 @@ public void testStoreAndLoadTasks() throws StoreException { Assert.assertTrue(tasksByWorkflowName.contains(taskTwo)); Map countTasksByStatus = taskStore.countByStatus(namespace, 0, System.currentTimeMillis()); - assertEquals(2, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); - assertEquals(0, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING).intValue()); - assertEquals(0, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED).intValue()); - assertEquals(0, countTasksByStatus.get(SUBMITTED) == null ? 0 : countTasksByStatus.get(SUBMITTED).intValue()); - assertEquals(0, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING).intValue()); - assertEquals(0, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED).intValue()); - assertEquals(0, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED).intValue()); - assertEquals(0, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL).intValue()); - assertEquals(0, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY).intValue()); + assertEquals(2, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); + assertEquals(0, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING)); + assertEquals(0, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED)); + assertEquals(0, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING)); + assertEquals(0, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED)); + assertEquals(0, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED)); + assertEquals(0, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL)); + assertEquals(0, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY)); ArrayList tasksAfterCreate = loadExistingTasks(); tasksAfterCreate.remove(taskOne); @@ -193,15 +190,14 @@ public void testCountTaskByStatus() throws StoreException { assertEquals(6, tasksByNs.size()); Map countTasksByStatus = taskStore.countByStatus(namespace, 0, System.currentTimeMillis()); - assertEquals(6, countTasksByStatus.get(CREATED).intValue()); - assertEquals(0, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING).intValue()); - assertEquals(0, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED).intValue()); - assertEquals(0, countTasksByStatus.get(SUBMITTED) == null ? 0 : countTasksByStatus.get(SUBMITTED).intValue()); - assertEquals(0, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING).intValue()); - assertEquals(0, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED).intValue()); - assertEquals(0, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED).intValue()); - assertEquals(0, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL).intValue()); - assertEquals(0, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY).intValue()); + assertEquals(6, countTasksByStatus.get(CREATED)); + assertEquals(0, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING)); + assertEquals(0, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED)); + assertEquals(0, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING)); + assertEquals(0, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED)); + assertEquals(0, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED)); + assertEquals(0, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL)); + assertEquals(0, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY)); taskOne.setStatus(RUNNING); taskStore.update(taskOne); @@ -211,21 +207,20 @@ public void testCountTaskByStatus() throws StoreException { taskStore.update(taskThree); taskFour.setStatus(SCHEDULED); taskStore.update(taskFour); - taskFive.setStatus(SUBMITTED); + taskFive.setStatus(RUNNING); taskStore.update(taskFive); taskSix.setStatus(UP_FOR_RETRY); taskStore.update(taskSix); countTasksByStatus = taskStore.countByStatus(namespace, 0, System.currentTimeMillis()); - assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); - assertEquals(0, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING).intValue()); - assertEquals(1, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED).intValue()); - assertEquals(1, countTasksByStatus.get(SUBMITTED) == null ? 0 : countTasksByStatus.get(SUBMITTED).intValue()); - assertEquals(1, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING).intValue()); - assertEquals(0, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED).intValue()); - assertEquals(1, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED).intValue()); - assertEquals(1, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL).intValue()); - assertEquals(1, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY).intValue()); + assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); + assertEquals(0, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING)); + assertEquals(1, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED)); + assertEquals(2, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING)); + assertEquals(0, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED)); + assertEquals(1, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED)); + assertEquals(1, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL)); + assertEquals(1, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY)); taskOne.setStatus(WAITING); taskStore.update(taskOne); @@ -233,15 +228,14 @@ public void testCountTaskByStatus() throws StoreException { taskStore.update(taskFour); countTasksByStatus = taskStore.countByStatus(namespace, 0, System.currentTimeMillis()); - assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); - assertEquals(1, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING).intValue()); - assertEquals(0, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED).intValue()); - assertEquals(1, countTasksByStatus.get(SUBMITTED) == null ? 0 : countTasksByStatus.get(SUBMITTED).intValue()); - assertEquals(0, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING).intValue()); - assertEquals(1, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED).intValue()); - assertEquals(1, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED).intValue()); - assertEquals(1, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL).intValue()); - assertEquals(1, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY).intValue()); + assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); + assertEquals(1, countTasksByStatus.get(WAITING) == null ? 0 : countTasksByStatus.get(WAITING)); + assertEquals(0, countTasksByStatus.get(SCHEDULED) == null ? 0 : countTasksByStatus.get(SCHEDULED)); + assertEquals(1, countTasksByStatus.get(RUNNING) == null ? 0 : countTasksByStatus.get(RUNNING)); + assertEquals(1, countTasksByStatus.get(SKIPPED) == null ? 0 : countTasksByStatus.get(SKIPPED)); + assertEquals(1, countTasksByStatus.get(FAILED) == null ? 0 : countTasksByStatus.get(FAILED)); + assertEquals(1, countTasksByStatus.get(SUCCESSFUL) == null ? 0 : countTasksByStatus.get(SUCCESSFUL)); + assertEquals(1, countTasksByStatus.get(UP_FOR_RETRY) == null ? 0 : countTasksByStatus.get(UP_FOR_RETRY)); ArrayList tasksAfterCreate = loadExistingTasks(); tasksAfterCreate.remove(taskOne); @@ -275,18 +269,18 @@ public void testCountTaskByStatusCreatedAfterAndBefore() throws StoreException { assertEquals(6, tasksByNs.size()); Map countTasksByStatus = taskStore.countByStatus(namespace, createdAt, createdAt + 100); - assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); countTasksByStatus = taskStore.countByStatus(namespace, createdAt - 100, createdAt); - assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); countTasksByStatus = taskStore.countByStatus(namespace, createdAt - 1, createdAt + 1); - assertEquals(4, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(4, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); Task taskSeven = createTask(namespace, workflow, trigger, job, UUID.randomUUID().toString(), createdAt); countTasksByStatus = taskStore.countByStatus(namespace, createdAt - 1, createdAt + 1); - assertEquals(5, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(5, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); ArrayList tasksAfterCreate = loadExistingTasks(); tasksAfterCreate.remove(taskOne); @@ -322,26 +316,26 @@ public void testCountTaskByStatusCreatedAfterAndBeforeAndWorkflowName() throws S Map countTasksByStatus = taskStore.countByStatusForWorkflowName(namespace, workflow, createdAt - 100, createdAt); - assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); countTasksByStatus = taskStore.countByStatusForWorkflowName(namespace, workflow, createdAt, createdAt + 100); - assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(0, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); countTasksByStatus = taskStore.countByStatusForWorkflowName(namespace, workflow, createdAt - 1, createdAt + 1); - assertEquals(2, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(2, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); Task taskSeven = createTask(namespace, workflow, trigger, job, UUID.randomUUID().toString(), createdAt); countTasksByStatus = taskStore.countByStatusForWorkflowName(namespace, workflow, createdAt - 1, createdAt + 1); - assertEquals(3, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(3, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); countTasksByStatus = taskStore.countByStatusForWorkflowName(namespace, taskFour.getWorkflow(), createdAt - 1, createdAt + 1); - assertEquals(1, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(1, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); countTasksByStatus = taskStore.countByStatusForWorkflowName(namespace, taskSix.getWorkflow(), createdAt - 1, createdAt + 1); - assertEquals(1, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED).intValue()); + assertEquals(1, countTasksByStatus.get(CREATED) == null ? 0 : countTasksByStatus.get(CREATED)); ArrayList tasksAfterCreate = loadExistingTasks(); tasksAfterCreate.remove(taskOne); diff --git a/app/src/test/resources/executor.yaml b/app/src/test/resources/executor.yaml index 88b32bf..4091913 100644 --- a/app/src/test/resources/executor.yaml +++ b/app/src/test/resources/executor.yaml @@ -6,5 +6,5 @@ taskHandlerConfig: handlerClass: com.cognitree.kronos.executor.handlers.MockFailureTaskHandler maxParallelTasks: 4 typeMock: - handlerClass: com.cognitree.kronos.executor.handlers.MockTaskHandler + handlerClass: com.cognitree.kronos.executor.handlers.MockAbortTaskHandler maxParallelTasks: 4 diff --git a/app/src/test/resources/queue.yaml b/app/src/test/resources/queue.yaml index 705e296..d978d74 100644 --- a/app/src/test/resources/queue.yaml +++ b/app/src/test/resources/queue.yaml @@ -2,6 +2,7 @@ producerConfig: producerClass: com.cognitree.kronos.queue.producer.RAMProducer consumerConfig: consumerClass: com.cognitree.kronos.queue.consumer.RAMConsumer - pollIntervalInMs: 10 taskStatusQueue: taskstatus configurationQueue: configurations +controlMessageQueue: controlmessages +pollIntervalInMs: 10 diff --git a/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml b/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml new file mode 100644 index 0000000..3357289 --- /dev/null +++ b/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml @@ -0,0 +1,14 @@ +# name and namespace will be set while creating a workflow +description: sample workflow +tasks: + - name: taskOne + type: typeMock + properties: + keyA: valA + keyB: valB + - name: taskTwo + type: typeMock + properties: + keyA: valA + keyB: valB + dependsOn: ["taskOne"] diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/ServiceException.java b/common/src/main/java/com/cognitree/kronos/ServiceException.java similarity index 96% rename from scheduler/src/main/java/com/cognitree/kronos/scheduler/ServiceException.java rename to common/src/main/java/com/cognitree/kronos/ServiceException.java index 714718b..bce21eb 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/ServiceException.java +++ b/common/src/main/java/com/cognitree/kronos/ServiceException.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package com.cognitree.kronos.scheduler; +package com.cognitree.kronos; public class ServiceException extends Exception { public ServiceException(String message) { diff --git a/common/src/main/java/com/cognitree/kronos/model/ControlMessage.java b/common/src/main/java/com/cognitree/kronos/model/ControlMessage.java new file mode 100644 index 0000000..ce61d57 --- /dev/null +++ b/common/src/main/java/com/cognitree/kronos/model/ControlMessage.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.cognitree.kronos.model; + +import java.util.Objects; + +public class ControlMessage { + private Task task; + private Task.Action action; + + public Task getTask() { + return task; + } + + public void setTask(Task task) { + this.task = task; + } + + public Task.Action getAction() { + return action; + } + + public void setAction(Task.Action action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ControlMessage)) return false; + ControlMessage message = (ControlMessage) o; + return Objects.equals(task, message.task) && + action == message.action; + } + + @Override + public int hashCode() { + return Objects.hash(task, action); + } + + @Override + public String toString() { + return "ControlMessage{" + + "task=" + task + + ", action=" + action + + '}'; + } +} diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Messages.java b/common/src/main/java/com/cognitree/kronos/model/Messages.java similarity index 51% rename from scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Messages.java rename to common/src/main/java/com/cognitree/kronos/model/Messages.java index 4e1646e..7dbf396 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Messages.java +++ b/common/src/main/java/com/cognitree/kronos/model/Messages.java @@ -15,12 +15,15 @@ * limitations under the License. */ -package com.cognitree.kronos.scheduler.model; +package com.cognitree.kronos.model; public interface Messages { - String FAILED_TO_RESOLVE_DEPENDENCY = "failed to resolve task dependency"; - String FAILED_DEPENDEE_TASK = FAILED_TO_RESOLVE_DEPENDENCY + ", dependee task has failed"; - String SKIPPED_DEPENDEE_TASK = FAILED_TO_RESOLVE_DEPENDENCY + ", dependee task has been skipped"; - String TIMED_OUT = "timed out executing task"; - String TASK_SUBMISSION_FAILED = "error submitting task to queue"; + String FAILED_TO_RESOLVE_DEPENDENCY_MESSAGE = "failed to resolve task dependency"; + String FAILED_DEPENDEE_TASK_MESSAGE = FAILED_TO_RESOLVE_DEPENDENCY_MESSAGE + ", dependee task has failed"; + String ABORTED_DEPENDEE_TASK_MESSAGE = FAILED_TO_RESOLVE_DEPENDENCY_MESSAGE + ", dependee task has been aborted"; + String SKIPPED_DEPENDEE_TASK_MESSAGE = FAILED_TO_RESOLVE_DEPENDENCY_MESSAGE + ", dependee task has been skipped"; + String TIMED_OUT_EXECUTING_TASK_MESSAGE = "timed out executing task"; + String TASK_SCHEDULING_FAILED_MESSAGE = "error scheduling task for execution"; + String TASK_ABORTED_MESSAGE = "task has been aborted"; + String MISSING_TASK_HANDLER_MESSAGE = "failed to resolve handler for the task"; } diff --git a/common/src/main/java/com/cognitree/kronos/model/Task.java b/common/src/main/java/com/cognitree/kronos/model/Task.java index 8d9efde..b238705 100755 --- a/common/src/main/java/com/cognitree/kronos/model/Task.java +++ b/common/src/main/java/com/cognitree/kronos/model/Task.java @@ -184,11 +184,11 @@ public enum Status { WAITING(false), UP_FOR_RETRY(false), SCHEDULED(false), - SUBMITTED(false), RUNNING(false), SUCCESSFUL(true), SKIPPED(true), // a task is marked as skipped it the task it depends on fails. - FAILED(true); + FAILED(true), + ABORTED(true); private final boolean isFinal; @@ -200,4 +200,8 @@ public boolean isFinal() { return this.isFinal; } } + + public enum Action { + ABORT, TIME_OUT + } } diff --git a/common/src/main/java/com/cognitree/kronos/model/TaskUpdate.java b/common/src/main/java/com/cognitree/kronos/model/TaskStatusUpdate.java similarity index 93% rename from common/src/main/java/com/cognitree/kronos/model/TaskUpdate.java rename to common/src/main/java/com/cognitree/kronos/model/TaskStatusUpdate.java index 313d67e..8c8483f 100644 --- a/common/src/main/java/com/cognitree/kronos/model/TaskUpdate.java +++ b/common/src/main/java/com/cognitree/kronos/model/TaskStatusUpdate.java @@ -24,7 +24,7 @@ import java.util.Map; import java.util.Objects; -public class TaskUpdate { +public class TaskStatusUpdate { @JsonSerialize(as = TaskId.class) @JsonDeserialize(as = TaskId.class) private TaskId taskId; @@ -67,8 +67,8 @@ public void setContext(Map context) { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof TaskUpdate)) return false; - TaskUpdate that = (TaskUpdate) o; + if (!(o instanceof TaskStatusUpdate)) return false; + TaskStatusUpdate that = (TaskStatusUpdate) o; return Objects.equals(taskId, that.taskId) && status == that.status && Objects.equals(statusMessage, that.statusMessage) && @@ -83,7 +83,7 @@ public int hashCode() { @Override public String toString() { - return "TaskUpdate{" + + return "TaskStatusUpdate{" + "taskId=" + taskId + ", status=" + status + ", statusMessage='" + statusMessage + '\'' + diff --git a/common/src/main/java/com/cognitree/kronos/queue/QueueConfig.java b/common/src/main/java/com/cognitree/kronos/queue/QueueConfig.java index 7eccf31..4e54bff 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/QueueConfig.java +++ b/common/src/main/java/com/cognitree/kronos/queue/QueueConfig.java @@ -21,6 +21,7 @@ import com.cognitree.kronos.queue.producer.ProducerConfig; import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * defines configuration required by the application to create producer and consumer to exchange message between @@ -31,6 +32,11 @@ public class QueueConfig { private ConsumerConfig consumerConfig; private String taskStatusQueue; private String configurationQueue; + private String controlMessageQueue; + /** + * time duration between successive poll to queue in millisecond, defaults to 1000ms. + */ + private long pollIntervalInMs = TimeUnit.SECONDS.toMillis(1); public ProducerConfig getProducerConfig() { return producerConfig; @@ -64,21 +70,38 @@ public void setConfigurationQueue(String configurationQueue) { this.configurationQueue = configurationQueue; } + public String getControlMessageQueue() { + return controlMessageQueue; + } + + public void setControlMessageQueue(String controlMessageQueue) { + this.controlMessageQueue = controlMessageQueue; + } + + public long getPollIntervalInMs() { + return pollIntervalInMs; + } + + public void setPollIntervalInMs(long pollIntervalInMs) { + this.pollIntervalInMs = pollIntervalInMs; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof QueueConfig)) return false; QueueConfig that = (QueueConfig) o; - return Objects.equals(producerConfig, that.producerConfig) && + return pollIntervalInMs == that.pollIntervalInMs && + Objects.equals(producerConfig, that.producerConfig) && Objects.equals(consumerConfig, that.consumerConfig) && Objects.equals(taskStatusQueue, that.taskStatusQueue) && - Objects.equals(configurationQueue, that.configurationQueue); + Objects.equals(configurationQueue, that.configurationQueue) && + Objects.equals(controlMessageQueue, that.controlMessageQueue); } @Override public int hashCode() { - - return Objects.hash(producerConfig, consumerConfig, taskStatusQueue, configurationQueue); + return Objects.hash(producerConfig, consumerConfig, taskStatusQueue, configurationQueue, controlMessageQueue, pollIntervalInMs); } @Override @@ -88,6 +111,8 @@ public String toString() { ", consumerConfig=" + consumerConfig + ", taskStatusQueue='" + taskStatusQueue + '\'' + ", configurationQueue='" + configurationQueue + '\'' + + ", controlMessageQueue='" + controlMessageQueue + '\'' + + ", pollIntervalInMs=" + pollIntervalInMs + '}'; } } diff --git a/common/src/main/java/com/cognitree/kronos/queue/QueueService.java b/common/src/main/java/com/cognitree/kronos/queue/QueueService.java new file mode 100644 index 0000000..3c38fc9 --- /dev/null +++ b/common/src/main/java/com/cognitree/kronos/queue/QueueService.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.cognitree.kronos.queue; + +import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; +import com.cognitree.kronos.ServiceProvider; +import com.cognitree.kronos.model.ControlMessage; +import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskId; +import com.cognitree.kronos.model.TaskStatusUpdate; +import com.cognitree.kronos.queue.consumer.Consumer; +import com.cognitree.kronos.queue.consumer.ConsumerConfig; +import com.cognitree.kronos.queue.producer.Producer; +import com.cognitree.kronos.queue.producer.ProducerConfig; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class QueueService implements Service { + public static final String EXECUTOR_QUEUE = "executor-queue"; + public static final String SCHEDULER_QUEUE = "scheduler-queue"; + + private static final Logger logger = LoggerFactory.getLogger(QueueService.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String CONSUMER_KEY = "consumerKey"; + + private final ConsumerConfig consumerConfig; + private final ProducerConfig producerConfig; + private final String taskStatusQueue; + private final String controlQueue; + + private final ConcurrentHashMap consumers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap producers = new ConcurrentHashMap<>(); + private String serviceName; + + public QueueService(QueueConfig queueConfig, String serviceName) { + this.serviceName = serviceName; + this.consumerConfig = queueConfig.getConsumerConfig(); + this.producerConfig = queueConfig.getProducerConfig(); + this.taskStatusQueue = queueConfig.getTaskStatusQueue(); + this.controlQueue = queueConfig.getControlMessageQueue(); + } + + public static QueueService getService(String serviceName) { + return (QueueService) ServiceProvider.getService(serviceName); + } + + @Override + public void init() { + logger.info("Initializing queue service {}", serviceName); + ServiceProvider.registerService(this); + } + + @Override + public void start() { + logger.info("Starting queue service {}", serviceName); + } + + /** + * Send task (not necessarily ordered) + * + * @param task + * @throws ServiceException + */ + public void send(Task task) throws ServiceException { + logger.debug("Received request to send task {}", task.getIdentity()); + final String type = task.getType(); + if (!producers.containsKey(type)) { + createProducer(type); + } + try { + producers.get(type).send(MAPPER.writeValueAsString(task)); + } catch (IOException e) { + logger.error("Error serializing task {}", task, e); + } + } + + /** + * Send the task status in an ordered manner. + * + * @param taskStatusUpdate + * @throws ServiceException + */ + public void send(TaskStatusUpdate taskStatusUpdate) throws ServiceException { + logger.debug("Received request to send task status update {}", taskStatusUpdate); + if (!producers.containsKey(taskStatusQueue)) { + createProducer(taskStatusQueue); + } + try { + producers.get(taskStatusQueue).sendInOrder(MAPPER.writeValueAsString(taskStatusUpdate), + getOrderingKey(taskStatusUpdate.getTaskId())); + } catch (IOException e) { + logger.error("Error serializing task status update {}", taskStatusUpdate, e); + } + } + + private String getOrderingKey(TaskId taskId) { + return taskId.getNamespace() + taskId.getWorkflow() + + taskId.getJob() + taskId.getName(); + } + + /** + * Broadcast control messages + * + * @param controlMessage + * @throws ServiceException + */ + public void send(ControlMessage controlMessage) throws ServiceException { + logger.debug("Received request to send task control message {}", controlMessage); + if (!producers.containsKey(controlQueue)) { + createProducer(controlQueue); + } + try { + producers.get(controlQueue).broadcast(MAPPER.writeValueAsString(controlMessage)); + } catch (IOException e) { + logger.error("Error serializing control message {}", controlMessage, e); + } + } + + public List consumeTask(String type, int maxTasksToPoll) throws ServiceException { + logger.debug("Received request to consume {} tasks of type {}", maxTasksToPoll, type); + if (!consumers.containsKey(type)) { + createConsumer(type, type); + } + final List records = consumers.get(type).poll(maxTasksToPoll); + if (records.isEmpty()) { + return Collections.emptyList(); + } + final ArrayList tasks = new ArrayList<>(); + for (String record : records) { + try { + tasks.add(MAPPER.readValue(record, Task.class)); + } catch (IOException e) { + logger.error("Error parsing record {} to Task", record, e); + } + } + return tasks; + } + + public List consumeTaskStatusUpdates() throws ServiceException { + logger.debug("Received request to consume task status update"); + if (!consumers.containsKey(taskStatusQueue)) { + createConsumer(taskStatusQueue, taskStatusQueue); + } + final List records = consumers.get(taskStatusQueue).poll(); + if (records.isEmpty()) { + return Collections.emptyList(); + } + final ArrayList taskStatusUpdates = new ArrayList<>(); + for (String record : records) { + try { + taskStatusUpdates.add(MAPPER.readValue(record, TaskStatusUpdate.class)); + } catch (IOException e) { + logger.error("Error parsing record {} to TaskStatusUpdate", record, e); + } + } + return taskStatusUpdates; + } + + public List consumeControlMessages() throws ServiceException { + logger.debug("Received request to consume control message"); + if (!consumers.containsKey(controlQueue)) { + createConsumer(controlQueue, "controlMessage-" + UUID.randomUUID().toString()); + } + final List records = consumers.get(controlQueue).poll(); + if (records.isEmpty()) { + return Collections.emptyList(); + } + + final ArrayList controlMessages = new ArrayList<>(); + for (String record : records) { + try { + controlMessages.add(MAPPER.readValue(record, ControlMessage.class)); + } catch (IOException e) { + logger.error("Error parsing record {} to ControlMessage", record, e); + } + } + return controlMessages; + } + + private synchronized void createProducer(String topic) throws ServiceException { + if (!producers.containsKey(topic)) { + logger.info("Creating producer with for topic {}", topic); + try { + final Producer producer = (Producer) Class.forName(producerConfig.getProducerClass()) + .getConstructor() + .newInstance(); + producer.init(topic, producerConfig.getConfig()); + producers.put(topic, producer); + } catch (Exception e) { + logger.error("Error creating producer for topic {}", topic, e); + throw new ServiceException("Error creating producer for topic " + topic, e.getCause()); + } + } + } + + private synchronized void createConsumer(String topic, String consumerKey) throws ServiceException { + if (!consumers.containsKey(topic)) { + logger.info("Creating consumer for topic {} with consumer key {}", topic, consumerKey); + try { + final ObjectNode consumerConfig = this.consumerConfig.getConfig() == null ? MAPPER.createObjectNode() + : this.consumerConfig.getConfig().deepCopy(); + // uniqueness to identify consumers in clustered setup + // a record should be consumed by only one consumer if they share the same consumer key + consumerConfig.put(CONSUMER_KEY, consumerKey); + final Consumer consumer = (Consumer) Class.forName(this.consumerConfig.getConsumerClass()) + .getConstructor() + .newInstance(); + consumer.init(topic, consumerConfig); + consumers.put(topic, consumer); + } catch (Exception e) { + logger.error("Error creating consumer for topic {}", topic, e); + throw new ServiceException("Error creating consumer for topic " + topic, e.getCause()); + } + } + } + + @Override + public void stop() { + logger.info("Stopping queue service {}", serviceName); + producers.forEach((s, producer) -> producer.close()); + consumers.forEach((s, consumer) -> consumer.close()); + } + + public void destroy() { + logger.info("Destroying queue service {}", serviceName); + consumers.forEach((s, consumer) -> consumer.destroy()); + } + + @Override + public String getName() { + return serviceName; + } +} diff --git a/common/src/main/java/com/cognitree/kronos/queue/consumer/Consumer.java b/common/src/main/java/com/cognitree/kronos/queue/consumer/Consumer.java index 8ef1902..19279bf 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/consumer/Consumer.java +++ b/common/src/main/java/com/cognitree/kronos/queue/consumer/Consumer.java @@ -28,26 +28,25 @@ public interface Consumer { * during initialization phase a call is made to initialize consumer using {@link ConsumerConfig#getConfig()}. * Any property required by the consumer¸ to instantiate itself should be part of {@link ConsumerConfig#getConfig()}. * - * @param consumerConfig configuration used to initialize the consumer. + * @param topic topic to create the consumer + * @param config configuration used to initialize the consumer. */ - void init(ObjectNode consumerConfig); + void init(String topic, ObjectNode config); /** * polls data from the underlying queue * - * @param topic topic to poll from * @return */ - List poll(String topic); + List poll(); /** * polls data from the underlying queue * - * @param topic topic to poll from * @param maxSize maximum number of records to poll * @return */ - List poll(String topic, int maxSize); + List poll(int maxSize); void close(); diff --git a/common/src/main/java/com/cognitree/kronos/queue/consumer/ConsumerConfig.java b/common/src/main/java/com/cognitree/kronos/queue/consumer/ConsumerConfig.java index 424bb7b..670b620 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/consumer/ConsumerConfig.java +++ b/common/src/main/java/com/cognitree/kronos/queue/consumer/ConsumerConfig.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Objects; -import java.util.concurrent.TimeUnit; /** * defines configuration for a {@link Consumer}. @@ -34,15 +33,10 @@ public class ConsumerConfig { /** * Configuration to be passed to consumer to instantiate itself. - * This will be passed as an arg to {@link Consumer#init(ObjectNode)} at the time of instantiation. + * This will be passed as an arg to {@link Consumer#init(String, ObjectNode)} at the time of instantiation. */ private ObjectNode config; - /** - * time duration between successive poll to queue in millisecond, defaults to 1000ms. - */ - private long pollIntervalInMs = TimeUnit.SECONDS.toMillis(1); - public String getConsumerClass() { return consumerClass; } @@ -59,28 +53,19 @@ public void setConfig(ObjectNode config) { this.config = config; } - public long getPollIntervalInMs() { - return pollIntervalInMs; - } - - public void setPollIntervalInMs(long pollIntervalInMs) { - this.pollIntervalInMs = pollIntervalInMs; - } - @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ConsumerConfig)) return false; ConsumerConfig that = (ConsumerConfig) o; return Objects.equals(consumerClass, that.consumerClass) && - Objects.equals(config, that.config) && - Objects.equals(pollIntervalInMs, that.pollIntervalInMs); + Objects.equals(config, that.config); } @Override public int hashCode() { - return Objects.hash(consumerClass, config, pollIntervalInMs); + return Objects.hash(consumerClass, config); } @Override @@ -88,7 +73,6 @@ public String toString() { return "ConsumerConfig{" + "consumerClass='" + consumerClass + '\'' + ", config=" + config + - ", pollIntervalInMs='" + pollIntervalInMs + '\'' + '}'; } } diff --git a/common/src/main/java/com/cognitree/kronos/queue/consumer/RAMConsumer.java b/common/src/main/java/com/cognitree/kronos/queue/consumer/RAMConsumer.java index e5d405c..aa03b7e 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/consumer/RAMConsumer.java +++ b/common/src/main/java/com/cognitree/kronos/queue/consumer/RAMConsumer.java @@ -29,21 +29,23 @@ public class RAMConsumer implements Consumer { private static final Logger logger = LoggerFactory.getLogger(RAMConsumer.class); + private LinkedBlockingQueue blockingQueue; + @Override - public void init(ObjectNode config) { - logger.info("Initializing consumer for RAM(in-memory) queue with config {}", config); + public void init(String topic, ObjectNode config) { + logger.info("Initializing consumer for RAM(in-memory) queue on topic {} with config {}", topic, config); + blockingQueue = RAMQueueFactory.getQueue(topic); } @Override - public List poll(String topic) { - return poll(topic, Integer.MAX_VALUE); + public List poll() { + return poll(Integer.MAX_VALUE); } @Override - public List poll(String topic, int size) { - logger.trace("Received request to poll messages from topic {} with max size {}", topic, size); - final LinkedBlockingQueue blockingQueue = RAMQueueFactory.getQueue(topic); - List records = new ArrayList<>(); + public List poll(int size) { + logger.trace("Received request to poll {} message", size); + final List records = new ArrayList<>(); while (!blockingQueue.isEmpty() && records.size() < size) records.add(blockingQueue.poll()); return records; diff --git a/common/src/main/java/com/cognitree/kronos/queue/producer/Producer.java b/common/src/main/java/com/cognitree/kronos/queue/producer/Producer.java index 4572721..bea9360 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/producer/Producer.java +++ b/common/src/main/java/com/cognitree/kronos/queue/producer/Producer.java @@ -22,31 +22,32 @@ public interface Producer { /** - * during initialization phase a call is made to initialize producer using {@link ProducerConfig#config}. - * Any property required by the producer¸ to instantiate itself should be part of {@link ProducerConfig#config}. + * during initialization phase a call is made to initialize producer using {@link ProducerConfig#getConfig()}. + * Any property required by the producer¸ to instantiate itself should be part of {@link ProducerConfig#getConfig()}. * - * @param producerConfig configuration used to initialize the producer. + * @param topic topic to create the producer + * @param config configuration used to initialize the producer. */ - void init(ObjectNode producerConfig); + void init(String topic, ObjectNode config); + + void broadcast(String record); /** * sends the record to the underlying queue. - * Records can be consumed out of order by the Consumer on {@link com.cognitree.kronos.queue.consumer.Consumer#poll(String)}. + * Records can be consumed out of order by the Consumer. * - * @param topic topic name to send data * @param record record to send */ - void send(String topic, String record); + void send(String record); /** * sends the record to the underlying queue in-order. - * Records should be consumed in-order by the Consumer on {@link com.cognitree.kronos.queue.consumer.Consumer#poll(String)}. + * Records should be consumed in-order by the Consumer. * - * @param topic topic name to send data * @param record record to send * @param orderingKey key to decide how the message is sent for in-order delivery */ - void sendInOrder(String topic, String record, String orderingKey); + void sendInOrder(String record, String orderingKey); void close(); } diff --git a/common/src/main/java/com/cognitree/kronos/queue/producer/ProducerConfig.java b/common/src/main/java/com/cognitree/kronos/queue/producer/ProducerConfig.java index 9477062..5055ae5 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/producer/ProducerConfig.java +++ b/common/src/main/java/com/cognitree/kronos/queue/producer/ProducerConfig.java @@ -33,7 +33,7 @@ public class ProducerConfig { /** * Configuration to be passed to producer to instantiate itself. - * This will be passed as an arg to {@link Producer#init(ObjectNode)} at the time of instantiation. + * This will be passed as an arg to {@link Producer#init(String, ObjectNode)} at the time of instantiation. */ private ObjectNode config; diff --git a/common/src/main/java/com/cognitree/kronos/queue/producer/RAMProducer.java b/common/src/main/java/com/cognitree/kronos/queue/producer/RAMProducer.java index 618fd4f..999e4fc 100755 --- a/common/src/main/java/com/cognitree/kronos/queue/producer/RAMProducer.java +++ b/common/src/main/java/com/cognitree/kronos/queue/producer/RAMProducer.java @@ -22,23 +22,33 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + public class RAMProducer implements Producer { private static final Logger logger = LoggerFactory.getLogger(RAMProducer.class); - public void init(ObjectNode config) { - logger.info("Initializing producer for RAM(in-memory) queue with config {}", config); + private LinkedBlockingQueue blockingQueue; + + @Override + public void init(String topic, ObjectNode config) { + logger.info("Initializing producer for RAM(in-memory) queue for topic {} with config {}", topic, config); + blockingQueue = RAMQueueFactory.getQueue(topic); + } + + @Override + public void broadcast(String record) { + send(record); } @Override - public void send(String topic, String record) { - sendInOrder(topic, record, null); + public void send(String record) { + sendInOrder(record, null); } @Override - public void sendInOrder(String topic, String record, String orderingKey) { - logger.trace("Received request to send message {} on topic {} with orderingKey {}", - record, topic, orderingKey); - RAMQueueFactory.getQueue(topic).add(record); + public void sendInOrder(String record, String orderingKey) { + logger.trace("Received request to send message {} with orderingKey {}", record, orderingKey); + blockingQueue.add(record); } @Override diff --git a/executor/src/main/java/com/cognitree/kronos/executor/ExecutorApp.java b/executor/src/main/java/com/cognitree/kronos/executor/ExecutorApp.java index b1b2f80..54b86eb 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/ExecutorApp.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/ExecutorApp.java @@ -18,6 +18,7 @@ package com.cognitree.kronos.executor; import com.cognitree.kronos.queue.QueueConfig; +import com.cognitree.kronos.queue.QueueService; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.slf4j.Logger; @@ -25,6 +26,8 @@ import java.io.InputStream; +import static com.cognitree.kronos.queue.QueueService.EXECUTOR_QUEUE; + /** * starts the executor app by reading configurations from classpath. */ @@ -48,14 +51,19 @@ public static void main(String[] args) { public void start() throws Exception { final InputStream executorConfigAsStream = getClass().getClassLoader().getResourceAsStream("executor.yaml"); - ExecutorConfig executorConfig = MAPPER.readValue(executorConfigAsStream, ExecutorConfig.class); + final ExecutorConfig executorConfig = MAPPER.readValue(executorConfigAsStream, ExecutorConfig.class); final InputStream queueConfigAsStream = getClass().getClassLoader().getResourceAsStream("queue.yaml"); - QueueConfig queueConfig = MAPPER.readValue(queueConfigAsStream, QueueConfig.class); - TaskExecutionService taskExecutionService = new TaskExecutionService(executorConfig, queueConfig); + final QueueConfig queueConfig = MAPPER.readValue(queueConfigAsStream, QueueConfig.class); + + final QueueService queueService = new QueueService(queueConfig, EXECUTOR_QUEUE); + final TaskExecutionService taskExecutionService = + new TaskExecutionService(executorConfig.getTaskHandlerConfig(), queueConfig.getPollIntervalInMs()); logger.info("Initializing executor app"); + queueService.init(); taskExecutionService.init(); logger.info("Starting executor app"); + queueService.start(); taskExecutionService.start(); } @@ -64,6 +72,9 @@ public void stop() { if (TaskExecutionService.getService() != null) { TaskExecutionService.getService().stop(); } + if (QueueService.getService(EXECUTOR_QUEUE) != null) { + QueueService.getService(EXECUTOR_QUEUE).stop(); + } } } diff --git a/executor/src/main/java/com/cognitree/kronos/executor/ExecutorConfig.java b/executor/src/main/java/com/cognitree/kronos/executor/ExecutorConfig.java index cb83b87..20f8c5a 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/ExecutorConfig.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/ExecutorConfig.java @@ -29,7 +29,7 @@ */ public class ExecutorConfig { /** - * Map of task handler configuration, required by the kronos to instantiate and start the handlers ({@link TaskHandler} + * Map of task handler configuration, required by the Kronos to instantiate and start the handlers ({@link TaskHandler} *

* Here key is the task type the handler is supposed to handle. */ @@ -53,7 +53,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(taskHandlerConfig); } diff --git a/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java b/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java index 9abdad5..af9acf2 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java @@ -18,35 +18,42 @@ package com.cognitree.kronos.executor; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.executor.handlers.TaskHandler; import com.cognitree.kronos.executor.handlers.TaskHandlerConfig; import com.cognitree.kronos.executor.model.TaskResult; +import com.cognitree.kronos.model.ControlMessage; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.model.Task.Status; import com.cognitree.kronos.model.TaskId; -import com.cognitree.kronos.model.TaskUpdate; -import com.cognitree.kronos.queue.QueueConfig; -import com.cognitree.kronos.queue.consumer.Consumer; -import com.cognitree.kronos.queue.consumer.ConsumerConfig; -import com.cognitree.kronos.queue.producer.Producer; -import com.cognitree.kronos.queue.producer.ProducerConfig; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.cognitree.kronos.model.TaskStatusUpdate; +import com.cognitree.kronos.queue.QueueService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import static com.cognitree.kronos.model.Messages.MISSING_TASK_HANDLER_MESSAGE; +import static com.cognitree.kronos.model.Messages.TASK_ABORTED_MESSAGE; +import static com.cognitree.kronos.model.Messages.TIMED_OUT_EXECUTING_TASK_MESSAGE; +import static com.cognitree.kronos.model.Task.Status.ABORTED; import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.RUNNING; -import static com.cognitree.kronos.model.Task.Status.SUBMITTED; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; +import static com.cognitree.kronos.queue.QueueService.EXECUTOR_QUEUE; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @@ -60,36 +67,32 @@ public final class TaskExecutionService implements Service { private static final Logger logger = LoggerFactory.getLogger(TaskExecutionService.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); - - // Task consumer and provider info - private final ConsumerConfig consumerConfig; - private final ProducerConfig producerConfig; - private final String statusQueue; // Task type mapping Info - private final Map taskTypeToHandlerConfig; - private final Map taskTypeToHandlerMap = new HashMap<>(); + private final Map taskTypeToHandlerConfigMap; + private final Map taskTypeToMaxParallelTasksCount = new HashMap<>(); private final Map taskTypeToRunningTasksCount = new HashMap<>(); - // used by internal tasks like polling new tasks from queue + private final Map taskHandlersMap = new ConcurrentHashMap<>(); + private final Map> taskFuturesMap = new ConcurrentHashMap<>(); + + // used by internal tasks to poll new tasks from queue private final ScheduledExecutorService taskConsumerThreadPool = Executors.newSingleThreadScheduledExecutor(); + // used by internal tasks to poll new control messages from queue + private final ScheduledExecutorService controlMessageConsumerThreadPool = Executors.newSingleThreadScheduledExecutor(); + // used by internal tasks to check task execution status + private final ScheduledExecutorService taskCompletionThreadPool = Executors.newSingleThreadScheduledExecutor(); + // used to execute tasks private final ExecutorService taskExecutorThreadPool = Executors.newCachedThreadPool(); - private Consumer consumer; - private Producer producer; - - public TaskExecutionService(ExecutorConfig executorConfig, QueueConfig queueConfig) { - if (queueConfig.getTaskStatusQueue() == null || queueConfig.getConsumerConfig() == null || - queueConfig.getProducerConfig() == null || executorConfig.getTaskHandlerConfig() == null) { - logger.error("missing one or more mandatory configuration: " + - "taskStatusQueue/ consumerConfig/ producerConfig/ taskHandlerConfig"); - throw new IllegalArgumentException("missing one or more mandatory configuration: " + - "taskStatusQueue/ consumerConfig/ producerConfig/ taskHandlerConfig"); + private long pollIntervalInMs; + + public TaskExecutionService(Map taskTypeToHandlerConfigMap, long pollIntervalInMs) { + if (taskTypeToHandlerConfigMap == null || taskTypeToHandlerConfigMap.isEmpty()) { + logger.error("missing one or more mandatory configuration: taskHandlerConfig"); + throw new IllegalArgumentException("missing one or more mandatory configuration: taskHandlerConfig"); } - this.consumerConfig = queueConfig.getConsumerConfig(); - this.producerConfig = queueConfig.getProducerConfig(); - this.statusQueue = queueConfig.getTaskStatusQueue(); - this.taskTypeToHandlerConfig = executorConfig.getTaskHandlerConfig(); + this.pollIntervalInMs = pollIntervalInMs; + this.taskTypeToHandlerConfigMap = taskTypeToHandlerConfigMap; } public static TaskExecutionService getService() { @@ -97,38 +100,15 @@ public static TaskExecutionService getService() { } @Override - public void init() throws Exception { - initConsumer(); - initProducer(); - initTaskHandlersAndExecutors(); + public void init() { + logger.info("Initializing task execution service"); + initCounters(); } - private void initConsumer() throws Exception { - logger.info("Initializing consumer with config {}", consumerConfig); - consumer = (Consumer) Class.forName(consumerConfig.getConsumerClass()) - .getConstructor() - .newInstance(); - consumer.init(consumerConfig.getConfig()); - } - - private void initProducer() throws Exception { - logger.info("Initializing producer with config {}", producerConfig); - producer = (Producer) Class.forName(producerConfig.getProducerClass()) - .getConstructor() - .newInstance(); - producer.init(producerConfig.getConfig()); - } - - private void initTaskHandlersAndExecutors() throws Exception { - for (Map.Entry taskTypeToHandlerConfigEntry : taskTypeToHandlerConfig.entrySet()) { + private void initCounters() { + for (Map.Entry taskTypeToHandlerConfigEntry : taskTypeToHandlerConfigMap.entrySet()) { final String taskType = taskTypeToHandlerConfigEntry.getKey(); final TaskHandlerConfig taskHandlerConfig = taskTypeToHandlerConfigEntry.getValue(); - logger.info("Initializing task handler of type {} with config {}", taskType, taskHandlerConfig); - final TaskHandler taskHandler = (TaskHandler) Class.forName(taskHandlerConfig.getHandlerClass()) - .newInstance(); - taskHandler.init(taskHandlerConfig.getConfig()); - taskTypeToHandlerMap.put(taskType, taskHandler); - int maxParallelTasks = taskHandlerConfig.getMaxParallelTasks(); maxParallelTasks = maxParallelTasks > 0 ? maxParallelTasks : Runtime.getRuntime().availableProcessors(); taskTypeToMaxParallelTasksCount.put(taskType, maxParallelTasks); @@ -138,111 +118,165 @@ private void initTaskHandlersAndExecutors() throws Exception { @Override public void start() { - final long pollInterval = consumerConfig.getPollIntervalInMs(); - taskConsumerThreadPool.scheduleAtFixedRate(this::consumeTasks, 0, pollInterval, MILLISECONDS); + logger.info("Starting task execution service"); + taskConsumerThreadPool.scheduleAtFixedRate(this::consumeTasks, 0, pollIntervalInMs, MILLISECONDS); + controlMessageConsumerThreadPool.scheduleAtFixedRate(this::consumeControlMessages, 0, pollIntervalInMs, MILLISECONDS); + taskCompletionThreadPool.scheduleAtFixedRate(new TaskCompletionChecker(), 0, pollIntervalInMs, MILLISECONDS); ServiceProvider.registerService(this); } private void consumeTasks() { taskTypeToMaxParallelTasksCount.forEach((taskType, maxParallelTasks) -> { - synchronized (taskTypeToRunningTasksCount) { - final int tasksToPoll = maxParallelTasks - taskTypeToRunningTasksCount.get(taskType); - if (tasksToPoll > 0) { - final List tasks = consumer.poll(taskType, tasksToPoll); - for (String taskAsString : tasks) { - try { - submit(MAPPER.readValue(taskAsString, Task.class)); - } catch (IOException e) { - logger.error("Error parsing task message {}", taskAsString, e); - } + synchronized (taskType) { + final int maxTasksToPoll = maxParallelTasks - taskTypeToRunningTasksCount.get(taskType); + if (maxTasksToPoll > 0) { + try { + final List tasks = QueueService.getService(EXECUTOR_QUEUE).consumeTask(taskType, maxTasksToPoll); + tasks.forEach(this::submit); + } catch (ServiceException e) { + logger.error("Error consuming tasks for execution", e); } } } }); } + private void consumeControlMessages() { + final List controlMessages; + try { + controlMessages = QueueService.getService(EXECUTOR_QUEUE).consumeControlMessages(); + } catch (ServiceException e) { + logger.error("Error consuming control messages", e); + return; + } + + for (ControlMessage controlMessage : controlMessages) { + logger.info("Received request to execute control message {}", controlMessage); + final Task task = controlMessage.getTask(); + if (!taskFuturesMap.containsKey(task)) { + continue; + } + final Future taskResultFuture = taskFuturesMap.get(task); + if (taskResultFuture != null) { + switch (controlMessage.getAction()) { + case ABORT: + logger.info("Received request to abort task with id {}", task.getIdentity()); + // interrupt the task first and then call the abort method + taskResultFuture.cancel(true); + taskHandlersMap.get(task).abort(); + sendTaskStatusUpdate(task, ABORTED, TASK_ABORTED_MESSAGE); + break; + case TIME_OUT: + logger.info("Received request to time out task with id {}", task.getIdentity()); + // interrupt the task first and then call the abort method + taskResultFuture.cancel(true); + taskHandlersMap.get(task).abort(); + sendTaskStatusUpdate(task, ABORTED, TIMED_OUT_EXECUTING_TASK_MESSAGE); + } + } + } + } + /** * submit the task for execution to appropriate handler based on task type. * * @param task task to submit for execution */ private void submit(Task task) { - logger.trace("Received task {} for execution from task queue", task); - sendTaskUpdate(task, SUBMITTED); + logger.info("Received request to submit task for execution: {}", task.getIdentity()); + final TaskHandler taskHandler; + try { + final TaskHandlerConfig taskHandlerConfig = taskTypeToHandlerConfigMap.get(task.getType()); + taskHandler = (TaskHandler) Class.forName(taskHandlerConfig.getHandlerClass()) + .getConstructor() + .newInstance(); + taskHandler.init(task, taskHandlerConfig.getConfig()); + } catch (InstantiationException | InvocationTargetException | NoSuchMethodException + | IllegalAccessException | ClassNotFoundException e) { + logger.error("Error initializing handler for task {}", task, e); + sendTaskStatusUpdate(task, FAILED, MISSING_TASK_HANDLER_MESSAGE); + return; + } taskTypeToRunningTasksCount.put(task.getType(), taskTypeToRunningTasksCount.get(task.getType()) + 1); - taskExecutorThreadPool.submit(() -> { - try { - sendTaskUpdate(task, RUNNING); - final TaskHandler handler = taskTypeToHandlerMap.get(task.getType()); - final TaskResult taskResult = handler.handle(task); - if (taskResult.isSuccess()) { - sendTaskUpdate(task, SUCCESSFUL, taskResult.getMessage(), taskResult.getContext()); - } else { - sendTaskUpdate(task, FAILED, taskResult.getMessage(), taskResult.getContext()); - } - } catch (Exception e) { - logger.error("Error executing task {}", task, e); - sendTaskUpdate(task, FAILED, e.getMessage()); - } finally { - synchronized (taskTypeToRunningTasksCount) { - taskTypeToRunningTasksCount.put(task.getType(), taskTypeToRunningTasksCount.get(task.getType()) - 1); - } - } + final Future taskResultFuture = taskExecutorThreadPool.submit(() -> { + logger.debug("Executing task {}", task.getIdentity()); + sendTaskStatusUpdate(task, RUNNING, null); + return taskHandler.execute(); }); + taskHandlersMap.put(task, taskHandler); + taskFuturesMap.put(task, taskResultFuture); } - private void sendTaskUpdate(TaskId taskId, Status status) { - sendTaskUpdate(taskId, status, null); + private void sendTaskStatusUpdate(TaskId taskId, Status status, String statusMessage) { + sendTaskStatusUpdate(taskId, status, statusMessage, null); } - private void sendTaskUpdate(TaskId taskId, Status status, String statusMessage) { - sendTaskUpdate(taskId, status, statusMessage, null); - } - - private void sendTaskUpdate(TaskId taskId, Status status, String statusMessage, Map context) { + private void sendTaskStatusUpdate(TaskId taskId, Status status, String statusMessage, Map context) { try { - TaskUpdate taskUpdate = new TaskUpdate(); - taskUpdate.setTaskId(taskId); - taskUpdate.setStatus(status); - taskUpdate.setStatusMessage(statusMessage); - taskUpdate.setContext(context); - producer.sendInOrder(statusQueue, MAPPER.writeValueAsString(taskUpdate), getOrderingKey(taskUpdate.getTaskId())); - } catch (IOException e) { + final TaskStatusUpdate taskStatusUpdate = new TaskStatusUpdate(); + taskStatusUpdate.setTaskId(taskId); + taskStatusUpdate.setStatus(status); + taskStatusUpdate.setStatusMessage(statusMessage); + taskStatusUpdate.setContext(context); + QueueService.getService(EXECUTOR_QUEUE).send(taskStatusUpdate); + } catch (ServiceException e) { logger.error("Error adding task status {} to queue", status, e); } } - private String getOrderingKey(TaskId taskId) { - return taskId.getNamespace() + taskId.getWorkflow() - + taskId.getJob() + taskId.getName(); - } - - // used in junit - Consumer getConsumer() { - return consumer; - } - - // used in junit - Producer getProducer() { - return producer; - } - @Override public void stop() { logger.info("Stopping task execution service"); - if (consumer != null) { - consumer.close(); - } try { taskConsumerThreadPool.shutdown(); taskConsumerThreadPool.awaitTermination(10, SECONDS); + controlMessageConsumerThreadPool.shutdown(); + controlMessageConsumerThreadPool.awaitTermination(10, SECONDS); taskExecutorThreadPool.shutdown(); taskExecutorThreadPool.awaitTermination(10, SECONDS); + taskCompletionThreadPool.shutdown(); + taskCompletionThreadPool.awaitTermination(10, SECONDS); } catch (InterruptedException e) { logger.error("Error stopping executor pool", e); } - if (producer != null) { - producer.close(); + } + + private class TaskCompletionChecker implements Runnable { + @Override + public void run() { + final ArrayList completedTasks = new ArrayList<>(); + taskFuturesMap.forEach((task, future) -> { + logger.debug("Checking task {} for completion", task.getIdentity()); + if (future.isDone()) { + try { + TaskResult taskResult = future.get(); + if (taskResult.isSuccess()) { + sendTaskStatusUpdate(task, SUCCESSFUL, taskResult.getMessage(), taskResult.getContext()); + } else { + sendTaskStatusUpdate(task, FAILED, taskResult.getMessage(), taskResult.getContext()); + } + } catch (InterruptedException e) { + logger.error("Thread interrupted waiting for task result for task {}", task.getIdentity(), e); + } catch (CancellationException e) { + logger.info("Task {} has been aborted", task.getIdentity()); + // do nothing the task is already marked as aborted + } catch (ExecutionException e) { + logger.error("Error executing task {}", task.getIdentity(), e); + sendTaskStatusUpdate(task, FAILED, "error executing task: " + e.getMessage()); + } finally { + synchronized (task.getType()) { + taskTypeToRunningTasksCount.put(task.getType(), taskTypeToRunningTasksCount.get(task.getType()) - 1); + } + completedTasks.add(task); + } + } + }); + if (!completedTasks.isEmpty()) { + logger.debug("Tasks {} completed execution", completedTasks.stream().map(Task::getIdentity) + .collect(Collectors.toList())); + } + completedTasks.forEach(taskFuturesMap::remove); + completedTasks.forEach(taskHandlersMap::remove); } } } diff --git a/executor/src/main/java/com/cognitree/kronos/executor/handlers/ShellCommandHandler.java b/executor/src/main/java/com/cognitree/kronos/executor/handlers/ShellCommandHandler.java index 5d548b8..97eedc9 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/handlers/ShellCommandHandler.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/handlers/ShellCommandHandler.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; @@ -38,14 +39,19 @@ public class ShellCommandHandler implements TaskHandler { private static final String PROP_ARGS = "args"; private static final String PROPERTY_WORKING_DIR = "workingDir"; private static final String PROPERTY_LOG_DIR = "logDir"; + private static final String JAVA_IO_TMPDIR = "java.io.tmpdir"; + + private Task task; + private boolean abort = false; @Override - public void init(ObjectNode handlerConfig) { + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { - logger.info("received request to handle task {}", task); + public TaskResult execute() { + logger.info("received request to execute task {}", task); final Map taskProperties = task.getProperties(); if (!taskProperties.containsKey(PROP_CMD)) { @@ -68,7 +74,7 @@ public TaskResult handle(Task task) { if (taskProperties.containsKey(PROPERTY_LOG_DIR)) { logDirPath = getProperty(taskProperties, PROPERTY_LOG_DIR); } else { - logDirPath = System.getProperty("java.io.tmpdir"); + logDirPath = System.getProperty(JAVA_IO_TMPDIR); } File logDir = new File(logDirPath); // create log directory is does not exist @@ -79,13 +85,17 @@ public TaskResult handle(Task task) { processBuilder.redirectError(new File(logDir, task.getName() + "_" + task.getJob() + "_stderr.log")); processBuilder.redirectOutput(new File(logDir, task.getName() + "_" + task.getJob() + "_stdout.log")); try { + if (abort) { + logger.info("Task has been aborted, skip running"); + return new TaskResult(false, "task has been aborted"); + } Process process = processBuilder.start(); int exitValue = process.waitFor(); logger.info("Process exited with code {} for command {}", exitValue, cmdWithArgs); if (exitValue != 0) { return new TaskResult(false, "process exited with error code " + exitValue); } - } catch (Exception e) { + } catch (IOException | InterruptedException e) { logger.error("Error executing command {}", cmdWithArgs, e); return new TaskResult(false, "process exited with exception: " + e.getMessage()); } @@ -95,4 +105,10 @@ public TaskResult handle(Task task) { private String getProperty(Map properties, String key) { return String.valueOf(properties.get(key)); } + + @Override + public void abort() { + logger.info("Received request to abort task {}", task); + abort = true; + } } diff --git a/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandler.java b/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandler.java index b1be5e3..d955be6 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandler.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandler.java @@ -28,17 +28,27 @@ public interface TaskHandler { /** * for each configured handler during initialization phase a call is made to initialize handler using - * {@link TaskHandlerConfig#config}. Any property required by the handler to instantiate itself - * should be part of {@link TaskHandlerConfig#config}. + * {@link TaskHandlerConfig#getConfig()}. Any property required by the handler to instantiate itself + * should be part of {@link TaskHandlerConfig#getConfig()}. * - * @param handlerConfig configuration used to initialize the handler. + * @param task task to execute. + * @param config configuration used to initialize the handler. */ - void init(ObjectNode handlerConfig); + void init(Task task, ObjectNode config); /** - * defines how to handle/ execute the task. + * defines how to execute the task. * - * @param task task to handle. + * @return */ - TaskResult handle(Task task); + TaskResult execute(); + + /** + * called when a task is aborted by user. + *

+ * The thread executing the task first receives an interrupt signal and then the abort method is called + */ + default void abort() { + + } } diff --git a/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandlerConfig.java b/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandlerConfig.java index f1d741b..3e9a956 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandlerConfig.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/handlers/TaskHandlerConfig.java @@ -17,6 +17,7 @@ package com.cognitree.kronos.executor.handlers; +import com.cognitree.kronos.model.Task; import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Objects; @@ -33,7 +34,7 @@ public class TaskHandlerConfig { /** * Configuration required by the handler to instantiate itself. - * This is passed as an arg to the {@link TaskHandler#init(ObjectNode)} method at the time of instantiation. + * This is passed as an arg to the {@link TaskHandler#init(Task, ObjectNode)} method at the time of instantiation. */ private ObjectNode config; diff --git a/executor/src/test/java/com/cognitree/kronos/executor/TaskExecutorServiceTest.java b/executor/src/test/java/com/cognitree/kronos/executor/TaskExecutorServiceTest.java index bff5780..3802b5c 100755 --- a/executor/src/test/java/com/cognitree/kronos/executor/TaskExecutorServiceTest.java +++ b/executor/src/test/java/com/cognitree/kronos/executor/TaskExecutorServiceTest.java @@ -17,20 +17,19 @@ package com.cognitree.kronos.executor; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.executor.handlers.TestTaskHandler; import com.cognitree.kronos.executor.handlers.TypeATaskHandler; import com.cognitree.kronos.executor.handlers.TypeBTaskHandler; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.model.TaskId; -import com.cognitree.kronos.model.TaskUpdate; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.cognitree.kronos.model.TaskStatusUpdate; +import com.cognitree.kronos.queue.QueueService; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; -import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.UUID; @@ -38,12 +37,11 @@ import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.RUNNING; import static com.cognitree.kronos.model.Task.Status.SCHEDULED; -import static com.cognitree.kronos.model.Task.Status.SUBMITTED; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; +import static com.cognitree.kronos.queue.QueueService.EXECUTOR_QUEUE; public class TaskExecutorServiceTest { - private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ExecutorApp EXECUTOR_APP = new ExecutorApp(); private static final String TASK_TYPE_TEST = "test"; private static final String TASK_TYPE_B = "typeB"; @@ -60,7 +58,7 @@ public static void stop() { } @Test - public void testTaskExecution() throws JsonProcessingException, InterruptedException { + public void testTaskExecution() throws Exception { final HashMap tasksMap = new HashMap<>(); String namespace = UUID.randomUUID().toString(); String jobId = UUID.randomUUID().toString(); @@ -71,12 +69,10 @@ public void testTaskExecution() throws JsonProcessingException, InterruptedExcep .setStatus(SCHEDULED) .build(); tasksMap.put(taskOne, taskOne); - TaskExecutionService.getService().getProducer().send(taskOne.getType(), MAPPER.writeValueAsString(taskOne)); + QueueService.getService(EXECUTOR_QUEUE).send(taskOne); Thread.sleep(1000); consumeTaskStatus(tasksMap); - // depending on the number of available cores task picked for execution - // will be in one of the two state RUNNING or SUBMITTED - Assert.assertTrue(taskOne.getStatus().equals(RUNNING) || taskOne.getStatus().equals(SUBMITTED)); + Assert.assertEquals(RUNNING, taskOne.getStatus()); TestTaskHandler.finishExecution(taskOne.getName()); Thread.sleep(1000); consumeTaskStatus(tasksMap); @@ -84,7 +80,7 @@ public void testTaskExecution() throws JsonProcessingException, InterruptedExcep } @Test - public void testTaskExecutionNegative() throws JsonProcessingException, InterruptedException { + public void testTaskExecutionNegative() throws Exception { final HashMap tasksMap = new HashMap<>(); String namespace = UUID.randomUUID().toString(); String jobId = UUID.randomUUID().toString(); @@ -95,7 +91,7 @@ public void testTaskExecutionNegative() throws JsonProcessingException, Interrup .setStatus(SCHEDULED) .build(); tasksMap.put(taskOne, taskOne); - TaskExecutionService.getService().getProducer().send(taskOne.getType(), MAPPER.writeValueAsString(taskOne)); + QueueService.getService(EXECUTOR_QUEUE).send(taskOne); Thread.sleep(1000); consumeTaskStatus(tasksMap); Assert.assertEquals(FAILED, taskOne.getStatus()); @@ -103,7 +99,7 @@ public void testTaskExecutionNegative() throws JsonProcessingException, Interrup } @Test - public void testMaxParallelTask() throws InterruptedException, JsonProcessingException { + public void testMaxParallelTask() throws Exception { final HashMap tasksMap = new HashMap<>(); String namespace = UUID.randomUUID().toString(); String jobId = UUID.randomUUID().toString(); @@ -114,7 +110,7 @@ public void testMaxParallelTask() throws InterruptedException, JsonProcessingExc .setStatus(SCHEDULED) .build(); tasksMap.put(taskOne, taskOne); - TaskExecutionService.getService().getProducer().send(taskOne.getType(), MAPPER.writeValueAsString(taskOne)); + QueueService.getService(EXECUTOR_QUEUE).send(taskOne); Task taskTwo = MockTaskBuilder.getTaskBuilder() .setJob(jobId) @@ -123,7 +119,7 @@ public void testMaxParallelTask() throws InterruptedException, JsonProcessingExc .setStatus(SCHEDULED) .build(); tasksMap.put(taskTwo, taskTwo); - TaskExecutionService.getService().getProducer().send(taskTwo.getType(), MAPPER.writeValueAsString(taskTwo)); + QueueService.getService(EXECUTOR_QUEUE).send(taskTwo); Task taskThree = MockTaskBuilder.getTaskBuilder() .setJob(jobId) @@ -132,7 +128,7 @@ public void testMaxParallelTask() throws InterruptedException, JsonProcessingExc .setStatus(SCHEDULED) .build(); tasksMap.put(taskThree, taskThree); - TaskExecutionService.getService().getProducer().send(taskThree.getType(), MAPPER.writeValueAsString(taskThree)); + QueueService.getService(EXECUTOR_QUEUE).send(taskThree); Task taskFour = MockTaskBuilder.getTaskBuilder() .setJob(jobId) @@ -141,7 +137,7 @@ public void testMaxParallelTask() throws InterruptedException, JsonProcessingExc .setStatus(SCHEDULED) .build(); tasksMap.put(taskFour, taskFour); - TaskExecutionService.getService().getProducer().send(taskFour.getType(), MAPPER.writeValueAsString(taskFour)); + QueueService.getService(EXECUTOR_QUEUE).send(taskFour); Task taskFive = MockTaskBuilder.getTaskBuilder() .setJob(jobId) @@ -150,22 +146,20 @@ public void testMaxParallelTask() throws InterruptedException, JsonProcessingExc .setStatus(SCHEDULED) .build(); tasksMap.put(taskFive, taskFive); - TaskExecutionService.getService().getProducer().send(taskFive.getType(), MAPPER.writeValueAsString(taskFive)); + QueueService.getService(EXECUTOR_QUEUE).send(taskFive); Thread.sleep(1000); consumeTaskStatus(tasksMap); - // depending on the number of available cores task picked for execution - // will be in one of the two state RUNNING or SUBMITTED - Assert.assertTrue(taskOne.getStatus().equals(RUNNING) || taskOne.getStatus().equals(SUBMITTED)); - Assert.assertTrue(taskTwo.getStatus().equals(RUNNING) || taskTwo.getStatus().equals(SUBMITTED)); - Assert.assertTrue(taskThree.getStatus().equals(RUNNING) || taskThree.getStatus().equals(SUBMITTED)); - Assert.assertTrue(taskFour.getStatus().equals(RUNNING) || taskFour.getStatus().equals(SUBMITTED)); + Assert.assertEquals(RUNNING, taskOne.getStatus()); + Assert.assertEquals(RUNNING, taskTwo.getStatus()); + Assert.assertEquals(RUNNING, taskThree.getStatus()); + Assert.assertEquals(RUNNING, taskFour.getStatus()); Assert.assertEquals(SCHEDULED, taskFive.getStatus()); TestTaskHandler.finishExecution(taskOne.getName()); Thread.sleep(1000); consumeTaskStatus(tasksMap); Assert.assertEquals(SUCCESSFUL, taskOne.getStatus()); - Assert.assertTrue(taskFive.getStatus().equals(RUNNING) || taskFive.getStatus().equals(SUBMITTED)); + Assert.assertEquals(RUNNING, taskFive.getStatus()); TestTaskHandler.finishExecution(taskTwo.getName()); TestTaskHandler.finishExecution(taskThree.getName()); TestTaskHandler.finishExecution(taskFour.getName()); @@ -178,22 +172,17 @@ public void testMaxParallelTask() throws InterruptedException, JsonProcessingExc Assert.assertEquals(SUCCESSFUL, taskFive.getStatus()); } - private void consumeTaskStatus(HashMap tasksMap) { - final List tasksStatus = TaskExecutionService.getService().getConsumer().poll("taskstatus"); - tasksStatus.forEach(taskStatus -> { - try { - final TaskUpdate taskUpdate = MAPPER.readValue(taskStatus, TaskUpdate.class); - final Task task = tasksMap.get(taskUpdate.getTaskId()); - task.setStatus(taskUpdate.getStatus()); - task.setStatusMessage(taskUpdate.getStatusMessage()); - } catch (IOException e) { - e.printStackTrace(); - } + private void consumeTaskStatus(HashMap tasksMap) throws ServiceException { + final List taskStatusUpdates = QueueService.getService(EXECUTOR_QUEUE).consumeTaskStatusUpdates(); + taskStatusUpdates.forEach(taskUpdate -> { + final Task task = tasksMap.get(taskUpdate.getTaskId()); + task.setStatus(taskUpdate.getStatus()); + task.setStatusMessage(taskUpdate.getStatusMessage()); }); } @Test - public void testTaskToHandlerMapping() throws InterruptedException, JsonProcessingException { + public void testTaskToHandlerMapping() throws Exception { String namespace = UUID.randomUUID().toString(); String jobId = UUID.randomUUID().toString(); Task taskOne = MockTaskBuilder.getTaskBuilder() @@ -202,35 +191,35 @@ public void testTaskToHandlerMapping() throws InterruptedException, JsonProcessi .setType(TASK_TYPE_A) .setStatus(SCHEDULED) .build(); - TaskExecutionService.getService().getProducer().send(taskOne.getType(), MAPPER.writeValueAsString(taskOne)); + QueueService.getService(EXECUTOR_QUEUE).send(taskOne); Task taskTwo = MockTaskBuilder.getTaskBuilder() .setJob(jobId) .setNamespace(namespace) .setType(TASK_TYPE_B) .setStatus(SCHEDULED) .build(); - TaskExecutionService.getService().getProducer().send(taskTwo.getType(), MAPPER.writeValueAsString(taskTwo)); + QueueService.getService(EXECUTOR_QUEUE).send(taskTwo); Task taskThree = MockTaskBuilder.getTaskBuilder() .setJob(jobId) .setNamespace(namespace) .setType(TASK_TYPE_A) .setStatus(SCHEDULED) .build(); - TaskExecutionService.getService().getProducer().send(taskThree.getType(), MAPPER.writeValueAsString(taskThree)); + QueueService.getService(EXECUTOR_QUEUE).send(taskThree); Task taskFour = MockTaskBuilder.getTaskBuilder() .setJob(jobId) .setNamespace(namespace) .setType(TASK_TYPE_A) .setStatus(SCHEDULED) .build(); - TaskExecutionService.getService().getProducer().send(taskFour.getType(), MAPPER.writeValueAsString(taskFour)); + QueueService.getService(EXECUTOR_QUEUE).send(taskFour); Task taskFive = MockTaskBuilder.getTaskBuilder() .setJob(jobId) .setNamespace(namespace) .setType(TASK_TYPE_B) .setStatus(SCHEDULED) .build(); - TaskExecutionService.getService().getProducer().send(taskFive.getType(), MAPPER.writeValueAsString(taskFive)); + QueueService.getService(EXECUTOR_QUEUE).send(taskFive); Thread.sleep(1000); Assert.assertTrue(TypeATaskHandler.isHandled(taskOne.getName())); Assert.assertFalse(TypeBTaskHandler.isHandled(taskOne.getName())); diff --git a/executor/src/test/java/com/cognitree/kronos/executor/handlers/TestTaskHandler.java b/executor/src/test/java/com/cognitree/kronos/executor/handlers/TestTaskHandler.java index 779e690..2151164 100755 --- a/executor/src/test/java/com/cognitree/kronos/executor/handlers/TestTaskHandler.java +++ b/executor/src/test/java/com/cognitree/kronos/executor/handlers/TestTaskHandler.java @@ -32,23 +32,25 @@ public class TestTaskHandler implements TaskHandler { private static final List tasks = Collections.synchronizedList(new ArrayList<>()); - public static void finishExecution(String taskId) { - tasks.add(taskId); + private Task task; + + public static void finishExecution(String taskName) { + tasks.add(taskName); } @Override - public void init(ObjectNode handlerConfig) { + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { - logger.info("Received request to handle task {}", task); + public TaskResult execute() { + logger.info("Received request to execute task {}", task); while (!tasks.contains(task.getName())) { try { Thread.sleep(50); - } catch (InterruptedException e) { - e.printStackTrace(); + } catch (InterruptedException ignored) { } } tasks.remove(task.getName()); diff --git a/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeATaskHandler.java b/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeATaskHandler.java index b3ce286..77ce80b 100755 --- a/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeATaskHandler.java +++ b/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeATaskHandler.java @@ -28,17 +28,19 @@ public class TypeATaskHandler implements TaskHandler { private static final Set handledTasks = Collections.synchronizedSet(new HashSet<>()); + private Task task; + public static boolean isHandled(String taskId) { return handledTasks.contains(taskId); } @Override - public void init(ObjectNode handlerConfig) { - + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { + public TaskResult execute() { handledTasks.add(task.getName()); return TaskResult.SUCCESS; } diff --git a/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeBTaskHandler.java b/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeBTaskHandler.java index 82e0910..2286703 100755 --- a/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeBTaskHandler.java +++ b/executor/src/test/java/com/cognitree/kronos/executor/handlers/TypeBTaskHandler.java @@ -28,17 +28,19 @@ public class TypeBTaskHandler implements TaskHandler { private static final Set handledTasks = Collections.synchronizedSet(new HashSet<>()); + private Task task; + public static boolean isHandled(String taskId) { return handledTasks.contains(taskId); } @Override - public void init(ObjectNode handlerConfig) { - + public void init(Task task, ObjectNode config) { + this.task = task; } @Override - public TaskResult handle(Task task) { + public TaskResult execute() { handledTasks.add(task.getName()); return new TaskResult(false, "error handling task"); } diff --git a/executor/src/test/resources/executor.yaml b/executor/src/test/resources/executor.yaml index f91a6f2..dee2f02 100755 --- a/executor/src/test/resources/executor.yaml +++ b/executor/src/test/resources/executor.yaml @@ -7,4 +7,4 @@ taskHandlerConfig: maxParallelTasks: 4 typeB: handlerClass: com.cognitree.kronos.executor.handlers.TypeBTaskHandler - maxParallelTasks: 4 \ No newline at end of file + maxParallelTasks: 4 diff --git a/executor/src/test/resources/queue.yaml b/executor/src/test/resources/queue.yaml index 0e67214..d978d74 100755 --- a/executor/src/test/resources/queue.yaml +++ b/executor/src/test/resources/queue.yaml @@ -2,6 +2,7 @@ producerConfig: producerClass: com.cognitree.kronos.queue.producer.RAMProducer consumerConfig: consumerClass: com.cognitree.kronos.queue.consumer.RAMConsumer - pollIntervalInMs: 10 taskStatusQueue: taskstatus -configurationQueue: configurations \ No newline at end of file +configurationQueue: configurations +controlMessageQueue: controlmessages +pollIntervalInMs: 10 diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/ConfigurationService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/ConfigurationService.java index 9f9cd5f..9b096af 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/ConfigurationService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/ConfigurationService.java @@ -18,14 +18,15 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.queue.QueueConfig; import com.cognitree.kronos.queue.consumer.Consumer; import com.cognitree.kronos.queue.consumer.ConsumerConfig; -import com.cognitree.kronos.scheduler.model.events.ConfigUpdate; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.Workflow; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; +import com.cognitree.kronos.scheduler.model.events.ConfigUpdate; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.quartz.SchedulerException; @@ -41,28 +42,20 @@ /** * A service that consumes config updates from a specified queue and processes them. - * */ public class ConfigurationService implements Service { private static final Logger logger = LoggerFactory.getLogger(ConfigurationService.class); - public static ConfigurationService getService() { - return (ConfigurationService) ServiceProvider.getService(ConfigurationService.class.getSimpleName()); - } - - private final ConsumerConfig consumerConfig; - // used in junit test case - final String configurationQueue; + private static final ObjectReader READER = new ObjectMapper().reader().forType(ConfigUpdate.class); - private Consumer consumer; - private long pollInterval; + private final String configurationQueue; - private static final ObjectReader READER = new ObjectMapper() - .reader().forType(ConfigUpdate.class); + private final ConsumerConfig consumerConfig; + private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); - private final ScheduledExecutorService scheduledExecutorService = - Executors.newScheduledThreadPool(1); + private Consumer configurationConsumer; + private long pollIntervalInMs; ConfigurationService(QueueConfig queueConfig) { if (queueConfig.getConfigurationQueue() == null || queueConfig.getConsumerConfig() == null) { @@ -72,6 +65,11 @@ public static ConfigurationService getService() { } this.consumerConfig = queueConfig.getConsumerConfig(); this.configurationQueue = queueConfig.getConfigurationQueue(); + this.pollIntervalInMs = queueConfig.getPollIntervalInMs(); + } + + public static ConfigurationService getService() { + return (ConfigurationService) ServiceProvider.getService(ConfigurationService.class.getSimpleName()); } @Override @@ -84,7 +82,8 @@ public void init() throws Exception { public void start() { logger.info("start: Starting configuration service"); ServiceProvider.registerService(this); - scheduledExecutorService.scheduleAtFixedRate(this::failSafeProcessUpdates, pollInterval, pollInterval, MILLISECONDS); + scheduledExecutorService.scheduleAtFixedRate(this::failSafeProcessUpdates, + pollIntervalInMs, pollIntervalInMs, MILLISECONDS); } @Override @@ -93,12 +92,11 @@ public void stop() { } private void initConsumer() throws Exception { - logger.info("Initializing consumer with config {}", consumerConfig); - consumer = (Consumer) Class.forName(consumerConfig.getConsumerClass()) + logger.info("Initializing configuration consumer with config {}", consumerConfig); + configurationConsumer = (Consumer) Class.forName(consumerConfig.getConsumerClass()) .getConstructor() .newInstance(); - consumer.init(consumerConfig.getConfig()); - pollInterval = consumerConfig.getPollIntervalInMs(); + configurationConsumer.init(configurationQueue, consumerConfig.getConfig()); } /** @@ -117,19 +115,22 @@ private void failSafeProcessUpdates() { * Poll, parse and process updates from the configured queue. */ private void processUpdates() { - final List configUpdates = consumer.poll(configurationQueue); + final List configUpdates = configurationConsumer.poll(); for (String configUpdateAsString : configUpdates) { - if (configUpdateAsString.trim().isEmpty()) - logger.trace("processUpdates: quietly skipping over empty config update..."); + if (configUpdateAsString == null || configUpdateAsString.trim().isEmpty()) { + logger.trace("processUpdates: quietly skipping over null/ empty config update..."); + continue; + } try { - ConfigUpdate configUpdate = READER.readValue(configUpdateAsString.trim()); + final ConfigUpdate configUpdate = READER.readValue(configUpdateAsString.trim()); processUpdate(configUpdate); } catch (IOException e) { // Do not throw the exception but continue to process other updates logger.error("processUpdates: unable to parse the config update received: " + configUpdateAsString, e); } catch (UnsupportedOperationException | ServiceException | ValidationException | SchedulerException e) { // Do not throw the exception but continue to process other updates - logger.error("processUpdates: error occurred in processing the config update received: " + configUpdateAsString, e); + logger.error("processUpdates: error occurred in processing the config update received: {}", + configUpdateAsString, e); } } } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java index b598ae2..0d758b3 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java @@ -18,6 +18,7 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.scheduler.model.Job; @@ -300,6 +301,29 @@ private void notifyListeners(Job job, Job.Status from, Job.Status to) { }); } + public void abortJob(JobId jobId) throws ServiceException, ValidationException { + logger.debug("Received request to abort job {}", jobId); + validateWorkflow(jobId.getNamespace(), jobId.getWorkflow()); + final Job job; + try { + job = jobStore.load(jobId); + } catch (StoreException e) { + logger.error("No job found with id {}", jobId, e); + throw new ServiceException(e.getMessage(), e.getCause()); + } + if (job == null) { + throw JOB_NOT_FOUND.createException(jobId.getId(), jobId.getWorkflow(), jobId.getNamespace()); + } + if (job.getStatus().isFinal()) { + return; + } + final List tasks = + TaskService.getService().get(jobId.getNamespace(), jobId.getId(), job.getWorkflow()); + for (Task task : tasks) { + TaskService.getService().abortTask(task); + } + } + public void delete(JobId jobId) throws ServiceException, ValidationException { logger.info("Received request to delete job {}", jobId); validateWorkflow(jobId.getNamespace(), jobId.getWorkflow()); diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/MailService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/MailService.java index c31bc8d..eec2b7f 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/MailService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/MailService.java @@ -18,6 +18,7 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.model.TaskId; diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/NamespaceService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/NamespaceService.java index 752285f..b431cc0 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/NamespaceService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/NamespaceService.java @@ -18,6 +18,7 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.NamespaceId; diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerApp.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerApp.java index 1f93595..31aff5a 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerApp.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerApp.java @@ -20,6 +20,7 @@ import com.cognitree.kronos.Service; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.queue.QueueConfig; +import com.cognitree.kronos.queue.QueueService; import com.cognitree.kronos.scheduler.store.StoreService; import com.cognitree.kronos.scheduler.store.StoreServiceConfig; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,6 +31,8 @@ import java.io.InputStream; +import static com.cognitree.kronos.queue.QueueService.SCHEDULER_QUEUE; + /** * starts the scheduler app by reading configurations from classpath. */ @@ -53,26 +56,28 @@ public static void main(String[] args) { public void start() throws Exception { final InputStream schedulerConfigAsStream = getClass().getClassLoader().getResourceAsStream("scheduler.yaml"); - SchedulerConfig schedulerConfig = MAPPER.readValue(schedulerConfigAsStream, SchedulerConfig.class); + final SchedulerConfig schedulerConfig = MAPPER.readValue(schedulerConfigAsStream, SchedulerConfig.class); final InputStream queueConfigAsStream = getClass().getClassLoader().getResourceAsStream("queue.yaml"); - QueueConfig queueConfig = MAPPER.readValue(queueConfigAsStream, QueueConfig.class); + final QueueConfig queueConfig = MAPPER.readValue(queueConfigAsStream, QueueConfig.class); final StoreServiceConfig storeServiceConfig = schedulerConfig.getStoreServiceConfig(); - StoreService storeService = (StoreService) Class.forName(storeServiceConfig.getStoreServiceClass()) + final StoreService storeService = (StoreService) Class.forName(storeServiceConfig.getStoreServiceClass()) .getConstructor(ObjectNode.class).newInstance(storeServiceConfig.getConfig()); - NamespaceService namespaceService = new NamespaceService(); - TaskService taskService = new TaskService(); - WorkflowService workflowService = new WorkflowService(); - JobService jobService = new JobService(); - WorkflowTriggerService workflowTriggerService = new WorkflowTriggerService(); - MailService mailService = new MailService(schedulerConfig.getMailConfig()); + final NamespaceService namespaceService = new NamespaceService(); + final TaskService taskService = new TaskService(); + final WorkflowService workflowService = new WorkflowService(); + final JobService jobService = new JobService(); + final WorkflowTriggerService workflowTriggerService = new WorkflowTriggerService(); + final MailService mailService = new MailService(schedulerConfig.getMailConfig()); + final QueueService queueService = new QueueService(queueConfig, SCHEDULER_QUEUE); // The order between task scheduler and workflow scheduler service is of importance // task scheduler service should be started before workflow scheduler service. // Workflow scheduler services starts the quartz scheduler which in turn might schedule some tasks // based on misfire policies and if the task scheduler service is not initialized that, it will result in NPE. - TaskSchedulerService taskSchedulerService = new TaskSchedulerService(queueConfig); - WorkflowSchedulerService workflowSchedulerService = new WorkflowSchedulerService(); + final TaskSchedulerService taskSchedulerService = + new TaskSchedulerService(queueConfig.getPollIntervalInMs()); + final WorkflowSchedulerService workflowSchedulerService = new WorkflowSchedulerService(); logger.info("Initializing scheduler app"); // initialize all service @@ -83,6 +88,7 @@ public void start() throws Exception { jobService.init(); workflowTriggerService.init(); mailService.init(); + queueService.init(); taskSchedulerService.init(); workflowSchedulerService.init(); @@ -95,6 +101,7 @@ public void start() throws Exception { jobService.start(); workflowTriggerService.start(); mailService.start(); + queueService.start(); taskSchedulerService.start(); workflowSchedulerService.start(); @@ -103,7 +110,7 @@ public void start() throws Exception { private void startAddOnServices(SchedulerConfig schedulerConfig, QueueConfig queueConfig) throws Exception { if (schedulerConfig.isEnableConfigurationService()) { - ConfigurationService configurationService = new ConfigurationService(queueConfig); + final ConfigurationService configurationService = new ConfigurationService(queueConfig); configurationService.init(); configurationService.start(); } @@ -119,6 +126,12 @@ public void stop() { if (TaskSchedulerService.getService() != null) { TaskSchedulerService.getService().stop(); } + if (QueueService.getService(SCHEDULER_QUEUE) != null) { + QueueService.getService(SCHEDULER_QUEUE).stop(); + } + if (MailService.getService() != null) { + MailService.getService().stop(); + } if (WorkflowTriggerService.getService() != null) { WorkflowTriggerService.getService().stop(); } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerConfig.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerConfig.java index 467e243..16f17d1 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerConfig.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/SchedulerConfig.java @@ -32,7 +32,6 @@ public class SchedulerConfig { */ private StoreServiceConfig storeServiceConfig; - /** * configuration required by {@link MailService} to configure itself */ @@ -70,7 +69,7 @@ public void setEnableConfigurationService(boolean enableConfigurationService) { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof SchedulerConfig)) return false; SchedulerConfig that = (SchedulerConfig) o; return enableConfigurationService == that.enableConfigurationService && Objects.equals(storeServiceConfig, that.storeServiceConfig) && diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskProvider.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskProvider.java index b15af4d..55557f8 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskProvider.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskProvider.java @@ -38,7 +38,6 @@ import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.RUNNING; -import static com.cognitree.kronos.model.Task.Status.SUBMITTED; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; import static com.cognitree.kronos.model.Task.Status.UP_FOR_RETRY; import static com.cognitree.kronos.model.Task.Status.WAITING; @@ -116,7 +115,7 @@ synchronized List getReadyTasks() { * return tasks currently being executed by executor */ synchronized List getActiveTasks() { - return getTasks(Arrays.asList(SUBMITTED, RUNNING)); + return getTasks(Collections.singletonList(RUNNING)); } synchronized List getDependentTasks(Task task) { diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java index 5293d25..3686681 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java @@ -18,22 +18,21 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; +import com.cognitree.kronos.model.ControlMessage; +import com.cognitree.kronos.model.Messages; import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.Task.Action; import com.cognitree.kronos.model.Task.Status; import com.cognitree.kronos.model.TaskId; -import com.cognitree.kronos.model.TaskUpdate; -import com.cognitree.kronos.queue.QueueConfig; -import com.cognitree.kronos.queue.consumer.Consumer; -import com.cognitree.kronos.queue.consumer.ConsumerConfig; +import com.cognitree.kronos.model.TaskStatusUpdate; +import com.cognitree.kronos.queue.QueueService; import com.cognitree.kronos.queue.producer.Producer; -import com.cognitree.kronos.queue.producer.ProducerConfig; import com.cognitree.kronos.scheduler.model.Namespace; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -45,19 +44,22 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; +import static com.cognitree.kronos.model.Messages.ABORTED_DEPENDEE_TASK_MESSAGE; +import static com.cognitree.kronos.model.Messages.FAILED_DEPENDEE_TASK_MESSAGE; +import static com.cognitree.kronos.model.Messages.FAILED_TO_RESOLVE_DEPENDENCY_MESSAGE; +import static com.cognitree.kronos.model.Messages.SKIPPED_DEPENDEE_TASK_MESSAGE; +import static com.cognitree.kronos.model.Messages.TASK_SCHEDULING_FAILED_MESSAGE; +import static com.cognitree.kronos.model.Messages.TIMED_OUT_EXECUTING_TASK_MESSAGE; +import static com.cognitree.kronos.model.Task.Status.ABORTED; import static com.cognitree.kronos.model.Task.Status.CREATED; import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.SCHEDULED; import static com.cognitree.kronos.model.Task.Status.SKIPPED; import static com.cognitree.kronos.model.Task.Status.UP_FOR_RETRY; import static com.cognitree.kronos.model.Task.Status.WAITING; +import static com.cognitree.kronos.queue.QueueService.SCHEDULER_QUEUE; import static com.cognitree.kronos.scheduler.model.Constants.DYNAMIC_VAR_PREFIX; import static com.cognitree.kronos.scheduler.model.Constants.DYNAMIC_VAR_SUFFFIX; -import static com.cognitree.kronos.scheduler.model.Messages.FAILED_DEPENDEE_TASK; -import static com.cognitree.kronos.scheduler.model.Messages.FAILED_TO_RESOLVE_DEPENDENCY; -import static com.cognitree.kronos.scheduler.model.Messages.SKIPPED_DEPENDEE_TASK; -import static com.cognitree.kronos.scheduler.model.Messages.TASK_SUBMISSION_FAILED; -import static com.cognitree.kronos.scheduler.model.Messages.TIMED_OUT; import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -74,7 +76,6 @@ final class TaskSchedulerService implements Service { private static final Logger logger = LoggerFactory.getLogger(TaskSchedulerService.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); // Periodically, tasks older than the specified interval and status of workflow (job) // it belongs to in one of the final state are purged from memory to prevent the system from going OOM. // task purge interval in hour @@ -89,28 +90,16 @@ final class TaskSchedulerService implements Service { } } - private final ProducerConfig producerConfig; - private final ConsumerConfig consumerConfig; - private final String statusQueue; - private final Map> taskTimeoutHandlersMap = new HashMap<>(); + private final Map> taskTimeoutHandlersMap = new HashMap<>(); // used by internal tasks for printing the dag/ delete stale tasks/ executing timeout tasks private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); - private Producer producer; - private Consumer consumer; - private TaskProvider taskProvider; - - public TaskSchedulerService(QueueConfig queueConfig) { - if (queueConfig.getTaskStatusQueue() == null || queueConfig.getConsumerConfig() == null || - queueConfig.getProducerConfig() == null) { - logger.error("missing one or more mandatory configuration: " + - "taskStatusQueue/ consumerConfig/ producerConfig"); - throw new IllegalArgumentException("missing one or more mandatory configuration: " + - "taskStatusQueue/ consumerConfig/ producerConfig"); - } - this.producerConfig = queueConfig.getProducerConfig(); - this.consumerConfig = queueConfig.getConsumerConfig(); - this.statusQueue = queueConfig.getTaskStatusQueue(); + private final long pollIntervalInMs; + + private final TaskProvider taskProvider = new TaskProvider(); + + public TaskSchedulerService(long pollIntervalInMs) { + this.pollIntervalInMs = pollIntervalInMs; } public static TaskSchedulerService getService() { @@ -118,27 +107,8 @@ public static TaskSchedulerService getService() { } @Override - public void init() throws Exception { + public void init() { logger.info("Initializing task scheduler service"); - taskProvider = new TaskProvider(); - initProducer(); - initConsumer(); - } - - private void initProducer() throws Exception { - logger.info("Initializing producer with config {}", producerConfig); - producer = (Producer) Class.forName(producerConfig.getProducerClass()) - .getConstructor() - .newInstance(); - producer.init(producerConfig.getConfig()); - } - - private void initConsumer() throws Exception { - logger.info("Initializing consumer with config {}", consumerConfig); - consumer = (Consumer) Class.forName(consumerConfig.getConsumerClass()) - .getConstructor() - .newInstance(); - consumer.init(consumerConfig.getConfig()); } /** @@ -149,6 +119,7 @@ private void initConsumer() throws Exception { * 2) Subscribe for task status update * 3) Initialize configured timeout policies * 4) Initialize timeout task for all the active tasks + * 5) Schedule tasks ready for execution * */ @Override @@ -160,6 +131,27 @@ public void start() throws Exception { resolveCreatedTasks(); scheduledExecutorService.scheduleAtFixedRate(this::deleteStaleTasks, TASK_PURGE_INTERVAL, TASK_PURGE_INTERVAL, HOURS); ServiceProvider.registerService(this); + scheduleReadyTasks(); + } + + /** + * abort a task + * + * @param task task to abort + */ + public void abort(Task task) throws ServiceException { + logger.info("Received request to abort task {}", task.getIdentity()); + + if (task.getStatus().equals(CREATED) || task.getStatus().equals(WAITING)) { + // task is not yet scheduled mark the task as ABORTED + updateStatus(task.getIdentity(), ABORTED, Messages.TASK_ABORTED_MESSAGE); + return; + } + + final ControlMessage controlMessage = new ControlMessage(); + controlMessage.setTask(task); + controlMessage.setAction(Action.ABORT); + QueueService.getService(SCHEDULER_QUEUE).send(controlMessage); } private void reInitTaskProvider() throws ServiceException, ValidationException { @@ -177,9 +169,8 @@ private void reInitTaskProvider() throws ServiceException, ValidationException { } private void startConsumer() { - consumeTaskStatus(); - final long pollInterval = consumerConfig.getPollIntervalInMs(); - scheduledExecutorService.scheduleAtFixedRate(this::consumeTaskStatus, pollInterval, pollInterval, MILLISECONDS); + scheduledExecutorService.scheduleAtFixedRate(this::consumeTaskStatusUpdates, 0, + pollIntervalInMs, MILLISECONDS); } /** @@ -190,8 +181,12 @@ private void startTimeoutTasks() { } private void createTimeoutTask(Task task) { - if (taskTimeoutHandlersMap.containsKey(task.getName())) { - logger.debug("Timeout task is already scheduled for task {}", task.getName()); + if (task.getMaxExecutionTimeInMs() == -1) { + logger.debug("Timeout is set to -1 for task {}, skip creating timeout task", task.getIdentity()); + return; + } + if (taskTimeoutHandlersMap.containsKey(task.getIdentity())) { + logger.debug("Timeout task is already scheduled for task {}", task.getIdentity()); return; } @@ -203,10 +198,10 @@ private void createTimeoutTask(Task task) { // submit timeout task now scheduledExecutorService.submit(timeoutTask); } else { - logger.info("Initializing timeout task for task {}, scheduled at {}", task.getName(), timeoutTaskTime); + logger.info("Initializing timeout task for task {}, scheduled at {}", task.getIdentity(), timeoutTaskTime); final ScheduledFuture timeoutTaskFuture = scheduledExecutorService.schedule(timeoutTask, timeoutTaskTime - currentTimeMillis, MILLISECONDS); - taskTimeoutHandlersMap.put(task.getName(), timeoutTaskFuture); + taskTimeoutHandlersMap.put(task.getIdentity(), timeoutTaskFuture); } } @@ -216,16 +211,17 @@ private void resolveCreatedTasks() { tasks.forEach(this::resolve); } - private void consumeTaskStatus() { - final List tasksStatus = consumer.poll(statusQueue); - for (String taskStatusAsString : tasksStatus) { - try { - final TaskUpdate taskUpdate = MAPPER.readValue(taskStatusAsString, TaskUpdate.class); - updateStatus(taskUpdate.getTaskId(), taskUpdate.getStatus(), - taskUpdate.getStatusMessage(), taskUpdate.getContext()); - } catch (IOException e) { - logger.error("Error parsing task status message {}", taskStatusAsString, e); - } + private void consumeTaskStatusUpdates() { + final List taskStatusUpdates; + try { + taskStatusUpdates = QueueService.getService(SCHEDULER_QUEUE).consumeTaskStatusUpdates(); + } catch (ServiceException e) { + logger.error("Error consuming task status updates", e); + return; + } + for (TaskStatusUpdate taskStatusUpdate : taskStatusUpdates) { + updateStatus(taskStatusUpdate.getTaskId(), taskStatusUpdate.getStatus(), + taskStatusUpdate.getStatusMessage(), taskStatusUpdate.getContext()); } } @@ -237,7 +233,7 @@ private void deleteStaleTasks() { } synchronized void schedule(Task task) { - logger.info("Received request to schedule task: {}", task); + logger.info("Received request to schedule task: {}", task.getIdentity()); final boolean isAdded = taskProvider.add(task); if (isAdded) { resolve(task); @@ -247,10 +243,10 @@ synchronized void schedule(Task task) { private void resolve(Task task) { final boolean isResolved = taskProvider.resolve(task); if (isResolved) { - updateStatus(task, WAITING, null); + updateStatus(task.getIdentity(), WAITING, null); } else { - logger.error("Unable to resolve dependency for task {}, marking it as {}", task, FAILED); - updateStatus(task, FAILED, FAILED_TO_RESOLVE_DEPENDENCY); + logger.error("Unable to resolve dependency for task {}, marking it as {}", task.getIdentity(), FAILED); + updateStatus(task.getIdentity(), FAILED, FAILED_TO_RESOLVE_DEPENDENCY_MESSAGE); } } @@ -260,19 +256,21 @@ private void updateStatus(TaskId taskId, Status status, String statusMessage) { private void updateStatus(TaskId taskId, Status status, String statusMessage, Map context) { - logger.info("Received request to update status of task {} to {} " + - "with status message {}", taskId, status, statusMessage); + logger.info("Received request to update status of task {} to {} with status message {}", + taskId, status, statusMessage); final Task task = taskProvider.getTask(taskId); if (task == null) { logger.error("No task found with id {}", taskId); return; } try { - TaskService.getService().updateStatus(task, status, statusMessage, context); - handleTaskStatusChange(task, status); - } catch (ServiceException | ValidationException e) { + boolean statusUpdated = TaskService.getService().updateStatus(task, status, statusMessage, context); + if (statusUpdated) { + handleTaskStatusChange(task, status); + } + } catch (ServiceException e) { logger.error("Error updating status of task {} to {} with status message {}", - task, status, statusMessage, e); + task.getIdentity(), status, statusMessage, e); } } @@ -286,15 +284,16 @@ private void handleTaskStatusChange(Task task, Status status) { break; case SCHEDULED: break; - case SUBMITTED: + case RUNNING: createTimeoutTask(task); break; case SKIPPED: case FAILED: + case ABORTED: markDependentTasksAsSkipped(task, status); // do not break case SUCCESSFUL: - final ScheduledFuture taskTimeoutFuture = taskTimeoutHandlersMap.remove(task.getName()); + final ScheduledFuture taskTimeoutFuture = taskTimeoutHandlersMap.remove(task.getIdentity()); if (taskTimeoutFuture != null) { taskTimeoutFuture.cancel(false); } @@ -306,14 +305,21 @@ private void handleTaskStatusChange(Task task, Status status) { private void markDependentTasksAsSkipped(Task task, Status parentStatus) { for (Task dependentTask : taskProvider.getDependentTasks(task)) { - if (dependentTask.getStatus() == SKIPPED) { - logger.debug("dependent task is already marked as SKIPPED, ignore updating task status"); + if (dependentTask.getStatus().isFinal()) { + logger.debug("dependent task is already in its final state {}, ignore updating task status to SKIPPED", + dependentTask.getStatus()); continue; } - if (parentStatus == FAILED) { - updateStatus(dependentTask, SKIPPED, FAILED_DEPENDEE_TASK); - } else { - updateStatus(dependentTask, SKIPPED, SKIPPED_DEPENDEE_TASK); + switch (parentStatus) { + case FAILED: + updateStatus(dependentTask.getIdentity(), SKIPPED, FAILED_DEPENDEE_TASK_MESSAGE); + break; + case ABORTED: + updateStatus(dependentTask.getIdentity(), SKIPPED, ABORTED_DEPENDEE_TASK_MESSAGE); + break; + default: + updateStatus(dependentTask.getIdentity(), SKIPPED, SKIPPED_DEPENDEE_TASK_MESSAGE); + break; } } } @@ -324,17 +330,18 @@ private void markDependentTasksAsSkipped(Task task, Status parentStatus) { private synchronized void scheduleReadyTasks() { final List readyTasks = taskProvider.getReadyTasks(); for (Task task : readyTasks) { + logger.info("Scheduling task {} for execution", task); try { // update dynamic task properties from the tasks it depends on before scheduling // only if the task is not being retried if (task.getStatus() != UP_FOR_RETRY) { updateTaskProperties(task); } - producer.send(task.getType(), MAPPER.writeValueAsString(task)); - updateStatus(task, SCHEDULED, null); - } catch (Exception e) { - logger.error("Error submitting task {} to queue", task, e); - updateStatus(task, FAILED, TASK_SUBMISSION_FAILED); + QueueService.getService(SCHEDULER_QUEUE).send(task); + updateStatus(task.getIdentity(), SCHEDULED, null); + } catch (ServiceException e) { + logger.error("Error scheduling task {} for execution", task.getIdentity(), e); + updateStatus(task.getIdentity(), FAILED, TASK_SCHEDULING_FAILED_MESSAGE); } } } @@ -421,36 +428,33 @@ private HashMap modifyAndGetTaskProperties(Map t @Override public void stop() { logger.info("Stopping task scheduler service"); - if (consumer != null) { - consumer.close(); - } try { scheduledExecutorService.shutdown(); scheduledExecutorService.awaitTermination(10, SECONDS); } catch (InterruptedException e) { logger.error("Error stopping thread pool", e); } - if (producer != null) { - producer.close(); - } - } - - //used in junit test - Consumer getConsumer() { - return consumer; } private class TimeoutTask implements Runnable { private final Task task; - TimeoutTask(Task task) { + private TimeoutTask(Task task) { this.task = task; } @Override public void run() { - logger.info("Task {} has timed out, marking task as failed", task); - updateStatus(task, FAILED, TIMED_OUT); + logger.info("Task {} has timed out, marking task as aborted", task.getIdentity()); + updateStatus(task.getIdentity(), ABORTED, TIMED_OUT_EXECUTING_TASK_MESSAGE); + try { + final ControlMessage controlMessage = new ControlMessage(); + controlMessage.setTask(task); + controlMessage.setAction(Action.TIME_OUT); + QueueService.getService(SCHEDULER_QUEUE).send(controlMessage); + } catch (ServiceException e) { + logger.error("Error sending control message to time out task {}", task.getIdentity(), e); + } } } } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java index 93b2a20..91593a6 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java @@ -18,6 +18,7 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.model.Policy; import com.cognitree.kronos.model.RetryPolicy; @@ -46,11 +47,11 @@ import java.util.Set; import java.util.UUID; +import static com.cognitree.kronos.model.Task.Status.ABORTED; import static com.cognitree.kronos.model.Task.Status.CREATED; import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.RUNNING; import static com.cognitree.kronos.model.Task.Status.SCHEDULED; -import static com.cognitree.kronos.model.Task.Status.SUBMITTED; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; import static com.cognitree.kronos.model.Task.Status.UP_FOR_RETRY; import static com.cognitree.kronos.model.Task.Status.WAITING; @@ -135,7 +136,7 @@ Task create(String namespace, WorkflowTask workflowTask, String jobId, String wo try { taskStore.store(task); } catch (StoreException e) { - logger.error("unable to add task {}", task, e); + logger.error("unable to add task {}", task.getIdentity(), e); throw new ServiceException(e.getMessage(), e.getCause()); } return task; @@ -162,7 +163,9 @@ private Map modifyAndGetTaskProperties(Map taskP valueToReplace = valueToReplace.substring(WORKFLOW_NAMESPACE_PREFIX.length()); modifiedTaskProperties.put(entry.getKey(), propertiesToOverride.get(valueToReplace)); } else if (value instanceof Map) { - modifiedTaskProperties.put(entry.getKey(), modifyAndGetTaskProperties((Map) value, propertiesToOverride)); + final Map nestedProperties = + modifyAndGetTaskProperties((Map) value, propertiesToOverride); + modifiedTaskProperties.put(entry.getKey(), nestedProperties); } else { modifiedTaskProperties.put(entry.getKey(), value); } @@ -221,6 +224,27 @@ public List get(String namespace, List statuses) throws ServiceExc } } + public void abortTask(TaskId taskId) throws ServiceException, ValidationException { + logger.debug("Received request to abort task {}", taskId); + validateJob(taskId.getNamespace(), taskId.getJob(), taskId.getWorkflow()); + final Task task; + try { + task = taskStore.load(taskId); + } catch (StoreException e) { + logger.error("No task found with id {}", taskId, e); + throw new ServiceException(e.getMessage(), e.getCause()); + } + if (task == null) { + throw TASK_NOT_FOUND.createException(taskId.getName(), taskId.getJob(), + taskId.getWorkflow(), taskId.getNamespace()); + } + if (task.getStatus().isFinal()) { + logger.warn("Task {} is already in its final state {}", task.getIdentity(), task.getStatus()); + return; + } + TaskSchedulerService.getService().abort(task); + } + public Map countByStatus(String namespace, long createdAfter, long createdBefore) throws ServiceException, ValidationException { logger.debug("Received request to count tasks by status under namespace {} created between {} to {}", @@ -249,23 +273,22 @@ public Map countByStatus(String namespace, String workflowName, } } - void updateStatus(Task task, Status status, String statusMessage, Map context) - throws ServiceException, ValidationException { + boolean updateStatus(Task task, Status status, String statusMessage, Map context) + throws ServiceException { try { - if (taskStore.load(task) == null) { - throw TASK_NOT_FOUND.createException(task.getName(), task.getJob(), task.getWorkflow(), task.getNamespace()); - } - validateJob(task.getNamespace(), task.getJob(), task.getWorkflow()); Status currentStatus = task.getStatus(); + if(status == currentStatus) { + logger.warn("Desired state transition is same as current state {}. Ignoring state transition", currentStatus); + return false; + } if (!isValidTransition(currentStatus, status)) { - logger.error("Invalid state transition for task {} from status {}, to {}", task, currentStatus, status); + logger.error("Invalid state transition for task {} from status {}, to {}", + task.getIdentity(), currentStatus, status); throw new ServiceException("Invalid state transition from " + currentStatus + " to " + status); } - if (status == FAILED && isRetryEnabled(task)) { logger.info("Resubmit the task {} for retry", task); - updateStatus(task, UP_FOR_RETRY, null, context); - return; + return updateStatus(task, UP_FOR_RETRY, null, context); } task.setStatus(status); @@ -275,23 +298,26 @@ void updateStatus(Task task, Status status, String statusMessage, Map jobStatusMap, Map taskStatusMap, long createdAfter, long createdBefore) { - WorkflowStatistics workflowStatistics = new WorkflowStatistics(); - ExecutionCounters jobExecutionCounters = new ExecutionCounters(); + final WorkflowStatistics workflowStatistics = new WorkflowStatistics(); + final JobExecutionCounters jobExecutionCounters = new JobExecutionCounters(); jobExecutionCounters.setTotal(jobStatusMap.values().stream().mapToInt(Integer::intValue).sum()); int activeJobs = 0; for (Job.Status status : ACTIVE_JOB_STATUS) { @@ -170,7 +172,7 @@ private WorkflowStatistics getWorkflowStatistics(Map jobSta jobExecutionCounters.setFailed(jobStatusMap.getOrDefault(Job.Status.FAILED, 0)); workflowStatistics.setJobs(jobExecutionCounters); - ExecutionCounters taskExecutionCounters = new ExecutionCounters(); + final TaskExecutionCounters taskExecutionCounters = new TaskExecutionCounters(); taskExecutionCounters.setTotal(taskStatusMap.values().stream().mapToInt(Integer::intValue).sum()); int activeTasks = 0; for (Task.Status status : ACTIVE_TASK_STATUS) { @@ -182,6 +184,7 @@ private WorkflowStatistics getWorkflowStatistics(Map jobSta taskExecutionCounters.setSuccessful(taskStatusMap.getOrDefault(Task.Status.SUCCESSFUL, 0)); taskExecutionCounters.setFailed(taskStatusMap.getOrDefault(Task.Status.FAILED, 0)); taskExecutionCounters.setSkipped(taskStatusMap.getOrDefault(Task.Status.SKIPPED, 0)); + taskExecutionCounters.setAborted(taskStatusMap.getOrDefault(Task.Status.ABORTED, 0)); workflowStatistics.setTasks(taskExecutionCounters); workflowStatistics.setFrom(createdAfter); diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/WorkflowTriggerService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/WorkflowTriggerService.java index b02afde..557973d 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/WorkflowTriggerService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/WorkflowTriggerService.java @@ -18,6 +18,7 @@ package com.cognitree.kronos.scheduler; import com.cognitree.kronos.Service; +import com.cognitree.kronos.ServiceException; import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.NamespaceId; diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/JobExecutionCounters.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/JobExecutionCounters.java new file mode 100644 index 0000000..45189aa --- /dev/null +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/JobExecutionCounters.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.cognitree.kronos.scheduler.model; + +public class JobExecutionCounters { + private int total; + private int active; + private int successful; + private int failed; + + public int getActive() { + return active; + } + + public void setActive(int active) { + this.active = active; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getSuccessful() { + return successful; + } + + public void setSuccessful(int successful) { + this.successful = successful; + } + + public int getFailed() { + return failed; + } + + public void setFailed(int failed) { + this.failed = failed; + } + + @Override + public String toString() { + return "TaskExecutionCounters{" + + "total=" + total + + ", active=" + active + + ", successful=" + successful + + ", failed=" + failed + + '}'; + } +} diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/ExecutionCounters.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/TaskExecutionCounters.java similarity index 87% rename from scheduler/src/main/java/com/cognitree/kronos/scheduler/model/ExecutionCounters.java rename to scheduler/src/main/java/com/cognitree/kronos/scheduler/model/TaskExecutionCounters.java index fab921b..b414654 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/ExecutionCounters.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/TaskExecutionCounters.java @@ -17,12 +17,13 @@ package com.cognitree.kronos.scheduler.model; -public class ExecutionCounters { +public class TaskExecutionCounters { private int total; private int active; private int successful; private int failed; private int skipped; + private int aborted; public int getActive() { return active; @@ -64,14 +65,23 @@ public void setSkipped(int skipped) { this.skipped = skipped; } + public int getAborted() { + return aborted; + } + + public void setAborted(int aborted) { + this.aborted = aborted; + } + @Override public String toString() { - return "ExecutionCounters{" + + return "TaskExecutionCounters{" + "total=" + total + ", active=" + active + ", successful=" + successful + ", failed=" + failed + ", skipped=" + skipped + + ", aborted=" + aborted + '}'; } } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Workflow.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Workflow.java index 47357f3..213cebd 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Workflow.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/Workflow.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.TimeUnit; @JsonSerialize(as = Workflow.class) @JsonDeserialize(as = Workflow.class) @@ -125,7 +124,7 @@ public static class WorkflowTask { private Map properties = new HashMap<>(); private List policies = new ArrayList<>(); - private long maxExecutionTimeInMs = TimeUnit.DAYS.toMillis(1); + private long maxExecutionTimeInMs = -1; private boolean enabled = true; public String getName() { diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/WorkflowStatistics.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/WorkflowStatistics.java index cd015a8..85015a9 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/WorkflowStatistics.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/model/WorkflowStatistics.java @@ -18,24 +18,24 @@ package com.cognitree.kronos.scheduler.model; public class WorkflowStatistics { - private ExecutionCounters jobs; - private ExecutionCounters tasks; + private JobExecutionCounters jobs; + private TaskExecutionCounters tasks; private long from; private long to; - public ExecutionCounters getJobs() { + public JobExecutionCounters getJobs() { return jobs; } - public void setJobs(ExecutionCounters jobs) { + public void setJobs(JobExecutionCounters jobs) { this.jobs = jobs; } - public ExecutionCounters getTasks() { + public TaskExecutionCounters getTasks() { return tasks; } - public void setTasks(ExecutionCounters tasks) { + public void setTasks(TaskExecutionCounters tasks) { this.tasks = tasks; } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java index 19f51bf..00c38e9 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java @@ -17,7 +17,6 @@ package com.cognitree.kronos.scheduler.store.impl; -import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.scheduler.store.JobStore; import com.cognitree.kronos.scheduler.store.NamespaceStore; import com.cognitree.kronos.scheduler.store.StoreService; diff --git a/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java b/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java index f85223e..923cc52 100644 --- a/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java +++ b/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java @@ -17,10 +17,12 @@ package com.cognitree.kronos; +import com.cognitree.kronos.scheduler.JobService; import com.cognitree.kronos.scheduler.NamespaceService; import com.cognitree.kronos.scheduler.WorkflowService; import com.cognitree.kronos.scheduler.WorkflowTriggerService; import com.cognitree.kronos.scheduler.model.CronSchedule; +import com.cognitree.kronos.scheduler.model.Job; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.Workflow; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; @@ -36,7 +38,9 @@ import java.io.InputStream; import java.text.ParseException; import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; public class TestUtil { @@ -44,6 +48,7 @@ public class TestUtil { private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); private static final CronSchedule SCHEDULE = new CronSchedule(); + static { SCHEDULE.setCronExpression("0/2 * * * * ?"); } @@ -113,10 +118,26 @@ public static WorkflowTrigger scheduleWorkflow(String workflowTemplate, Map 0) { + final List jobs = JobService.getService().get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), + workflowTrigger.getName(), 0, System.currentTimeMillis()); + if (jobs.size() > 0) { + Optional optionalJob = jobs.stream().filter(job -> !job.getStatus().isFinal()).findFirst(); + if (!optionalJob.isPresent()) { + break; + } + } + Thread.sleep(1000); + maxCount--; + } + } + + public static void waitForTriggerToComplete(WorkflowTrigger workflowTrigger, Scheduler scheduler) throws Exception { // wait for both the job to be triggered - TriggerKey workflowOneTriggerKey = new TriggerKey(workflowTriggerOne.getName(), - workflowTriggerOne.getWorkflow() + ":" + workflowTriggerOne.getNamespace()); + TriggerKey workflowOneTriggerKey = new TriggerKey(workflowTrigger.getName(), + workflowTrigger.getWorkflow() + ":" + workflowTrigger.getNamespace()); int maxCount = 50; while (maxCount > 0 && scheduler.checkExists(workflowOneTriggerKey)) { Thread.sleep(100); diff --git a/scheduler/src/test/java/com.cognitree.kronos/scheduler/ConfigurationServiceTest.java b/scheduler/src/test/java/com.cognitree.kronos/scheduler/ConfigurationServiceTest.java index fbe2ef1..8ef2b23 100644 --- a/scheduler/src/test/java/com.cognitree.kronos/scheduler/ConfigurationServiceTest.java +++ b/scheduler/src/test/java/com.cognitree.kronos/scheduler/ConfigurationServiceTest.java @@ -17,19 +17,22 @@ package com.cognitree.kronos.scheduler; -import com.cognitree.kronos.queue.RAMQueueFactory; +import com.cognitree.kronos.ServiceException; +import com.cognitree.kronos.queue.QueueConfig; +import com.cognitree.kronos.queue.producer.Producer; import com.cognitree.kronos.scheduler.model.Namespace; import com.cognitree.kronos.scheduler.model.Workflow; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; import com.cognitree.kronos.scheduler.model.events.ConfigUpdate; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import java.io.InputStream; import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; import static com.cognitree.kronos.TestUtil.createConfigUpdate; @@ -42,8 +45,18 @@ public class ConfigurationServiceTest { private static final SchedulerApp SCHEDULER_APP = new SchedulerApp(); private static final ObjectMapper MAPPER = new ObjectMapper(); + private static Producer CONFIG_PRODUCER; + @BeforeClass public static void start() throws Exception { + final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + final InputStream queueConfigAsStream = + ConfigurationServiceTest.class.getClassLoader().getResourceAsStream("queue.yaml"); + final QueueConfig queueConfig = YAML_MAPPER.readValue(queueConfigAsStream, QueueConfig.class); + CONFIG_PRODUCER = (Producer) Class.forName(queueConfig.getProducerConfig().getProducerClass()) + .getConstructor() + .newInstance(); + CONFIG_PRODUCER.init(queueConfig.getConfigurationQueue(), queueConfig.getProducerConfig().getConfig()); SCHEDULER_APP.start(); } @@ -54,16 +67,13 @@ public static void stop() { @Test public void testNamespaceUpdates() throws Exception { - ConfigurationService configurationService = ConfigurationService.getService(); - LinkedBlockingQueue queue = RAMQueueFactory.getQueue(configurationService.configurationQueue); - // Create Namespace String testNsName = "testNamespaceUpdates"; List namespacesBeforeCreate = getNamespacesWithName(testNsName); Assert.assertEquals(0, namespacesBeforeCreate.size()); Namespace namespace = createNamespace(testNsName); ConfigUpdate createNamespaceConfigUpdate = createConfigUpdate(ConfigUpdate.Action.create, namespace); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(createNamespaceConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(createNamespaceConfigUpdate)); Thread.sleep(1000); List namespacesAfterCreate = getNamespacesWithName(testNsName); Assert.assertEquals(1, namespacesAfterCreate.size()); @@ -72,7 +82,7 @@ public void testNamespaceUpdates() throws Exception { // Update Namespace namespace.setDescription("updated description"); ConfigUpdate updateNamespaceConfigUpdate = createConfigUpdate(ConfigUpdate.Action.update, namespace); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(updateNamespaceConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(updateNamespaceConfigUpdate)); Thread.sleep(1000); List namespacesAfterUpdate = getNamespacesWithName(testNsName); Assert.assertFalse(namespacesAfterUpdate.isEmpty()); @@ -89,15 +99,11 @@ private List getNamespacesWithName(String testNsName) throws ServiceE @Test public void testWorkflowUpdates() throws Exception { - ConfigurationService configurationService = ConfigurationService.getService(); - LinkedBlockingQueue queue = RAMQueueFactory - .getQueue(configurationService.configurationQueue); - String testNsName = "testWorkflowUpdatesNs"; // Create Namespace Namespace namespace = createNamespace(testNsName); ConfigUpdate createNamespaceConfigUpdate = createConfigUpdate(ConfigUpdate.Action.create, namespace); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(createNamespaceConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(createNamespaceConfigUpdate)); Thread.sleep(1000); String testWorkflowName = "testWorkflowUpdatesWf"; @@ -106,7 +112,7 @@ public void testWorkflowUpdates() throws Exception { // Create Workflow Workflow workflow = createWorkflow("workflow.yaml", testWorkflowName, testNsName); ConfigUpdate createWorkflowConfigUpdate = createConfigUpdate(ConfigUpdate.Action.create, workflow); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(createWorkflowConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(createWorkflowConfigUpdate)); Thread.sleep(1000); List workflowsAfterCreate = getWorkflowsWithName(testNsName, testWorkflowName); Assert.assertEquals(1, workflowsAfterCreate.size()); @@ -115,7 +121,7 @@ public void testWorkflowUpdates() throws Exception { // Update Workflow workflow.setDescription("Updated Description"); ConfigUpdate updateWorkflowConfigUpdate = createConfigUpdate(ConfigUpdate.Action.update, workflow); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(updateWorkflowConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(updateWorkflowConfigUpdate)); Thread.sleep(1000); List workflowsAfterUpdate = getWorkflowsWithName(testNsName, testWorkflowName); Assert.assertEquals(1, workflowsAfterUpdate.size()); @@ -123,7 +129,7 @@ public void testWorkflowUpdates() throws Exception { // Delete Workflow ConfigUpdate deleteWorkflowConfigUpdate = createConfigUpdate(ConfigUpdate.Action.delete, workflow); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(deleteWorkflowConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(deleteWorkflowConfigUpdate)); Thread.sleep(1000); List workflowsAfterDelete = getWorkflowsWithName(testNsName, testWorkflowName); Assert.assertEquals(0, workflowsAfterDelete.size()); @@ -138,20 +144,16 @@ private List getWorkflowsWithName(String testNsName, String workflowNa @Test public void testWorkflowTriggerUpdates() throws Exception { - ConfigurationService configurationService = ConfigurationService.getService(); - LinkedBlockingQueue queue = RAMQueueFactory - .getQueue(configurationService.configurationQueue); - // Create Namespace String testNsName = "testWorkflowTriggerUpdatesNs"; Namespace namespace = createNamespace(testNsName); ConfigUpdate createNamespaceConfigUpdate = createConfigUpdate(ConfigUpdate.Action.create, namespace); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(createNamespaceConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(createNamespaceConfigUpdate)); // Create Workflow String testWorkflowName = "testWorkflowTriggerUpdatesWf"; Workflow workflow = createWorkflow("workflow.yaml", testWorkflowName, testNsName); ConfigUpdate createWorkflowConfigUpdate = createConfigUpdate(ConfigUpdate.Action.create, workflow); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(createWorkflowConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(createWorkflowConfigUpdate)); Thread.sleep(1000); String testTriggerName = "testWorkflowTriggerUpdatesTgr"; @@ -162,7 +164,7 @@ public void testWorkflowTriggerUpdates() throws Exception { trigger.setEndAt(null); trigger.setStartAt(null); ConfigUpdate createWorkflowTriggerConfigUpdate = createConfigUpdate(ConfigUpdate.Action.create, trigger); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(createWorkflowTriggerConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(createWorkflowTriggerConfigUpdate)); Thread.sleep(1000); List workflowTriggersAfterCreate = getWorkflowTriggersWithName(testNsName, testWorkflowName, testTriggerName); Assert.assertEquals(1, workflowTriggersAfterCreate.size()); @@ -171,7 +173,7 @@ public void testWorkflowTriggerUpdates() throws Exception { // Update Workflow trigger trigger.setEnabled(false); ConfigUpdate updateWorkflowTriggerConfigUpdate = createConfigUpdate(ConfigUpdate.Action.update, trigger); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(updateWorkflowTriggerConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(updateWorkflowTriggerConfigUpdate)); Thread.sleep(1000); List workflowTriggersAfterUpdate = getWorkflowTriggersWithName(testNsName, testWorkflowName, testTriggerName); Assert.assertEquals(1, workflowTriggersAfterUpdate.size()); @@ -179,7 +181,7 @@ public void testWorkflowTriggerUpdates() throws Exception { // Update Workflow trigger ConfigUpdate deleteWorkflowTriggerConfigUpdate = createConfigUpdate(ConfigUpdate.Action.delete, trigger); - queue.offer(MAPPER.writerFor(ConfigUpdate.class).writeValueAsString(deleteWorkflowTriggerConfigUpdate)); + CONFIG_PRODUCER.send(MAPPER.writeValueAsString(deleteWorkflowTriggerConfigUpdate)); Thread.sleep(1000); List workflowTriggersAfterDelete = getWorkflowTriggersWithName(testNsName, testWorkflowName, testTriggerName); Assert.assertEquals(0, workflowTriggersAfterDelete.size()); diff --git a/scheduler/src/test/resources/queue.yaml b/scheduler/src/test/resources/queue.yaml index b8c02a0..d978d74 100755 --- a/scheduler/src/test/resources/queue.yaml +++ b/scheduler/src/test/resources/queue.yaml @@ -2,6 +2,7 @@ producerConfig: producerClass: com.cognitree.kronos.queue.producer.RAMProducer consumerConfig: consumerClass: com.cognitree.kronos.queue.consumer.RAMConsumer - pollIntervalInMs: 100 taskStatusQueue: taskstatus configurationQueue: configurations +controlMessageQueue: controlmessages +pollIntervalInMs: 10 diff --git a/scheduler/src/test/resources/scheduler.yaml b/scheduler/src/test/resources/scheduler.yaml index 3a7f884..242610d 100755 --- a/scheduler/src/test/resources/scheduler.yaml +++ b/scheduler/src/test/resources/scheduler.yaml @@ -1,4 +1,3 @@ storeServiceConfig: storeServiceClass: com.cognitree.kronos.scheduler.store.impl.RAMStoreService - enableConfigurationService: true \ No newline at end of file From 72e5c2d2d2708597d55bc413467ee28035a92c80 Mon Sep 17 00:00:00 2001 From: Ankit Nanglia Date: Fri, 28 Jun 2019 18:53:58 +0530 Subject: [PATCH 02/12] Release Kronos 3.0.0-RC1 --- Dockerfile | 2 +- api/pom.xml | 2 +- app/pom.xml | 2 +- common/pom.xml | 2 +- executor/pom.xml | 2 +- pom.xml | 2 +- scheduler/pom.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9ca2987..70a114f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM openjdk:8u151-jre-alpine -ENV KRONOS_VERSION 2.2.4 +ENV KRONOS_VERSION 3.0.0-RC1 ENV KRONOS_HOME /home/kronos-${KRONOS_VERSION} ENV MODE all diff --git a/api/pom.xml b/api/pom.xml index cd0ba34..90a2be1 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,7 +20,7 @@ kronos com.cognitree - 2.2.4 + 3.0.0-RC1 4.0.0 diff --git a/app/pom.xml b/app/pom.xml index debf8bf..25f66e7 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 2.2.4 + 3.0.0-RC1 4.0.0 diff --git a/common/pom.xml b/common/pom.xml index 41686a6..ef104fc 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -24,7 +24,7 @@ kronos com.cognitree - 2.2.4 + 3.0.0-RC1 com.cognitree.kronos diff --git a/executor/pom.xml b/executor/pom.xml index 2d577db..055b3c8 100755 --- a/executor/pom.xml +++ b/executor/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 2.2.4 + 3.0.0-RC1 4.0.0 diff --git a/pom.xml b/pom.xml index 05469f8..2f6ac72 100755 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ kronos kronos pom - 2.2.4 + 3.0.0-RC1 1.8 diff --git a/scheduler/pom.xml b/scheduler/pom.xml index 003e9fa..42b6849 100755 --- a/scheduler/pom.xml +++ b/scheduler/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 2.2.4 + 3.0.0-RC1 4.0.0 From 288775ad04c8f22a9e4e6c6af43073407255f98e Mon Sep 17 00:00:00 2001 From: Ankit Nanglia Date: Wed, 3 Jul 2019 21:06:25 +0530 Subject: [PATCH 03/12] Ability to retry a task on timeout (#53) * Ability to retry a task on timeout * Fix issues with control message on retry * Integration test to test basic restart scenarios --- Dockerfile | 2 +- api/pom.xml | 2 +- app/pom.xml | 2 +- .../com/cognitree/kronos/ApplicationTest.java | 69 ++++++++++++++++ .../handlers/MockRetryTaskHandler.java | 65 ++++++++++++++++ .../kronos/queue/QueueServiceTest.java | 6 +- .../kronos/scheduler/JobServiceTest.java | 70 ++++++++++++++++- .../kronos/scheduler/ServiceTest.java | 25 +++--- .../kronos/scheduler/TaskServiceTest.java | 19 +++++ app/src/test/resources/executor.yaml | 5 +- .../workflow-template-abort-tasks.yaml | 4 +- ...low-template-timeout-tasks-with-retry.yaml | 24 ++++++ .../workflow-template-timeout-tasks.yaml | 2 +- common/pom.xml | 2 +- .../cognitree/kronos/model/RetryPolicy.java | 26 ++++++- .../java/com/cognitree/kronos/model/Task.java | 1 + .../cognitree/kronos/queue/QueueService.java | 2 +- executor/pom.xml | 2 +- .../kronos/executor/TaskExecutionService.java | 78 +++++++++++++------ pom.xml | 2 +- scheduler/pom.xml | 2 +- .../kronos/scheduler/JobService.java | 7 +- .../scheduler/TaskSchedulerService.java | 40 +++++----- .../kronos/scheduler/TaskService.java | 41 +++++++--- .../kronos/scheduler/ValidationError.java | 4 +- .../kronos/scheduler/store/StoreService.java | 15 +++- .../scheduler/store/impl/RAMStoreService.java | 5 ++ .../resources/validation-errors.properties | 4 +- .../java/com.cognitree.kronos/TestUtil.java | 22 ++++-- 29 files changed, 451 insertions(+), 97 deletions(-) create mode 100644 app/src/test/java/com/cognitree/kronos/ApplicationTest.java create mode 100644 app/src/test/java/com/cognitree/kronos/executor/handlers/MockRetryTaskHandler.java create mode 100644 app/src/test/resources/workflows/workflow-template-timeout-tasks-with-retry.yaml diff --git a/Dockerfile b/Dockerfile index 70a114f..b6be2b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM openjdk:8u151-jre-alpine -ENV KRONOS_VERSION 3.0.0-RC1 +ENV KRONOS_VERSION 3.0.0-RC2 ENV KRONOS_HOME /home/kronos-${KRONOS_VERSION} ENV MODE all diff --git a/api/pom.xml b/api/pom.xml index 90a2be1..fef9b2f 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,7 +20,7 @@ kronos com.cognitree - 3.0.0-RC1 + 3.0.0-RC2 4.0.0 diff --git a/app/pom.xml b/app/pom.xml index 25f66e7..d54f312 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0-RC1 + 3.0.0-RC2 4.0.0 diff --git a/app/src/test/java/com/cognitree/kronos/ApplicationTest.java b/app/src/test/java/com/cognitree/kronos/ApplicationTest.java new file mode 100644 index 0000000..47c40de --- /dev/null +++ b/app/src/test/java/com/cognitree/kronos/ApplicationTest.java @@ -0,0 +1,69 @@ +package com.cognitree.kronos; + +import com.cognitree.kronos.scheduler.JobService; +import com.cognitree.kronos.scheduler.ServiceTest; +import com.cognitree.kronos.scheduler.model.Job; +import com.cognitree.kronos.scheduler.model.WorkflowTrigger; +import com.cognitree.kronos.scheduler.store.StoreService; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +import static com.cognitree.kronos.TestUtil.scheduleWorkflow; +import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; + +public class ApplicationTest extends ServiceTest { + + @Test + public void testSchedulerDown() throws Exception { + final StoreService storeService = (StoreService) ServiceProvider.getService(StoreService.class.getSimpleName()); + if (!storeService.isPersistent()) { + return; + } + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + SCHEDULER_APP.stop(); + Thread.sleep(1000); + SCHEDULER_APP.start(); + + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); + + JobService jobService = JobService.getService(); + final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace()); + Assert.assertEquals(1, workflowOneJobs.size()); + Assert.assertNotNull(jobService.get(workflowOneJobs.get(0))); + Assert.assertFalse(workflowOneJobs.stream().anyMatch(t -> t.getStatus() != Job.Status.SUCCESSFUL)); + + final List workflowTwoJobs = jobService.get(workflowTriggerTwo.getNamespace()); + Assert.assertEquals(1, workflowTwoJobs.size()); + Assert.assertNotNull(jobService.get(workflowTwoJobs.get(0))); + } + + + @Test + public void testExecutorDown() throws Exception { + final StoreService storeService = (StoreService) ServiceProvider.getService(StoreService.class.getSimpleName()); + if (!storeService.isPersistent()) { + return; + } + EXECUTOR_APP.stop(); + final WorkflowTrigger workflowTriggerOne = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + final WorkflowTrigger workflowTriggerTwo = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + Thread.sleep(1000); + EXECUTOR_APP.start(); + + waitForJobsToTriggerAndComplete(workflowTriggerOne); + waitForJobsToTriggerAndComplete(workflowTriggerTwo); + + JobService jobService = JobService.getService(); + final List workflowOneJobs = jobService.get(workflowTriggerOne.getNamespace()); + Assert.assertEquals(1, workflowOneJobs.size()); + Assert.assertNotNull(jobService.get(workflowOneJobs.get(0))); + + final List workflowTwoJobs = jobService.get(workflowTriggerTwo.getNamespace()); + Assert.assertEquals(1, workflowTwoJobs.size()); + Assert.assertNotNull(jobService.get(workflowTwoJobs.get(0))); + } +} diff --git a/app/src/test/java/com/cognitree/kronos/executor/handlers/MockRetryTaskHandler.java b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockRetryTaskHandler.java new file mode 100644 index 0000000..b457564 --- /dev/null +++ b/app/src/test/java/com/cognitree/kronos/executor/handlers/MockRetryTaskHandler.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.cognitree.kronos.executor.handlers; + +import com.cognitree.kronos.executor.model.TaskResult; +import com.cognitree.kronos.model.Task; +import com.cognitree.kronos.model.TaskId; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MockRetryTaskHandler implements TaskHandler { + private static final Logger logger = LoggerFactory.getLogger(MockRetryTaskHandler.class); + + private static final List tasks = Collections.synchronizedList(new ArrayList<>()); + private boolean abort = false; + private Task task; + + public static boolean isHandled(TaskId taskId) { + return tasks.contains(taskId); + } + + @Override + public void init(Task task, ObjectNode config) { + this.task = task; + } + + @Override + public TaskResult execute() { + logger.info("Received request to execute task {}", task); + tasks.add(task); + while (!abort && task.getRetryCount() < 2) { // the task should pass on the third retry + try { + Thread.sleep(50); + } catch (InterruptedException e) { + logger.error("Thread has been interrupted"); + } + } + return TaskResult.SUCCESS; + } + + @Override + public void abort() { + abort = true; + } +} diff --git a/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java b/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java index 37f5c6d..074d554 100644 --- a/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/queue/QueueServiceTest.java @@ -58,8 +58,8 @@ public static void init() throws IOException, ServiceException { QUEUE_SERVICE.init(); QUEUE_SERVICE.start(); // initial call so that the topics are created - QUEUE_SERVICE.consumeTask(TASK_TYPE_A, 1); - QUEUE_SERVICE.consumeTask(TASK_TYPE_B, 1); + QUEUE_SERVICE.consumeTasks(TASK_TYPE_A, 1); + QUEUE_SERVICE.consumeTasks(TASK_TYPE_B, 1); QUEUE_SERVICE.consumeTaskStatusUpdates(); QUEUE_SERVICE.consumeControlMessages(); } @@ -192,7 +192,7 @@ private List getTaskStatusUpdate() throws ServiceException { private List getTasks(String taskType, int size) throws ServiceException { int count = 10; while (count > 0) { - List tasks = QUEUE_SERVICE.consumeTask(taskType, size); + List tasks = QUEUE_SERVICE.consumeTasks(taskType, size); if (!tasks.isEmpty()) { return tasks; } diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java index 7929342..d8cbcc5 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/JobServiceTest.java @@ -17,14 +17,17 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.executor.handlers.MockAbortTaskHandler; import com.cognitree.kronos.executor.handlers.MockFailureTaskHandler; +import com.cognitree.kronos.executor.handlers.MockRetryTaskHandler; import com.cognitree.kronos.executor.handlers.MockSuccessTaskHandler; import com.cognitree.kronos.model.Messages; import com.cognitree.kronos.model.Task; import com.cognitree.kronos.scheduler.model.Job; import com.cognitree.kronos.scheduler.model.JobId; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; +import com.cognitree.kronos.scheduler.store.StoreService; import org.junit.Assert; import org.junit.Test; @@ -33,7 +36,10 @@ import static com.cognitree.kronos.TestUtil.scheduleWorkflow; import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; +import static com.cognitree.kronos.TestUtil.waitForTaskToBeRunning; import static com.cognitree.kronos.TestUtil.waitForTriggerToComplete; +import static com.cognitree.kronos.model.Task.Status.ABORTED; +import static com.cognitree.kronos.model.Task.Status.SKIPPED; public class JobServiceTest extends ServiceTest { @@ -167,7 +173,7 @@ public void testGetJobTasksFailedDueToTimeout() throws Exception { Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); break; case "taskThree": - Assert.assertEquals(Task.Status.ABORTED, task.getStatus()); + Assert.assertEquals(Task.Status.TIMED_OUT, task.getStatus()); Assert.assertEquals(Messages.TIMED_OUT_EXECUTING_TASK_MESSAGE, task.getStatusMessage()); Assert.assertTrue(MockAbortTaskHandler.isHandled(task.getIdentity())); break; @@ -177,6 +183,41 @@ public void testGetJobTasksFailedDueToTimeout() throws Exception { } } + @Test + public void testGetJobTasksFailedDueToTimeoutWithRetry() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_TIMEOUT_TASKS_WITH_RETRY_YAML); + + waitForJobsToTriggerAndComplete(workflowTrigger); + + JobService jobService = JobService.getService(); + final List workflowOneJobs = jobService.get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), + workflowTrigger.getName(), 0, System.currentTimeMillis()); + Assert.assertEquals(1, workflowOneJobs.size()); + + final Job job = workflowOneJobs.get(0); + final List tasks = jobService.getTasks(job); + Assert.assertEquals(3, tasks.size()); + for (Task task : tasks) { + switch (task.getName()) { + case "taskOne": + Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); + break; + case "taskTwo": + Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertEquals(2, task.getRetryCount()); + Assert.assertTrue(MockRetryTaskHandler.isHandled(task.getIdentity())); + break; + case "taskThree": + Assert.assertEquals(Task.Status.SUCCESSFUL, task.getStatus()); + Assert.assertTrue(MockSuccessTaskHandler.isHandled(task.getIdentity())); + break; + default: + Assert.fail(); + } + } + } + @Test public void testGetJobTasksFailedDueToHandler() throws Exception { final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_FAILED_HANDLER_YAML); @@ -203,7 +244,7 @@ public void testGetJobTasksFailedDueToHandler() throws Exception { Assert.assertTrue(MockFailureTaskHandler.isHandled(task.getIdentity())); break; case "taskThree": - Assert.assertEquals(Task.Status.SKIPPED, task.getStatus()); + Assert.assertEquals(SKIPPED, task.getStatus()); Assert.assertEquals(Messages.FAILED_DEPENDEE_TASK_MESSAGE, task.getStatusMessage()); break; default: @@ -229,6 +270,10 @@ public void testAbortJob() throws Exception { waitForTriggerToComplete(workflowTrigger, WorkflowSchedulerService.getService().getScheduler()); + Task taskOne = TaskService.getService().get(workflowTrigger.getNamespace()) + .stream().filter(task -> task.getName().equals("taskOne")).findFirst().get(); + waitForTaskToBeRunning(taskOne); + List jobs = JobService.getService().get(workflowTrigger.getNamespace()); JobService.getService().abortJob(jobs.get(0).getIdentity()); @@ -237,11 +282,12 @@ public void testAbortJob() throws Exception { for (Task tsk : tasks) { switch (tsk.getName()) { case "taskOne": - Assert.assertEquals(Task.Status.ABORTED, tsk.getStatus()); + Assert.assertEquals(ABORTED, tsk.getStatus()); Assert.assertTrue(MockAbortTaskHandler.isHandled(tsk.getIdentity())); break; case "taskTwo": - Assert.assertEquals(Task.Status.ABORTED, tsk.getStatus()); + Assert.assertTrue((tsk.getStatus() == ABORTED) || + (tsk.getStatus() == SKIPPED)); break; default: Assert.fail(); @@ -252,6 +298,22 @@ public void testAbortJob() throws Exception { JobService.getService().get(workflowTrigger.getNamespace()).get(0).getStatus()); } + @Test(expected = ValidationException.class) + public void testAbortJobWithTaskInScheduledState() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + waitForJobsToTriggerAndComplete(workflowTrigger); + + List jobs = JobService.getService().get(workflowTrigger.getNamespace()); + Assert.assertFalse(jobs.isEmpty()); + StoreService storeService = (StoreService) ServiceProvider.getService(StoreService.class.getSimpleName()); + List tasks = TaskService.getService().get(workflowTrigger.getNamespace()); + // move back the job to running state and task to scheduled state + JobService.getService().updateStatus(jobs.get(0).getIdentity(), Job.Status.RUNNING); + Task task = tasks.get(0); + task.setStatus(Task.Status.SCHEDULED); + storeService.getTaskStore().update(task); + JobService.getService().abortJob(jobs.get(0)); + } @Test public void testDeleteJob() throws Exception { diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java index 38e6cc0..70cb84e 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/ServiceTest.java @@ -36,19 +36,20 @@ import java.util.List; public class ServiceTest { - static final String WORKFLOW_TEMPLATE_YAML = "workflows/workflow-template.yaml"; - static final String WORKFLOW_TEMPLATE_TIMEOUT_TASKS_YAML = "workflows/workflow-template-timeout-tasks.yaml"; - static final String WORKFLOW_TEMPLATE_FAILED_HANDLER_YAML = "workflows/workflow-template-failed-handler.yaml"; - static final String INVALID_WORKFLOW_MISSING_TASKS_TEMPLATE_YAML = "workflows/invalid-workflow-missing-tasks-template.yaml"; - static final String INVALID_WORKFLOW_DISABLED_TASKS_TEMPLATE_YAML = "workflows/invalid-workflow-disabled-tasks-template.yaml"; - static final String WORKFLOW_TEMPLATE_ABORT_TASKS_YAML = "workflows/workflow-template-abort-tasks.yaml"; - static final String WORKFLOW_TEMPLATE_WITH_TASK_CONTEXT_YAML = "workflows/workflow-template-with-task-context.yaml"; - static final String WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML = "workflows/workflow-template-with-properties.yaml"; - static final String WORKFLOW_TEMPLATE_WITH_DUPLICATE_POLICY_YAML = "workflows/workflow-template-with-duplicate-policy.yaml"; + protected static final SchedulerApp SCHEDULER_APP = new SchedulerApp(); + protected static final ExecutorApp EXECUTOR_APP = new ExecutorApp(); + protected static final String WORKFLOW_TEMPLATE_YAML = "workflows/workflow-template.yaml"; + protected static final String WORKFLOW_TEMPLATE_TIMEOUT_TASKS_YAML = "workflows/workflow-template-timeout-tasks.yaml"; + protected static final String WORKFLOW_TEMPLATE_TIMEOUT_TASKS_WITH_RETRY_YAML = "workflows/workflow-template-timeout-tasks-with-retry.yaml"; + protected static final String WORKFLOW_TEMPLATE_FAILED_HANDLER_YAML = "workflows/workflow-template-failed-handler.yaml"; + protected static final String INVALID_WORKFLOW_MISSING_TASKS_TEMPLATE_YAML = "workflows/invalid-workflow-missing-tasks-template.yaml"; + protected static final String INVALID_WORKFLOW_DISABLED_TASKS_TEMPLATE_YAML = "workflows/invalid-workflow-disabled-tasks-template.yaml"; + protected static final String WORKFLOW_TEMPLATE_ABORT_TASKS_YAML = "workflows/workflow-template-abort-tasks.yaml"; + protected static final String WORKFLOW_TEMPLATE_WITH_TASK_CONTEXT_YAML = "workflows/workflow-template-with-task-context.yaml"; + protected static final String WORKFLOW_TEMPLATE_WITH_PROPERTIES_YAML = "workflows/workflow-template-with-properties.yaml"; + protected static final String WORKFLOW_TEMPLATE_WITH_DUPLICATE_POLICY_YAML = "workflows/workflow-template-with-duplicate-policy.yaml"; private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); - private static final SchedulerApp SCHEDULER_APP = new SchedulerApp(); - private static final ExecutorApp EXECUTOR_APP = new ExecutorApp(); private static final List EXISTING_NAMESPACE = new ArrayList<>(); @BeforeClass @@ -69,7 +70,7 @@ private static void createTopics() throws ServiceException, java.io.IOException executorConfig.getTaskHandlerConfig().forEach((type, taskHandlerConfig) -> { try { QueueService.getService(QueueService.EXECUTOR_QUEUE) - .consumeTask(type, 0); + .consumeTasks(type, 0); } catch (ServiceException e) { // do nothing } diff --git a/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java b/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java index 9d92fdd..b6aecfe 100644 --- a/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java +++ b/app/src/test/java/com/cognitree/kronos/scheduler/TaskServiceTest.java @@ -17,6 +17,7 @@ package com.cognitree.kronos.scheduler; +import com.cognitree.kronos.ServiceProvider; import com.cognitree.kronos.executor.handlers.MockAbortTaskHandler; import com.cognitree.kronos.executor.handlers.MockSuccessTaskHandler; import com.cognitree.kronos.model.Messages; @@ -24,6 +25,7 @@ import com.cognitree.kronos.model.TaskId; import com.cognitree.kronos.scheduler.model.Job; import com.cognitree.kronos.scheduler.model.WorkflowTrigger; +import com.cognitree.kronos.scheduler.store.StoreService; import org.junit.Assert; import org.junit.Test; @@ -33,6 +35,7 @@ import static com.cognitree.kronos.TestUtil.scheduleWorkflow; import static com.cognitree.kronos.TestUtil.waitForJobsToTriggerAndComplete; +import static com.cognitree.kronos.TestUtil.waitForTaskToBeRunning; import static com.cognitree.kronos.TestUtil.waitForTriggerToComplete; public class TaskServiceTest extends ServiceTest { @@ -106,6 +109,21 @@ public void testAbortTasksNotFound() throws Exception { UUID.randomUUID().toString(), jobs.get(0).getId(), workflowTrigger.getWorkflow())); } + @Test(expected = ValidationException.class) + public void testAbortTasksInScheduledState() throws Exception { + final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_YAML); + waitForJobsToTriggerAndComplete(workflowTrigger); + + List jobs = JobService.getService().get(workflowTrigger.getNamespace()); + Assert.assertFalse(jobs.isEmpty()); + StoreService storeService = (StoreService) ServiceProvider.getService(StoreService.class.getSimpleName()); + List tasks = TaskService.getService().get(workflowTrigger.getNamespace()); + Task task = tasks.get(0); + task.setStatus(Task.Status.SCHEDULED); + storeService.getTaskStore().update(task); + TaskService.getService().abortTask(task.getIdentity()); + } + @Test public void testAbortTasks() throws Exception { final WorkflowTrigger workflowTrigger = scheduleWorkflow(WORKFLOW_TEMPLATE_ABORT_TASKS_YAML); @@ -116,6 +134,7 @@ public void testAbortTasks() throws Exception { final List workflowTasks = taskService.get(workflowTrigger.getNamespace()); Assert.assertEquals(2, workflowTasks.size()); Task task = workflowTasks.stream().filter(t -> t.getName().equals("taskOne")).findFirst().get(); + waitForTaskToBeRunning(task); TaskService.getService().abortTask(task); waitForJobsToTriggerAndComplete(workflowTrigger); diff --git a/app/src/test/resources/executor.yaml b/app/src/test/resources/executor.yaml index 4091913..1930189 100644 --- a/app/src/test/resources/executor.yaml +++ b/app/src/test/resources/executor.yaml @@ -5,6 +5,9 @@ taskHandlerConfig: typeFailure: handlerClass: com.cognitree.kronos.executor.handlers.MockFailureTaskHandler maxParallelTasks: 4 - typeMock: + typeMockAbort: handlerClass: com.cognitree.kronos.executor.handlers.MockAbortTaskHandler maxParallelTasks: 4 + typeMockRetry: + handlerClass: com.cognitree.kronos.executor.handlers.MockRetryTaskHandler + maxParallelTasks: 4 diff --git a/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml b/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml index 3357289..ebae8d1 100644 --- a/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml +++ b/app/src/test/resources/workflows/workflow-template-abort-tasks.yaml @@ -2,12 +2,12 @@ description: sample workflow tasks: - name: taskOne - type: typeMock + type: typeMockAbort properties: keyA: valA keyB: valB - name: taskTwo - type: typeMock + type: typeMockAbort properties: keyA: valA keyB: valB diff --git a/app/src/test/resources/workflows/workflow-template-timeout-tasks-with-retry.yaml b/app/src/test/resources/workflows/workflow-template-timeout-tasks-with-retry.yaml new file mode 100644 index 0000000..037abef --- /dev/null +++ b/app/src/test/resources/workflows/workflow-template-timeout-tasks-with-retry.yaml @@ -0,0 +1,24 @@ +# name and namespace will be set while creating a workflow +description: sample workflow +tasks: + - name: taskOne + type: typeSuccess + properties: + keyA: valA + keyB: valB + - name: taskTwo + type: typeMockRetry + properties: + keyA: valA + keyB: valB + policies: + - type: retry + maxRetryCount: 3 + retryOnTimeout: true + maxExecutionTimeInMs: 100 + - name: taskThree + type: typeSuccess + properties: + keyA: valA + keyB: valB + dependsOn: ["taskTwo"] \ No newline at end of file diff --git a/app/src/test/resources/workflows/workflow-template-timeout-tasks.yaml b/app/src/test/resources/workflows/workflow-template-timeout-tasks.yaml index d9f7599..ca77384 100644 --- a/app/src/test/resources/workflows/workflow-template-timeout-tasks.yaml +++ b/app/src/test/resources/workflows/workflow-template-timeout-tasks.yaml @@ -12,7 +12,7 @@ tasks: keyA: valA keyB: valB - name: taskThree - type: typeMock + type: typeMockAbort properties: keyA: valA keyB: valB diff --git a/common/pom.xml b/common/pom.xml index ef104fc..0b0afa9 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -24,7 +24,7 @@ kronos com.cognitree - 3.0.0-RC1 + 3.0.0-RC2 com.cognitree.kronos diff --git a/common/src/main/java/com/cognitree/kronos/model/RetryPolicy.java b/common/src/main/java/com/cognitree/kronos/model/RetryPolicy.java index 0d3e7dd..898ac0d 100644 --- a/common/src/main/java/com/cognitree/kronos/model/RetryPolicy.java +++ b/common/src/main/java/com/cognitree/kronos/model/RetryPolicy.java @@ -26,6 +26,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class RetryPolicy extends Policy { private int maxRetryCount = 1; + private boolean retryOnFailure = true; + private boolean retryOnTimeout = false; public RetryPolicy() { super(retry); @@ -39,24 +41,44 @@ public void setMaxRetryCount(int maxRetryCount) { this.maxRetryCount = maxRetryCount; } + public boolean isRetryOnFailure() { + return retryOnFailure; + } + + public void setRetryOnFailure(boolean retryOnFailure) { + this.retryOnFailure = retryOnFailure; + } + + public boolean isRetryOnTimeout() { + return retryOnTimeout; + } + + public void setRetryOnTimeout(boolean retryOnTimeout) { + this.retryOnTimeout = retryOnTimeout; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof RetryPolicy)) return false; if (!super.equals(o)) return false; RetryPolicy that = (RetryPolicy) o; - return maxRetryCount == that.maxRetryCount; + return maxRetryCount == that.maxRetryCount && + retryOnFailure == that.retryOnFailure && + retryOnTimeout == that.retryOnTimeout; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), maxRetryCount); + return Objects.hash(super.hashCode(), maxRetryCount, retryOnFailure, retryOnTimeout); } @Override public String toString() { return "RetryPolicy{" + "maxRetryCount=" + maxRetryCount + + ", retryOnFailure=" + retryOnFailure + + ", retryOnTimeout=" + retryOnTimeout + "} " + super.toString(); } } diff --git a/common/src/main/java/com/cognitree/kronos/model/Task.java b/common/src/main/java/com/cognitree/kronos/model/Task.java index b238705..833660f 100755 --- a/common/src/main/java/com/cognitree/kronos/model/Task.java +++ b/common/src/main/java/com/cognitree/kronos/model/Task.java @@ -188,6 +188,7 @@ public enum Status { SUCCESSFUL(true), SKIPPED(true), // a task is marked as skipped it the task it depends on fails. FAILED(true), + TIMED_OUT(true), ABORTED(true); private final boolean isFinal; diff --git a/common/src/main/java/com/cognitree/kronos/queue/QueueService.java b/common/src/main/java/com/cognitree/kronos/queue/QueueService.java index 3c38fc9..873314d 100644 --- a/common/src/main/java/com/cognitree/kronos/queue/QueueService.java +++ b/common/src/main/java/com/cognitree/kronos/queue/QueueService.java @@ -141,7 +141,7 @@ public void send(ControlMessage controlMessage) throws ServiceException { } } - public List consumeTask(String type, int maxTasksToPoll) throws ServiceException { + public List consumeTasks(String type, int maxTasksToPoll) throws ServiceException { logger.debug("Received request to consume {} tasks of type {}", maxTasksToPoll, type); if (!consumers.containsKey(type)) { createConsumer(type, type); diff --git a/executor/pom.xml b/executor/pom.xml index 055b3c8..0221b77 100755 --- a/executor/pom.xml +++ b/executor/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0-RC1 + 3.0.0-RC2 4.0.0 diff --git a/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java b/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java index af9acf2..1c582ca 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java @@ -37,6 +37,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -47,9 +48,6 @@ import java.util.stream.Collectors; import static com.cognitree.kronos.model.Messages.MISSING_TASK_HANDLER_MESSAGE; -import static com.cognitree.kronos.model.Messages.TASK_ABORTED_MESSAGE; -import static com.cognitree.kronos.model.Messages.TIMED_OUT_EXECUTING_TASK_MESSAGE; -import static com.cognitree.kronos.model.Task.Status.ABORTED; import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.RUNNING; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; @@ -72,8 +70,8 @@ public final class TaskExecutionService implements Service { private final Map taskTypeToMaxParallelTasksCount = new HashMap<>(); private final Map taskTypeToRunningTasksCount = new HashMap<>(); - private final Map taskHandlersMap = new ConcurrentHashMap<>(); - private final Map> taskFuturesMap = new ConcurrentHashMap<>(); + private final Map taskHandlersMap = new ConcurrentHashMap<>(); + private final Map> taskFuturesMap = new ConcurrentHashMap<>(); // used by internal tasks to poll new tasks from queue private final ScheduledExecutorService taskConsumerThreadPool = Executors.newSingleThreadScheduledExecutor(); @@ -131,7 +129,7 @@ private void consumeTasks() { final int maxTasksToPoll = maxParallelTasks - taskTypeToRunningTasksCount.get(taskType); if (maxTasksToPoll > 0) { try { - final List tasks = QueueService.getService(EXECUTOR_QUEUE).consumeTask(taskType, maxTasksToPoll); + final List tasks = QueueService.getService(EXECUTOR_QUEUE).consumeTasks(taskType, maxTasksToPoll); tasks.forEach(this::submit); } catch (ServiceException e) { logger.error("Error consuming tasks for execution", e); @@ -153,25 +151,20 @@ private void consumeControlMessages() { for (ControlMessage controlMessage : controlMessages) { logger.info("Received request to execute control message {}", controlMessage); final Task task = controlMessage.getTask(); - if (!taskFuturesMap.containsKey(task)) { + final TaskExecutionContext taskExecutionContext = new TaskExecutionContext(task, task.getRetryCount()); + if (!taskFuturesMap.containsKey(taskExecutionContext)) { continue; } - final Future taskResultFuture = taskFuturesMap.get(task); + final Future taskResultFuture = taskFuturesMap.get(taskExecutionContext); if (taskResultFuture != null) { switch (controlMessage.getAction()) { case ABORT: - logger.info("Received request to abort task with id {}", task.getIdentity()); - // interrupt the task first and then call the abort method - taskResultFuture.cancel(true); - taskHandlersMap.get(task).abort(); - sendTaskStatusUpdate(task, ABORTED, TASK_ABORTED_MESSAGE); - break; case TIME_OUT: - logger.info("Received request to time out task with id {}", task.getIdentity()); + logger.info("Received request to {} task with id {}", + controlMessage.getAction(), task.getIdentity()); // interrupt the task first and then call the abort method taskResultFuture.cancel(true); - taskHandlersMap.get(task).abort(); - sendTaskStatusUpdate(task, ABORTED, TIMED_OUT_EXECUTING_TASK_MESSAGE); + taskHandlersMap.get(taskExecutionContext).abort(); } } } @@ -203,8 +196,9 @@ private void submit(Task task) { sendTaskStatusUpdate(task, RUNNING, null); return taskHandler.execute(); }); - taskHandlersMap.put(task, taskHandler); - taskFuturesMap.put(task, taskResultFuture); + final TaskExecutionContext taskExecutionContext = new TaskExecutionContext(task, task.getRetryCount()); + taskHandlersMap.put(taskExecutionContext, taskHandler); + taskFuturesMap.put(taskExecutionContext, taskResultFuture); } private void sendTaskStatusUpdate(TaskId taskId, Status status, String statusMessage) { @@ -244,8 +238,9 @@ public void stop() { private class TaskCompletionChecker implements Runnable { @Override public void run() { - final ArrayList completedTasks = new ArrayList<>(); - taskFuturesMap.forEach((task, future) -> { + final ArrayList completedTasks = new ArrayList<>(); + taskFuturesMap.forEach((taskExecutionContext, future) -> { + final Task task = taskExecutionContext.getTask(); logger.debug("Checking task {} for completion", task.getIdentity()); if (future.isDone()) { try { @@ -267,16 +262,53 @@ public void run() { synchronized (task.getType()) { taskTypeToRunningTasksCount.put(task.getType(), taskTypeToRunningTasksCount.get(task.getType()) - 1); } - completedTasks.add(task); + completedTasks.add(taskExecutionContext); } } }); if (!completedTasks.isEmpty()) { - logger.debug("Tasks {} completed execution", completedTasks.stream().map(Task::getIdentity) + logger.debug("Tasks {} completed execution", completedTasks.stream().map(t -> t.getTask().getIdentity()) .collect(Collectors.toList())); } completedTasks.forEach(taskFuturesMap::remove); completedTasks.forEach(taskHandlersMap::remove); } } + + /** + * We use this wrapper class to uniquely identify the different instance of task sent for execution on retry. + *

+ * Same task (having same TaskId) will be sent for execution in case of retry. + */ + private class TaskExecutionContext { + private final Task task; + private final int executionId; + + private TaskExecutionContext(Task task, int executionId) { + this.task = task; + this.executionId = executionId; + } + + public Task getTask() { + return task; + } + + public int getExecutionId() { + return executionId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TaskExecutionContext)) return false; + TaskExecutionContext taskExecutionContext = (TaskExecutionContext) o; + return executionId == taskExecutionContext.executionId && + Objects.equals(task, taskExecutionContext.task); + } + + @Override + public int hashCode() { + return Objects.hash(task, executionId); + } + } } diff --git a/pom.xml b/pom.xml index 2f6ac72..9e40dd5 100755 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ kronos kronos pom - 3.0.0-RC1 + 3.0.0-RC2 1.8 diff --git a/scheduler/pom.xml b/scheduler/pom.xml index 42b6849..e298147 100755 --- a/scheduler/pom.xml +++ b/scheduler/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0-RC1 + 3.0.0-RC2 4.0.0 diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java index 0d758b3..748dd2c 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/JobService.java @@ -40,6 +40,8 @@ import java.util.Set; import java.util.UUID; +import static com.cognitree.kronos.model.Task.Status.SCHEDULED; +import static com.cognitree.kronos.scheduler.ValidationError.CANNOT_ABORT_JOB_WITH_SCHEDULED_TASK; import static com.cognitree.kronos.scheduler.ValidationError.JOB_NOT_FOUND; import static com.cognitree.kronos.scheduler.ValidationError.NAMESPACE_NOT_FOUND; import static com.cognitree.kronos.scheduler.ValidationError.WORKFLOW_NOT_FOUND; @@ -308,7 +310,7 @@ public void abortJob(JobId jobId) throws ServiceException, ValidationException { try { job = jobStore.load(jobId); } catch (StoreException e) { - logger.error("No job found with id {}", jobId, e); + logger.error("Error retrieving job from store with id {}", jobId, e); throw new ServiceException(e.getMessage(), e.getCause()); } if (job == null) { @@ -319,6 +321,9 @@ public void abortJob(JobId jobId) throws ServiceException, ValidationException { } final List tasks = TaskService.getService().get(jobId.getNamespace(), jobId.getId(), job.getWorkflow()); + if (tasks.stream().anyMatch(task -> task.getStatus() == SCHEDULED)) { + throw CANNOT_ABORT_JOB_WITH_SCHEDULED_TASK.createException(); + } for (Task task : tasks) { TaskService.getService().abortTask(task); } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java index 3686681..29e4a4c 100755 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskSchedulerService.java @@ -55,6 +55,7 @@ import static com.cognitree.kronos.model.Task.Status.FAILED; import static com.cognitree.kronos.model.Task.Status.SCHEDULED; import static com.cognitree.kronos.model.Task.Status.SKIPPED; +import static com.cognitree.kronos.model.Task.Status.TIMED_OUT; import static com.cognitree.kronos.model.Task.Status.UP_FOR_RETRY; import static com.cognitree.kronos.model.Task.Status.WAITING; import static com.cognitree.kronos.queue.QueueService.SCHEDULER_QUEUE; @@ -142,16 +143,14 @@ public void start() throws Exception { public void abort(Task task) throws ServiceException { logger.info("Received request to abort task {}", task.getIdentity()); - if (task.getStatus().equals(CREATED) || task.getStatus().equals(WAITING)) { - // task is not yet scheduled mark the task as ABORTED - updateStatus(task.getIdentity(), ABORTED, Messages.TASK_ABORTED_MESSAGE); - return; + if (!task.getStatus().equals(CREATED) && !task.getStatus().equals(WAITING)) { + // sends control message only when the task is picked by executor + final ControlMessage controlMessage = new ControlMessage(); + controlMessage.setTask(task); + controlMessage.setAction(Action.ABORT); + QueueService.getService(SCHEDULER_QUEUE).send(controlMessage); } - - final ControlMessage controlMessage = new ControlMessage(); - controlMessage.setTask(task); - controlMessage.setAction(Action.ABORT); - QueueService.getService(SCHEDULER_QUEUE).send(controlMessage); + updateStatus(task.getIdentity(), ABORTED, Messages.TASK_ABORTED_MESSAGE); } private void reInitTaskProvider() throws ServiceException, ValidationException { @@ -266,7 +265,7 @@ private void updateStatus(TaskId taskId, Status status, String statusMessage, try { boolean statusUpdated = TaskService.getService().updateStatus(task, status, statusMessage, context); if (statusUpdated) { - handleTaskStatusChange(task, status); + handleTaskStatusChange(task); } } catch (ServiceException e) { logger.error("Error updating status of task {} to {} with status message {}", @@ -274,43 +273,42 @@ private void updateStatus(TaskId taskId, Status status, String statusMessage, } } - private void handleTaskStatusChange(Task task, Status status) { - switch (status) { + private void handleTaskStatusChange(Task task) { + switch (task.getStatus()) { case CREATED: break; - case UP_FOR_RETRY: - case WAITING: - scheduleReadyTasks(); - break; case SCHEDULED: break; case RUNNING: createTimeoutTask(task); break; case SKIPPED: + case TIMED_OUT: case FAILED: case ABORTED: - markDependentTasksAsSkipped(task, status); + markDependentTasksAsSkipped(task); // do not break case SUCCESSFUL: + case UP_FOR_RETRY: final ScheduledFuture taskTimeoutFuture = taskTimeoutHandlersMap.remove(task.getIdentity()); if (taskTimeoutFuture != null) { taskTimeoutFuture.cancel(false); } // If the task is finished (reached terminal state), proceed to schedule the next set of tasks + case WAITING: scheduleReadyTasks(); break; } } - private void markDependentTasksAsSkipped(Task task, Status parentStatus) { + private void markDependentTasksAsSkipped(Task task) { for (Task dependentTask : taskProvider.getDependentTasks(task)) { if (dependentTask.getStatus().isFinal()) { logger.debug("dependent task is already in its final state {}, ignore updating task status to SKIPPED", dependentTask.getStatus()); continue; } - switch (parentStatus) { + switch (task.getStatus()) { case FAILED: updateStatus(dependentTask.getIdentity(), SKIPPED, FAILED_DEPENDEE_TASK_MESSAGE); break; @@ -445,8 +443,7 @@ private TimeoutTask(Task task) { @Override public void run() { - logger.info("Task {} has timed out, marking task as aborted", task.getIdentity()); - updateStatus(task.getIdentity(), ABORTED, TIMED_OUT_EXECUTING_TASK_MESSAGE); + logger.info("Task {} has timed out", task.getIdentity()); try { final ControlMessage controlMessage = new ControlMessage(); controlMessage.setTask(task); @@ -455,6 +452,7 @@ public void run() { } catch (ServiceException e) { logger.error("Error sending control message to time out task {}", task.getIdentity(), e); } + updateStatus(task.getIdentity(), TIMED_OUT, TIMED_OUT_EXECUTING_TASK_MESSAGE); } } } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java index 91593a6..ac2ebfa 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/TaskService.java @@ -53,8 +53,10 @@ import static com.cognitree.kronos.model.Task.Status.RUNNING; import static com.cognitree.kronos.model.Task.Status.SCHEDULED; import static com.cognitree.kronos.model.Task.Status.SUCCESSFUL; +import static com.cognitree.kronos.model.Task.Status.TIMED_OUT; import static com.cognitree.kronos.model.Task.Status.UP_FOR_RETRY; import static com.cognitree.kronos.model.Task.Status.WAITING; +import static com.cognitree.kronos.scheduler.ValidationError.CANNOT_ABORT_TASK_IN_SCHEDULED_STATE; import static com.cognitree.kronos.scheduler.ValidationError.JOB_NOT_FOUND; import static com.cognitree.kronos.scheduler.ValidationError.NAMESPACE_NOT_FOUND; import static com.cognitree.kronos.scheduler.ValidationError.TASK_NOT_FOUND; @@ -231,7 +233,7 @@ public void abortTask(TaskId taskId) throws ServiceException, ValidationExceptio try { task = taskStore.load(taskId); } catch (StoreException e) { - logger.error("No task found with id {}", taskId, e); + logger.error("Error retrieving task from store with id {}", taskId, e); throw new ServiceException(e.getMessage(), e.getCause()); } if (task == null) { @@ -242,6 +244,9 @@ public void abortTask(TaskId taskId) throws ServiceException, ValidationExceptio logger.warn("Task {} is already in its final state {}", task.getIdentity(), task.getStatus()); return; } + if (task.getStatus() == SCHEDULED) { + throw CANNOT_ABORT_TASK_IN_SCHEDULED_STATE.createException(); + } TaskSchedulerService.getService().abort(task); } @@ -277,7 +282,7 @@ boolean updateStatus(Task task, Status status, String statusMessage, Map retryPolicyOpt = task.getPolicies().stream().filter(t -> t.getType() == Policy.Type.retry).findFirst(); + Optional retryPolicyOpt = task.getPolicies().stream() + .filter(t -> t.getType() == Policy.Type.retry).findFirst(); if (retryPolicyOpt.isPresent()) { - int maxRetryCount = ((RetryPolicy) retryPolicyOpt.get()).getMaxRetryCount(); - return maxRetryCount > task.getRetryCount(); + RetryPolicy retryPolicy = (RetryPolicy) retryPolicyOpt.get(); + if ((desiredStatus == FAILED && retryPolicy.isRetryOnFailure()) + || (desiredStatus == TIMED_OUT && retryPolicy.isRetryOnTimeout())) { + int maxRetryCount = retryPolicy.getMaxRetryCount(); + return maxRetryCount > task.getRetryCount(); + } + return false; } return false; } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/ValidationError.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/ValidationError.java index 12a8001..682359e 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/ValidationError.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/ValidationError.java @@ -33,8 +33,10 @@ public enum ValidationError { WORKFLOW_TRIGGER_ALREADY_EXISTS(3003, "workflow_trigger_already_exists", 409), JOB_NOT_FOUND(4001, "job_not_found", 404), + CANNOT_ABORT_JOB_WITH_SCHEDULED_TASK(4002, "cannot_abort_job_with_scheduled_task", 400), - TASK_NOT_FOUND(5001, "task_not_found", 404); + TASK_NOT_FOUND(5001, "task_not_found", 404), + CANNOT_ABORT_TASK_IN_SCHEDULED_STATE(5002, "cannot_abort_task_in_scheduled_state", 400); private int errorCode; private String errorMsg; diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/StoreService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/StoreService.java index f8732b7..7dba27d 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/StoreService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/StoreService.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; /** - * * There must be only one implementation of this service registered with the {@link ServiceProvider} by calling * {@link ServiceProvider#registerService(Service)}. */ @@ -41,9 +40,23 @@ public final String getName() { } public abstract NamespaceStore getNamespaceStore(); + public abstract WorkflowStore getWorkflowStore(); + public abstract WorkflowTriggerStore getWorkflowTriggerStore(); + public abstract JobStore getJobStore(); + public abstract TaskStore getTaskStore(); + public abstract org.quartz.spi.JobStore getQuartzJobStore(); + + /** + * return true is store is persistent, false if the store is in memory store + * + * @return + */ + public boolean isPersistent() { + return true; + } } diff --git a/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java b/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java index 00c38e9..f310b6f 100644 --- a/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java +++ b/scheduler/src/main/java/com/cognitree/kronos/scheduler/store/impl/RAMStoreService.java @@ -87,6 +87,11 @@ public org.quartz.spi.JobStore getQuartzJobStore() { return quartzJobStore; } + @Override + public boolean isPersistent() { + return false; + } + @Override public void stop() { logger.info("Stopping RAM store service"); diff --git a/scheduler/src/main/resources/validation-errors.properties b/scheduler/src/main/resources/validation-errors.properties index 04eb6ba..c364cda 100644 --- a/scheduler/src/main/resources/validation-errors.properties +++ b/scheduler/src/main/resources/validation-errors.properties @@ -26,4 +26,6 @@ workflow_trigger_not_found=No workflow trigger found with name {0} for workflow invalid_workflow_trigger=Workflow trigger is not valid. Reason: {0} workflow_trigger_already_exists=Workflow trigger already exists with name {0} for workflow {1} under namespace {2} job_not_found=No job found with id {0} for workflow {1} under namespace {2} -task_not_found=No task found with name {0} for job with id {1} for workflow {2} under namespace {3} \ No newline at end of file +cannot_abort_job_with_scheduled_task=Job with task in scheduled state cannot be aborted +task_not_found=No task found with name {0} for job with id {1} for workflow {2} under namespace {3} +cannot_abort_task_in_scheduled_state=Task in scheduled state cannot be aborted \ No newline at end of file diff --git a/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java b/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java index 923cc52..b436148 100644 --- a/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java +++ b/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java @@ -17,8 +17,11 @@ package com.cognitree.kronos; +import com.cognitree.kronos.model.Task; import com.cognitree.kronos.scheduler.JobService; import com.cognitree.kronos.scheduler.NamespaceService; +import com.cognitree.kronos.scheduler.TaskService; +import com.cognitree.kronos.scheduler.ValidationException; import com.cognitree.kronos.scheduler.WorkflowService; import com.cognitree.kronos.scheduler.WorkflowTriggerService; import com.cognitree.kronos.scheduler.model.CronSchedule; @@ -29,7 +32,6 @@ import com.cognitree.kronos.scheduler.model.events.ConfigUpdate; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.junit.Assert; import org.quartz.CronExpression; import org.quartz.Scheduler; import org.quartz.TriggerKey; @@ -43,6 +45,8 @@ import java.util.Optional; import java.util.UUID; +import static com.cognitree.kronos.model.Task.Status.RUNNING; + public class TestUtil { private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); @@ -138,13 +142,21 @@ public static void waitForTriggerToComplete(WorkflowTrigger workflowTrigger, Sch // wait for both the job to be triggered TriggerKey workflowOneTriggerKey = new TriggerKey(workflowTrigger.getName(), workflowTrigger.getWorkflow() + ":" + workflowTrigger.getNamespace()); - int maxCount = 50; + int maxCount = 120; while (maxCount > 0 && scheduler.checkExists(workflowOneTriggerKey)) { - Thread.sleep(100); + Thread.sleep(1000); maxCount--; } - if (maxCount < 0) { - Assert.fail("failed while waiting for trigger to complete"); + } + + public static void waitForTaskToBeRunning(Task task) throws ServiceException, ValidationException, InterruptedException { + int maxCount = 120; + while (maxCount > 0) { + if (TaskService.getService().get(task).getStatus() == RUNNING) { + break; + } + Thread.sleep(1000); + maxCount--; } } } From 3d4bf43c1e9c2ad82507467d8ddd16e2983bb299 Mon Sep 17 00:00:00 2001 From: Srinath Chandrashekhar Date: Fri, 8 Nov 2019 15:57:44 +0530 Subject: [PATCH 04/12] Updating version to release 3.0.0 --- api/pom.xml | 2 +- app/pom.xml | 2 +- common/pom.xml | 2 +- executor/pom.xml | 2 +- pom.xml | 2 +- scheduler/pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index fef9b2f..aaf7c00 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,7 +20,7 @@ kronos com.cognitree - 3.0.0-RC2 + 3.0.0 4.0.0 diff --git a/app/pom.xml b/app/pom.xml index d54f312..5a09696 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0-RC2 + 3.0.0 4.0.0 diff --git a/common/pom.xml b/common/pom.xml index 0b0afa9..809471e 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -24,7 +24,7 @@ kronos com.cognitree - 3.0.0-RC2 + 3.0.0 com.cognitree.kronos diff --git a/executor/pom.xml b/executor/pom.xml index 0221b77..1bfe05c 100755 --- a/executor/pom.xml +++ b/executor/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0-RC2 + 3.0.0 4.0.0 diff --git a/pom.xml b/pom.xml index 9e40dd5..ac31796 100755 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ kronos kronos pom - 3.0.0-RC2 + 3.0.0 1.8 diff --git a/scheduler/pom.xml b/scheduler/pom.xml index e298147..a80a3b2 100755 --- a/scheduler/pom.xml +++ b/scheduler/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0-RC2 + 3.0.0 4.0.0 From 8b562683971fb868fe8c9cdca45c726b00baf53e Mon Sep 17 00:00:00 2001 From: Srinath Chandrashekhar Date: Fri, 8 Nov 2019 16:03:10 +0530 Subject: [PATCH 05/12] Update java version in travis.ci to openjdk8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c4f11b7..b179f30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java jdk: - - oraclejdk8 \ No newline at end of file + - openjdk8 From 8ba1e1e058600a9d522b0264e37aaeb5fec8350b Mon Sep 17 00:00:00 2001 From: Srinath Chandrashekhar Date: Fri, 8 Nov 2019 16:14:08 +0530 Subject: [PATCH 06/12] Update version in Dockerfile to 3.0.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b6be2b4..639c488 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM openjdk:8u151-jre-alpine -ENV KRONOS_VERSION 3.0.0-RC2 +ENV KRONOS_VERSION 3.0.0 ENV KRONOS_HOME /home/kronos-${KRONOS_VERSION} ENV MODE all From 2722e9e66dbef4583d3ef0f610cf77408aa9c0c8 Mon Sep 17 00:00:00 2001 From: manikvenkat4 Date: Tue, 12 Nov 2019 11:09:27 +0530 Subject: [PATCH 07/12] Converted version to 3.0.1 --- Dockerfile | 2 +- api/pom.xml | 2 +- app/pom.xml | 2 +- common/pom.xml | 2 +- executor/pom.xml | 2 +- pom.xml | 2 +- scheduler/pom.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 639c488..c3b0f2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM openjdk:8u151-jre-alpine -ENV KRONOS_VERSION 3.0.0 +ENV KRONOS_VERSION 3.0.1 ENV KRONOS_HOME /home/kronos-${KRONOS_VERSION} ENV MODE all diff --git a/api/pom.xml b/api/pom.xml index aaf7c00..1ec0a8d 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,7 +20,7 @@ kronos com.cognitree - 3.0.0 + 3.0.1 4.0.0 diff --git a/app/pom.xml b/app/pom.xml index 5a09696..1dd3d69 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0 + 3.0.1 4.0.0 diff --git a/common/pom.xml b/common/pom.xml index 809471e..e9574be 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -24,7 +24,7 @@ kronos com.cognitree - 3.0.0 + 3.0.1 com.cognitree.kronos diff --git a/executor/pom.xml b/executor/pom.xml index 1bfe05c..0929940 100755 --- a/executor/pom.xml +++ b/executor/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0 + 3.0.1 4.0.0 diff --git a/pom.xml b/pom.xml index ac31796..3d9f465 100755 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ kronos kronos pom - 3.0.0 + 3.0.1 1.8 diff --git a/scheduler/pom.xml b/scheduler/pom.xml index a80a3b2..7f82ff3 100755 --- a/scheduler/pom.xml +++ b/scheduler/pom.xml @@ -22,7 +22,7 @@ kronos com.cognitree - 3.0.0 + 3.0.1 4.0.0 From 13563636f0e6113212fd7c12299a1191c7360e21 Mon Sep 17 00:00:00 2001 From: Arpit Sinha <37374354+rptsinha00@users.noreply.github.com> Date: Mon, 11 Nov 2019 14:12:02 +0530 Subject: [PATCH 08/12] Log4j2 migration has been completed (#54) * added log4j2 configuration file * added comments in the log4j2 configuration file. --- app/src/main/conf/log4j.properties | 20 ------------------- app/src/main/conf/log4j2.properties | 16 +++++++++++++++ pom.xml | 13 ++++++------ scheduler/src/test/resources/log4j.properties | 20 ------------------- .../src/test/resources/log4j2.properties | 13 ++++++++++++ 5 files changed, 36 insertions(+), 46 deletions(-) delete mode 100755 app/src/main/conf/log4j.properties create mode 100644 app/src/main/conf/log4j2.properties delete mode 100755 scheduler/src/test/resources/log4j.properties create mode 100644 scheduler/src/test/resources/log4j2.properties diff --git a/app/src/main/conf/log4j.properties b/app/src/main/conf/log4j.properties deleted file mode 100755 index 721c81a..0000000 --- a/app/src/main/conf/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -log4j.rootLogger=INFO, STDOUT -log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender -log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout -log4j.appender.STDOUT.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p [%t] (%F:%L) - %m%n diff --git a/app/src/main/conf/log4j2.properties b/app/src/main/conf/log4j2.properties new file mode 100644 index 0000000..55be63c --- /dev/null +++ b/app/src/main/conf/log4j2.properties @@ -0,0 +1,16 @@ +name = Log4j2Config +#The level of internal Log4j events that should be logged to the console +status = error +#The minimum amount of time, in seconds, that must elapse before the file configuration is checked for changes. +#Any changes to the configuration file during runtime will come into effect. +monitorInterval = 10 + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %5p [%t] (%F:%L) - %m%n +appender.console.immediateFlush=true + +rootLogger.level = INFO +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = STDOUT \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3d9f465..a68a51e 100755 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 1.7.12 2.9.9 4.12 + 2.11.1 @@ -65,18 +66,18 @@ ${slf4j.version} compile - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - runtime - junit junit ${junit.version} test + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j2.version} + runtime + diff --git a/scheduler/src/test/resources/log4j.properties b/scheduler/src/test/resources/log4j.properties deleted file mode 100755 index 9140ad7..0000000 --- a/scheduler/src/test/resources/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -log4j.rootLogger=INFO, STDOUT -log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender -log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout -log4j.appender.STDOUT.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/scheduler/src/test/resources/log4j2.properties b/scheduler/src/test/resources/log4j2.properties new file mode 100644 index 0000000..cf38e8a --- /dev/null +++ b/scheduler/src/test/resources/log4j2.properties @@ -0,0 +1,13 @@ +name = Log4j2Config +status = error +monitorInterval = 10 + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %5p [%t] (%F:%L) - %m%n +appender.console.immediateFlush=true + +rootLogger.level = INFO +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = STDOUT \ No newline at end of file From 8006dad1d124c965c18d849909425b7914dabe6d Mon Sep 17 00:00:00 2001 From: manikvenkat4 Date: Wed, 13 Nov 2019 15:51:21 +0530 Subject: [PATCH 09/12] added mvn source jar plugin, and fixed catching null pointer exception --- app/pom.xml | 14 ++++++++++++++ .../kronos/executor/TaskExecutionService.java | 3 +-- .../test/java/com.cognitree.kronos/TestUtil.java | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/pom.xml b/app/pom.xml index 1dd3d69..10be867 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -32,6 +32,7 @@ 9.4.18.v20190429 1.4 + 3.2.0 @@ -124,6 +125,19 @@ + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin} + + + attach-sources + + jar + + + + diff --git a/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java b/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java index 1c582ca..86f6e58 100755 --- a/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java +++ b/executor/src/main/java/com/cognitree/kronos/executor/TaskExecutionService.java @@ -184,8 +184,7 @@ private void submit(Task task) { .getConstructor() .newInstance(); taskHandler.init(task, taskHandlerConfig.getConfig()); - } catch (InstantiationException | InvocationTargetException | NoSuchMethodException - | IllegalAccessException | ClassNotFoundException e) { + } catch (Exception e) { logger.error("Error initializing handler for task {}", task, e); sendTaskStatusUpdate(task, FAILED, MISSING_TASK_HANDLER_MESSAGE); return; diff --git a/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java b/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java index b436148..0719fa8 100644 --- a/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java +++ b/scheduler/src/test/java/com.cognitree.kronos/TestUtil.java @@ -123,7 +123,7 @@ public static WorkflowTrigger scheduleWorkflow(String workflowTemplate, Map 0) { final List jobs = JobService.getService().get(workflowTrigger.getNamespace(), workflowTrigger.getWorkflow(), workflowTrigger.getName(), 0, System.currentTimeMillis()); From 7e597dc620c9c32ef9dabece7c2dfbd2743cce1f Mon Sep 17 00:00:00 2001 From: manikvenkat4 Date: Wed, 13 Nov 2019 17:07:13 +0530 Subject: [PATCH 10/12] Broken swagger ui got fixed by changing the verion of swagger ui to 3.24.2 --- app/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pom.xml b/app/pom.xml index 10be867..4001788 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -118,7 +118,7 @@ wget - https://github.com/swagger-api/swagger-ui/archive/v2.1.1.tar.gz + https://github.com/swagger-api/swagger-ui/archive/v3.24.2.tar.gz true ${project.build.directory} From a90d1459e0e5f7f4e48807a91fa3e5ca017ed002 Mon Sep 17 00:00:00 2001 From: manikvenkat4 Date: Wed, 13 Nov 2019 17:25:52 +0530 Subject: [PATCH 11/12] swagger version changed in assembly.xml --- app/src/main/assembly/assembly.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assembly/assembly.xml b/app/src/main/assembly/assembly.xml index 1cf9124..c0f2969 100755 --- a/app/src/main/assembly/assembly.xml +++ b/app/src/main/assembly/assembly.xml @@ -50,7 +50,7 @@ webapp - ${project.build.directory}/swagger-ui-2.1.1/dist + ${project.build.directory}/swagger-ui-3.24.2/dist webapp index.html From bad1b1dd00ff817b049c9d8447d14d0388de312e Mon Sep 17 00:00:00 2001 From: manikvenkat4 Date: Fri, 15 Nov 2019 13:03:06 +0530 Subject: [PATCH 12/12] updated the swagger json file --- app/src/main/webapp/swagger.json | 1610 +++++++++++++++++++++++++++++- 1 file changed, 1609 insertions(+), 1 deletion(-) diff --git a/app/src/main/webapp/swagger.json b/app/src/main/webapp/swagger.json index 584a273..6808a4e 100644 --- a/app/src/main/webapp/swagger.json +++ b/app/src/main/webapp/swagger.json @@ -1 +1,1609 @@ -{"swagger":"2.0","info":{"version":"Swagger Server","title":""},"tags":[{"name":"jobs"},{"name":"workflows"},{"name":"namespaces"},{"name":"workflow triggers"},{"name":"workflow statistics"}],"schemes":["http"],"paths":{"/jobs":{"get":{"tags":["jobs"],"summary":"Get all running or executed jobs","description":"query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp","operationId":"getAllJobs","produces":["application/json"],"parameters":[{"name":"status","in":"query","description":"job status","required":false,"type":"array","items":{"type":"string","enum":["CREATED","RUNNING","SUCCESSFUL","FAILED"]},"collectionFormat":"csv"},{"name":"from","in":"query","description":"Start time of the range","required":false,"type":"integer","format":"int64"},{"name":"to","in":"query","description":"End time of the range","required":false,"type":"integer","format":"int64"},{"name":"date_range","in":"query","description":"Number of days to fetch jobs from today","required":false,"type":"integer","default":"10","format":"int32"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Job"}}}}}},"/namespaces":{"get":{"tags":["namespaces"],"summary":"Get all namespaces","description":"","operationId":"getAllNamespaces","produces":["application/json"],"parameters":[],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Namespace"}}}}},"post":{"tags":["namespaces"],"summary":"Add new namespace","description":"","operationId":"addNamespace","produces":["application/json"],"parameters":[{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/Namespace"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Namespace"}},"409":{"description":"Namespace already exists"}}}},"/namespaces/{name}":{"get":{"tags":["namespaces"],"summary":"Get namespace by name","description":"","operationId":"getNamespace","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"namespace name","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Namespace"}},"404":{"description":"Namespace not found"}}},"put":{"tags":["namespaces"],"summary":"Update namespace","description":"","operationId":"updateNamespace","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"namespace name","required":true,"type":"string"},{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/Namespace"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Namespace"}},"404":{"description":"Namespace not found"}}},"delete":{"tags":["namespaces"],"summary":"Delete namespace (not implemented)","description":"","operationId":"deleteNamespace","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"namespace name","required":true,"type":"string"}],"responses":{"501":{"description":"not implemented"}}}},"/statistics/workflows":{"get":{"tags":["workflow statistics"],"summary":"Get statistics across workflows","description":"query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp","operationId":"getAllWorkflowStatistics","produces":["application/json"],"parameters":[{"name":"from","in":"query","description":"Start time of the range","required":false,"type":"integer","format":"int64"},{"name":"to","in":"query","description":"End time of the range","required":false,"type":"integer","format":"int64"},{"name":"date_range","in":"query","description":"Number of days to fetch jobs from today","required":false,"type":"integer","default":"10","format":"int32"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/WorkflowStatistics"}}}}},"/statistics/workflows/{name}":{"get":{"tags":["workflow statistics"],"summary":"Get statistics for workflow by name","description":"query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp","operationId":"getWorkflowStatistics","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"from","in":"query","description":"Start time of the range","required":false,"type":"integer","format":"int64"},{"name":"to","in":"query","description":"End time of the range","required":false,"type":"integer","format":"int64"},{"name":"date_range","in":"query","description":"Number of days to fetch jobs from today","required":false,"type":"integer","default":"10","format":"int32"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/WorkflowStatistics"}},"404":{"description":"Workflow not found"}}}},"/workflows":{"get":{"tags":["workflows"],"summary":"Get all workflows","description":"","operationId":"getAllWorkflows","produces":["application/json"],"parameters":[{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Workflow"}}}}},"post":{"tags":["workflows"],"summary":"Add new workflow","description":"","operationId":"addWorkflow","produces":["application/json"],"parameters":[{"name":"namespace","in":"header","required":false,"type":"string"},{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/Workflow"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Workflow"}},"409":{"description":"Workflow already exists"}}}},"/workflows/{name}":{"get":{"tags":["workflows"],"summary":"Get workflow by name","description":"","operationId":"getWorkflow","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Workflow"}},"404":{"description":"Workflow not found"}}},"put":{"tags":["workflows"],"summary":"Update workflow by name","description":"","operationId":"updateWorkflow","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"},{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/Workflow"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Workflow"}},"404":{"description":"Workflow not found"}}},"delete":{"tags":["workflows"],"summary":"Delete workflow by name","description":"","operationId":"deleteWorkflow","produces":["application/json"],"parameters":[{"name":"name","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"404":{"description":"Workflow not found"}}}},"/workflows/{workflow}/jobs":{"get":{"tags":["jobs"],"summary":"Get all running or executed jobs for a workflow","description":"query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp","operationId":"getAllJobs","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"trigger","in":"query","description":"workflow trigger name","required":false,"type":"string"},{"name":"status","in":"query","description":"job status","required":false,"type":"array","items":{"type":"string","enum":["CREATED","RUNNING","SUCCESSFUL","FAILED"]},"collectionFormat":"csv"},{"name":"from","in":"query","description":"Start time of the range","required":false,"type":"integer","format":"int64"},{"name":"to","in":"query","description":"End time of the range","required":false,"type":"integer","format":"int64"},{"name":"date_range","in":"query","description":"Number of days to fetch jobs from today","required":false,"type":"integer","default":"10","format":"int32"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Job"}}}}}},"/workflows/{workflow}/jobs/{id}":{"get":{"tags":["jobs"],"summary":"Get job by id for a workflow","description":"","operationId":"getJob","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"id","in":"path","description":"job id","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/JobResponse"}},"404":{"description":"Job not found"}}}},"/workflows/{workflow}/triggers":{"get":{"tags":["workflow triggers"],"summary":"Get all workflow triggers","description":"","operationId":"getAllWorkflowTriggers","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","required":true,"type":"string"},{"name":"enable","in":"query","required":false,"type":"boolean"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/WorkflowTrigger"}}}}},"post":{"tags":["workflow triggers"],"summary":"Create workflow trigger","description":"","operationId":"createWorkflowTrigger","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"},{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/WorkflowTrigger"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/WorkflowTrigger"}},"404":{"description":"Workflow trigger not found"}}},"put":{"tags":["workflow triggers"],"summary":"Enable/ Disable all triggers for workflow by name","description":"","operationId":"updateWorkflow","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"enable","in":"query","description":"enable/ disable all workflow triggers","required":true,"type":"boolean","default":"true"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"list of affected triggers"},"404":{"description":"Workflow not found"}}}},"/workflows/{workflow}/triggers/{name}":{"get":{"tags":["workflow triggers"],"summary":"Get workflow trigger by name","description":"","operationId":"getWorkflowTrigger","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"name","in":"path","description":"workflow trigger name","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/WorkflowTrigger"}},"404":{"description":"Workflow trigger not found"}}},"put":{"tags":["workflow triggers"],"summary":"Enable/Disable workflow trigger","description":"","operationId":"updateWorkflowTrigger","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"name","in":"path","description":"workflow trigger name","required":true,"type":"string"},{"name":"enable","in":"query","description":"enable/ disable the workflow trigger","required":true,"type":"boolean","default":"true"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"200":{"description":"Affected workflow trigger. If empty it means workflow trigger was already in resultant state"},"404":{"description":"Workflow trigger not found"}}},"delete":{"tags":["workflow triggers"],"summary":"Delete workflow trigger by name","description":"","operationId":"deleteWorkflowTrigger","produces":["application/json"],"parameters":[{"name":"workflow","in":"path","description":"workflow name","required":true,"type":"string"},{"name":"name","in":"path","description":"workflow trigger name","required":true,"type":"string"},{"name":"namespace","in":"header","required":false,"type":"string"}],"responses":{"404":{"description":"Workflow trigger not found"}}}}},"definitions":{"DailyTimeIntervalSchedule":{"allOf":[{"$ref":"#/definitions/Schedule"},{"type":"object","properties":{"type":{"type":"string","enum":["cron","simple","fixed","daily_time","calendar"]},"misfireInstruction":{"type":"integer","format":"int32"},"repeatInterval":{"type":"integer","format":"int32"},"repeatIntervalUnit":{"type":"string","enum":["MILLISECOND","SECOND","MINUTE","HOUR","DAY","WEEK","MONTH","YEAR"]},"repeatCount":{"type":"integer","format":"int32"},"startTimeOfDay":{"$ref":"#/definitions/TimeOfDay"},"endTimeOfDay":{"$ref":"#/definitions/TimeOfDay"},"timezone":{"type":"string"},"daysOfWeek":{"type":"array","uniqueItems":true,"items":{"type":"integer","format":"int32"}}}}]},"TimeOfDay":{"type":"object","properties":{"hour":{"type":"integer","format":"int32"},"minute":{"type":"integer","format":"int32"},"second":{"type":"integer","format":"int32"}}},"Task":{"type":"object","properties":{"namespace":{"type":"string"},"name":{"type":"string"},"job":{"type":"string"},"workflow":{"type":"string"},"type":{"type":"string"},"maxExecutionTimeInMs":{"type":"integer","format":"int64"},"dependsOn":{"type":"array","items":{"type":"string"}},"properties":{"type":"object","additionalProperties":{"type":"object"}},"context":{"type":"object","additionalProperties":{"type":"object"}},"status":{"type":"string","enum":["CREATED","WAITING","SCHEDULED","SUBMITTED","RUNNING","SUCCESSFUL","SKIPPED","FAILED"]},"statusMessage":{"type":"string"},"createdAt":{"type":"integer","format":"int64"},"submittedAt":{"type":"integer","format":"int64"},"completedAt":{"type":"integer","format":"int64"}}},"JobResponse":{"type":"object","properties":{"namespace":{"type":"string"},"id":{"type":"string"},"workflow":{"type":"string"},"trigger":{"type":"string"},"status":{"type":"string","enum":["CREATED","RUNNING","SUCCESSFUL","FAILED"]},"createdAt":{"type":"integer","format":"int64"},"completedAt":{"type":"integer","format":"int64"},"tasks":{"type":"array","items":{"$ref":"#/definitions/Task"}}}},"Workflow":{"type":"object","properties":{"namespace":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"tasks":{"type":"array","items":{"$ref":"#/definitions/WorkflowTask"}},"emailOnFailure":{"type":"array","items":{"type":"string"}},"emailOnSuccess":{"type":"array","items":{"type":"string"}}}},"WorkflowTask":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"properties":{"type":"object","additionalProperties":{"type":"object"}},"maxExecutionTimeInMs":{"type":"integer","format":"int64"},"enabled":{"type":"boolean","default":false}}},"WorkflowTrigger":{"type":"object","properties":{"namespace":{"type":"string"},"name":{"type":"string"},"workflow":{"type":"string"},"startAt":{"type":"integer","format":"int64"},"schedule":{"$ref":"#/definitions/Schedule"},"endAt":{"type":"integer","format":"int64"},"enabled":{"type":"boolean","default":false}}},"WorkflowStatistics":{"type":"object","properties":{"jobs":{"$ref":"#/definitions/ExecutionCounters"},"tasks":{"$ref":"#/definitions/ExecutionCounters"},"from":{"type":"integer","format":"int64"},"to":{"type":"integer","format":"int64"}}},"Namespace":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"CronSchedule":{"allOf":[{"$ref":"#/definitions/Schedule"},{"type":"object","properties":{"type":{"type":"string","enum":["cron","simple","fixed","daily_time","calendar"]},"misfireInstruction":{"type":"integer","format":"int32"},"cronExpression":{"type":"string"},"timezone":{"type":"string"}}}]},"ExecutionCounters":{"type":"object","properties":{"total":{"type":"integer","format":"int32"},"active":{"type":"integer","format":"int32"},"successful":{"type":"integer","format":"int32"},"failed":{"type":"integer","format":"int32"}}},"FixedDelaySchedule":{"allOf":[{"$ref":"#/definitions/Schedule"},{"type":"object","properties":{"type":{"type":"string","enum":["cron","simple","fixed","daily_time","calendar"]},"misfireInstruction":{"type":"integer","format":"int32"},"interval":{"type":"integer","format":"int32"}}}]},"Schedule":{"type":"object","discriminator":"type","properties":{"type":{"type":"string","enum":["cron","simple","fixed","daily_time","calendar"]},"misfireInstruction":{"type":"integer","format":"int32"}}},"Job":{"type":"object","properties":{"namespace":{"type":"string"},"id":{"type":"string"},"workflow":{"type":"string"},"trigger":{"type":"string"},"status":{"type":"string","enum":["CREATED","RUNNING","SUCCESSFUL","FAILED"]},"createdAt":{"type":"integer","format":"int64"},"completedAt":{"type":"integer","format":"int64"}}},"SimpleSchedule":{"allOf":[{"$ref":"#/definitions/Schedule"},{"type":"object","properties":{"type":{"type":"string","enum":["cron","simple","fixed","daily_time","calendar"]},"misfireInstruction":{"type":"integer","format":"int32"},"repeatForever":{"type":"boolean","default":false},"repeatIntervalInMs":{"type":"integer","format":"int64"},"repeatCount":{"type":"integer","format":"int32"}}}]},"CalendarIntervalSchedule":{"allOf":[{"$ref":"#/definitions/Schedule"},{"type":"object","properties":{"type":{"type":"string","enum":["cron","simple","fixed","daily_time","calendar"]},"misfireInstruction":{"type":"integer","format":"int32"},"repeatInterval":{"type":"integer","format":"int32"},"repeatIntervalUnit":{"type":"string","enum":["MILLISECOND","SECOND","MINUTE","HOUR","DAY","WEEK","MONTH","YEAR"]},"timezone":{"type":"string"},"preserveHourOfDayAcrossDaylightSavings":{"type":"boolean","default":false},"skipDayIfHourDoesNotExist":{"type":"boolean","default":false}}}]}}} \ No newline at end of file +{ + "swagger": "2.0", + "info": { + "version": "Swagger Server", + "title": "" + }, + "tags": [ + { + "name": "jobs" + }, + { + "name": "workflows" + }, + { + "name": "namespaces" + }, + { + "name": "workflow triggers" + }, + { + "name": "workflow statistics" + }, + { + "name": "tasks" + } + ], + "schemes": [ + "http" + ], + "paths": { + "/jobs": { + "get": { + "tags": [ + "jobs" + ], + "summary": "Get all running or executed jobs", + "description": "query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp", + "operationId": "getAllJobs", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "job status", + "required": false, + "type": "array", + "items": { + "type": "string", + "enum": [ + "CREATED", + "RUNNING", + "SUCCESSFUL", + "FAILED" + ] + }, + "collectionFormat": "csv" + }, + { + "name": "from", + "in": "query", + "description": "Start time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "to", + "in": "query", + "description": "End time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "date_range", + "in": "query", + "description": "Number of days to fetch jobs from today", + "required": false, + "type": "integer", + "default": "10", + "format": "int32" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Job" + } + } + } + } + } + }, + "/namespaces": { + "get": { + "tags": [ + "namespaces" + ], + "summary": "Get all namespaces", + "description": "", + "operationId": "getAllNamespaces", + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Namespace" + } + } + } + } + }, + "post": { + "tags": [ + "namespaces" + ], + "summary": "Add new namespace", + "description": "", + "operationId": "addNamespace", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Namespace" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Namespace" + } + }, + "409": { + "description": "Namespace already exists" + } + } + } + }, + "/namespaces/{name}": { + "get": { + "tags": [ + "namespaces" + ], + "summary": "Get namespace by name", + "description": "", + "operationId": "getNamespace", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "namespace name", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Namespace" + } + }, + "404": { + "description": "Namespace not found" + } + } + }, + "put": { + "tags": [ + "namespaces" + ], + "summary": "Update namespace", + "description": "", + "operationId": "updateNamespace", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "namespace name", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Namespace" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Namespace" + } + }, + "404": { + "description": "Namespace not found" + } + } + }, + "delete": { + "tags": [ + "namespaces" + ], + "summary": "Delete namespace (not implemented)", + "description": "", + "operationId": "deleteNamespace", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "namespace name", + "required": true, + "type": "string" + } + ], + "responses": { + "501": { + "description": "not implemented" + } + } + } + }, + "/statistics/workflows": { + "get": { + "tags": [ + "workflow statistics" + ], + "summary": "Get statistics across workflows", + "description": "query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp", + "operationId": "getAllWorkflowStatistics", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "from", + "in": "query", + "description": "Start time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "to", + "in": "query", + "description": "End time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "date_range", + "in": "query", + "description": "Number of days to fetch jobs from today", + "required": false, + "type": "integer", + "default": "10", + "format": "int32" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/WorkflowStatistics" + } + } + } + } + }, + "/statistics/workflows/{name}": { + "get": { + "tags": [ + "workflow statistics" + ], + "summary": "Get statistics for workflow by name", + "description": "query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp", + "operationId": "getWorkflowStatistics", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "from", + "in": "query", + "description": "Start time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "to", + "in": "query", + "description": "End time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "date_range", + "in": "query", + "description": "Number of days to fetch jobs from today", + "required": false, + "type": "integer", + "default": "10", + "format": "int32" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/WorkflowStatistics" + } + }, + "404": { + "description": "Workflow not found" + } + } + } + }, + "/workflows": { + "get": { + "tags": [ + "workflows" + ], + "summary": "Get all workflows", + "description": "", + "operationId": "getAllWorkflows", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Workflow" + } + } + } + } + }, + "post": { + "tags": [ + "workflows" + ], + "summary": "Add new workflow", + "description": "", + "operationId": "addWorkflow", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Workflow" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Workflow" + } + }, + "409": { + "description": "Workflow already exists" + } + } + } + }, + "/workflows/{name}": { + "get": { + "tags": [ + "workflows" + ], + "summary": "Get workflow by name", + "description": "", + "operationId": "getWorkflow", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Workflow" + } + }, + "404": { + "description": "Workflow not found" + } + } + }, + "put": { + "tags": [ + "workflows" + ], + "summary": "Update workflow by name", + "description": "", + "operationId": "updateWorkflow", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Workflow" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Workflow" + } + }, + "404": { + "description": "Workflow not found" + } + } + }, + "delete": { + "tags": [ + "workflows" + ], + "summary": "Delete workflow by name", + "description": "", + "operationId": "deleteWorkflow", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "404": { + "description": "Workflow not found" + } + } + } + }, + "/workflows/{workflow}/jobs": { + "get": { + "tags": [ + "jobs" + ], + "summary": "Get all running or executed jobs for a workflow", + "description": "query param 'from' and 'to' takes precedence over 'date_range'. If 'from' is specified without 'to' it means get all jobs from 'from' timestamp till now.If 'to' is specified without 'from' it means get all jobs from beginning till 'from' timestamp", + "operationId": "getAllJobs", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "trigger", + "in": "query", + "description": "workflow trigger name", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "query", + "description": "job status", + "required": false, + "type": "array", + "items": { + "type": "string", + "enum": [ + "CREATED", + "RUNNING", + "SUCCESSFUL", + "FAILED" + ] + }, + "collectionFormat": "csv" + }, + { + "name": "from", + "in": "query", + "description": "Start time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "to", + "in": "query", + "description": "End time of the range", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "date_range", + "in": "query", + "description": "Number of days to fetch jobs from today", + "required": false, + "type": "integer", + "default": "10", + "format": "int32" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Job" + } + } + } + } + } + }, + "/workflows/{workflow}/jobs/{id}": { + "get": { + "tags": [ + "jobs" + ], + "summary": "Get job by id for a workflow", + "description": "", + "operationId": "getJob", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "path", + "description": "job id", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/JobResponse" + } + }, + "404": { + "description": "Job not found" + } + } + }, + "post": { + "tags": [ + "jobs" + ], + "summary": "Execute an action on a job. Supported actions - abort", + "description": "", + "operationId": "performAction", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "path", + "description": "job id", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "action", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "operation success" + }, + "400": { + "description": "invalid action" + } + } + } + }, + "/workflows/{workflow}/triggers": { + "get": { + "tags": [ + "workflow triggers" + ], + "summary": "Get all workflow triggers", + "description": "", + "operationId": "getAllWorkflowTriggers", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "enable", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/WorkflowTrigger" + } + } + } + } + }, + "post": { + "tags": [ + "workflow triggers" + ], + "summary": "Create workflow trigger", + "description": "", + "operationId": "createWorkflowTrigger", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/WorkflowTrigger" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/WorkflowTrigger" + } + }, + "404": { + "description": "Workflow trigger not found" + } + } + }, + "put": { + "tags": [ + "workflow triggers" + ], + "summary": "Enable/ Disable all triggers for workflow by name", + "description": "", + "operationId": "updateWorkflow", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "enable", + "in": "query", + "description": "enable/ disable all workflow triggers", + "required": true, + "type": "boolean", + "default": "true" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "list of affected triggers" + }, + "404": { + "description": "Workflow not found" + } + } + } + }, + "/workflows/{workflow}/jobs/{job}/tasks/{task}": { + "post": { + "tags": [ + "tasks" + ], + "summary": "Execute an action on a task. Supported actions - abort", + "description": "", + "operationId": "performAction", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflowname", + "required": true, + "type": "string" + }, + { + "name": "job", + "in": "path", + "description": "job id", + "required": true, + "type": "string" + }, + { + "name": "task", + "in": "path", + "description": "task name", + "type": "string", + "required": true + }, + { + "name": "action", + "in": "query", + "description": "defines what type of action to be performed on the task", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation" + }, + "400": { + "description": "invalid action" + } + } + } + }, + "/workflows/{workflow}/triggers/{name}": { + "get": { + "tags": [ + "workflow triggers" + ], + "summary": "Get workflow trigger by name", + "description": "", + "operationId": "getWorkflowTrigger", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "name", + "in": "path", + "description": "workflow trigger name", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/WorkflowTrigger" + } + }, + "404": { + "description": "Workflow trigger not found" + } + } + }, + "put": { + "tags": [ + "workflow triggers" + ], + "summary": "Enable/Disable workflow trigger", + "description": "", + "operationId": "updateWorkflowTrigger", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "name", + "in": "path", + "description": "workflow trigger name", + "required": true, + "type": "string" + }, + { + "name": "enable", + "in": "query", + "description": "enable/ disable the workflow trigger", + "required": true, + "type": "boolean", + "default": "true" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Affected workflow trigger. If empty it means workflow trigger was already in resultant state" + }, + "404": { + "description": "Workflow trigger not found" + } + } + }, + "delete": { + "tags": [ + "workflow triggers" + ], + "summary": "Delete workflow trigger by name", + "description": "", + "operationId": "deleteWorkflowTrigger", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "workflow", + "in": "path", + "description": "workflow name", + "required": true, + "type": "string" + }, + { + "name": "name", + "in": "path", + "description": "workflow trigger name", + "required": true, + "type": "string" + }, + { + "name": "namespace", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": { + "404": { + "description": "Workflow trigger not found" + } + } + } + } + }, + "definitions": { + "DailyTimeIntervalSchedule": { + "allOf": [ + { + "$ref": "#/definitions/Schedule" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "simple", + "fixed", + "daily_time", + "calendar" + ] + }, + "misfireInstruction": { + "type": "integer", + "format": "int32" + }, + "repeatInterval": { + "type": "integer", + "format": "int32" + }, + "repeatIntervalUnit": { + "type": "string", + "enum": [ + "MILLISECOND", + "SECOND", + "MINUTE", + "HOUR", + "DAY", + "WEEK", + "MONTH", + "YEAR" + ] + }, + "repeatCount": { + "type": "integer", + "format": "int32" + }, + "startTimeOfDay": { + "$ref": "#/definitions/TimeOfDay" + }, + "endTimeOfDay": { + "$ref": "#/definitions/TimeOfDay" + }, + "timezone": { + "type": "string" + }, + "daysOfWeek": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer", + "format": "int32" + } + } + } + } + ] + }, + "TimeOfDay": { + "type": "object", + "properties": { + "hour": { + "type": "integer", + "format": "int32" + }, + "minute": { + "type": "integer", + "format": "int32" + }, + "second": { + "type": "integer", + "format": "int32" + } + } + }, + "Task": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "name": { + "type": "string" + }, + "job": { + "type": "string" + }, + "workflow": { + "type": "string" + }, + "type": { + "type": "string" + }, + "maxExecutionTimeInMs": { + "type": "integer", + "format": "int64" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "context": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "status": { + "type": "string", + "enum": [ + "CREATED", + "WAITING", + "SCHEDULED", + "SUBMITTED", + "RUNNING", + "SUCCESSFUL", + "SKIPPED", + "FAILED" + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "submittedAt": { + "type": "integer", + "format": "int64" + }, + "completedAt": { + "type": "integer", + "format": "int64" + } + } + }, + "JobResponse": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "id": { + "type": "string" + }, + "workflow": { + "type": "string" + }, + "trigger": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "CREATED", + "RUNNING", + "SUCCESSFUL", + "FAILED" + ] + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "completedAt": { + "type": "integer", + "format": "int64" + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/Task" + } + } + } + }, + "Workflow": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/WorkflowTask" + } + }, + "emailOnFailure": { + "type": "array", + "items": { + "type": "string" + } + }, + "emailOnSuccess": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "WorkflowTask": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "maxExecutionTimeInMs": { + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean", + "default": false + } + } + }, + "WorkflowTrigger": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "name": { + "type": "string" + }, + "workflow": { + "type": "string" + }, + "startAt": { + "type": "integer", + "format": "int64" + }, + "schedule": { + "$ref": "#/definitions/Schedule" + }, + "endAt": { + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean", + "default": false + } + } + }, + "WorkflowStatistics": { + "type": "object", + "properties": { + "jobs": { + "$ref": "#/definitions/ExecutionCounters" + }, + "tasks": { + "$ref": "#/definitions/ExecutionCounters" + }, + "from": { + "type": "integer", + "format": "int64" + }, + "to": { + "type": "integer", + "format": "int64" + } + } + }, + "Namespace": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "CronSchedule": { + "allOf": [ + { + "$ref": "#/definitions/Schedule" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "simple", + "fixed", + "daily_time", + "calendar" + ] + }, + "misfireInstruction": { + "type": "integer", + "format": "int32" + }, + "cronExpression": { + "type": "string" + }, + "timezone": { + "type": "string" + } + } + } + ] + }, + "ExecutionCounters": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int32" + }, + "active": { + "type": "integer", + "format": "int32" + }, + "successful": { + "type": "integer", + "format": "int32" + }, + "failed": { + "type": "integer", + "format": "int32" + } + } + }, + "FixedDelaySchedule": { + "allOf": [ + { + "$ref": "#/definitions/Schedule" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "simple", + "fixed", + "daily_time", + "calendar" + ] + }, + "misfireInstruction": { + "type": "integer", + "format": "int32" + }, + "interval": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "Schedule": { + "type": "object", + "discriminator": "type", + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "simple", + "fixed", + "daily_time", + "calendar" + ] + }, + "misfireInstruction": { + "type": "integer", + "format": "int32" + } + } + }, + "Job": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "id": { + "type": "string" + }, + "workflow": { + "type": "string" + }, + "trigger": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "CREATED", + "RUNNING", + "SUCCESSFUL", + "FAILED" + ] + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "completedAt": { + "type": "integer", + "format": "int64" + } + } + }, + "SimpleSchedule": { + "allOf": [ + { + "$ref": "#/definitions/Schedule" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "simple", + "fixed", + "daily_time", + "calendar" + ] + }, + "misfireInstruction": { + "type": "integer", + "format": "int32" + }, + "repeatForever": { + "type": "boolean", + "default": false + }, + "repeatIntervalInMs": { + "type": "integer", + "format": "int64" + }, + "repeatCount": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "CalendarIntervalSchedule": { + "allOf": [ + { + "$ref": "#/definitions/Schedule" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "simple", + "fixed", + "daily_time", + "calendar" + ] + }, + "misfireInstruction": { + "type": "integer", + "format": "int32" + }, + "repeatInterval": { + "type": "integer", + "format": "int32" + }, + "repeatIntervalUnit": { + "type": "string", + "enum": [ + "MILLISECOND", + "SECOND", + "MINUTE", + "HOUR", + "DAY", + "WEEK", + "MONTH", + "YEAR" + ] + }, + "timezone": { + "type": "string" + }, + "preserveHourOfDayAcrossDaylightSavings": { + "type": "boolean", + "default": false + }, + "skipDayIfHourDoesNotExist": { + "type": "boolean", + "default": false + } + } + } + ] + } + } +} \ No newline at end of file