mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
fix(channelserver): configure DB pool and add transactions for guild ops
sqlx.Open was called with no pool configuration, risking PostgreSQL connection exhaustion under load. Set max open/idle conns and lifetimes. CreatePost INSERT + soft-delete UPDATE were two separate queries with no transaction, risking inconsistent state on partial failure. CollectAdventure used SELECT then UPDATE without a lock, allowing concurrent guild members to double-collect. Now uses SELECT FOR UPDATE within a transaction.
This commit is contained in:
7
main.go
7
main.go
@@ -136,6 +136,13 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
preventClose(config, fmt.Sprintf("Database: Failed to ping, %s", err.Error()))
|
preventClose(config, fmt.Sprintf("Database: Failed to ping, %s", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure connection pool to avoid exhausting PostgreSQL under load.
|
||||||
|
db.SetMaxOpenConns(50)
|
||||||
|
db.SetMaxIdleConns(10)
|
||||||
|
db.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
db.SetConnMaxIdleTime(2 * time.Minute)
|
||||||
|
|
||||||
logger.Info("Database: Started successfully")
|
logger.Info("Database: Started successfully")
|
||||||
|
|
||||||
// Pre-compute all server IDs this instance will own, so we only
|
// Pre-compute all server IDs this instance will own, so we only
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -498,16 +499,24 @@ func (r *GuildRepository) ListPosts(guildID uint32, postType int) ([]*MessageBoa
|
|||||||
|
|
||||||
// CreatePost inserts a new guild post and soft-deletes excess posts beyond maxPosts.
|
// CreatePost inserts a new guild post and soft-deletes excess posts beyond maxPosts.
|
||||||
func (r *GuildRepository) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error {
|
func (r *GuildRepository) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error {
|
||||||
if _, err := r.db.Exec(
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if _, err := tx.Exec(
|
||||||
`INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)`,
|
`INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
guildID, authorID, stampID, postType, title, body); err != nil {
|
guildID, authorID, stampID, postType, title, body); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := r.db.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN (
|
if _, err := tx.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN (
|
||||||
SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
|
SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
|
||||||
ORDER BY created_at DESC OFFSET $3
|
ORDER BY created_at DESC OFFSET $3
|
||||||
)`, guildID, postType, maxPosts)
|
)`, guildID, postType, maxPosts); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePost soft-deletes a guild post by ID.
|
// DeletePost soft-deletes a guild post by ID.
|
||||||
@@ -698,15 +707,24 @@ func (r *GuildRepository) CreateAdventureWithCharge(guildID, destination, charge
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CollectAdventure marks an adventure as collected by the given character (CSV append).
|
// CollectAdventure marks an adventure as collected by the given character (CSV append).
|
||||||
|
// Uses SELECT FOR UPDATE to prevent concurrent double-collect.
|
||||||
func (r *GuildRepository) CollectAdventure(adventureID uint32, charID uint32) error {
|
func (r *GuildRepository) CollectAdventure(adventureID uint32, charID uint32) error {
|
||||||
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
var collectedBy string
|
var collectedBy string
|
||||||
err := r.db.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1", adventureID).Scan(&collectedBy)
|
err = tx.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1 FOR UPDATE", adventureID).Scan(&collectedBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
collectedBy = stringsupport.CSVAdd(collectedBy, int(charID))
|
collectedBy = stringsupport.CSVAdd(collectedBy, int(charID))
|
||||||
_, err = r.db.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID)
|
if _, err = tx.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChargeAdventure adds charge to a guild adventure.
|
// ChargeAdventure adds charge to a guild adventure.
|
||||||
|
|||||||
Reference in New Issue
Block a user