Files
insar-management-system-v2/docs/archive/ANALYSIS_FRONTEND_LOCKING_2026-03-05.md
T

9.7 KiB
Raw Blame History

前端锁定问题分析(2026-03-05)

问题描述

用户报告在执行以下操作时前端没有锁定:

  1. D-InSAR 结果扫描
  2. 数据分发(复制任务)

根本原因分析

问题 1:扫描任务前端未锁定

原因:任务执行太快(< 3 秒),前端轮询间隔(3 秒)来不及捕获。

证据

[性能] Footprint 提取耗时: 1622.1ms (文件: Task_20250120_20250217_2_geo_disp)

单个文件处理只需 1.6 秒,如果只有少量文件,整个扫描任务可能在 2-3 秒内完成。

前端轮询逻辑

// useGlobalTaskControl.js
useEffect(() => {
  const interval = setInterval(() => {
    syncActiveTasks();  // 每 3 秒轮询一次
  }, 3000);
  return () => clearInterval(interval);
}, []);

时间线

T=0s:   用户点击"扫描"按钮
T=0.1s: 后端创建 SystemTask (status=PENDING)
T=0.2s: job_worker 开始执行,更新为 RUNNING
T=2.0s: 扫描完成,更新为 COMPLETED
T=3.0s: 前端第一次轮询,任务已经完成

结论:前端来不及捕获 RUNNING 状态,直接看到 COMPLETED。


问题 2:数据分发任务前端未锁定

根本原因DataCopierPanel 没有调用全局锁定机制。

证据

  1. App.jsx 中的调用对比
// IDLAutomationPanel - 有 onJobQueued 回调 ✅
<IDLAutomationPanel
    apiEndpoint={apiClient.defaults.baseURL}
    readOnly={isReadOnlyUser}
    onJobQueued={(taskId) => handleTaskStart(taskId, '任务已入队,等待处理...')}
/>

// DataMonitorPanel - 有 onTaskStart 回调 ✅
<DataMonitorPanel
    apiEndpoint={apiClient.defaults.baseURL}
    onTaskStart={handleTaskStart}
    readOnly={isReadOnlyUser}
/>

// DataCopierPanel - 没有任何回调 ❌
<DataCopierPanel
    apiEndpoint={apiClient.defaults.baseURL}
    readOnly={isReadOnlyUser}
/>
  1. DataCopierPanel 内部逻辑
// DataCopierPanel.jsx:97-133
const handleStartCopy = async () => {
    // ... 验证逻辑 ...

    setIsUploading(true);
    setLogs([]);
    setStatus('RUNNING');  // 只更新组件内部状态

    try {
        const response = await axios.post(endpoint, {
            batch_id: selectedBatchId,
            dest_dir: destDir,
            copy_statuses: copyStatuses,
        }, { withCredentials: true });
        setTaskId(response.data.task_id);  // 只保存 task_id
        // ❌ 没有调用 onJobQueued 或 onTaskStart
    } catch (error) {
        // ... 错误处理 ...
    } finally {
        setIsUploading(false);
    }
};
  1. 组件自己管理状态

DataCopierPanel 使用自己的状态管理(statustaskIdlogs),通过轮询 /tools/copy-status/${taskId} 获取进度,完全独立于全局任务控制系统

// DataCopierPanel.jsx:82-95
const fetchLogs = async () => {
    if (!taskId) return;
    try {
        const response = await axios.get(`${apiEndpoint}/tools/copy-status/${taskId}`, { withCredentials: true });
        setLogs(response.data.logs);
        const nextStatus = normalizeStatus(response.data.status);
        if (nextStatus && nextStatus !== 'UNKNOWN') {
            setStatus(nextStatus);  // 只更新组件内部状态
        }
    } catch (error) {
        console.error('Failed to load logs:', error);
    }
};

结论DataCopierPanel 是一个独立的任务管理系统,不与全局锁定机制集成。


架构对比

全局锁定机制(IDLAutomationPanel、DataMonitorPanel

用户操作 → 组件调用 API → 后端创建 SystemTask
                    ↓
        组件调用 onJobQueued(taskId)
                    ↓
        App.jsx 调用 handleTaskStart(taskId)
                    ↓
        setPendingTaskIds([...prev, taskId])
                    ↓
        useGlobalTaskControl 轮询 /tasks/active
                    ↓
        检测到 RUNNING 任务 → setIsGlobalLocked(true)
                    ↓
        任务完成 → setIsGlobalLocked(false)

独立任务管理(DataCopierPanel

用户操作 → 组件调用 API → 后端创建 SystemTask
                    ↓
        组件保存 taskId(内部状态)
                    ↓
        组件轮询 /tools/copy-status/${taskId}
                    ↓
        更新组件内部状态(status、logs)
                    ↓
        ❌ 不触发全局锁定

影响评估

扫描任务未锁定

影响等级🟢

原因

  • 扫描任务通常很快(< 3 秒)
  • 扫描是只读操作,不会修改数据
  • 即使用户在扫描时操作,也不太可能造成冲突

是否需要修复:可选

  • 如果希望用户明确感知到扫描正在进行,可以修复
  • 如果接受"快速任务不锁定"的行为,可以不修复

数据分发任务未锁定

影响等级🟡 中等

原因

  • 数据分发任务可能耗时较长(几分钟到几十分钟)
  • 用户可能在分发过程中启动其他任务
  • 可能导致资源竞争(磁盘 I/O、网络带宽)

潜在问题

  1. 用户可能同时启动多个分发任务,导致磁盘 I/O 饱和
  2. 用户可能在分发过程中启动 ENVI 工作流,导致系统卡顿
  3. 用户可能不知道分发任务正在后台运行

是否需要修复:建议修复

  • 数据分发是长时间运行的任务
  • 应该与全局锁定机制集成
  • 提升用户体验和系统稳定性

修复方案

方案 A:集成到全局锁定机制(推荐)

修改文件

  1. App.jsx
  2. DataCopierPanel.jsx

步骤

  1. 在 App.jsx 中传递回调
{leftPanelTab === 'copier' && (
    <div className="panel-content" style={{ flex: '1 1 auto', padding: 0, overflow: 'hidden' }}>
        <DataCopierPanel
            apiEndpoint={apiClient.defaults.baseURL}
            readOnly={isReadOnlyUser}
            onJobQueued={(taskId) => handleTaskStart(taskId, '数据分发任务已入队')}  // 添加此行
        />
    </div>
)}
  1. 在 DataCopierPanel.jsx 中接收并调用回调
// 修改组件签名
const DataCopierPanel = ({ apiEndpoint, readOnly = false, onJobQueued }) => {
    // ... 现有代码 ...

    const handleStartCopy = async () => {
        // ... 验证逻辑 ...

        setIsUploading(true);
        setLogs([]);
        setStatus('RUNNING');

        try {
            const response = await axios.post(endpoint, {
                batch_id: selectedBatchId,
                dest_dir: destDir,
                copy_statuses: copyStatuses,
            }, { withCredentials: true });

            const taskId = response.data.task_id;
            setTaskId(taskId);

            // 调用全局锁定回调
            if (onJobQueued) {
                onJobQueued(taskId);
            }
        } catch (error) {
            // ... 错误处理 ...
        } finally {
            setIsUploading(false);
        }
    };
};

优点

  • 与现有架构一致
  • 最小改动
  • 复用全局锁定机制

缺点

  • 组件仍然保留自己的状态管理(有一定冗余)

方案 B:完全重构为全局任务管理

修改范围:大

步骤

  1. 移除 DataCopierPanel 的内部状态管理
  2. 使用全局 activeTasksActiveTasksOverlay 显示进度
  3. 移除组件内的轮询逻辑

优点

  • 架构统一
  • 减少代码冗余

缺点

  • 改动较大
  • 可能影响现有功能
  • 需要充分测试

方案 C:保持现状,添加警告提示

修改文件DataCopierPanel.jsx

步骤 在任务运行时显示警告提示:

{status === 'RUNNING' && (
    <div className="warning-banner">
        ⚠️ 数据分发任务正在后台运行请勿关闭浏览器或启动其他耗时任务
    </div>
)}

优点

  • 改动最小
  • 不影响现有架构

缺点

  • 不解决根本问题
  • 用户仍可能启动冲突任务

扫描任务修复方案(可选)

方案 A:前端立即锁定

在提交扫描任务后立即锁定,不等待轮询:

// DataManagementPanel.jsx
const handleScanDinsarResults = async () => {
    try {
        // 立即锁定前端
        if (onTaskStart) {
            onTaskStart(null, 'SCAN_DINSAR', '正在扫描 D-InSAR 结果...');
        }

        const response = await scanDinsarResults(selectedDirs);
        const taskId = response.data.task_id;

        // 更新任务 ID
        if (onTaskStart) {
            onTaskStart(taskId, 'SCAN_DINSAR', '正在扫描 D-InSAR 结果...');
        }

        addLog('info', `扫描任务已提交: ${taskId}`);
    } catch (error) {
        // 解锁前端
        setIsGlobalLocked(false);
        addLog('error', `扫描失败: ${error.message}`);
    }
};

方案 B:后端添加最小执行时间

# job_handlers.py
async def _handle_scan_dinsar(job: SystemJobORM) -> None:
    start_time = time.time()

    # ... 执行扫描 ...

    # 确保任务至少运行 2 秒
    elapsed = time.time() - start_time
    if elapsed < 2.0:
        await asyncio.sleep(2.0 - elapsed)

推荐修复顺序

  1. 优先修复数据分发任务(方案 A

    • 影响较大
    • 修改简单
    • 风险低
  2. 可选修复扫描任务(方案 A

    • 影响较小
    • 提升用户体验
    • 风险低
  3. 长期考虑重构(方案 B

    • 统一架构
    • 减少冗余
    • 需要充分测试

总结

  • 扫描任务未锁定:任务太快,轮询来不及捕获(影响小,可选修复)
  • 数据分发任务未锁定:组件独立管理状态,未集成全局锁定(影响中,建议修复)
  • 推荐方案:为 DataCopierPanel 添加 onJobQueued 回调,集成到全局锁定机制