refactor: update task refund logic and remove legacy handling
This commit is contained in:
+3
-101
@@ -41,9 +41,9 @@ const (
|
||||
TaskStatusUnknown = "UNKNOWN"
|
||||
)
|
||||
|
||||
// TaskRefundLegacyCutoff separates legacy timeout tasks that intentionally
|
||||
// do not receive automatic refunds from tasks covered by reconciliation.
|
||||
const TaskRefundLegacyCutoff int64 = 1740182400 // 2025-02-22 00:00:00 UTC
|
||||
// TaskRefundLegacyCutoff separates tasks created before timeout refunds were
|
||||
// introduced. Those legacy tasks are failed without an automatic refund.
|
||||
const TaskRefundLegacyCutoff int64 = 1771718400 // 2026-02-22 00:00:00 UTC
|
||||
|
||||
type Task struct {
|
||||
ID int64 `json:"id" gorm:"primary_key;AUTO_INCREMENT"`
|
||||
@@ -308,28 +308,6 @@ func GetTimedOutUnfinishedTasks(cutoffUnix int64, limit int) []*Task {
|
||||
return tasks
|
||||
}
|
||||
|
||||
// GetUnrefundedFailedTasks returns failed tasks whose non-zero quota marks a
|
||||
// pending refund. Legacy timeout tasks are excluded before LIMIT is applied so
|
||||
// they cannot starve refundable tasks from the reconciliation sweep.
|
||||
func GetUnrefundedFailedTasks(updatedBefore int64, limit int) []*Task {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tasks []*Task
|
||||
err := DB.Where("status = ?", TaskStatusFailure).
|
||||
Where("quota != ?", 0).
|
||||
Where("updated_at <= ?", updatedBefore).
|
||||
Where("(submit_time <= ? OR submit_time >= ?)", 0, TaskRefundLegacyCutoff).
|
||||
Order("id").
|
||||
Limit(limit).
|
||||
Find(&tasks).Error
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func GetAllUnFinishSyncTasks(limit int) []*Task {
|
||||
var tasks []*Task
|
||||
var err error
|
||||
@@ -356,38 +334,6 @@ func HasUnfinishedSyncTasks() bool {
|
||||
return err == nil && id != 0
|
||||
}
|
||||
|
||||
// HasTaskPollingWork reports whether polling has either an unfinished task or
|
||||
// a failed task with a pending, non-legacy refund. The latter keeps the system
|
||||
// task scheduler active when reconciliation is the only work left.
|
||||
func HasTaskPollingWork() bool {
|
||||
if HasUnfinishedSyncTasks() {
|
||||
return true
|
||||
}
|
||||
|
||||
var id int64
|
||||
err := DB.Model(&Task{}).
|
||||
Where("status = ?", TaskStatusFailure).
|
||||
Where("quota != ?", 0).
|
||||
Where("(submit_time <= ? OR submit_time >= ?)", 0, TaskRefundLegacyCutoff).
|
||||
Limit(1).
|
||||
Pluck("id", &id).Error
|
||||
return err == nil && id != 0
|
||||
}
|
||||
|
||||
func GetByOnlyTaskId(taskId string) (*Task, bool, error) {
|
||||
if taskId == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
var task *Task
|
||||
var err error
|
||||
err = DB.Where("task_id = ?", taskId).First(&task).Error
|
||||
exist, err := RecordExist(err)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return task, exist, err
|
||||
}
|
||||
|
||||
func GetByTaskId(userId int, taskId string) (*Task, bool, error) {
|
||||
if taskId == "" {
|
||||
return nil, false, nil
|
||||
@@ -465,39 +411,6 @@ func (t *Task) UpdateQuota() error {
|
||||
return DB.Model(t).Update("quota", t.Quota).Error
|
||||
}
|
||||
|
||||
// ClaimQuotaForRefund atomically clears an expected non-zero quota. A true
|
||||
// result grants the caller ownership of the corresponding refund attempt.
|
||||
func ClaimQuotaForRefund(id int64, expectedQuota int) (bool, error) {
|
||||
if expectedQuota == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
result := DB.Model(&Task{}).
|
||||
Where("id = ? AND quota = ?", id, expectedQuota).
|
||||
Update("quota", 0)
|
||||
if result.Error != nil {
|
||||
return false, result.Error
|
||||
}
|
||||
return result.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// RestoreQuotaAfterFailedRefund restores a claimed quota marker only while it
|
||||
// is still zero. It is used when the observable funding adjustment fails, so a
|
||||
// later reconciliation pass can retry without overwriting another writer.
|
||||
func RestoreQuotaAfterFailedRefund(id int64, quota int) (bool, error) {
|
||||
if quota == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
result := DB.Model(&Task{}).
|
||||
Where("id = ? AND quota = ?", id, 0).
|
||||
Update("quota", quota)
|
||||
if result.Error != nil {
|
||||
return false, result.Error
|
||||
}
|
||||
return result.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// UpdateWithStatus performs a conditional UPDATE guarded by fromStatus (CAS).
|
||||
// Returns (true, nil) if this caller won the update, (false, nil) if
|
||||
// another process already moved the task out of fromStatus. MySQL commonly
|
||||
@@ -515,17 +428,6 @@ func (t *Task) UpdateWithStatus(fromStatus TaskStatus) (bool, error) {
|
||||
return result.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// TaskBulkUpdate performs an unconditional bulk UPDATE by upstream task_id strings.
|
||||
// Same caveats as TaskBulkUpdateByID — no CAS guard.
|
||||
func TaskBulkUpdate(taskIds []string, params map[string]any) error {
|
||||
if len(taskIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
return DB.Model(&Task{}).
|
||||
Where("task_id in (?)", taskIds).
|
||||
Updates(params).Error
|
||||
}
|
||||
|
||||
// TaskBulkUpdateByID performs an unconditional bulk UPDATE by primary key IDs.
|
||||
// WARNING: This function has NO CAS (Compare-And-Swap) guard — it will overwrite
|
||||
// any concurrent status changes. DO NOT use in billing/quota lifecycle flows
|
||||
|
||||
Reference in New Issue
Block a user