Implement ticketed chunk lifecycle
This commit is contained in:
parent
cae06eb97e
commit
cbd5c7b546
12 changed files with 696 additions and 100 deletions
|
|
@ -1,8 +1,14 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"regionio/internal/protocol"
|
||||
"regionio/internal/world"
|
||||
)
|
||||
|
||||
// TestSpiralOrderCenterFirst confirms the centre coordinate is returned first
|
||||
|
|
@ -16,7 +22,7 @@ func TestSpiralOrderCenterFirst(t *testing.T) {
|
|||
// only needs to be non-decreasing within ring blocks. Verify the max
|
||||
// distance of the first k entries grows as expected by checking the full
|
||||
// set contains exactly the (2*2+1)² = 49 distinct coords.
|
||||
want := (2*2 + 1) * (2 * 2 + 1)
|
||||
want := (2*2 + 1) * (2*2 + 1)
|
||||
if len(order) != want {
|
||||
t.Errorf("spiralOrder len = %d, want %d", len(order), want)
|
||||
}
|
||||
|
|
@ -82,3 +88,174 @@ func TestRequestRecenterNonBlocking(t *testing.T) {
|
|||
t.Fatal("requestRecenter blocked for 2s")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamerSendsStrictlyNearFirst(t *testing.T) {
|
||||
cache := world.NewCache(-1, func(cx, cz int32) *world.Chunk {
|
||||
return world.NewChunk(cx, cz, world.BiomePlains)
|
||||
})
|
||||
recorder := &recordingConn{}
|
||||
s := newStreamer(cache, NewConn(recorder), nil, 2)
|
||||
s.genRadius = s.viewRadius
|
||||
s.poolSize = 4
|
||||
if _, superseded := s.processRecenter(context.Background(), 7, -3); superseded {
|
||||
t.Fatal("unexpected recenter supersession")
|
||||
}
|
||||
defer s.tickets.Close()
|
||||
|
||||
lastDistance := int32(-1)
|
||||
chunks := 0
|
||||
for _, packet := range recorder.take(t) {
|
||||
if packet.ID != protocol.PlayLevelChunk {
|
||||
continue
|
||||
}
|
||||
r := packet.Body()
|
||||
x, err := r.Int32()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
z, err := r.Int32()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
distance := chunkDistanceFrom(7, -3, [2]int32{x, z})
|
||||
if distance < lastDistance {
|
||||
t.Fatalf("chunk (%d,%d) at distance %d arrived after distance %d", x, z, distance, lastDistance)
|
||||
}
|
||||
lastDistance = distance
|
||||
chunks++
|
||||
}
|
||||
if chunks != 25 {
|
||||
t.Fatalf("level chunks sent = %d, want 25", chunks)
|
||||
}
|
||||
if got := cache.Stats().Tickets; got != 25 {
|
||||
t.Fatalf("tickets = %d, want 25", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamerQueuedRecenterStopsOldOuterRings(t *testing.T) {
|
||||
cache := world.NewCache(-1, func(cx, cz int32) *world.Chunk {
|
||||
return world.NewChunk(cx, cz, world.BiomePlains)
|
||||
})
|
||||
s := newStreamer(cache, nil, nil, 2)
|
||||
s.genRadius = s.viewRadius
|
||||
s.poolSize = 4
|
||||
s.requestRecenter(100, 100)
|
||||
next, superseded := s.processRecenter(context.Background(), 0, 0)
|
||||
defer s.tickets.Close()
|
||||
if !superseded || next != (recenterReq{cx: 100, cz: 100}) {
|
||||
t.Fatalf("superseded=%v next=%+v, want latest (100,100)", superseded, next)
|
||||
}
|
||||
if len(s.loaded) != 1 || !s.loaded[[2]int32{0, 0}] {
|
||||
t.Fatalf("old loaded set = %v, want only old center", s.loaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamerRecenterForgetsViewAndReplacesPrefetchTickets(t *testing.T) {
|
||||
cache := world.NewCacheWithLimit(-1, func(cx, cz int32) *world.Chunk {
|
||||
return world.NewChunk(cx, cz, world.BiomePlains)
|
||||
}, nil, 9)
|
||||
recorder := &recordingConn{}
|
||||
s := newStreamer(cache, NewConn(recorder), nil, 2)
|
||||
s.viewRadius, s.genRadius, s.poolSize = 0, 1, 4
|
||||
defer s.tickets.Close()
|
||||
if _, superseded := s.processRecenter(context.Background(), 0, 0); superseded {
|
||||
t.Fatal("unexpected first recenter supersession")
|
||||
}
|
||||
if len(s.loaded) != 1 || cache.Stats().Tickets != 9 {
|
||||
t.Fatalf("first lifecycle loaded=%v stats=%+v", s.loaded, cache.Stats())
|
||||
}
|
||||
recorder.take(t)
|
||||
|
||||
if _, superseded := s.processRecenter(context.Background(), 10, 10); superseded {
|
||||
t.Fatal("unexpected second recenter supersession")
|
||||
}
|
||||
if len(s.loaded) != 1 || !s.loaded[[2]int32{10, 10}] {
|
||||
t.Fatalf("second loaded set = %v, want only (10,10)", s.loaded)
|
||||
}
|
||||
stats := cache.Stats()
|
||||
if stats.Tickets != 9 || stats.Chunks > 9 || hasLevelChunkPacket(recorder.take(t), protocol.PlayForgetLevelChunk) != 1 {
|
||||
t.Fatalf("second lifecycle stats=%+v; want 9 tickets, <=9 chunks and one forget", stats)
|
||||
}
|
||||
}
|
||||
|
||||
func hasLevelChunkPacket(packets []protocol.Packet, id int32) int {
|
||||
count := 0
|
||||
for _, packet := range packets {
|
||||
if packet.ID == id {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func TestLoadSixteenClientStreamersBoundedAndReleasesTickets(t *testing.T) {
|
||||
var active, peak atomic.Int32
|
||||
gen := func(cx, cz int32) *world.Chunk {
|
||||
now := active.Add(1)
|
||||
for {
|
||||
old := peak.Load()
|
||||
if now <= old || peak.CompareAndSwap(old, now) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
active.Add(-1)
|
||||
return world.NewChunk(cx, cz, world.BiomePlains)
|
||||
}
|
||||
cache := world.NewCacheWithLimit(-1, gen, nil, 32)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
const clients = 16
|
||||
const groups = 4
|
||||
recorders := make([]*recordingConn, clients)
|
||||
for i := 0; i < clients; i++ {
|
||||
recorders[i] = &recordingConn{}
|
||||
s := newStreamer(cache, NewConn(recorders[i]), nil, 2)
|
||||
// A 3x3 view produces 144 ticket claims. Four spawn regions exercise
|
||||
// both shared tickets and concurrent independent generation.
|
||||
s.viewRadius, s.genRadius, s.poolSize = 1, 1, 4
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
group := int32(index % groups)
|
||||
s.requestRecenter(group*64, group*64)
|
||||
s.run(ctx)
|
||||
}(i)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(120 * time.Second)
|
||||
for (cache.Stats().Frames < groups*9 || cache.Stats().Tickets < clients*9 || clientsWithPacket(recorders, protocol.PlayLevelChunk) < clients) && time.Now().Before(deadline) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
stats := cache.Stats()
|
||||
if stats.Frames < groups*9 || stats.Tickets != clients*9 || clientsWithPacket(recorders, protocol.PlayLevelChunk) != clients {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
t.Fatalf("loaded stats = %+v, want at least %d frames and %d tickets", stats, groups*9, clients*9)
|
||||
}
|
||||
if got := peak.Load(); got > 8 {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
t.Fatalf("peak concurrent generators = %d, want <= shared limit 8", got)
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
deadline = time.Now().Add(5 * time.Second)
|
||||
for cache.Stats().Tickets != 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if stats = cache.Stats(); stats.Tickets != 0 || stats.Chunks > 32 {
|
||||
t.Fatalf("after disconnect stats = %+v, want zero tickets and <=32 chunks", stats)
|
||||
}
|
||||
}
|
||||
|
||||
func clientsWithPacket(recorders []*recordingConn, id int32) int {
|
||||
count := 0
|
||||
for _, recorder := range recorders {
|
||||
if recorder.countPacketID(id) > 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue