Files
MyGoNavi/internal/app/import_task_runtime.go
Syngnat 390a249105 feat(data-import): 重构大文件导入与安全任务运行时
- SQL、CSV、JSON 与 XLSX 改为资源受限的流式解析,并支持压缩、编码和方言预检

- 增加遇错停止/继续策略、持久任务、错误行导出、源文件身份与安全取消

- 为数据库驱动补齐上下文事务和写入结果未知语义,避免失败重放与脏会话复用
2026-08-08 20:51:23 +08:00

161 lines
3.9 KiB
Go

package app
import (
"context"
"errors"
"strings"
"sync"
"time"
"GoNavi-Wails/internal/importjob"
"github.com/google/uuid"
)
var errImportTaskNotFound = errors.New("import task not found")
type importTaskRegistration struct {
token string
kind importjob.Kind
cancel context.CancelFunc
lifecycle *managedImportJob
stopRequested bool
cancelDispatched bool
}
func (a *App) registerImportTask(jobID string, cancel context.CancelFunc, kinds ...importjob.Kind) (func(), bool) {
kind := importjob.Kind("")
if len(kinds) > 0 {
kind = kinds[0]
}
a.importTaskMu.Lock()
closing := a.importTasksClosing
a.importTaskMu.Unlock()
if closing {
return func() {}, false
}
cleanupQuery, registered := a.registerExclusiveRunningQuery(jobID, cancel, true)
if !registered {
return func() {}, false
}
token := uuid.NewString()
a.importTaskMu.Lock()
if a.importTasksClosing {
a.importTaskMu.Unlock()
cleanupQuery()
return func() {}, false
}
if a.importTasks == nil {
a.importTasks = make(map[string]importTaskRegistration)
}
a.importTasks[jobID] = importTaskRegistration{token: token, kind: kind, cancel: cancel}
a.importTasksWG.Add(1)
a.importTaskMu.Unlock()
var once sync.Once
return func() {
once.Do(func() {
cleanupQuery()
a.importTaskMu.Lock()
if current, exists := a.importTasks[jobID]; exists && current.token == token {
delete(a.importTasks, jobID)
}
a.importTaskMu.Unlock()
a.importTasksWG.Done()
})
}, true
}
// bindImportTaskLifecycle attaches durable state after task registration. A
// cancel request that arrives while the durable job is still being created is
// remembered and persisted as stopping immediately after the bind completes.
func (a *App) bindImportTaskLifecycle(jobID string, kind importjob.Kind, lifecycle *managedImportJob) (bool, error) {
jobID = strings.TrimSpace(jobID)
if lifecycle == nil {
return false, errors.New("import job lifecycle is unavailable")
}
a.importTaskMu.Lock()
task, exists := a.importTasks[jobID]
if !exists {
a.importTaskMu.Unlock()
return false, nil
}
if task.kind != "" && kind != "" && task.kind != kind {
a.importTaskMu.Unlock()
return false, errors.New("import task kind does not match durable job kind")
}
if kind != "" {
task.kind = kind
}
task.lifecycle = lifecycle
stopRequested := task.stopRequested
a.importTasks[jobID] = task
a.importTaskMu.Unlock()
if stopRequested {
if err := lifecycle.requestStop(); err != nil {
return true, err
}
}
return true, nil
}
// requestImportTaskCancellation only resolves registrations owned by the
// import runtime. It intentionally never falls back to runningQueries, where
// ordinary query executions are tracked too.
func (a *App) requestImportTaskCancellation(jobID string, expectedKind importjob.Kind) error {
jobID = strings.TrimSpace(jobID)
a.importTaskMu.Lock()
task, exists := a.importTasks[jobID]
if !exists || (expectedKind != "" && task.kind != expectedKind) {
a.importTaskMu.Unlock()
return errImportTaskNotFound
}
task.stopRequested = true
var cancel context.CancelFunc
if !task.cancelDispatched {
task.cancelDispatched = true
cancel = task.cancel
}
lifecycle := task.lifecycle
a.importTasks[jobID] = task
a.importTaskMu.Unlock()
var stopErr error
if lifecycle != nil {
stopErr = lifecycle.requestStop()
}
if cancel != nil {
cancel()
}
return stopErr
}
func (a *App) cancelAndWaitImportTasks(timeout time.Duration) bool {
a.importTaskMu.Lock()
a.importTasksClosing = true
jobIDs := make([]string, 0, len(a.importTasks))
for jobID := range a.importTasks {
jobIDs = append(jobIDs, jobID)
}
a.importTaskMu.Unlock()
for _, jobID := range jobIDs {
_ = a.requestImportTaskCancellation(jobID, "")
}
done := make(chan struct{})
go func() {
a.importTasksWG.Wait()
close(done)
}()
if timeout <= 0 {
<-done
return true
}
select {
case <-done:
return true
case <-time.After(timeout):
return false
}
}