feat: harden multi-user download concurrency

This commit is contained in:
OpenClaw Agent
2026-09-18 09:06:59 +08:00
parent 0735cb2e09
commit b116303abe
11 changed files with 1003 additions and 202 deletions
+60 -33
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"gorm.io/gorm"
@@ -16,50 +17,76 @@ var (
// reserveCredit places one credit on hold for a task. The hold is released on
// failure or converted into a debit after a successful new download.
func (a *App) reserveCredit(userID, taskID uint, amount int64) (*CreditReservation, error) {
func reserveCreditTx(tx *gorm.DB, userID, taskID uint, amount int64) (*CreditReservation, error) {
if amount <= 0 {
return nil, fmt.Errorf("invalid credit amount")
}
var reservation CreditReservation
err := a.db.Transaction(func(tx *gorm.DB) error {
var account CreditAccount
if err := tx.Where("user_id = ?", userID).First(&account).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
account = CreditAccount{UserID: userID}
if err := tx.Create(&account).Error; err != nil {
return err
var account CreditAccount
if err := tx.Where("user_id = ?", userID).First(&account).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
account = CreditAccount{UserID: userID}
if err := tx.Create(&account).Error; err != nil {
if err := tx.Where("user_id = ?", userID).First(&account).Error; err != nil {
return nil, err
}
}
var existing CreditReservation
if err := tx.Where("task_id = ? AND status = ?", taskID, "reserved").Order("id desc").First(&existing).Error; err == nil {
reservation = existing
return nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if account.Balance-account.Reserved < amount {
return errInsufficientCredits
}
account.Reserved += amount
if err := tx.Save(&account).Error; err != nil {
return err
}
reservation = CreditReservation{UserID: userID, TaskID: taskID, Amount: amount, Status: "reserved"}
if err := tx.Create(&reservation).Error; err != nil {
return err
}
return tx.Create(&CreditTransaction{UserID: userID, Type: "reserve", Amount: 0, BalanceAfter: account.Balance, ReferenceType: "task", ReferenceID: strconv.FormatUint(uint64(taskID), 10), Reason: "download reservation"}).Error
}
var existing CreditReservation
if err := tx.Where("task_id = ? AND status = ?", taskID, "reserved").
Order("id desc").First(&existing).Error; err == nil {
return &existing, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if account.Balance-account.Reserved < amount {
return nil, errInsufficientCredits
}
account.Reserved += amount
if err := tx.Save(&account).Error; err != nil {
return nil, err
}
reservation := &CreditReservation{
UserID: userID, TaskID: taskID, Amount: amount, Status: "reserved",
}
if err := tx.Create(reservation).Error; err != nil {
return nil, err
}
if err := tx.Create(&CreditTransaction{
UserID: userID, Type: "reserve", Amount: 0,
BalanceAfter: account.Balance, ReferenceType: "task",
ReferenceID: strconv.FormatUint(uint64(taskID), 10),
Reason: "download reservation",
}).Error; err != nil {
return nil, err
}
return reservation, nil
}
func isSQLiteBusy(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "sqlite_busy") || strings.Contains(msg, "database is locked")
}
func (a *App) reserveCredit(userID, taskID uint, amount int64) (*CreditReservation, error) {
var reservation *CreditReservation
err := a.transactionRetry(func(tx *gorm.DB) error {
var txErr error
reservation, txErr = reserveCreditTx(tx, userID, taskID, amount)
return txErr
})
return &reservation, err
return reservation, err
}
func (a *App) commitCredit(task *Task) error {
if task.CreditReservationID == 0 {
return nil
}
return a.db.Transaction(func(tx *gorm.DB) error {
return a.transactionRetry(func(tx *gorm.DB) error {
var r CreditReservation
if err := tx.First(&r, task.CreditReservationID).Error; err != nil {
return err
@@ -97,7 +124,7 @@ func (a *App) refundCredit(task *Task) error {
if task.CreditReservationID == 0 {
return nil
}
return a.db.Transaction(func(tx *gorm.DB) error {
return a.transactionRetry(func(tx *gorm.DB) error {
var r CreditReservation
if err := tx.First(&r, task.CreditReservationID).Error; err != nil {
return err