panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 658bda1d24
2160 changed files with 300413 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
package dispatch
import (
"sync"
"testing"
"time"
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)
// blockingSender simulates a controller that has stopped draining the stream:
// every Send blocks until released. This is the backpressure condition that
// used to wedge the whole agent when sendEnv called Send inline.
type blockingSender struct {
release chan struct{}
mu sync.Mutex
sent int
}
func (b *blockingSender) Send(*panelv1.AgentEnvelope) error {
<-b.release // block until the test lets it through
b.mu.Lock()
b.sent++
b.mu.Unlock()
return nil
}
func logEnv() *panelv1.AgentEnvelope {
return &panelv1.AgentEnvelope{Payload: &panelv1.AgentEnvelope_Log{Log: &panelv1.LogLine{Line: "x"}}}
}
func stateEnv() *panelv1.AgentEnvelope {
return &panelv1.AgentEnvelope{Payload: &panelv1.AgentEnvelope_InstanceState{InstanceState: &panelv1.InstanceStateUpdate{InstanceId: "i"}}}
}
// newTestDispatcher builds a Dispatcher with just the send machinery wired —
// enough to exercise sendEnv / sendLoop without a real runtime.
func newTestDispatcher() *Dispatcher {
d := &Dispatcher{sendCh: make(chan *panelv1.AgentEnvelope, 4096)}
go d.sendLoop()
return d
}
// TestSendEnvDropsTelemetryUnderBackpressure is the core regression guard for
// the VEIN-exposed wedge: with the sender blocked, a flood of log envelopes far
// exceeding the buffer must NOT block the producer. Pre-fix, sendEnv called
// Send inline and the producer (and the whole agent) froze here.
func TestSendEnvDropsTelemetryUnderBackpressure(t *testing.T) {
d := newTestDispatcher()
bs := &blockingSender{release: make(chan struct{})}
d.SetSender(bs)
done := make(chan struct{})
go func() {
// 50k log frames — 12x the 4096 buffer. Must all return promptly.
for i := 0; i < 50000; i++ {
d.sendEnv(logEnv())
}
close(done)
}()
select {
case <-done:
// Producer completed without blocking on the stalled sender. Pass.
case <-time.After(5 * time.Second):
t.Fatal("sendEnv blocked under backpressure — the wedge regressed")
}
}
// TestCriticalEnvelopeNotDropped verifies an instance-state envelope is queued
// (not silently dropped) even when telemetry is being shed. We fill the buffer
// with the sender blocked, then confirm a critical send blocks until drained
// (i.e. it's preserved, not discarded).
func TestCriticalEnvelopeNotDropped(t *testing.T) {
d := &Dispatcher{sendCh: make(chan *panelv1.AgentEnvelope, 2)}
// No drainer started yet — fill the buffer.
d.sendEnv(logEnv())
d.sendEnv(logEnv())
// Buffer is full (cap 2). A telemetry frame must drop (non-blocking).
dropDone := make(chan struct{})
go func() { d.sendEnv(logEnv()); close(dropDone) }()
select {
case <-dropDone:
case <-time.After(time.Second):
t.Fatal("telemetry send blocked on full buffer — should have dropped")
}
// A critical frame must NOT drop: it blocks until space frees. Start a
// drainer to free space and confirm the critical send then completes.
critDone := make(chan struct{})
go func() { d.sendEnv(stateEnv()); close(critDone) }()
select {
case <-critDone:
t.Fatal("critical send returned before buffer had space — it was dropped")
case <-time.After(100 * time.Millisecond):
// Correctly still blocking. Now drain and it should complete.
}
go d.sendLoop()
select {
case <-critDone:
// Drained and delivered. Pass.
case <-time.After(time.Second):
t.Fatal("critical send never completed after drain")
}
}
// TestConcurrentProducersNoRace runs many producers through sendEnv at once.
// Pre-fix, each called stream.Send directly — concurrent Sends on one gRPC
// stream are a data race. The single-drainer design serializes them. Run with
// -race to catch a regression.
func TestConcurrentProducersNoRace(t *testing.T) {
d := newTestDispatcher()
bs := &blockingSender{release: make(chan struct{})}
close(bs.release) // never block — just count
d.SetSender(bs)
var wg sync.WaitGroup
for p := 0; p < 32; p++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
d.sendEnv(logEnv())
}
}()
}
wg.Wait()
// Give the drainer a moment to flush.
time.Sleep(200 * time.Millisecond)
// No assertion on exact count (some may still be in flight / dropped);
// the point is the -race detector finds no concurrent Send.
}