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

@@ -1,6 +1,7 @@
package channelserver
import (
"context"
"database/sql"
"errors"
"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.
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)`,
guildID, authorID, stampID, postType, title, body); err != nil {
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
ORDER BY created_at DESC OFFSET $3
)`, guildID, postType, maxPosts)
return err
)`, guildID, postType, maxPosts); err != nil {
return err
}
return tx.Commit()
}
// 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).
// Uses SELECT FOR UPDATE to prevent concurrent double-collect.
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
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 {
return err
}
collectedBy = stringsupport.CSVAdd(collectedBy, int(charID))
_, err = r.db.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID)
return err
if _, err = tx.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID); err != nil {
return err
}
return tx.Commit()
}
// ChargeAdventure adds charge to a guild adventure.