Umbau-Projekt (SSR und Rest)

This commit is contained in:
2026-09-03 22:20:55 +02:00
parent 13363cb568
commit c66df283d1
37 changed files with 3719 additions and 344 deletions
+47 -13
View File
@@ -147,26 +147,60 @@ func lookupASN(ip net.IP) string {
return label
}
// Impressions laufen über einen einzelnen Worker statt einer Goroutine pro
// Request. Das deckelt die Nebenläufigkeit (ein Flood erzeugt keine unbegrenzten
// Goroutinen mit je einem ASN-Lookup) und macht den Shutdown sicher: der Worker
// wird gestoppt, BEVOR db und geoASN geschlossen werden -- eine freilaufende
// Goroutine könnte sonst nach closeGeoASN() auf den unmap-ten Reader zugreifen
// (SIGSEGV; middleware.Recoverer fängt Panics in eigenen Goroutinen nicht).
// Bei vollem Puffer wird der Zählwert verworfen -- ein Seitenaufruf darf nie
// auf die Statistik warten.
type impressionEvent struct {
remote, path string
}
var (
impressionCh = make(chan impressionEvent, 256)
impressionDone = make(chan struct{})
)
// startImpressionWorker startet den Schreib-Worker (nach initDB/initGeoASN).
func startImpressionWorker() {
go func() {
defer close(impressionDone)
for ev := range impressionCh {
netw := maskIP(clientIP(ev.remote))
anon := ""
if netw != nil {
anon = netw.String()
}
recordImpression(ev.path, anon, lookupASN(netw))
}
}()
}
// stopImpressionWorker lässt den Worker die gepufferten Events noch schreiben
// und kehrt erst zurück, wenn er fertig ist (vor db.Close/closeGeoASN rufen).
func stopImpressionWorker() {
close(impressionCh)
<-impressionDone
}
// trackImpression zählt einen Seitenaufruf pro (Tag, anonymisiertes Netz, Pfad)
// und vermerkt das autonome System. Die IP wird zuerst maskiert; sowohl die
// Speicherung als auch die ASN-Auflösung arbeiten nur auf dem anonymisierten
// Netz. Gezielt nur auf den HTML-Seiten-Routen, nicht auf API oder Assets.
//
// Die Erfassung läuft asynchron: der Seitenaufruf darf nicht auf den DB-Schreib
// warten (SQLite serialisiert Schreiber, synchronous=FULL macht pro Commit ein
// fsync). r ist nach Rückkehr des Handlers nicht mehr sicher nutzbar -> wir
// kopieren die zwei Werte vorher heraus.
// Die Erfassung läuft asynchron über den Impression-Worker: der Seitenaufruf
// darf nicht auf den DB-Schreib warten (SQLite serialisiert Schreiber). r ist
// nach Rückkehr des Handlers nicht mehr sicher nutzbar -> wir kopieren die zwei
// Werte vorher heraus.
func trackImpression(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
remote, path := r.RemoteAddr, r.URL.Path
go func() {
netw := maskIP(clientIP(remote))
anon := ""
if netw != nil {
anon = netw.String()
}
recordImpression(path, anon, lookupASN(netw))
}()
select {
case impressionCh <- impressionEvent{remote: r.RemoteAddr, path: r.URL.Path}:
default: // Puffer voll oder Worker nicht gestartet (Tests): verwerfen
}
next.ServeHTTP(w, r)
})
}