diff --git a/config.yaml.example b/config.yaml.example index e8840a9..c46a89c 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -10,6 +10,11 @@ database: name: "himewiki" sslmode: "disable" +vacuum: + check-every: 250 + threshold: 68719476736 + image-threshold: 68719476736 + site: base: "https://wiki.example.org/" name: "HimeWiki" diff --git a/internal/action/edit.go b/internal/action/edit.go index 1b530ad..7a30c7c 100644 --- a/internal/action/edit.go +++ b/internal/action/edit.go @@ -80,7 +80,7 @@ func Edit(cfg *config.Config, w http.ResponseWriter, r *http.Request, params *Pa title, normalized, _, rendered := format.Apply(cfg, params.DbName, filtered) if previewed && save != "" { - if err := data.Save(params.DbName, normalized, revisionID); err != nil { + if err := data.Save(cfg, params.DbName, normalized, revisionID); err != nil { http.Error(w, "Failed to save", http.StatusInternalServerError) return } diff --git a/internal/action/image.go b/internal/action/image.go index a4404ac..e4b4e4d 100644 --- a/internal/action/image.go +++ b/internal/action/image.go @@ -78,7 +78,7 @@ func Upload(cfg *config.Config, w http.ResponseWriter, r *http.Request, params * return } - if err := data.SaveImage(name, filtered); err != nil { + if err := data.SaveImage(cfg, name, filtered); err != nil { http.Error(w, "Failed to save", http.StatusInternalServerError) return } diff --git a/internal/config/config.go b/internal/config/config.go index 2c20406..8a0b9e3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,6 +22,12 @@ type Config struct { SSLMode string `yaml:"sslmode"` } `yaml:"database"` + Vacuum struct { + CheckEvery int `yaml:"check-every"` + Threshold int64 `yaml:"threshold"` + ImageThreshold int64 `yaml:"image-threshold"` + } `yaml:"vacuum"` + Site struct { Base string `yaml:"base"` Name string `yaml:"name"` diff --git a/internal/data/data.go b/internal/data/data.go index 26164cd..9b92978 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -199,7 +199,7 @@ func diff(oldText, newText string) string { return text } -func Save(name, content string, baseRevID int) error { +func Save(cfg *config.Config, name, content string, baseRevID int) error { ctx := context.Background() tx, err := db.Begin(ctx) if err != nil { @@ -240,16 +240,68 @@ func Save(name, content string, baseRevID int) error { return err } - _, err = tx.Exec(ctx, - `UPDATE state - SET page_counter = page_counter + 1 - WHERE id = 1`, - ) + var pageCount int64 + err = tx.QueryRow(ctx, ` + UPDATE state + SET page_counter = page_counter + 1 + WHERE id = 1 + RETURNING page_counter + `).Scan(&pageCount) if err != nil { return err } - return tx.Commit(ctx) + err = tx.Commit(ctx) + if err != nil { + return err + } + + if pageCount % int64(cfg.Vacuum.CheckEvery) == 0 { + var sizeBytes int64 + err = db.QueryRow(ctx, ` + SELECT pg_total_relation_size('pages') + + pg_total_relation_size('revisions') + `).Scan(&sizeBytes) + if err != nil { + return err + } + + if sizeBytes >= cfg.Vacuum.Threshold { + _, err = db.Exec(ctx, "VACUUM FULL pages") + if err != nil { + return err + } + + _, err = db.Exec(ctx, ` + WITH cutoff AS ( + SELECT COUNT(*) / 2 AS limit_count + FROM revisions + GROUP BY name + ORDER BY COUNT(*) DESC + LIMIT 1 + ) + DELETE FROM revisions r + USING cutoff + WHERE r.id IN ( + SELECT id + FROM revisions r2, cutoff + WHERE r2.name = r.name + ORDER BY r2.created_at ASC + OFFSET cutoff.limit_count + ) + `) + if err != nil { + return err + } + + _, err = db.Exec(ctx, "VACUUM FULL revisions") + if err != nil { + return err + } + } + } + + return nil } func LoadAll(page int, perPage int) ([]string, error) { diff --git a/internal/data/image.go b/internal/data/image.go index bf02371..cd3cb3c 100644 --- a/internal/data/image.go +++ b/internal/data/image.go @@ -2,6 +2,8 @@ package data import ( "context" + + "github.com/akikareha/himewiki/internal/config" ) func LoadImage(name string) (int, []byte, error) { @@ -17,7 +19,7 @@ func LoadImage(name string) (int, []byte, error) { return id, content, nil } -func SaveImage(name string, content []byte) error { +func SaveImage(cfg *config.Config, name string, content []byte) error { ctx := context.Background() tx, err := db.Begin(ctx) if err != nil { @@ -47,14 +49,66 @@ func SaveImage(name string, content []byte) error { return err } - _, err = tx.Exec(ctx, - `UPDATE state - SET image_counter = image_counter + 1 - WHERE id = 1`, - ) + var imageCount int64 + err = tx.QueryRow(ctx, ` + UPDATE state + SET image_counter = image_counter + 1 + WHERE id = 1 + RETURNING image_counter + `).Scan(&imageCount) if err != nil { return err } - return tx.Commit(ctx) + err = tx.Commit(ctx) + if err != nil { + return err + } + + if imageCount % int64(cfg.Vacuum.CheckEvery) == 0 { + var sizeBytes int64 + err = db.QueryRow(ctx, ` + SELECT pg_total_relation_size('images') + + pg_total_relation_size('image_revisions') + `).Scan(&sizeBytes) + if err != nil { + return err + } + + if sizeBytes >= cfg.Vacuum.ImageThreshold { + _, err = db.Exec(ctx, "VACUUM FULL images") + if err != nil { + return err + } + + _, err = db.Exec(ctx, ` + WITH cutoff AS ( + SELECT COUNT(*) / 2 AS limit_count + FROM image_revisions + GROUP BY name + ORDER BY COUNT(*) DESC + LIMIT 1 + ) + DELETE FROM image_revisions r + USING cutoff + WHERE r.id IN ( + SELECT id + FROM image_revisions r2, cutoff + WHERE r2.name = r.name + ORDER BY r2.created_at ASC + OFFSET cutoff.limit_count + ) + `) + if err != nil { + return err + } + + _, err = db.Exec(ctx, "VACUUM FULL image_revisions") + if err != nil { + return err + } + } + } + + return nil }