fix(backup): bezbedna zamena baze pri obnovi (sync.RWMutex + drain)

Obnova backupa (VratiBackup) je menjala Handler.DB i repozitorijume bez
zaključavanja dok drugi zahtevi rade — data race i moguć upit nad zatvorenom
konekcijom. Uveden Handler.mu (sync.RWMutex): zahtevi drže deljeno zaključavanje
preko middleware-a ZakljucajCitanje, a sama zamena (close+copy+reopen+reinit)
uzima ekskluzivno zaključavanje u zasebnoj gorutini, pa sačeka da svi tekući
zahtevi završe (drain) pre zamene. Zasebna gorutina je nužna jer zahtev još
drži deljeno zaključavanje (inače deadlock).

Poznato ograničenje: pozadinske gorutine i dalje koriste stari db handle iz
main.go — zaseban slučaj za kasnije.
This commit is contained in:
2026-06-12 19:43:40 +02:00
parent 4e40dd7296
commit 2dc2cf5245
3 changed files with 53 additions and 25 deletions
+22 -1
View File
@@ -5,6 +5,7 @@ import (
"html/template"
"io/fs"
"net/http"
"sync"
"ntech/internal/db"
"ntech/internal/db/sqlite"
@@ -36,6 +37,24 @@ type Handler struct {
Templates map[string]*template.Template
TemplatesFS fs.FS
totpKljuc []byte // ključ za šifrovanje TOTP tajni (prosleđuje se KorisniciRepo)
// mu štiti DB i sve repozitorijume od zamene u letu (obnova backupa).
// Zahtevi drže deljeno (read) zaključavanje preko ZakljucajCitanje, a obnova
// uzima ekskluzivno (write) zaključavanje pre zamene konekcije.
mu sync.RWMutex
}
// ZakljucajCitanje je middleware koji za vreme obrade zahteva drži deljeno
// zaključavanje baze. Više zahteva se izvršava paralelno (RLock ne blokira druge
// čitaoce), ali obnova baze (VratiBackup), koja traži ekskluzivno zaključavanje,
// sačeka da svi tekući zahtevi završe pre zamene konekcije. Time se sprečava
// data race nad Handler.DB / repozitorijumima i upit nad zatvorenom konekcijom.
func (h *Handler) ZakljucajCitanje(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.mu.RLock()
defer h.mu.RUnlock()
next.ServeHTTP(w, r)
})
}
// Novi kreira novi Handler sa datom bazom.
@@ -62,7 +81,9 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
}
}
// reinicijalizujRepozitorijume zamenjuje sve repozitorijume posle obnove baze
// reinicijalizujRepozitorijume zamenjuje sve repozitorijume posle obnove baze.
// MORA se pozivati dok je h.mu ekskluzivno zaključan (vidi VratiBackup), inače
// nastaje data race sa zahtevima koji čitaju repozitorijume.
func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
h.DB = novaDB
h.Artikli = sqlite.NoviArtikalRepo(novaDB)