fix(ai): allow stopping and deleting restarting model containers (#14250)

This commit is contained in:
ssongliu
2026-10-30 12:52:32 +08:00
committed by GitHub
parent ecb264f087
commit 01feaa6059
5 changed files with 58 additions and 20 deletions
+32 -5
View File
@@ -259,6 +259,15 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
if err != nil {
return err
}
if install.Status == constant.StatusRestarting && (req.Operate == constant.Stop || req.Operate == constant.Delete) && !req.ForceDelete {
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart))
if err != nil {
return err
}
if count > 0 {
return buserr.New("TaskIsExecuting")
}
}
if !req.ForceDelete && !files.NewFileOp().Stat(install.GetPath()) {
return buserr.New("ErrInstallDirNotFound")
}
@@ -342,9 +351,6 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
return errors.New("lifecycle script operation not supported")
}
install.Message = ""
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
@@ -355,10 +361,17 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
}
return err
}
ready := make(chan error, 1)
started := make(chan struct{})
operation := string(req.Operate)
operationTask.AddSubTaskWithOps(
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
func(t *task.Task) error {
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
close(started)
ready <- nil
if err := runScript(t, &install, operation); err != nil {
return err
}
@@ -385,6 +398,12 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
)
go func() {
if taskErr := operationTask.Execute(); taskErr != nil {
select {
case <-started:
default:
ready <- taskErr
return
}
if onFailure != nil {
onFailure(taskErr)
return
@@ -394,7 +413,7 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
_ = appInstallRepo.Save(context.Background(), &install)
}
}()
return nil
return <-ready
}
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
@@ -1036,8 +1055,16 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
}
switch appInstall.Status {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
constant.StatusStarting, constant.StatusWaiting:
return nil
case constant.StatusRestarting:
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(appInstall.ID), taskRepo.WithOperate(task.TaskRestart))
if err != nil {
return err
}
if count > 0 {
return nil
}
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
if !force {
return nil
+2
View File
@@ -316,6 +316,8 @@ func (t *Task) ExecuteQueued(slots chan struct{}, waitingStep string, onFinish f
func (t *Task) Execute() error {
if err := t.taskRepo.Save(context.Background(), t.Task); err != nil {
_ = t.logFile.Close()
global.RemoveTaskCancel(t.TaskID)
return err
}
return t.executeRegistered()
@@ -357,7 +357,9 @@ const deploymentButtons = [
{
label: i18n.global.t('commons.button.delete'),
permission: true,
disabled: (row: Deployment) => operationDisabled(row) && row.status !== 'Uninstalling',
disabled: (row: Deployment) =>
deleteLoading.value ||
(operationDisabled(row) && !['Uninstalling', 'ReStarting', 'Restarting'].includes(row.status)),
click: (row: Deployment) => {
deleting.value = row;
forceDelete.value = false;
+20 -13
View File
@@ -230,8 +230,13 @@
</el-table-column>
<el-table-column :label="$t('commons.table.status')" min-width="150">
<template #default="{ row }">
<el-dropdown placement="bottom" :disabled="statusDisabled(row)">
<Status v-permission :status="row.status" operate :disabled="statusDisabled(row)" />
<el-dropdown placement="bottom" :disabled="statusDisabled(row, 'stop')">
<Status
v-permission
:status="row.status"
operate
:disabled="statusDisabled(row, 'stop')"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@@ -242,7 +247,8 @@
</el-dropdown-item>
<el-dropdown-item
:disabled="
statusDisabled(row) || ['Stopped', 'Exited'].includes(row.status)
statusDisabled(row, 'stop') ||
['Stopped', 'Exited'].includes(row.status)
"
@click="operate(row, 'stop')"
>
@@ -569,22 +575,23 @@ const accountButtons = [
];
const taskRef = ref();
const pending = ref('');
const operationDisabled = (row: Deployment) =>
const operationDisabled = (row: Deployment, operation = '') =>
Boolean(pending.value) ||
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Rebuilding', 'Waiting', 'Uninstalling'].includes(
row.status,
);
const statusDisabled = (row: Deployment) =>
operationDisabled(row) || !hasManagePermissionAccess() || (row.engine === 'ollama' && !hasPermission('app_manage'));
['Installing', 'Starting', 'Rebuilding', 'Waiting', 'Uninstalling'].includes(row.status) ||
(['ReStarting', 'Restarting'].includes(row.status) && !['stop', 'delete'].includes(operation));
const statusDisabled = (row: Deployment, operation = '') =>
operationDisabled(row, operation) ||
!hasManagePermissionAccess() ||
(row.engine === 'ollama' && !hasPermission('app_manage'));
const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart') => {
if (statusDisabled(row)) return;
if (statusDisabled(row, operation)) return;
const node = currentNode.value;
await ElMessageBox.confirm(
i18n.global.t('app.operatorHelper', [i18n.global.t('commons.operate.' + operation)]),
i18n.global.t('commons.operate.' + operation),
{ type: 'warning' },
);
if (node !== currentNode.value || statusDisabled(row)) return;
if (node !== currentNode.value || statusDisabled(row, operation)) return;
pending.value = `${row.engine}:${row.id}`;
try {
if (row.engine === 'ollama') {
@@ -652,7 +659,7 @@ const deploymentButtons = [
label: i18n.global.t('commons.button.delete'),
permission: true,
show: (row: Deployment) => row.engine === 'ollama' && hasPermission('app_manage'),
disabled: operationDisabled,
disabled: (row: Deployment) => operationDisabled(row, 'delete'),
click: async (row: Deployment) => {
const node = currentNode.value;
const path = route.fullPath;
@@ -682,7 +689,7 @@ const deploymentButtons = [
label: i18n.global.t('commons.button.delete'),
permission: true,
show: (row: Deployment) => row.engine === 'vllm',
disabled: (row: Deployment) => operationDisabled(row) && row.status !== 'Uninstalling',
disabled: (row: Deployment) => operationDisabled(row, 'delete') && row.status !== 'Uninstalling',
click: (row: Deployment) => manageRef.value.open(row, 'delete', true),
},
];
@@ -211,7 +211,7 @@ const buttons = [
label: i18n.global.t('commons.button.stop'),
permission: true,
disabled: (row: AI.TensorRTLLM) => {
return row.status !== 'Running';
return !['Running', 'ReStarting', 'Restarting'].includes(row.status);
},
click: (row: AI.TensorRTLLM) => {
operate(row, 'stop');