mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-27 10:03:06 +01:00
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:
190
server/channelserver/channel_registry_test.go
Normal file
190
server/channelserver/channel_registry_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createTestChannels(count int) []*Server {
|
||||
channels := make([]*Server, count)
|
||||
for i := 0; i < count; i++ {
|
||||
s := createTestServer()
|
||||
s.ID = uint16(0x1010 + i)
|
||||
s.IP = "10.0.0.1"
|
||||
s.Port = uint16(54001 + i)
|
||||
s.GlobalID = "0101"
|
||||
s.userBinaryParts = make(map[userBinaryPartID][]byte)
|
||||
channels[i] = s
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
func TestLocalRegistryFindSessionByCharID(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
conn1 := &mockConn{}
|
||||
sess1 := createTestSessionForServer(channels[0], conn1, 100, "Alice")
|
||||
channels[0].Lock()
|
||||
channels[0].sessions[conn1] = sess1
|
||||
channels[0].Unlock()
|
||||
|
||||
conn2 := &mockConn{}
|
||||
sess2 := createTestSessionForServer(channels[1], conn2, 200, "Bob")
|
||||
channels[1].Lock()
|
||||
channels[1].sessions[conn2] = sess2
|
||||
channels[1].Unlock()
|
||||
|
||||
// Find on first channel
|
||||
found := reg.FindSessionByCharID(100)
|
||||
if found == nil || found.charID != 100 {
|
||||
t.Errorf("FindSessionByCharID(100) = %v, want session with charID 100", found)
|
||||
}
|
||||
|
||||
// Find on second channel
|
||||
found = reg.FindSessionByCharID(200)
|
||||
if found == nil || found.charID != 200 {
|
||||
t.Errorf("FindSessionByCharID(200) = %v, want session with charID 200", found)
|
||||
}
|
||||
|
||||
// Not found
|
||||
found = reg.FindSessionByCharID(999)
|
||||
if found != nil {
|
||||
t.Errorf("FindSessionByCharID(999) = %v, want nil", found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistryFindChannelForStage(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
channels[0].GlobalID = "0101"
|
||||
channels[1].GlobalID = "0102"
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
channels[1].stagesLock.Lock()
|
||||
channels[1].stages["sl2Qs123p0a0u42"] = NewStage("sl2Qs123p0a0u42")
|
||||
channels[1].stagesLock.Unlock()
|
||||
|
||||
gid := reg.FindChannelForStage("u42")
|
||||
if gid != "0102" {
|
||||
t.Errorf("FindChannelForStage(u42) = %q, want %q", gid, "0102")
|
||||
}
|
||||
|
||||
gid = reg.FindChannelForStage("u999")
|
||||
if gid != "" {
|
||||
t.Errorf("FindChannelForStage(u999) = %q, want empty", gid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistryDisconnectUser(t *testing.T) {
|
||||
channels := createTestChannels(1)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
conn := &mockConn{}
|
||||
sess := createTestSessionForServer(channels[0], conn, 42, "Target")
|
||||
channels[0].Lock()
|
||||
channels[0].sessions[conn] = sess
|
||||
channels[0].Unlock()
|
||||
|
||||
reg.DisconnectUser([]uint32{42})
|
||||
|
||||
if !conn.WasClosed() {
|
||||
t.Error("DisconnectUser should have closed the connection for charID 42")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistrySearchSessions(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
// Add 3 sessions across 2 channels
|
||||
for i, ch := range channels {
|
||||
conn := &mockConn{}
|
||||
sess := createTestSessionForServer(ch, conn, uint32(i+1), "Player")
|
||||
sess.stage = NewStage("sl1Ns200p0a0u0")
|
||||
ch.Lock()
|
||||
ch.sessions[conn] = sess
|
||||
ch.Unlock()
|
||||
}
|
||||
conn3 := &mockConn{}
|
||||
sess3 := createTestSessionForServer(channels[0], conn3, 3, "Player")
|
||||
sess3.stage = NewStage("sl1Ns200p0a0u0")
|
||||
channels[0].Lock()
|
||||
channels[0].sessions[conn3] = sess3
|
||||
channels[0].Unlock()
|
||||
|
||||
// Search all
|
||||
results := reg.SearchSessions(func(s SessionSnapshot) bool { return true }, 10)
|
||||
if len(results) != 3 {
|
||||
t.Errorf("SearchSessions(all) returned %d results, want 3", len(results))
|
||||
}
|
||||
|
||||
// Search with max
|
||||
results = reg.SearchSessions(func(s SessionSnapshot) bool { return true }, 2)
|
||||
if len(results) != 2 {
|
||||
t.Errorf("SearchSessions(max=2) returned %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
// Search with predicate
|
||||
results = reg.SearchSessions(func(s SessionSnapshot) bool { return s.CharID == 1 }, 10)
|
||||
if len(results) != 1 {
|
||||
t.Errorf("SearchSessions(charID==1) returned %d results, want 1", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistrySearchStages(t *testing.T) {
|
||||
channels := createTestChannels(1)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
channels[0].stagesLock.Lock()
|
||||
channels[0].stages["sl2Ls210test1"] = NewStage("sl2Ls210test1")
|
||||
channels[0].stages["sl2Ls210test2"] = NewStage("sl2Ls210test2")
|
||||
channels[0].stages["sl1Ns200other"] = NewStage("sl1Ns200other")
|
||||
channels[0].stagesLock.Unlock()
|
||||
|
||||
results := reg.SearchStages("sl2Ls210", 10)
|
||||
if len(results) != 2 {
|
||||
t.Errorf("SearchStages(sl2Ls210) returned %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
results = reg.SearchStages("sl2Ls210", 1)
|
||||
if len(results) != 1 {
|
||||
t.Errorf("SearchStages(sl2Ls210, max=1) returned %d results, want 1", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistryConcurrentAccess(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
// Populate some sessions
|
||||
for _, ch := range channels {
|
||||
for i := 0; i < 10; i++ {
|
||||
conn := &mockConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 50000 + i}}
|
||||
sess := createTestSessionForServer(ch, conn, uint32(i+1), "Player")
|
||||
sess.stage = NewStage("sl1Ns200p0a0u0")
|
||||
ch.Lock()
|
||||
ch.sessions[conn] = sess
|
||||
ch.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Run concurrent operations
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(3)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_ = reg.FindSessionByCharID(uint32(id%10 + 1))
|
||||
}(i)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = reg.FindChannelForStage("u0")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = reg.SearchSessions(func(s SessionSnapshot) bool { return true }, 5)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user