mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 02:32:06 +00:00
fix(multiplayer): harden reconnect lifecycle fencing
This commit is contained in:
@@ -91,6 +91,9 @@ func (r *RankedConnections) validate(auth JoinAuthorisation, now time.Time) erro
|
||||
// slot with the next server-owned generation. A newer generation fences every
|
||||
// older connection, even if the backend is temporarily unavailable.
|
||||
func (r *RankedConnections) Admit(auth JoinAuthorisation, now time.Time) (uint64, error) {
|
||||
if now.IsZero() {
|
||||
return 0, ErrJoinAuthorisation
|
||||
}
|
||||
if err := r.validate(auth, now); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -107,7 +110,13 @@ func (r *RankedConnections) Admit(auth JoinAuthorisation, now time.Time) (uint64
|
||||
if player.Abandoned {
|
||||
return 0, ErrReconnectExpired
|
||||
}
|
||||
if !player.ConnectedAt.IsZero() && player.LostAt.IsZero() {
|
||||
return 0, ErrConnectionFenced
|
||||
}
|
||||
if !player.LostAt.IsZero() {
|
||||
if now.Before(player.LostAt) {
|
||||
return 0, ErrJoinAuthorisation
|
||||
}
|
||||
if now.Sub(player.LostAt) > RankedReconnectGrace {
|
||||
return 0, ErrReconnectExpired
|
||||
}
|
||||
@@ -120,6 +129,9 @@ func (r *RankedConnections) Admit(auth JoinAuthorisation, now time.Time) (uint64
|
||||
}
|
||||
|
||||
func (r *RankedConnections) Disconnect(playerID string, generation uint64, now time.Time) error {
|
||||
if now.IsZero() {
|
||||
return ErrJoinAuthorisation
|
||||
}
|
||||
player, ok := r.players[playerID]
|
||||
if !ok {
|
||||
return ErrJoinAuthorisation
|
||||
@@ -130,6 +142,9 @@ func (r *RankedConnections) Disconnect(playerID string, generation uint64, now t
|
||||
if player.Abandoned {
|
||||
return ErrReconnectExpired
|
||||
}
|
||||
if player.ConnectedAt.IsZero() || !player.LostAt.IsZero() || now.Before(player.ConnectedAt) {
|
||||
return ErrConnectionFenced
|
||||
}
|
||||
player.LostAt = now
|
||||
r.players[playerID] = player
|
||||
return nil
|
||||
|
||||
@@ -26,19 +26,19 @@ func TestRankedReconnectReclaimsWithinGraceAndFencesOldGeneration(t *testing.T)
|
||||
if gen, err := r.Admit(auth, now); err != nil || gen != 1 {
|
||||
t.Fatalf("initial admit = %d, %v", gen, err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now); err != nil {
|
||||
if err := r.Disconnect("a", 1, now.Add(time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gen, err := r.Admit(auth, now.Add(RankedReconnectGrace)); err != nil || gen != 2 {
|
||||
if gen, err := r.Admit(auth, now.Add(time.Second+RankedReconnectGrace)); err != nil || gen != 2 {
|
||||
t.Fatalf("boundary reclaim = %d, %v", gen, err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now.Add(31*time.Second)); !errors.Is(err, ErrConnectionFenced) {
|
||||
if err := r.Disconnect("a", 1, now.Add(62*time.Second)); !errors.Is(err, ErrConnectionFenced) {
|
||||
t.Fatalf("old connection was not fenced: %v", err)
|
||||
}
|
||||
if err := r.Disconnect("a", 2, now.Add(31*time.Second)); err != nil {
|
||||
if err := r.Disconnect("a", 2, now.Add(62*time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gen, err := r.Admit(auth, now.Add(32*time.Second)); err != nil || gen != 3 {
|
||||
if gen, err := r.Admit(auth, now.Add(63*time.Second)); err != nil || gen != 3 {
|
||||
t.Fatalf("repeated reclaim with existing authorisation = %d, %v", gen, err)
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ func TestRankedReconnectRejectsWrongBindingAndExpiredGrace(t *testing.T) {
|
||||
if _, err := r.Admit(wrongIdentity, now); !errors.Is(err, ErrJoinAuthorisation) {
|
||||
t.Fatalf("wrong SteamID accepted: %v", err)
|
||||
}
|
||||
if _, err := r.Admit(testRoster(now)[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -67,6 +70,36 @@ func TestRankedReconnectRejectsWrongBindingAndExpiredGrace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankedReconnectRejectsDuplicateAndTimeReversedLifecycle(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
r, err := NewRankedConnections("match-1", "server-1", "v1", testRoster(now))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
auth := testRoster(now)[0]
|
||||
if err := r.Disconnect("a", 1, now); !errors.Is(err, ErrConnectionFenced) {
|
||||
t.Fatalf("disconnect before admission error = %v", err)
|
||||
}
|
||||
if _, err := r.Admit(auth, time.Time{}); !errors.Is(err, ErrJoinAuthorisation) {
|
||||
t.Fatalf("zero-time admission error = %v", err)
|
||||
}
|
||||
if _, err := r.Admit(auth, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.Admit(auth, now.Add(time.Second)); !errors.Is(err, ErrConnectionFenced) {
|
||||
t.Fatalf("duplicate active admission error = %v", err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now.Add(2*time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now.Add(30*time.Second)); !errors.Is(err, ErrConnectionFenced) {
|
||||
t.Fatalf("duplicate disconnect error = %v", err)
|
||||
}
|
||||
if _, err := r.Admit(auth, now.Add(time.Second)); !errors.Is(err, ErrJoinAuthorisation) {
|
||||
t.Fatalf("time-reversed reclaim error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankedRosterRejectsDuplicateSlots(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
roster := testRoster(now)
|
||||
@@ -91,6 +124,9 @@ func TestRankedAbandonCooldownUsesRollingSevenDayLadder(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.Admit(testRoster(now)[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user