Implemented retry logic on all transactions

This commit is contained in:
Jason Streifling 2024-03-15 18:37:24 +01:00
parent 6d3a28a6ce
commit c45df4bf1a
4 changed files with 118 additions and 75 deletions

View File

@ -3,9 +3,14 @@ package model
import (
"fmt"
"log"
"math"
"math/rand/v2"
"time"
)
func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error {
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
@ -28,6 +33,17 @@ func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) {

View File

@ -18,20 +18,16 @@ import (
var TxMaxRetries = 3
type DB struct {
*sql.DB
}
type Tx struct {
*sql.Tx
}
type Attribute struct {
type (
DB struct{ *sql.DB }
Tx struct{ *sql.Tx }
Attribute struct {
Value interface{}
Table string
AttName string
ID int64
}
)
func getUsername() (string, error) {
user := os.Getenv("DB_USER")
@ -126,8 +122,8 @@ func (db *DB) UpdateAttributes(a ...*Attribute) error {
if err == nil {
return nil
}
log.Println(err)
log.Println(err)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)

View File

@ -3,6 +3,9 @@ package model
import (
"fmt"
"log"
"math"
"math/rand/v2"
"time"
"golang.org/x/crypto/bcrypt"
)
@ -135,3 +138,61 @@ func (db *DB) GetUser(id int64) (*User, error) {
return user, nil
}
func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
return fmt.Errorf("error: passwords do not match")
}
passwordEmpty = false
}
tx := new(Tx)
var err error
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx.Tx, err = db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
if !passwordEmpty {
if err = tx.ChangePassword(id, oldPass, newPass); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error changing password: %v", err)
}
}
if err = tx.UpdateAttributes(
&Attribute{Table: "users", ID: id, AttName: "username", Value: user},
&Attribute{Table: "users", ID: id, AttName: "first_name", Value: first},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: last},
); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error updating attributes in DB: %v", err)
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}

View File

@ -192,47 +192,17 @@ func UpdateUser(db *model.DB, s *control.CookieStore) http.HandlerFunc {
}
}
tx, err := db.StartTransaction()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
tx.RollbackTransaction()
userData.Msg = "Die Passwörter stimmen nicht überein."
if err = db.UpdateUserAttributes(
userData.ID,
userData.UserName,
userData.FirstName,
userData.LastName,
oldPass,
newPass,
newPass2); err != nil {
userData.Msg = "Aktualisierung der Benutzerdaten fehlgeschlagen."
tmpl, err := template.ParseFiles("web/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
if err = tx.ChangePassword(userData.ID, oldPass, newPass); err != nil {
log.Println(err)
userData.Msg = "Das alte Passwort ist nicht korrekt."
tmpl, err := template.ParseFiles("web/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
}
if err = tx.UpdateAttributes(
&model.Attribute{Table: "users", ID: userData.ID, AttName: "username", Value: userData.UserName},
&model.Attribute{Table: "users", ID: userData.ID, AttName: "first_name", Value: userData.FirstName},
&model.Attribute{Table: "users", ID: userData.ID, AttName: "last_name", Value: userData.LastName},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = tx.Commit(); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData)
}
tmpl, err := template.ParseFiles("web/templates/hub.html")