feat (任务中心): 停止任务

This commit is contained in:
fit2-zhao 2021-09-03 14:34:02 +08:00 committed by fit2-zhao
parent a99c89d001
commit b37d9a8425
14 changed files with 337 additions and 94 deletions

View File

@ -17,6 +17,7 @@ import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.ScheduleRequest;
import io.metersphere.log.annotation.MsAuditLog;
import io.metersphere.notice.annotation.SendNotice;
import io.metersphere.task.service.TaskService;
import io.metersphere.track.request.testcase.ApiCaseRelevanceRequest;
import io.metersphere.track.request.testplan.FileOperationRequest;
import org.apache.commons.lang3.StringUtils;
@ -35,7 +36,9 @@ import java.util.List;
public class ApiAutomationController {
@Resource
ApiAutomationService apiAutomationService;
private ApiAutomationService apiAutomationService;
@Resource
private TaskService taskService;
@PostMapping("/list/{goPage}/{pageSize}")
@RequiresPermissions("PROJECT_API_SCENARIO:READ")
@ -315,5 +318,11 @@ public class ApiAutomationController {
public void stop(@PathVariable String reportId) {
new LocalRunner().stop(reportId);
}
@PostMapping(value = "/stop/batch")
public String stopBatch(@RequestBody List<TaskRequest> reportIds) {
return taskService.stop(reportIds);
}
}

View File

@ -0,0 +1,10 @@
package io.metersphere.api.dto.automation;
import lombok.Data;
@Data
public class TaskRequest {
private String type;
private String reportId;
}

View File

@ -47,7 +47,7 @@ import java.lang.reflect.Field;
@Service
@Transactional(rollbackFor = Exception.class)
public class JMeterService {
private static final String BASE_URL = "http://%s:%d";
public static final String BASE_URL = "http://%s:%d";
@Resource
private JmeterProperties jmeterProperties;
@Resource

View File

@ -6,6 +6,7 @@ import javax.websocket.Session;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
public class MessageCache {
public static Map<String, ReportCounter> cache = new HashMap<>();
@ -14,4 +15,5 @@ public class MessageCache {
public static ConcurrentHashMap<String, StandardJMeterEngine> runningEngine = new ConcurrentHashMap<>();
public static ConcurrentLinkedDeque<String> terminationOrderDeque = new ConcurrentLinkedDeque<>();
}

View File

@ -6,6 +6,7 @@ package io.metersphere.api.service.task;
import io.metersphere.api.dto.RunModeDataDTO;
import io.metersphere.api.dto.automation.RunScenarioRequest;
import io.metersphere.api.jmeter.JMeterService;
import io.metersphere.api.jmeter.MessageCache;
import io.metersphere.base.domain.ApiScenarioReport;
import io.metersphere.base.mapper.ApiScenarioReportMapper;
import io.metersphere.commons.constants.APITestStatus;
@ -33,6 +34,10 @@ public class SerialScenarioExecTask<T> implements Callable<T> {
@Override
public T call() {
try {
if (MessageCache.terminationOrderDeque.contains(runModeDataDTO.getReport().getId())) {
MessageCache.terminationOrderDeque.remove(runModeDataDTO.getReport().getId());
return (T) report;
}
if (request.getConfig() != null && StringUtils.isNotBlank(request.getConfig().getResourcePoolId())) {
jMeterService.runTest(runModeDataDTO.getTestId(), runModeDataDTO.getReport().getId(), request.getRunMode(), request.getPlanScenarioId(), request.getConfig());
} else {
@ -47,6 +52,10 @@ public class SerialScenarioExecTask<T> implements Callable<T> {
if (report != null && !report.getStatus().equals(APITestStatus.Running.name())) {
break;
}
if (MessageCache.terminationOrderDeque.contains(runModeDataDTO.getReport().getId())) {
MessageCache.terminationOrderDeque.remove(runModeDataDTO.getReport().getId());
break;
}
}
// 执行失败了恢复报告状态
if (index == 200 && report != null && report.getStatus().equals(APITestStatus.Running.name())) {

View File

@ -72,14 +72,14 @@
SELECT count(tt.id) FROM (
(select t.id,t.create_time as executionTime
from api_scenario_report t left join `user` t1 ON t.user_id = t1.id left join test_resource_pool t2 on t.actuator = t2.id
where to_days(FROM_UNIXTIME(t.create_time/1000))= to_days(now()) and t.execute_type !='Debug' and t.execute_type !='Marge' and t.project_id= #{request.projectId} and t.status not in ("saved","completed","success","error")
where to_days(FROM_UNIXTIME(t.create_time/1000))= to_days(now()) and t.execute_type !='Debug' and t.execute_type !='Marge' and t.project_id= #{request.projectId} and t.status not in ("saved","completed","success","error","STOP")
)
UNION ALL
(select t.id,t.create_time as executionTime
from api_definition_exec_result t left join `user` t1 ON t.user_id = t1.id left join test_resource_pool t2 on t.actuator = t2.id
left join api_definition t3 on t.resource_id = t3.id left join api_test_case t4 on t4.id = t.resource_id
left join test_plan_api_case t5 on t.resource_id = t5.id left join test_plan t6 on t5.test_plan_id = t6.id
where to_days(FROM_UNIXTIME(t.create_time/1000))= to_days(now()) and (t3.project_id =#{request.projectId} OR t4.project_id =#{request.projectId} OR t6.project_id = #{request.projectId}) and t.status not in ("saved","completed","success","error")
where to_days(FROM_UNIXTIME(t.create_time/1000))= to_days(now()) and (t3.project_id =#{request.projectId} OR t4.project_id =#{request.projectId} OR t6.project_id = #{request.projectId}) and t.status not in ("saved","completed","success","error","STOP")
)
UNION ALL
(select t.id,t.create_time as executionTime

View File

@ -1,21 +1,51 @@
package io.metersphere.task.service;
import com.alibaba.fastjson.JSON;
import io.metersphere.api.dto.automation.TaskRequest;
import io.metersphere.api.jmeter.JMeterService;
import io.metersphere.api.jmeter.LocalRunner;
import io.metersphere.api.jmeter.MessageCache;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiDefinitionExecResultMapper;
import io.metersphere.base.mapper.ApiScenarioReportMapper;
import io.metersphere.base.mapper.TestResourceMapper;
import io.metersphere.base.mapper.TestResourcePoolMapper;
import io.metersphere.base.mapper.ext.ExtTaskMapper;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.dto.NodeDTO;
import io.metersphere.performance.service.PerformanceTestService;
import io.metersphere.task.dto.TaskCenterDTO;
import io.metersphere.task.dto.TaskCenterRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
public class TaskService {
@Resource
private ExtTaskMapper extTaskMapper;
@Resource
private ApiDefinitionExecResultMapper apiDefinitionExecResultMapper;
@Resource
private ApiScenarioReportMapper apiScenarioReportMapper;
@Resource
private PerformanceTestService performanceTestService;
@Resource
private RestTemplate restTemplate;
@Resource
private TestResourcePoolMapper testResourcePoolMapper;
@Resource
private TestResourceMapper testResourceMapper;
public List<TaskCenterDTO> getTasks(TaskCenterRequest request) {
if (StringUtils.isEmpty(request.getProjectId())) {
@ -38,4 +68,75 @@ public class TaskService {
public List<TaskCenterDTO> getScenario(String id) {
return extTaskMapper.getScenario(id);
}
public void send(Map<String, List<String>> poolMap) {
try {
for (String poolId : poolMap.keySet()) {
TestResourcePoolExample example = new TestResourcePoolExample();
example.createCriteria().andStatusEqualTo("VALID").andTypeEqualTo("NODE").andIdEqualTo(poolId);
List<TestResourcePool> pools = testResourcePoolMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(pools)) {
List<String> poolIds = pools.stream().map(pool -> pool.getId()).collect(Collectors.toList());
TestResourceExample resourceExample = new TestResourceExample();
resourceExample.createCriteria().andTestResourcePoolIdIn(poolIds);
List<TestResource> testResources = testResourceMapper.selectByExampleWithBLOBs(resourceExample);
for (TestResource testResource : testResources) {
String configuration = testResource.getConfiguration();
NodeDTO node = JSON.parseObject(configuration, NodeDTO.class);
String nodeIp = node.getIp();
Integer port = node.getPort();
String uri = String.format(JMeterService.BASE_URL + "/jmeter/stop", nodeIp, port);
restTemplate.postForEntity(uri, poolMap.get(poolId), void.class);
}
}
}
} catch (Exception e) {
LogUtil.error(e.getMessage());
}
}
public String stop(List<TaskRequest> reportIds) {
if (CollectionUtils.isNotEmpty(reportIds)) {
// 聚类同一批资源池的一批发送
Map<String, List<String>> poolMap = new HashMap<>();
for (TaskRequest request : reportIds) {
String actuator = null;
if (StringUtils.equals(request.getType(), "API")) {
ApiDefinitionExecResult result = apiDefinitionExecResultMapper.selectByPrimaryKey(request.getReportId());
if (result != null) {
result.setStatus("STOP");
apiDefinitionExecResultMapper.updateByPrimaryKeySelective(result);
actuator = result.getActuator();
}
} else if (StringUtils.equals(request.getType(), "SCENARIO")) {
ApiScenarioReport report = apiScenarioReportMapper.selectByPrimaryKey(request.getReportId());
if (report != null) {
report.setStatus("STOP");
apiScenarioReportMapper.updateByPrimaryKeySelective(report);
actuator = report.getActuator();
}
} else if (StringUtils.equals(request.getType(), "PERFORMANCE")) {
performanceTestService.stopTest(request.getReportId(), false);
}
if (StringUtils.isNotEmpty(actuator)) {
if (poolMap.containsKey(actuator)) {
poolMap.get(actuator).add(request.getReportId());
} else {
poolMap.put(actuator, new ArrayList<String>() {{
this.add(request.getReportId());
}});
}
} else {
new LocalRunner().stop(request.getReportId());
}
MessageCache.cache.remove(request.getReportId());
MessageCache.terminationOrderDeque.add(request.getReportId());
}
if (!poolMap.isEmpty()) {
this.send(poolMap);
}
}
return "SUCCESS";
}
}

View File

@ -80,23 +80,20 @@
</template>
<script>
import MsTablePagination from "../../../common/pagination/TablePagination";
import MsTableHeader from "../../../common/components/MsTableHeader";
import MsContainer from "../../../common/components/MsContainer";
import MsMainContainer from "../../../common/components/MsMainContainer";
import MsApiReportStatus from "./ApiReportStatus";
import {getCurrentProjectID} from "@/common/js/utils";
import MsTableOperatorButton from "../../../common/components/MsTableOperatorButton";
import ReportTriggerModeItem from "../../../common/tableItem/ReportTriggerModeItem";
import {REPORT_CONFIGS} from "../../../common/components/search/search-components";
import ShowMoreBtn from "../../../track/case/components/ShowMoreBtn";
import {_filter, _sort} from "@/common/js/tableUtils";
export default {
components: {
ReportTriggerModeItem,
MsTableOperatorButton,
MsApiReportStatus, MsMainContainer, MsContainer, MsTableHeader, MsTablePagination, ShowMoreBtn
ReportTriggerModeItem: () => import("../../../common/tableItem/ReportTriggerModeItem"),
MsTableOperatorButton: () => import("../../../common/components/MsTableOperatorButton"),
MsApiReportStatus: () => import("./ApiReportStatus"),
MsMainContainer: () => import("../../../common/components/MsMainContainer"),
MsContainer: () => import("../../../common/components/MsContainer"),
MsTableHeader: () => import("../../../common/components/MsTableHeader"),
MsTablePagination: () => import("../../../common/pagination/TablePagination"),
ShowMoreBtn: () => import("../../../track/case/components/ShowMoreBtn")
},
data() {
return {

View File

@ -19,14 +19,11 @@
</div>
<ms-chart id="chart" ref="chart" :options="options" :autoresize="true" v-else/>
<el-row type="flex" justify="center" align="middle">
<!-- <i class="circle success"/>-->
<span class="ms-point-success"/>
<div class="metric-box">
<div class="value">{{ content.success }}</div>
<div class="name">{{ $t('api_report.success') }}</div>
</div>
<div style="width: 40px"></div>
<!-- <i class="circle fail"/>-->
<span class="ms-point-error"/>
<div class="metric-box">
<div class="value">{{ content.error }}</div>

View File

@ -238,13 +238,16 @@ import {
} from "@/common/js/tableUtils";
import {API_SCENARIO_FILTERS} from "@/common/js/table-constants";
import {scenario} from "@/business/components/track/plan/event-bus";
import MsTable from "@/business/components/common/components/table/MsTable";
import MsTableColumn from "@/business/components/common/components/table/MsTableColumn";
import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate";
export default {
name: "MsApiScenarioList",
components: {
MsTable: () => import("@/business/components/common/components/table/MsTable"),
MsTableColumn: () => import("@/business/components/common/components/table/MsTableColumn"),
HeaderLabelOperate: () => import("@/business/components/common/head/HeaderLabelOperate"),
MsTable,
MsTableColumn,
HeaderLabelOperate,
HeaderCustom: () => import("@/business/components/common/head/HeaderCustom"),
BatchMove: () => import("../../../track/case/components/BatchMove"),
EnvironmentSelect: () => import("../../definition/components/environment/EnvironmentSelect"),

View File

@ -22,26 +22,40 @@
</el-menu>
<el-drawer :visible.sync="taskVisible" :destroy-on-close="true" direction="rtl"
:withHeader="true" :modal="false" :title="$t('commons.task_center')" size="600px"
:withHeader="true" :modal="false" :title="$t('commons.task_center')" :size="size"
custom-class="ms-drawer-task">
<div style="color: #2B415C;margin: 0px 20px 0px">
<el-form label-width="68px">
<el-card style="float: left;width: 800px" v-if="size > 550 ">
<div class="ms-task-opt-btn" @click="packUp">收起</div>
<!-- 接口用例结果 -->
<ms-request-result-tail :response="response" ref="debugResult" v-if="reportType === 'API'"/>
<ms-api-report-detail :reportId="reportId" v-if="reportType === 'SCENARIO'"/>
<performance-report-view :reportId="reportId" v-if="reportType === 'PERFORMANCE'"/>
</el-card>
<el-card style="width: 550px;float: right">
<div style="color: #2B415C;margin: 0px 20px 0px;">
<el-form label-width="68px" class="ms-el-form-item">
<el-row>
<el-col :span="8">
<el-col :span="12">
<el-form-item :label="$t('test_track.report.list.trigger_mode')" prop="runMode">
<el-select size="small" style="margin-right: 10px" v-model="condition.triggerMode" @change="init">
<el-option v-for="item in runMode" :key="item.id" :value="item.id" :label="item.label"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item :label="$t('commons.status')" prop="status">
<el-select size="small" style="margin-right: 10px" v-model="condition.executionStatus" @change="init">
<el-option v-for="item in runStatus" :key="item.id" :value="item.id" :label="item.label"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
</el-row>
<el-row>
<el-col :span="12">
<el-form-item :label="$t('commons.executor')" prop="status">
<el-select v-model="condition.executor" :placeholder="$t('commons.executor')" filterable size="small"
style="margin-right: 10px" @change="init">
@ -54,6 +68,11 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-button size="small" class="ms-task-stop" @click="stop(null)">
{{ $t('report.stop_btn_all') }}
</el-button>
</el-col>
</el-row>
</el-form>
</div>
@ -61,7 +80,12 @@
<div class="report-container">
<div v-for="item in taskData" :key="item.id" style="margin-bottom: 5px">
<el-card class="ms-card-task" @click.native="showReport(item,$event)">
<span><el-link type="primary">{{ getModeName(item.executionModule) }} </el-link>: {{ item.name }} </span><br/>
<span class="ms-task-name-width"><el-link type="primary">
{{ getModeName(item.executionModule) }} </el-link>: {{ item.name }} </span>
<el-button size="mini" class="ms-task-stop" @click.stop @click="stop(item)" v-if="showStop(item.executionStatus)">
{{ $t('report.stop_btn') }}
</el-button>
<br/>
<span>
执行器{{ item.actuator }} {{ item.executor }}
{{ item.executionTime | timestampFormatDate }}
@ -79,32 +103,29 @@
<span v-else-if="item.executionStatus && item.executionStatus.toLowerCase() === 'success'" class="ms-task-success">
success
</span>
<i class="el-icon-video-pause" v-else-if="item.executionStatus && item.executionStatus.toLowerCase() === 'stop'"/>
<span v-else>{{ item.executionStatus ? item.executionStatus.toLowerCase() : item.executionStatus }}</span>
</el-col>
</el-row>
</el-card>
</div>
</div>
</el-card>
</el-drawer>
<el-dialog :close-on-click-modal="false" :title="$t('test_track.plan_view.test_result')" width="60%"
:visible.sync="visible" class="api-import" destroy-on-close @close="close">
<ms-request-result-tail :response="response" ref="debugResult"/>
</el-dialog>
</div>
</template>
<script>
import MsDrawer from "../common/components/MsDrawer";
import {getCurrentProjectID, getCurrentUser, hasPermissions} from "@/common/js/utils";
import MsRequestResultTail from "../../components/api/definition/components/response/RequestResultTail";
export default {
name: "MsTaskCenter",
components: {
MsDrawer,
MsRequestResultTail
MsRequestResultTail: () => import("../../components/api/definition/components/response/RequestResultTail"),
MsApiReportDetail: () => import("../../components/api/automation/report/ApiReportDetail"),
PerformanceReportView: () => import("../../components/performance/report/PerformanceReportView")
},
inject: [
'reload'
@ -139,6 +160,9 @@ export default {
condition: {triggerMode: "", executionStatus: ""},
maintainerOptions: [],
websocket: Object,
size: 550,
reportId: "",
reportType: "",
};
},
props: {
@ -160,6 +184,31 @@ export default {
format(item) {
return '';
},
packUp(){
this.size = 550;
},
stop(row) {
let array = [];
if (row) {
let request = {type: row.executionModule, reportId: row.id};
array = [request];
} else {
this.taskData.forEach(item => {
if (this.showStop(item.executionStatus)) {
let request = {type: item.executionModule, reportId: item.id};
array.push(request);
}
})
}
if (array.length === 0) {
this.$warning("没有需要停止的任务");
return;
}
this.$post('/api/automation/stop/batch', array, response => {
this.$success(this.$t('report.test_stop_success'));
this.init();
});
},
getMaintainerOptions() {
this.$post('/user/project/member/tester/list', {projectId: getCurrentProjectID()}, response => {
this.maintainerOptions = response.data;
@ -203,7 +252,7 @@ export default {
},
close() {
this.visible = false;
// this.taskVisible = false;
this.size = 550;
this.showType = "";
if (this.websocket && this.websocket.close instanceof Function) {
this.websocket.close();
@ -225,6 +274,15 @@ export default {
}
return 60;
},
showStop(status) {
if (status) {
status = status.toLowerCase();
if (status === "stop" || status === 'saved' || status === 'completed' || status === 'success' || status === 'error') {
return false;
}
}
return true;
},
getModeName(executionModule) {
switch (executionModule) {
case "SCENARIO":
@ -240,18 +298,19 @@ export default {
if (status) {
status = row.executionStatus.toLowerCase();
if (status === 'saved' || status === 'completed' || status === 'success' || status === 'error') {
this.size = 1350;
this.reportId = row.id;
this.reportType = row.executionModule;
switch (row.executionModule) {
case "SCENARIO":
this.taskVisible = false;
this.$router.push({
path: '/api/automation/report/view/' + row.id,
});
// this.$router.push({
// path: '/api/automation/report/view/' + row.id,
// });
break;
case "PERFORMANCE":
this.taskVisible = false;
this.$router.push({
path: '/performance/report/view/' + row.id,
});
// this.$router.push({
// path: '/performance/report/view/' + row.id,
// });
break;
case "API":
this.getExecResult(row.id);
@ -354,7 +413,7 @@ export default {
.report-container {
height: calc(100vh - 180px);
min-height: 600px;
min-height: 550px;
overflow-y: auto;
}
@ -437,7 +496,60 @@ export default {
color: #F56C6C;
}
.ms-task-stop {
color: #F56C6C;
float: right;
margin-right: 20px;
}
.ms-task-success {
color: #67C23A;
}
.ms-task-name-width {
display: inline-block;
overflow-x: hidden;
padding-bottom: 0;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 360px;
}
.ms-el-form-item >>> .el-form-item {
margin-bottom: 6px;
}
.ms-task-opt-btn {
position: fixed;
right: 1322px;
top: 50%;
z-index: 1;
width: 20px;
height: 60px;
padding: 3px;
line-height: 30px;
border-radius: 0 15px 15px 0;
background-color: #783887;
color: white;
display: inline-block;
cursor: pointer;
opacity: 0.5;
font-size: 10px;
font-weight: bold;
margin-left: 1px;
}
.ms-task-opt-btn i {
margin-left: -2px;
}
.ms-task-opt-btn:hover {
opacity: 0.8;
}
.ms-task-opt-btn:hover i {
margin-left: 0;
color: white;
}
</style>

View File

@ -596,6 +596,7 @@ export default {
stop_tips: 'A <strong>Graceful shutdown</strong> will archive the JTL files and then stop the servers.',
force_stop_btn: 'Terminating',
stop_btn: 'Graceful shutdown',
stop_btn_all: 'All Graceful shutdown ',
not_exist: "Test report does not exist",
batch_delete: "Delete reports in bulk",
delete_batch_confirm: 'Confirm batch delete report',

View File

@ -600,6 +600,7 @@ export default {
stop_tips: '<strong>停止</strong>测试会结束当前测试并保留报告数据',
force_stop_btn: '强制停止',
stop_btn: '停止',
stop_btn_all: '全部停止',
not_exist: "测试报告不存在",
batch_delete: "批量删除报告",
delete_batch_confirm: '确认批量删除报告',

View File

@ -600,6 +600,7 @@ export default {
stop_tips: '<strong>停止</strong>測試會結束當前測試並保留報告數據',
force_stop_btn: '強制停止',
stop_btn: '停止',
stop_btn_all: '全部停止',
not_exist: "測試報告不存在",
batch_delete: "批量刪除報告",
delete_batch_confirm: '確認批量刪除報告',