diff --git a/.env.example b/.env.example index 09f20c1..f0f3468 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ ENV=debug PORT=3000 SEED_DB=true IS_RATE_LIMITER_ENABLED=true +THREADS_MAX_COUNT=50 POSTGRES_URL=postgres://localhost/micrach?pool_max_conns=5 \ No newline at end of file diff --git a/config/config.go b/config/config.go index bfbafbd..0fe73fd 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "log" "os" "strconv" ) @@ -11,6 +12,7 @@ type AppConfig struct { Port int SeedDb bool IsRateLimiterEnabled bool + ThreadsMaxCount int } type DbConfig struct { @@ -29,7 +31,7 @@ func getAppConfig() AppConfig { } port, err := strconv.Atoi(portString) if err != nil { - panic(fmt.Sprintf("Could not parse %s to int", portString)) + log.Panicln(fmt.Sprintf("Could not parse %s to int", portString)) } seedDbString := os.Getenv("SEED_DB") @@ -38,11 +40,18 @@ func getAppConfig() AppConfig { isRateLimiterEnabledString := os.Getenv("IS_RATE_LIMITER_ENABLED") isRateLimiterEnabled := isRateLimiterEnabledString == "true" + threadsMaxCountString := os.Getenv("THREADS_MAX_COUNT") + threadsMaxCount, err := strconv.Atoi(threadsMaxCountString) + if err != nil { + log.Panicln(fmt.Sprintf("Could not parse %s to int", threadsMaxCountString)) + } + return AppConfig{ Env: env, Port: port, SeedDb: seedDb, IsRateLimiterEnabled: isRateLimiterEnabled, + ThreadsMaxCount: threadsMaxCount, } } diff --git a/controllers/threads_controller.go b/controllers/threads_controller.go index dc7dc15..9604907 100644 --- a/controllers/threads_controller.go +++ b/controllers/threads_controller.go @@ -12,6 +12,7 @@ import ( "github.com/dchest/captcha" "github.com/gin-gonic/gin" + Config "micrach/config" Db "micrach/db" Repositories "micrach/repositories" Utils "micrach/utils" @@ -133,6 +134,28 @@ func CreateThread(c *gin.Context) { } defer conn.Release() + threadsCount, err := Repositories.Posts.GetCount() + if err != nil { + log.Println("error:", err) + c.HTML(http.StatusInternalServerError, "500.html", nil) + return + } + + if threadsCount >= Config.App.ThreadsMaxCount { + oldestThreadUpdatedAt, err := Repositories.Posts.GetOldestThreadUpdatedAt() + if err != nil { + log.Println("error:", err) + c.HTML(http.StatusInternalServerError, "500.html", nil) + return + } + err = Repositories.Posts.ArchiveThreadsFrom(oldestThreadUpdatedAt) + if err != nil { + log.Println("error:", err) + c.HTML(http.StatusInternalServerError, "500.html", nil) + return + } + } + tx, err := conn.Begin(context.TODO()) if err != nil { log.Println("error:", err) diff --git a/migrations/1-init.sql b/migrations/1-init.sql index 2201639..59f25c7 100644 --- a/migrations/1-init.sql +++ b/migrations/1-init.sql @@ -33,4 +33,4 @@ CREATE TABLE migrations id INT NOT NULL, name VARCHAR NOT NULL, created_at TIMESTAMP DEFAULT NOW() NOT NULL -) \ No newline at end of file +) diff --git a/migrations/3-remove_posts_updated_at_default.sql b/migrations/3-remove_posts_updated_at_default.sql new file mode 100644 index 0000000..3b4e032 --- /dev/null +++ b/migrations/3-remove_posts_updated_at_default.sql @@ -0,0 +1,9 @@ +ALTER TABLE posts +ALTER COLUMN updated_at DROP NOT NULL; + +ALTER TABLE posts +ALTER COLUMN updated_at DROP DEFAULT; + +UPDATE posts +SET updated_at = null +WHERE is_parent != true; diff --git a/migrations/4-posts_timefields_now_with_timezone.sql b/migrations/4-posts_timefields_now_with_timezone.sql new file mode 100644 index 0000000..9d2fa5f --- /dev/null +++ b/migrations/4-posts_timefields_now_with_timezone.sql @@ -0,0 +1,5 @@ +ALTER TABLE posts +ALTER COLUMN created_at TYPE timestamptz; + +ALTER TABLE posts +ALTER COLUMN updated_at TYPE timestamptz; diff --git a/repositories/posts_repository.go b/repositories/posts_repository.go index a1489d0..b02d4c4 100644 --- a/repositories/posts_repository.go +++ b/repositories/posts_repository.go @@ -2,7 +2,9 @@ package repositories import ( "context" + Config "micrach/config" Db "micrach/db" + "time" "github.com/jackc/pgx/v4" ) @@ -66,6 +68,7 @@ func (r *PostsRepository) GetCount() (int, error) { WHERE is_parent = true AND is_deleted != true + AND is_archived != true ` row := Db.Pool.QueryRow(context.TODO(), sql) @@ -79,19 +82,19 @@ func (r *PostsRepository) GetCount() (int, error) { func (r *PostsRepository) Create(p Post) (int, error) { sql := ` - INSERT INTO posts (is_parent, parent_id, title, text, is_sage) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO posts (is_parent, parent_id, title, text, is_sage, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id ` var row pgx.Row if p.IsParent { row = Db.Pool.QueryRow( - context.TODO(), sql, p.IsParent, nil, p.Title, p.Text, p.IsSage, + context.TODO(), sql, p.IsParent, nil, p.Title, p.Text, p.IsSage, time.Now(), ) } else { row = Db.Pool.QueryRow( - context.TODO(), sql, p.IsParent, p.ParentID, p.Title, p.Text, p.IsSage, + context.TODO(), sql, p.IsParent, p.ParentID, p.Title, p.Text, p.IsSage, nil, ) } @@ -165,19 +168,19 @@ func (r *PostsRepository) GetThreadByPostID(ID int) ([]Post, error) { func (r *PostsRepository) CreateInTx(tx pgx.Tx, p Post) (int, error) { sql := ` - INSERT INTO posts (is_parent, parent_id, title, text, is_sage) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO posts (is_parent, parent_id, title, text, is_sage, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id ` var row pgx.Row if p.IsParent { row = tx.QueryRow( - context.TODO(), sql, p.IsParent, nil, p.Title, p.Text, p.IsSage, + context.TODO(), sql, p.IsParent, nil, p.Title, p.Text, p.IsSage, time.Now(), ) } else { row = tx.QueryRow( - context.TODO(), sql, p.IsParent, p.ParentID, p.Title, p.Text, p.IsSage, + context.TODO(), sql, p.IsParent, p.ParentID, p.Title, p.Text, p.IsSage, nil, ) } @@ -206,3 +209,38 @@ func (r *PostsRepository) CreateInTx(tx pgx.Tx, p Post) (int, error) { return createdPost.ID, nil } + +func (r *PostsRepository) GetOldestThreadUpdatedAt() (time.Time, error) { + sql := ` + SELECT updated_at + FROM posts + WHERE + is_parent = true + AND is_deleted != true + AND is_archived != true + ORDER BY updated_at DESC + OFFSET $1 - 1 + LIMIT 1 + ` + + row := Db.Pool.QueryRow(context.TODO(), sql, Config.App.ThreadsMaxCount) + var updatedAt time.Time + err := row.Scan(&updatedAt) + if err != nil { + return time.Time{}, err + } + return updatedAt, nil +} + +func (r *PostsRepository) ArchiveThreadsFrom(t time.Time) error { + sql := ` + UPDATE posts + SET is_archived = true + WHERE + is_archived != true + AND updated_at <= $1 + ` + + _, err := Db.Pool.Exec(context.TODO(), sql, t) + return err +} diff --git a/utils/utils.go b/utils/utils.go index c496314..503333d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,3 +1,4 @@ +// TODO: move all functions to different packages package utils import (