feat(channelserver): decouple channel servers for independent operation (#33)

Enable multiple Erupe instances to share a single PostgreSQL database
without destroying each other's state, fix existing data races in
cross-channel access, and lay groundwork for future distributed
channel server deployments.

Phase 1 — DB safety:
- Scope DELETE FROM servers/sign_sessions to this instance's server IDs
- Fix ci++ bug where failed channel start shifted subsequent IDs

Phase 2 — Fix data races in cross-channel access:
- Lock sessions map in FindSessionByCharID and DisconnectUser
- Lock stagesLock in handleMsgSysLockGlobalSema
- Snapshot sessions/stages under lock in TransitMessage types 1-4
- Lock channel when finding mail notification targets

Phase 3 — ChannelRegistry interface:
- Define ChannelRegistry interface with 7 cross-channel operations
- Implement LocalChannelRegistry with proper locking
- Add SessionSnapshot/StageSnapshot immutable copy types
- Delegate WorldcastMHF, FindSessionByCharID, DisconnectUser to Registry
- Migrate LockGlobalSema and guild mail handlers to use Registry
- Add comprehensive tests including concurrent access

Phase 4 — Per-channel enable/disable:
- Add Enabled *bool to EntranceChannelInfo (nil defaults to true)
- Skip disabled channels in startup loop, preserving ID stability
- Add IsEnabled() helper with backward-compatible default
- Update config.example.json with Enabled field
This commit is contained in:
Houmgaor
2026-02-19 18:13:34 +01:00
parent ba9fce153d
commit 754b5a3bff
10 changed files with 661 additions and 79 deletions

38
main.go
View File

@@ -16,6 +16,7 @@ import (
"erupe-ce/server/discordbot"
"erupe-ce/server/entranceserver"
"erupe-ce/server/signserver"
"strings"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
@@ -129,11 +130,30 @@ func main() {
}
logger.Info("Database: Started successfully")
// Clear stale data
if config.DebugOptions.ProxyPort == 0 {
_ = db.MustExec("DELETE FROM sign_sessions")
// Pre-compute all server IDs this instance will own, so we only
// delete our own rows (safe for multi-instance on the same DB).
var ownedServerIDs []string
{
si := 0
for _, ee := range config.Entrance.Entries {
ci := 0
for range ee.Channels {
sid := (4096 + si*256) + (16 + ci)
ownedServerIDs = append(ownedServerIDs, fmt.Sprint(sid))
ci++
}
si++
}
}
// Clear stale data scoped to this instance's server IDs
if len(ownedServerIDs) > 0 {
idList := strings.Join(ownedServerIDs, ",")
if config.DebugOptions.ProxyPort == 0 {
_ = db.MustExec("DELETE FROM sign_sessions WHERE server_id IN (" + idList + ")")
}
_ = db.MustExec("DELETE FROM servers WHERE server_id IN (" + idList + ")")
}
_ = db.MustExec("DELETE FROM servers")
_ = db.MustExec(`UPDATE guild_characters SET treasure_hunt=NULL`)
// Clean the DB if the option is on.
@@ -213,6 +233,12 @@ func main() {
for j, ee := range config.Entrance.Entries {
for i, ce := range ee.Channels {
sid := (4096 + si*256) + (16 + ci)
if !ce.IsEnabled() {
logger.Info(fmt.Sprintf("Channel %d (%d): Disabled via config", count, ce.Port))
ci++
count++
continue
}
c := *channelserver.NewServer(&channelserver.Config{
ID: uint16(sid),
Logger: logger.Named("channel-" + fmt.Sprint(count)),
@@ -237,9 +263,9 @@ func main() {
)
channels = append(channels, &c)
logger.Info(fmt.Sprintf("Channel %d (%d): Started successfully", count, ce.Port))
ci++
count++
}
ci++
}
ci = 0
si++
@@ -248,8 +274,10 @@ func main() {
// Register all servers in DB
_ = db.MustExec(channelQuery)
registry := channelserver.NewLocalChannelRegistry(channels)
for _, c := range channels {
c.Channels = channels
c.Registry = registry
}
}