package mysql import ( "context" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" filemanager "wucher/internal/domain/file_manager" ) func (r *FileManagerFileRepository) WithTransaction( ctx context.Context, fn func(repo filemanager.FileRepository, outbox filemanager.FileProcessingOutboxWriter) error, ) error { if fn == nil { return nil } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { txRepo := &FileManagerFileRepository{db: tx} return fn(txRepo, txRepo) }) } func (r *FileManagerFileRepository) CreateFileProcessingOutboxMessage(ctx context.Context, message *filemanager.FileProcessingOutboxMessage) error { if message == nil { return gorm.ErrInvalidData } return r.db.WithContext(ctx).Create(message).Error } func (r *FileManagerFileRepository) ClaimPendingFileProcessingOutboxMessages( ctx context.Context, limit int, now time.Time, lockTTL time.Duration, workerID string, ) ([]filemanager.FileProcessingOutboxMessage, error) { if limit <= 0 { limit = 1 } if lockTTL <= 0 { lockTTL = time.Minute } now = now.UTC() staleBefore := now.Add(-lockTTL) workerID = strings.TrimSpace(workerID) if workerID == "" { workerID = "worker" } var messages []filemanager.FileProcessingOutboxMessage err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx. Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}). Where( "((status = ? AND available_at <= ?) OR (status = ? AND locked_at IS NOT NULL AND locked_at < ?))", filemanager.FileProcessingOutboxStatusPending, now, filemanager.FileProcessingOutboxStatusProcessing, staleBefore, ). Order("available_at ASC, created_at ASC"). Limit(limit). Find(&messages).Error; err != nil { return err } if len(messages) == 0 { return nil } ids := make([][]byte, 0, len(messages)) for i := range messages { ids = append(ids, messages[i].ID) } if err := tx.Model(&filemanager.FileProcessingOutboxMessage{}). Where("id IN ?", ids). Updates(map[string]any{ "status": filemanager.FileProcessingOutboxStatusProcessing, "locked_at": now, "locked_by": workerID, "updated_at": now, "attempts": gorm.Expr("attempts + ?", 1), }).Error; err != nil { return err } for i := range messages { messages[i].Status = filemanager.FileProcessingOutboxStatusProcessing messages[i].Attempts++ ts := now messages[i].LockedAt = &ts messages[i].LockedBy = workerID messages[i].UpdatedAt = now } return nil }) return messages, err } func (r *FileManagerFileRepository) MarkFileProcessingOutboxMessagePublished(ctx context.Context, id []byte, publishedAt time.Time) error { publishedAt = publishedAt.UTC() return r.db.WithContext(ctx). Model(&filemanager.FileProcessingOutboxMessage{}). Where("id = ?", id). Updates(map[string]any{ "status": filemanager.FileProcessingOutboxStatusPublished, "published_at": publishedAt, "locked_at": nil, "locked_by": "", "last_error": "", "updated_at": publishedAt, }).Error } func (r *FileManagerFileRepository) MarkFileProcessingOutboxMessageRetry(ctx context.Context, id []byte, availableAt time.Time, lastErr string) error { availableAt = availableAt.UTC() return r.db.WithContext(ctx). Model(&filemanager.FileProcessingOutboxMessage{}). Where("id = ?", id). Updates(map[string]any{ "status": filemanager.FileProcessingOutboxStatusPending, "available_at": availableAt, "locked_at": nil, "locked_by": "", "last_error": truncateOutboxError(strings.TrimSpace(lastErr), 4096), "updated_at": time.Now().UTC(), }).Error } func (r *FileManagerFileRepository) MarkFileProcessingOutboxMessageDead(ctx context.Context, id []byte, failedAt time.Time, lastErr string) error { failedAt = failedAt.UTC() return r.db.WithContext(ctx). Model(&filemanager.FileProcessingOutboxMessage{}). Where("id = ?", id). Updates(map[string]any{ "status": filemanager.FileProcessingOutboxStatusDead, "available_at": failedAt, "locked_at": nil, "locked_by": "", "last_error": truncateOutboxError(strings.TrimSpace(lastErr), 4096), "updated_at": failedAt, }).Error } func (r *FileManagerFileRepository) CountPendingFileProcessingOutboxMessages(ctx context.Context, now time.Time) (int64, error) { var count int64 err := r.db.WithContext(ctx). Model(&filemanager.FileProcessingOutboxMessage{}). Where( "(status = ? AND available_at <= ?) OR status = ?", filemanager.FileProcessingOutboxStatusPending, now.UTC(), filemanager.FileProcessingOutboxStatusProcessing, ). Count(&count).Error return count, err }