This commit is contained in:
2026-08-27 09:15:32 +08:00
parent 4a451aeadc
commit e889059d3e
5 changed files with 39 additions and 7 deletions
+12
View File
@@ -184,6 +184,18 @@ func (d *modelTrainingDao) ListRunning(ctx context.Context) ([]*entity.ModelTrai
return list, nil
}
// FailUnstarted 重启恢复:running 且 pid 未落(发起准备阶段服务中断,进程已随服务消亡)的任务置 failed
func (d *modelTrainingDao) FailUnstarted(ctx context.Context) error {
_, err := g.DB().Model(consts.TableTraining).Ctx(ctx).
Where("status", consts.TrainingStatusRunning).Where("pid", 0).
Data(g.Map{
"status": consts.TrainingStatusFailed,
"error": "服务器重启,训练发起未完成",
"finished_at": gtime.Now(),
}).Update()
return err
}
// LatestByDatasets 批量取各数据集最新一条训练记录(列表卡片训练状态用;
// IN 一次取回按 id 倒序,应用层按 dataset_id 去重;数据集表小、记录少,单次查询足够)
func (d *modelTrainingDao) LatestByDatasets(ctx context.Context, datasetIds []int64) (map[int64]*entity.ModelTraining, error) {
+1
View File
@@ -33,6 +33,7 @@ type AdminDatasetItem struct {
Description string `json:"description"` // 描述
TrainingId int64 `json:"trainingId"` // 最新训练记录 id(发布/详情用)
TrainingStatus string `json:"trainingStatus"` // 最新训练记录状态 running|success|failed|空
TrainingError string `json:"trainingError"` // 最新训练记录失败原因(failed 时展示用)
TrainingCurrentEpoch int `json:"trainingCurrentEpoch"`
TrainingTotalEpochs int `json:"trainingTotalEpochs"`
CreatedAt *gtime.Time `json:"createdAt"`
+1
View File
@@ -75,6 +75,7 @@ func (s *datasetService) AdminListDatasets(ctx context.Context, req *dto.AdminDa
if t, ok := latest[v.Id]; ok {
item.TrainingId = t.Id
item.TrainingStatus = t.Status
item.TrainingError = t.Error
item.TrainingCurrentEpoch = t.CurrentEpoch
item.TrainingTotalEpochs = t.TotalEpochs
}
+19 -6
View File
@@ -31,6 +31,9 @@ var Training = &trainingService{}
// 单协程生命周期任务(非并行工作负载),不做池封装。
func (s *trainingService) StartBackgroundJobs(ctx context.Context) {
LabelTask.recoverLabelTasks(ctx)
if err := dao.Training.FailUnstarted(ctx); err != nil {
g.Log().Errorf(ctx, "恢复未启动训练任务失败: %+v", err)
}
go s.pollTrainings(ctx)
}
@@ -92,12 +95,16 @@ func (s *trainingService) pollOne(ctx context.Context, runner common.TrainingRun
s.finishSuccess(ctx, runner, job, t, result, tail)
return
}
alive, err := runner.IsAlive(ctx, job)
if err != nil {
g.Log().Errorf(ctx, "训练 %d 存活探测失败: %+v", t.Id, err)
return
alive := true
if t.Pid != 0 {
a, err := runner.IsAlive(ctx, job)
if err != nil {
g.Log().Errorf(ctx, "训练 %d 存活探测失败: %+v", t.Id, err)
return
}
alive = a
}
// 超时判死(started_at 起算)
// 超时判死(started_at 起算;含发起准备阶段
cfg, _ := common.TrainingConfigOf(ctx)
timeout := time.Duration(cfg.TimeoutMins) * time.Minute
if t.StartedAt != nil && time.Since(t.StartedAt.Time) > timeout {
@@ -105,6 +112,10 @@ func (s *trainingService) pollOne(ctx context.Context, runner common.TrainingRun
_ = s.finishFailed(ctx, t, "训练超时(超过 %d 分钟无结果,已终止)", cfg.TimeoutMins)
return
}
// pid 未落 = 发起准备阶段(写任务参数/同步数据集/启动进程)尚未完成,不判死
if t.Pid == 0 {
return
}
if !alive {
_ = s.finishFailed(ctx, t, "训练进程已退出(无结果文件)")
return
@@ -186,7 +197,9 @@ func mergeNamesIntoMetrics(metricsJSON, namesJSON string) string {
}
func (s *trainingService) finishFailed(ctx context.Context, t *entity.ModelTraining, format string, args ...any) error {
return dao.Training.Finish(ctx, t.Id, consts.TrainingStatusFailed, "", "", fmt.Sprintf(format, args...))
msg := fmt.Sprintf(format, args...)
g.Log().Errorf(ctx, "训练 %d 置失败: %s", t.Id, msg)
return dao.Training.Finish(ctx, t.Id, consts.TrainingStatusFailed, "", "", msg)
}
// buildJob 组装训练机路径布局的 runner 任务
+6 -1
View File
@@ -278,7 +278,12 @@ onBeforeUnmount(() => {
</div>
</template>
<template v-else>
<el-tag :type="trainStatusTag[row.trainingStatus] || 'info'" size="small">
<el-tooltip v-if="row.trainingStatus === 'failed' && row.trainingError" :content="row.trainingError" placement="top">
<el-tag :type="trainStatusTag[row.trainingStatus] || 'info'" size="small">
{{ trainStatusMap[row.trainingStatus] || row.trainingStatus }}
</el-tag>
</el-tooltip>
<el-tag v-else :type="trainStatusTag[row.trainingStatus] || 'info'" size="small">
{{ trainStatusMap[row.trainingStatus] || row.trainingStatus }}
</el-tag>
<el-button v-if="row.trainingStatus === 'success' && row.trainingId" size="small" type="success" @click="publishModel(row)">