Merge grane feature/backup-swap-lock: bezbedna zamena baze pri obnovi
This commit is contained in:
@@ -155,6 +155,9 @@ func main() {
|
|||||||
r.Use(ntechmw.BezbednostHeaders())
|
r.Use(ntechmw.BezbednostHeaders())
|
||||||
r.Use(ntechmw.CsrfMiddleware)
|
r.Use(ntechmw.CsrfMiddleware)
|
||||||
r.Use(middleware.Compress(5))
|
r.Use(middleware.Compress(5))
|
||||||
|
// deljeno zaključavanje baze za vreme zahteva — obnova backupa (VratiBackup)
|
||||||
|
// čeka da svi zahtevi završe pre zamene konekcije (vidi handler.ZakljucajCitanje)
|
||||||
|
r.Use(h.ZakljucajCitanje)
|
||||||
|
|
||||||
// uploads su uvek na disku — korisnički fajlovi, ne ugrađuju se
|
// uploads su uvek na disku — korisnički fajlovi, ne ugrađuju se
|
||||||
r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/",
|
r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"ntech/internal/db"
|
"ntech/internal/db"
|
||||||
"ntech/internal/db/sqlite"
|
"ntech/internal/db/sqlite"
|
||||||
@@ -36,6 +37,24 @@ type Handler struct {
|
|||||||
Templates map[string]*template.Template
|
Templates map[string]*template.Template
|
||||||
TemplatesFS fs.FS
|
TemplatesFS fs.FS
|
||||||
totpKljuc []byte // ključ za šifrovanje TOTP tajni (prosleđuje se KorisniciRepo)
|
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.
|
// 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) {
|
func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
||||||
h.DB = novaDB
|
h.DB = novaDB
|
||||||
h.Artikli = sqlite.NoviArtikalRepo(novaDB)
|
h.Artikli = sqlite.NoviArtikalRepo(novaDB)
|
||||||
|
|||||||
@@ -165,32 +165,36 @@ func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("vrati backup: wal_checkpoint greška: %v", err)
|
log.Printf("vrati backup: wal_checkpoint greška: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// zatvori sve konekcije
|
// Sama zamena baze (zatvaranje stare, kopiranje, otvaranje nove) radi se u
|
||||||
if err := h.DB.Close(); err != nil {
|
// zasebnoj gorutini pod EKSKLUZIVNIM zaključavanjem (h.mu.Lock). Razlog: ovaj
|
||||||
log.Printf("vrati backup: greška pri zatvaranju baze: %v", err)
|
// zahtev još drži deljeno zaključavanje (ZakljucajCitanje middleware), pa bi
|
||||||
}
|
// uzimanje ekskluzivnog u istoj gorutini izazvalo deadlock. Ekskluzivno
|
||||||
|
// zaključavanje sačeka da svi tekući zahtevi (uključujući ovaj, čim vrati
|
||||||
|
// odgovor) završe, pa tek onda menja konekciju — bez data race-a i bez upita
|
||||||
|
// nad zatvorenom bazom. Sledeći zahtev (redirect na /podesavanja) prirodno
|
||||||
|
// sačeka da zamena završi jer čeka na deljeno zaključavanje.
|
||||||
|
go func() {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
// kopiraj backup fajl na mesto trenutne baze
|
if err := h.DB.Close(); err != nil {
|
||||||
if err := kopiraFajl(putanjaBackupa, h.PutanjaBaze); err != nil {
|
log.Printf("vrati backup: greška pri zatvaranju baze: %v", err)
|
||||||
log.Printf("vrati backup: greška pri kopiranju: %v", err)
|
}
|
||||||
http.Redirect(w, r, "/podesavanja?backup_greska=Greška+pri+obnovi+baze", http.StatusSeeOther)
|
if err := kopiraFajl(putanjaBackupa, h.PutanjaBaze); err != nil {
|
||||||
return
|
log.Printf("vrati backup: greška pri kopiranju (baza je zatvorena, potreban restart): %v", err)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
os.Remove(h.PutanjaBaze + "-wal")
|
||||||
|
os.Remove(h.PutanjaBaze + "-shm")
|
||||||
|
|
||||||
// ukloni WAL i SHM fajlove stare baze
|
novaDB, err := ntechsqlite.OtvoriDB(h.PutanjaBaze)
|
||||||
os.Remove(h.PutanjaBaze + "-wal")
|
if err != nil {
|
||||||
os.Remove(h.PutanjaBaze + "-shm")
|
log.Printf("vrati backup: greška pri otvaranju nove baze (potreban restart): %v", err)
|
||||||
|
return
|
||||||
// otvori novu konekciju
|
}
|
||||||
novaDB, err := ntechsqlite.OtvoriDB(h.PutanjaBaze)
|
h.reinicijalizujRepozitorijume(novaDB)
|
||||||
if err != nil {
|
log.Printf("Baza uspešno obnovljena iz: %s", ime)
|
||||||
log.Printf("vrati backup: greška pri otvaranju nove baze: %v", err)
|
}()
|
||||||
http.Redirect(w, r, "/podesavanja?backup_greska=Baza+obnovljena+ali+je+potreban+restart", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.reinicijalizujRepozitorijume(novaDB)
|
|
||||||
log.Printf("Baza uspešno obnovljena iz: %s", ime)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/podesavanja?sacuvano=vraceno", http.StatusSeeOther)
|
http.Redirect(w, r, "/podesavanja?sacuvano=vraceno", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user