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:
Houmgaor
2026-02-21 00:29:09 +01:00
parent 0a489e7cc5
commit f9d9260274
2 changed files with 32 additions and 7 deletions

View File

@@ -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

View File

@@ -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.