feat: harden multi-user download concurrency
This commit is contained in:
+108
-4
@@ -1,21 +1,125 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// recoverMissingLocalUpload re-queues a task when its shared media file has
|
||||
// disappeared. This can happen after an older version deleted a shared file
|
||||
// after another user's upload. The asset is invalidated so the next worker
|
||||
// downloads it again instead of retrying an impossible WebDAV upload forever.
|
||||
func (a *App) recoverMissingLocalUpload(t *Task) bool {
|
||||
if t.AssetID == 0 || t.LocalPath == "" {
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat(t.LocalPath); err == nil || !os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
var queued bool
|
||||
err := a.transactionRetry(func(tx *gorm.DB) error {
|
||||
var asset MediaAsset
|
||||
if err := tx.First(&asset, t.AssetID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if asset.Status == "done" && asset.LocalPath != "" {
|
||||
if _, statErr := os.Stat(asset.LocalPath); statErr == nil {
|
||||
if err := tx.Model(&Task{}).Where("id = ?", t.ID).Updates(map[string]any{
|
||||
"local_path": asset.LocalPath, "format": asset.Format,
|
||||
"actual_quality": asset.ActualQuality,
|
||||
"status": "upload_pending", "error": "共享资源已恢复,正在继续上传",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
queued = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if asset.LocalPath != "" && asset.LocalPath != t.LocalPath {
|
||||
return nil
|
||||
}
|
||||
asset.Status = "error"
|
||||
asset.OwnerTaskID = 0
|
||||
asset.LeaseUntil = time.Time{}
|
||||
asset.LocalPath = ""
|
||||
asset.Size = 0
|
||||
if err := tx.Save(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&Task{}).Where("id = ?", t.ID).Updates(map[string]any{
|
||||
"status": "pending", "error": "本地共享文件不存在,正在重新下载",
|
||||
"progress": 0, "downloaded_bytes": 0,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
queued = true
|
||||
return nil
|
||||
})
|
||||
if err != nil || !queued {
|
||||
return false
|
||||
}
|
||||
a.enqueueTask(t.ID)
|
||||
return true
|
||||
}
|
||||
|
||||
// resyncPendingUploads repairs tasks that finished locally while WebDAV was
|
||||
// unavailable or the user's WebDAV permission was not yet configured.
|
||||
func (a *App) resyncPendingUploads() {
|
||||
var tasks []Task
|
||||
if err := a.db.Where("status IN ? AND local_path <> ? AND (remote_path = ? OR remote_path IS NULL)", []string{"done", "upload_pending", "error"}, "", "").Order("id asc").Limit(100).Find(&tasks).Error; err != nil {
|
||||
if err := a.db.Where("local_path <> ? AND ((status IN ? AND (remote_path = '' OR remote_path IS NULL)) OR status = ?)",
|
||||
"", []string{"done", "upload_pending"}, "error").Order("id asc").Limit(100).Find(&tasks).Error; err != nil {
|
||||
return
|
||||
}
|
||||
for i := range tasks {
|
||||
t := tasks[i]
|
||||
if t.Status == "error" {
|
||||
status, err := a.taskReservationStatus(t.ID, t.CreditReservationID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if status == "committed" {
|
||||
if err := a.repairCommittedTask(&t); err != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
var asset MediaAsset
|
||||
if t.AssetID == 0 || a.db.First(&asset, t.AssetID).Error != nil ||
|
||||
asset.Status != "done" || asset.LocalPath == "" {
|
||||
continue
|
||||
}
|
||||
if _, statErr := os.Stat(asset.LocalPath); statErr != nil {
|
||||
continue
|
||||
}
|
||||
t.LocalPath = asset.LocalPath
|
||||
t.Format = asset.Format
|
||||
t.ActualQuality = asset.ActualQuality
|
||||
t.Status = "upload_pending"
|
||||
t.Error = "共享资源已完成,历史失败任务恢复上传"
|
||||
if err := a.db.Model(&t).Updates(map[string]any{
|
||||
"local_path": t.LocalPath, "format": t.Format,
|
||||
"actual_quality": t.ActualQuality,
|
||||
"status": t.Status, "error": t.Error,
|
||||
}).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, _, ok := a.storageForTask(&t); !ok {
|
||||
continue
|
||||
}
|
||||
a.upload(&t)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if a.recoverMissingLocalUpload(&t) {
|
||||
continue
|
||||
}
|
||||
var current Task
|
||||
if a.db.First(¤t, t.ID).Error != nil {
|
||||
continue
|
||||
}
|
||||
if current.Status == "done" || current.Status == "upload_pending" {
|
||||
a.enqueueTask(current.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user