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

View File

@@ -297,6 +297,15 @@ type EntranceChannelInfo struct {
Port uint16
MaxPlayers uint16
CurrentPlayers uint16
Enabled *bool // nil defaults to true for backward compatibility
}
// IsEnabled returns whether this channel is enabled. Defaults to true if Enabled is nil.
func (c *EntranceChannelInfo) IsEnabled() bool {
if c.Enabled == nil {
return true
}
return *c.Enabled
}
var ErupeConfig *Config

View File

@@ -536,6 +536,34 @@ func TestEntranceChannelInfo(t *testing.T) {
}
}
// TestEntranceChannelInfoIsEnabled tests the Enabled field and IsEnabled helper
func TestEntranceChannelInfoIsEnabled(t *testing.T) {
trueVal := true
falseVal := false
tests := []struct {
name string
enabled *bool
want bool
}{
{"nil defaults to true", nil, true},
{"explicit true", &trueVal, true},
{"explicit false", &falseVal, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info := EntranceChannelInfo{
Port: 10001,
Enabled: tt.enabled,
}
if got := info.IsEnabled(); got != tt.want {
t.Errorf("IsEnabled() = %v, want %v", got, tt.want)
}
})
}
}
// TestDiscord verifies Discord struct
func TestDiscord(t *testing.T) {
discord := Discord{