fix: isolate deletion notifications into per-user transactions

On Postgres, a failed operation puts the transaction in an error state
where subsequent operations fail. The previous loop with continue would
keep trying to use a broken transaction. Each user now gets its own
transaction so a single notification failure doesn't affect others.
This commit is contained in:
kolaente 2026-02-24 13:13:58 +01:00
parent 312648d7d6
commit eea59c33c7
3 changed files with 30 additions and 21 deletions

View File

@ -28,10 +28,10 @@ import (
type disabledMailNotifiable struct{}
func (d *disabledMailNotifiable) RouteForMail() (string, error) { return "test@example.com", nil }
func (d *disabledMailNotifiable) RouteForDB() int64 { return 1 }
func (d *disabledMailNotifiable) RouteForMail() (string, error) { return "test@example.com", nil }
func (d *disabledMailNotifiable) RouteForDB() int64 { return 1 }
func (d *disabledMailNotifiable) ShouldNotify(_ ...*xorm.Session) (bool, error) { return true, nil }
func (d *disabledMailNotifiable) Lang() string { return "en" }
func (d *disabledMailNotifiable) Lang() string { return "en" }
type disabledMailNotification struct{}

View File

@ -17,6 +17,7 @@
package user
import (
"fmt"
"time"
"code.vikunja.io/api/pkg/cron"
@ -50,6 +51,9 @@ func notifyUsersScheduledForDeletion() {
return
}
// Close the read-only session before processing each user in its own transaction.
s.Close()
log.Debugf("Found %d users scheduled for deletion to notify", len(users))
for _, user := range users {
@ -67,27 +71,33 @@ func notifyUsersScheduledForDeletion() {
log.Debugf("Notifying user %d of the deletion of their account...", user.ID)
err = notifications.Notify(user, &AccountDeletionNotification{
User: user,
NotificationNumber: number,
}, s)
if err != nil {
log.Errorf("Could not notify user %d of their deletion: %s", user.ID, err)
continue
}
user.DeletionLastReminderSent = time.Now()
_, err = s.Where("id = ?", user.ID).
Cols("deletion_last_reminder_sent").
Update(user)
if err != nil {
log.Errorf("Could update user %d last deletion reminder sent date: %s", user.ID, err)
if err := notifyUserOfDeletion(user, number); err != nil {
log.Errorf("Could not process deletion notification for user %d: %s", user.ID, err)
}
}
}
if err := s.Commit(); err != nil {
log.Errorf("Could not commit user deletion notifications: %s", err)
func notifyUserOfDeletion(user *User, number int) error {
s := db.NewSession()
defer s.Close()
err := notifications.Notify(user, &AccountDeletionNotification{
User: user,
NotificationNumber: number,
}, s)
if err != nil {
return fmt.Errorf("could not notify user: %w", err)
}
user.DeletionLastReminderSent = time.Now()
_, err = s.Where("id = ?", user.ID).
Cols("deletion_last_reminder_sent").
Update(user)
if err != nil {
return fmt.Errorf("could not update last deletion reminder sent date: %w", err)
}
return s.Commit()
}
// RequestDeletion creates a user deletion confirm token and sends a notification to the user

View File

@ -93,7 +93,6 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (userID int64, err err
return
}
// PasswordTokenRequest defines the request format for password reset resqest
type PasswordTokenRequest struct {
Email string `json:"email" valid:"email,length(0|250)" maxLength:"250"`