Files
vikunja/pkg/notifications/notification.go
kolaente 49bba7f830 fix: eliminate nested database sessions to prevent table locks
Refactor functions that created their own sessions when called from
within existing transactions, which caused "database table is locked"
errors in SQLite's shared-cache mode.

Changes:
- Add files.CreateWithSession() to reuse caller's session
- Refactor DeleteBackgroundFileIfExists() to accept session parameter
- Add variadic session parameter to notifications.Notify() and
  Notifiable.ShouldNotify() interface
- Update all Notify callers (~17 sites) to pass their session through
- Use files.CreateWithSession in SaveBackgroundFile and NewAttachment
- Fix test code to commit sessions before assertions
2026-02-25 11:03:02 +01:00

145 lines
3.7 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package notifications
import (
"encoding/json"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"xorm.io/xorm"
)
// Notification is a notification which can be sent via mail or db.
type Notification interface {
ToMail(lang string) *Mail
ToDB() interface{}
Name() string
}
type SubjectID interface {
SubjectID() int64
}
type NotificationWithSubject interface {
Notification
SubjectID
}
type ThreadID interface {
ThreadID() string
}
// Notifiable is an entity which can be notified. Usually a user.
type Notifiable interface {
// RouteForMail should return the email address this notifiable has.
RouteForMail() (string, error)
// RouteForDB should return the id of the notifiable entity to save it in the database.
RouteForDB() int64
// ShouldNotify provides a last-minute way to cancel a notification. It will be called immediately before
// sending a notification. An optional session can be passed to reuse an existing transaction.
ShouldNotify(sessions ...*xorm.Session) (should bool, err error)
// Lang provides the language which should be used for translations in the mail.
Lang() string
}
// Notify notifies a notifiable of a notification.
// An optional xorm session can be passed to reuse an existing transaction for the DB notification.
func Notify(notifiable Notifiable, notification Notification, sessions ...*xorm.Session) (err error) {
if isUnderTest {
sentTestNotifications = append(sentTestNotifications, notification)
return nil
}
should, err := notifiable.ShouldNotify(sessions...)
if err != nil || !should {
log.Debugf("Not notifying user %d because they are disabled", notifiable.RouteForDB())
return err
}
err = notifyMail(notifiable, notification)
if err != nil {
return
}
var s *xorm.Session
if len(sessions) > 0 && sessions[0] != nil {
s = sessions[0]
}
return notifyDB(notifiable, notification, s)
}
func notifyMail(notifiable Notifiable, notification Notification) error {
mail := notification.ToMail(notifiable.Lang())
if mail == nil {
return nil
}
to, err := notifiable.RouteForMail()
if err != nil {
return err
}
mail.To(to)
if threadID, is := notification.(ThreadID); is {
mail.ThreadID(threadID.ThreadID())
}
return SendMail(mail, notifiable.Lang())
}
func notifyDB(notifiable Notifiable, notification Notification, existingSession *xorm.Session) (err error) {
dbContent := notification.ToDB()
if dbContent == nil {
return nil
}
content, err := json.Marshal(dbContent)
if err != nil {
return err
}
dbNotification := &DatabaseNotification{
NotifiableID: notifiable.RouteForDB(),
Notification: content,
Name: notification.Name(),
}
if subject, is := notification.(SubjectID); is {
dbNotification.SubjectID = subject.SubjectID()
}
if existingSession != nil {
_, err = existingSession.Insert(dbNotification)
return err
}
s := db.NewSession()
defer s.Close()
_, err = s.Insert(dbNotification)
if err != nil {
_ = s.Rollback()
return err
}
return s.Commit()
}