fix(ticket): harden lightweight backend core

This commit is contained in:
mlogclub
2026-05-02 19:19:18 +08:00
parent a06ce7074e
commit fa9d431cbe
9 changed files with 232 additions and 51 deletions
+52 -8
View File
@@ -5,6 +5,7 @@ import (
"cs-agent/internal/repositories"
"fmt"
"strings"
"sync"
"time"
"gorm.io/gorm"
@@ -12,6 +13,8 @@ import (
var TicketNoService = newTicketNoService()
var ticketNoSQLiteMu sync.Mutex
func newTicketNoService() *ticketNoService {
return &ticketNoService{}
}
@@ -22,9 +25,25 @@ func (s *ticketNoService) Next(tx *gorm.DB, now time.Time) (string, error) {
if tx == nil {
return "", fmt.Errorf("ticket number transaction is required")
}
if tx.Dialector.Name() == "sqlite" {
ticketNoSQLiteMu.Lock()
defer ticketNoSQLiteMu.Unlock()
return s.nextWithRetry(tx, now)
}
return s.nextWithRetry(tx, now)
}
func (s *ticketNoService) nextWithRetry(tx *gorm.DB, now time.Time) (string, error) {
dateKey := now.Format("20060102")
for attempt := 0; attempt < 20; attempt++ {
current := repositories.TicketNoSequenceRepository.GetByDateKey(tx, dateKey)
for attempt := 0; attempt < 100; attempt++ {
current, err := repositories.TicketNoSequenceRepository.GetByDateKeyForUpdate(tx, dateKey)
if err != nil {
if isRetriableTicketNoError(err) {
sleepTicketNoRetry(attempt)
continue
}
return "", err
}
if current == nil {
item := &models.TicketNoSequence{
DateKey: dateKey,
@@ -39,14 +58,25 @@ func (s *ticketNoService) Next(tx *gorm.DB, now time.Time) (string, error) {
if !isRetriableTicketNoError(err) {
return "", err
}
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
continue
current, err = repositories.TicketNoSequenceRepository.GetByDateKeyForUpdate(tx, dateKey)
if err != nil {
if isRetriableTicketNoError(err) {
sleepTicketNoRetry(attempt)
continue
}
return "", err
}
if current == nil {
sleepTicketNoRetry(attempt)
continue
}
}
allocated := current.NextSeq
ok, err := repositories.TicketNoSequenceRepository.UpdateNextSeq(tx, current.ID, current.NextSeq, current.NextSeq+1, now)
ok, err := repositories.TicketNoSequenceRepository.UpdateNextSeq(tx, current.ID, allocated, allocated+1, now)
if err != nil {
if isRetriableTicketNoError(err) {
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
sleepTicketNoRetry(attempt)
continue
}
return "", err
@@ -54,11 +84,19 @@ func (s *ticketNoService) Next(tx *gorm.DB, now time.Time) (string, error) {
if ok {
return formatTicketNo(dateKey, allocated), nil
}
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
sleepTicketNoRetry(attempt)
}
return "", fmt.Errorf("failed to allocate ticket number")
}
func sleepTicketNoRetry(attempt int) {
delay := time.Duration(attempt+1) * 10 * time.Millisecond
if delay > 200*time.Millisecond {
delay = 200 * time.Millisecond
}
time.Sleep(delay)
}
func formatTicketNo(dateKey string, seq int64) string {
return fmt.Sprintf("TK%s%05d", dateKey, seq)
}
@@ -80,5 +118,11 @@ func isDatabaseLockedError(err error) bool {
return false
}
message := strings.ToLower(err.Error())
return strings.Contains(message, "database is locked") || strings.Contains(message, "database table is locked")
return strings.Contains(message, "database is locked") ||
strings.Contains(message, "database table is locked") ||
strings.Contains(message, "database is busy") ||
strings.Contains(message, "lock wait timeout") ||
strings.Contains(message, "deadlock") ||
strings.Contains(message, "try restarting transaction") ||
strings.Contains(message, "could not serialize")
}