aboutsummaryrefslogtreecommitdiff
path: root/internal/game/owner_disconnect_test.go
blob: 81196a4b7736bed51a547d73b2dc7a907c55cd73 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package game

// Disconnect-cleanup regressions: DisconnectTick runs on the owner
// goroutine, so the Hub removal callback it triggers must execute inline
// rather than re-submitting through run() — which would deadlock waiting
// for the running tick to finish (freezing every tick afterward).

import (
	"testing"
	"time"

	"thehouseoficarus/internal/net"
)

// TestDisconnectTickHardRemoveCompletesOnOwner guards the single-owner
// re-entrancy fix: DisconnectTick (owner) -> Hub.HardRemove -> onRemove
// callback must not block on the owner from the owner.
func TestDisconnectTickHardRemoveCompletesOnOwner(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice")
	h.seedCharacter("alice", "alice")
	sess, _ := h.connect("alice")

	done := make(chan struct{})
	h.g.RunOnOwner(func() {
		sess.StartDisconnect(1)
	})
	h.barrier()

	go func() {
		h.g.RunOnOwner(func() { h.g.DisconnectTick() })
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("DisconnectTick deadlocked: Hub.HardRemove's onRemove callback re-submitted to the owner from the owner")
	}

	// The session was hard-removed and its character persisted.
	if sessions := h.g.Hub.AllSessions(); len(sessions) != 0 {
		t.Fatalf("session still in hub after disconnect tick: %d", len(sessions))
	}
	if _, err := h.g.AccountStore.LoadCharacter("alice"); err != nil {
		t.Fatalf("character file unreadable after disconnect save: %v", err)
	}
}

// TestHubRemoveNilPlayerOnOwner: DisconnectTick removes sessions without a
// loaded player through Hub.Remove, which runs the same onRemove callback.
func TestHubRemoveNilPlayerOnOwner(t *testing.T) {
	h := newHarness(t)
	sess := &net.Session{Conn: newChanConn()}
	sess.SetState(net.StateAccountName)
	sess.StartWriter()
	h.hub.Add(sess)

	done := make(chan struct{})
	go func() {
		h.g.RunOnOwner(func() { h.g.Hub.Remove(sess) })
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("Hub.Remove with a nil player deadlocked on the owner")
	}
	if sessions := h.g.Hub.AllSessions(); len(sessions) != 0 {
		t.Fatalf("player-less session still in hub: %d", len(sessions))
	}
}