分布式调度框架。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

495 lines
24 KiB

/*
* 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 org.apache.dolphinscheduler.server.master.runner.taskgroup;
import org.apache.dolphinscheduler.common.constants.Constants;
import org.apache.dolphinscheduler.common.enums.Flag;
import org.apache.dolphinscheduler.common.enums.TaskGroupQueueStatus;
import org.apache.dolphinscheduler.common.enums.WorkflowExecutionStatus;
import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager;
import org.apache.dolphinscheduler.common.thread.BaseDaemonThread;
import org.apache.dolphinscheduler.common.thread.ThreadUtils;
import org.apache.dolphinscheduler.dao.entity.ProcessInstance;
import org.apache.dolphinscheduler.dao.entity.TaskGroup;
import org.apache.dolphinscheduler.dao.entity.TaskGroupQueue;
import org.apache.dolphinscheduler.dao.entity.TaskInstance;
import org.apache.dolphinscheduler.dao.repository.ProcessInstanceDao;
import org.apache.dolphinscheduler.dao.repository.TaskGroupDao;
import org.apache.dolphinscheduler.dao.repository.TaskGroupQueueDao;
import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao;
import org.apache.dolphinscheduler.extract.base.client.SingletonJdkDynamicRpcClientProxyFactory;
import org.apache.dolphinscheduler.extract.master.IWorkflowInstanceService;
import org.apache.dolphinscheduler.extract.master.transportor.TaskInstanceWakeupRequest;
import org.apache.dolphinscheduler.extract.master.transportor.TaskInstanceWakeupResponse;
import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
import org.apache.dolphinscheduler.plugin.task.api.utils.LogUtils;
import org.apache.dolphinscheduler.registry.api.RegistryClient;
import org.apache.dolphinscheduler.registry.api.enums.RegistryNodeType;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.time.StopWatch;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* The TaskGroupCoordinator use to manage the task group slot. The task group slot is used to limit the number of {@link TaskInstance} that can be run at the same time.
* <p>
* The {@link TaskGroupQueue} is used to represent the task group slot. When a {@link TaskGroupQueue} which inQueue is YES means the {@link TaskGroupQueue} is using by a {@link TaskInstance}.
* <p>
* When the {@link TaskInstance} need to use task group, we should use @{@link TaskGroupCoordinator#acquireTaskGroupSlot(TaskInstance)} to acquire the task group slot,
* this method doesn't block should always acquire successfully, and you should directly stop dispatch the task instance.
* When the task group slot is available, the TaskGroupCoordinator will wake up the waiting {@link TaskInstance} to dispatch.
* <pre>
* if(needAcquireTaskGroupSlot(taskInstance)) {
* taskGroupCoordinator.acquireTaskGroupSlot(taskInstance);
* return;
* }
* </pre>
* <p>
* When the {@link TaskInstance} is finished, we should use @{@link TaskGroupCoordinator#releaseTaskGroupSlot(TaskInstance)} to release the task group slot.
* <pre>
* if(needToReleaseTaskGroupSlot(taskInstance)) {
* taskGroupCoordinator.releaseTaskGroupSlot(taskInstance);
* }
* </pre>
*/
@Slf4j
@Component
public class TaskGroupCoordinator extends BaseDaemonThread {
@Autowired
private RegistryClient registryClient;
@Autowired
private TaskGroupDao taskGroupDao;
@Autowired
private TaskGroupQueueDao taskGroupQueueDao;
@Autowired
private TaskInstanceDao taskInstanceDao;
@Autowired
private ProcessInstanceDao processInstanceDao;
private static int DEFAULT_LIMIT = 1000;
public TaskGroupCoordinator() {
super("TaskGroupCoordinator");
}
@Override
public synchronized void start() {
log.info("TaskGroupCoordinator starting...");
super.start();
log.info("TaskGroupCoordinator started...");
}
@Override
public void run() {
while (!ServerLifeCycleManager.isStopped()) {
try {
if (!ServerLifeCycleManager.isRunning()) {
continue;
}
try {
registryClient.getLock(RegistryNodeType.MASTER_TASK_GROUP_COORDINATOR_LOCK.getRegistryPath());
StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted();
amendTaskGroupUseSize();
amendTaskGroupQueueStatus();
dealWithForceStartTaskGroupQueue();
dealWithWaitingTaskGroupQueue();
taskGroupCoordinatorRoundTimeCost.stop();
log.info("TaskGroupCoordinator round time cost: {}/ms",
taskGroupCoordinatorRoundTimeCost.getTime());
} finally {
registryClient.releaseLock(RegistryNodeType.MASTER_TASK_GROUP_COORDINATOR_LOCK.getRegistryPath());
}
} catch (Throwable e) {
log.error("TaskGroupCoordinator error", e);
} finally {
// sleep 5s
ThreadUtils.sleep(Constants.SLEEP_TIME_MILLIS * 5);
}
}
}
/**
* Make sure the TaskGroup useSize is equal to the TaskGroupQueue which status is {@link TaskGroupQueueStatus#ACQUIRE_SUCCESS} and forceStart is {@link org.apache.dolphinscheduler.common.enums.Flag#NO}.
*/
private void amendTaskGroupUseSize() {
// The TaskGroup useSize should equal to the TaskGroupQueue which inQueue is YES and forceStart is NO
List<TaskGroup> taskGroups = taskGroupDao.queryAllTaskGroups();
if (CollectionUtils.isEmpty(taskGroups)) {
return;
}
StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted();
for (TaskGroup taskGroup : taskGroups) {
int actualUseSize = taskGroupQueueDao.countUsingTaskGroupQueueByGroupId(taskGroup.getId());
if (taskGroup.getUseSize() == actualUseSize) {
continue;
}
log.warn("The TaskGroup: {} useSize is {}, but the actual use size is {}, will amend it",
taskGroup.getName(),
taskGroup.getUseSize(), actualUseSize);
taskGroup.setUseSize(actualUseSize);
taskGroupDao.updateById(taskGroup);
}
log.info("Success amend TaskGroup useSize cost: {}/ms", taskGroupCoordinatorRoundTimeCost.getTime());
}
/**
* Make sure the TaskGroupQueue status is {@link TaskGroupQueueStatus#RELEASE} when the related {@link TaskInstance} is not exist or status is finished.
*/
private void amendTaskGroupQueueStatus() {
int minTaskGroupQueueId = -1;
int limit = DEFAULT_LIMIT;
StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted();
while (true) {
List<TaskGroupQueue> taskGroupQueues =
taskGroupQueueDao.queryInQueueTaskGroupQueue(minTaskGroupQueueId, limit);
if (CollectionUtils.isEmpty(taskGroupQueues)) {
break;
}
amendTaskGroupQueueStatus(taskGroupQueues);
if (taskGroupQueues.size() < limit) {
break;
}
minTaskGroupQueueId = taskGroupQueues.get(taskGroupQueues.size() - 1).getId();
}
log.info("Success amend TaskGroupQueue status cost: {}/ms", taskGroupCoordinatorRoundTimeCost.getTime());
}
/**
* Make sure the TaskGroupQueue status is {@link TaskGroupQueueStatus#RELEASE} when the related {@link TaskInstance} is not exist or status is finished.
*/
private void amendTaskGroupQueueStatus(List<TaskGroupQueue> taskGroupQueues) {
List<Integer> taskInstanceIds = taskGroupQueues.stream()
.map(TaskGroupQueue::getTaskId)
.collect(Collectors.toList());
Map<Integer, TaskInstance> taskInstanceMap = taskInstanceDao.queryByIds(taskInstanceIds)
.stream()
.collect(Collectors.toMap(TaskInstance::getId, Function.identity()));
for (TaskGroupQueue taskGroupQueue : taskGroupQueues) {
int taskId = taskGroupQueue.getTaskId();
TaskInstance taskInstance = taskInstanceMap.get(taskId);
if (taskInstance == null) {
log.warn("The TaskInstance: {} is not exist, will release the TaskGroupQueue: {}", taskId,
taskGroupQueue);
releaseTaskGroupQueueSlot(taskGroupQueue);
continue;
}
if (taskInstance.getState().isFinished()) {
log.warn("The TaskInstance: {} state: {} finished, will release the TaskGroupQueue: {}",
taskInstance.getName(), taskInstance.getState(), taskGroupQueue);
releaseTaskGroupQueueSlot(taskGroupQueue);
continue;
}
}
}
private void dealWithForceStartTaskGroupQueue() {
// Find the force start task group queue(Which is inQueue and forceStart is YES)
// Notify the related waiting task instance
// Set the taskGroupQueue status to RELEASE and remove it from queue
// We use limit here to avoid OOM, and we will retry to notify force start queue at next time
int minTaskGroupQueueId = -1;
int limit = DEFAULT_LIMIT;
StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted();
while (true) {
List<TaskGroupQueue> taskGroupQueues =
taskGroupQueueDao.queryWaitNotifyForceStartTaskGroupQueue(minTaskGroupQueueId, limit);
if (CollectionUtils.isEmpty(taskGroupQueues)) {
break;
}
dealWithForceStartTaskGroupQueue(taskGroupQueues);
if (taskGroupQueues.size() < limit) {
break;
}
minTaskGroupQueueId = taskGroupQueues.get(taskGroupQueues.size() - 1).getId();
}
log.info("Success deal with force start TaskGroupQueue cost: {}/ms",
taskGroupCoordinatorRoundTimeCost.getTime());
}
private void dealWithForceStartTaskGroupQueue(List<TaskGroupQueue> taskGroupQueues) {
// Find the force start task group queue(Which is inQueue and forceStart is YES)
// Notify the related waiting task instance
// Set the taskGroupQueue status to RELEASE and remove it from queue
for (TaskGroupQueue taskGroupQueue : taskGroupQueues) {
try {
LogUtils.setTaskInstanceIdMDC(taskGroupQueue.getTaskId());
// notify the waiting task instance
// We notify first, it notify failed, the taskGroupQueue will be in queue, and then we will retry it
// next time.
notifyWaitingTaskInstance(taskGroupQueue);
log.info("Notify the ForceStart waiting TaskInstance: {} for taskGroupQueue: {} success",
taskGroupQueue.getTaskName(),
taskGroupQueue.getId());
taskGroupQueue.setInQueue(Flag.NO.getCode());
taskGroupQueue.setStatus(TaskGroupQueueStatus.RELEASE);
taskGroupQueue.setUpdateTime(new Date());
taskGroupQueueDao.updateById(taskGroupQueue);
log.info("Release the force start TaskGroupQueue {}", taskGroupQueue);
} catch (UnsupportedOperationException unsupportedOperationException) {
releaseTaskGroupQueueSlot(taskGroupQueue);
log.info(
"Notify the ForceStart TaskInstance: {} for taskGroupQueue: {} failed, will release the taskGroupQueue",
taskGroupQueue.getTaskName(), taskGroupQueue.getId(), unsupportedOperationException);
} catch (Throwable throwable) {
log.info("Notify the force start TaskGroupQueue {} failed", taskGroupQueue, throwable);
} finally {
LogUtils.removeTaskInstanceIdMDC();
}
}
}
private void dealWithWaitingTaskGroupQueue() {
// Find the TaskGroup which usage < maxSize.
// Find the highest priority inQueue task group queue(Which is inQueue and status is Waiting and force start is
// NO) belong to the
// task group.
List<TaskGroup> taskGroups = taskGroupDao.queryAvailableTaskGroups();
if (CollectionUtils.isEmpty(taskGroups)) {
log.debug("There is no available task group");
return;
}
for (TaskGroup taskGroup : taskGroups) {
int availableSize = taskGroup.getGroupSize() - taskGroup.getUseSize();
if (availableSize <= 0) {
log.info("TaskGroup {} is full, available size is {}", taskGroup, availableSize);
continue;
}
List<TaskGroupQueue> taskGroupQueues =
taskGroupQueueDao.queryAllInQueueTaskGroupQueueByGroupId(taskGroup.getId())
.stream()
.filter(taskGroupQueue -> Flag.NO.getCode() == taskGroupQueue.getForceStart())
.filter(taskGroupQueue -> TaskGroupQueueStatus.WAIT_QUEUE == taskGroupQueue.getStatus())
.limit(availableSize)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(taskGroupQueues)) {
log.debug("There is no waiting task group queue for task group {}", taskGroup.getName());
continue;
}
for (TaskGroupQueue taskGroupQueue : taskGroupQueues) {
try {
LogUtils.setTaskInstanceIdMDC(taskGroupQueue.getTaskId());
// Reduce the taskGroupSize
boolean acquireResult = taskGroupDao.acquireTaskGroupSlot(taskGroup.getId());
if (!acquireResult) {
log.error("Failed to acquire task group slot for task group {}", taskGroup);
continue;
}
// Notify the waiting task instance
// We notify first, it notify failed, the taskGroupQueue will be in queue, and then we will retry it
// next time.
notifyWaitingTaskInstance(taskGroupQueue);
// Set the taskGroupQueue status to RUNNING and remove from queue
taskGroupQueue.setInQueue(Flag.YES.getCode());
taskGroupQueue.setStatus(TaskGroupQueueStatus.ACQUIRE_SUCCESS);
taskGroupQueue.setUpdateTime(new Date());
taskGroupQueueDao.updateById(taskGroupQueue);
} catch (UnsupportedOperationException unsupportedOperationException) {
releaseTaskGroupQueueSlot(taskGroupQueue);
log.info(
"Notify the Waiting TaskInstance: {} for taskGroupQueue: {} failed, will release the taskGroupQueue",
taskGroupQueue.getTaskName(), taskGroupQueue.getId(), unsupportedOperationException);
} catch (Throwable throwable) {
log.error("Notify Waiting TaskGroupQueue: {} failed", taskGroupQueue, throwable);
} finally {
LogUtils.removeTaskInstanceIdMDC();
}
}
}
}
/**
* If the {@link TaskInstance#getTaskGroupId()} > 0, and the TaskGroup flag is {@link Flag#YES} then the task instance need to use task group.
*
* @param taskInstance task instance
* @return true if the TaskInstance need to acquireTaskGroupSlot
*/
public boolean needAcquireTaskGroupSlot(TaskInstance taskInstance) {
if (taskInstance == null) {
throw new IllegalArgumentException("The TaskInstance is null");
}
if (taskInstance.getTaskGroupId() <= 0) {
log.debug("The current TaskInstance doesn't use TaskGroup, no need to acquire TaskGroupSlot");
return false;
}
TaskGroup taskGroup = taskGroupDao.queryById(taskInstance.getTaskGroupId());
if (taskGroup == null) {
log.warn("The current TaskGroup: {} does not exist, will not acquire TaskGroupSlot",
taskInstance.getTaskGroupId());
return false;
}
return Flag.YES.equals(taskGroup.getStatus());
}
/**
* Acquire the task group slot for the given {@link TaskInstance}.
* <p>
* When taskInstance want to acquire a TaskGroup slot, should call this method. If acquire successfully, will create a TaskGroupQueue in db which is in queue and status is {@link TaskGroupQueueStatus#WAIT_QUEUE}.
* The TaskInstance shouldn't dispatch until there exist available slot, the taskGroupCoordinator notify it.
*
* @param taskInstance the task instance which want to acquire task group slot.
* @throws IllegalArgumentException if the taskInstance is null or the used taskGroup doesn't exist.
*/
public void acquireTaskGroupSlot(TaskInstance taskInstance) {
if (taskInstance == null || taskInstance.getTaskGroupId() <= 0) {
throw new IllegalArgumentException("The current TaskInstance does not use task group");
}
TaskGroup taskGroup = taskGroupDao.queryById(taskInstance.getTaskGroupId());
if (taskGroup == null) {
throw new IllegalArgumentException(
"The current TaskGroup: " + taskInstance.getTaskGroupId() + " does not exist");
}
// Write TaskGroupQueue in db, and then return wait TaskGroupCoordinator to notify it
// Set the taskGroupQueue status to WAIT_QUEUE and add to queue
// The queue only contains the taskGroupQueue which status is WAIT_QUEUE or ACQUIRE_SUCCESS
Date now = new Date();
TaskGroupQueue taskGroupQueue = TaskGroupQueue
.builder()
.taskId(taskInstance.getId())
.taskName(taskInstance.getName())
.groupId(taskInstance.getTaskGroupId())
.processId(taskInstance.getProcessInstanceId())
.priority(taskInstance.getTaskGroupPriority())
.inQueue(Flag.YES.getCode())
.forceStart(Flag.NO.getCode())
.status(TaskGroupQueueStatus.WAIT_QUEUE)
.createTime(now)
.updateTime(now)
.build();
log.info("Success insert TaskGroupQueue: {} for TaskInstance: {}", taskGroupQueue, taskInstance.getName());
taskGroupQueueDao.insert(taskGroupQueue);
}
/**
* If the TaskInstance is using TaskGroup then it need to release TaskGroupSlot.
*
* @param taskInstance taskInsatnce
* @return true if the TaskInstance need to release TaskGroupSlot
*/
public boolean needToReleaseTaskGroupSlot(TaskInstance taskInstance) {
if (taskInstance == null) {
throw new IllegalArgumentException("The TaskInstance is null");
}
if (taskInstance.getTaskGroupId() <= 0) {
log.debug("The current TaskInstance doesn't use TaskGroup, no need to release TaskGroupSlot");
return false;
}
return true;
}
/**
* Release the task group slot for the given {@link TaskInstance}.
* <p>
* When taskInstance want to release a TaskGroup slot, should call this method. The release method will move the TaskGroupQueue out queue and set status to {@link TaskGroupQueueStatus#RELEASE}.
* This method is idempotent, this means that if the task group slot is already released, this method will do nothing.
*
* @param taskInstance the task instance which want to release task group slot.
* @throws IllegalArgumentException If the taskInstance is null or the task doesn't use task group.
*/
public void releaseTaskGroupSlot(TaskInstance taskInstance) {
if (taskInstance == null || taskInstance.getTaskGroupId() <= 0) {
throw new IllegalArgumentException("The current TaskInstance does not use task group");
}
List<TaskGroupQueue> taskGroupQueues = taskGroupQueueDao.queryByTaskInstanceId(taskInstance.getId());
for (TaskGroupQueue taskGroupQueue : taskGroupQueues) {
releaseTaskGroupQueueSlot(taskGroupQueue);
}
}
private void notifyWaitingTaskInstance(TaskGroupQueue taskGroupQueue) {
// Find the related waiting task instance
// send RPC to notify the waiting task instance
TaskInstance taskInstance = taskInstanceDao.queryById(taskGroupQueue.getTaskId());
if (taskInstance == null) {
throw new UnsupportedOperationException(
"The TaskInstance: " + taskGroupQueue.getTaskId() + " is not exist, no need to notify");
}
// todo: We may need to add a new status to represent the task instance is waiting for task group slot
if (taskInstance.getState() != TaskExecutionStatus.SUBMITTED_SUCCESS) {
throw new UnsupportedOperationException(
"The TaskInstance: " + taskInstance.getId() + " state is " + taskInstance.getState()
+ ", no need to notify");
}
ProcessInstance processInstance = processInstanceDao.queryById(taskInstance.getProcessInstanceId());
if (processInstance == null) {
throw new UnsupportedOperationException(
"The WorkflowInstance: " + taskInstance.getProcessInstanceId()
+ " is not exist, no need to notify");
}
if (processInstance.getState() != WorkflowExecutionStatus.RUNNING_EXECUTION) {
throw new UnsupportedOperationException(
"The WorkflowInstance: " + processInstance.getId() + " state is " + processInstance.getState()
+ ", no need to notify");
}
if (processInstance.getHost() == null || Constants.NULL.equals(processInstance.getHost())) {
throw new UnsupportedOperationException(
"WorkflowInstance host is null, maybe it is in failover: " + processInstance);
}
TaskInstanceWakeupRequest taskInstanceWakeupRequest = TaskInstanceWakeupRequest.builder()
.processInstanceId(processInstance.getId())
.taskInstanceId(taskInstance.getId())
.build();
IWorkflowInstanceService iWorkflowInstanceService = SingletonJdkDynamicRpcClientProxyFactory
.getProxyClient(processInstance.getHost(), IWorkflowInstanceService.class);
TaskInstanceWakeupResponse taskInstanceWakeupResponse =
iWorkflowInstanceService.wakeupTaskInstance(taskInstanceWakeupRequest);
if (!taskInstanceWakeupResponse.isSuccess()) {
throw new UnsupportedOperationException(
"Notify TaskInstance: " + taskInstance.getId() + " failed: " + taskInstanceWakeupResponse);
}
log.info("Wake up TaskInstance: {} success", taskInstance.getName());
}
private void releaseTaskGroupQueueSlot(TaskGroupQueue taskGroupQueue) {
if (TaskGroupQueueStatus.RELEASE.equals(taskGroupQueue.getStatus())
&& Flag.NO.getCode() == taskGroupQueue.getInQueue()) {
log.info("The TaskGroupQueue: {} is already released", taskGroupQueue);
return;
}
taskGroupQueue.setInQueue(Flag.NO.getCode());
taskGroupQueue.setStatus(TaskGroupQueueStatus.RELEASE);
taskGroupQueue.setUpdateTime(new Date());
taskGroupQueueDao.updateById(taskGroupQueue);
log.info("Success release TaskGroupQueue: {}", taskGroupQueue);
}
}