From 09d69dabbadf721cd7deea93f5aeb4a3afa53e99 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:39:04 +0300 Subject: [PATCH] rtc: keep update cache eviction order when listing recent disconnects (#4975) GetRecentlyDisconnectedParticipants walked the participant update cache with Get, which moves each entry to the front of the LRU. Walking from newest to oldest left the cache in reverse order, so after a reconnect the next updates evicted the most recently updated participants first. In a room where the subscriber has tracked 128 or more participants, a participant that had just disconnected was then dropped from the cache and a later reconnect did not report it as disconnected, and the out-of-order version check was lost for recently updated participants. Use Peek, which reads an entry without changing its recency. --- pkg/rtc/participant_internal_test.go | 36 ++++++++++++++++++++++++++++ pkg/rtc/participant_signal.go | 3 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/pkg/rtc/participant_internal_test.go b/pkg/rtc/participant_internal_test.go index 5827d3f9f..f3271eea4 100644 --- a/pkg/rtc/participant_internal_test.go +++ b/pkg/rtc/participant_internal_test.go @@ -305,6 +305,42 @@ func TestOutOfOrderUpdates(t *testing.T) { require.Equal(t, "second update", sent.GetUpdate().Participants[0].Metadata) } +func TestRecentlyDisconnectedKeepsUpdateOrder(t *testing.T) { + p := newParticipantForTest("test") + + // fill the update cache (128 entries), the last update is a disconnect + const cacheSize = 128 + for i := range cacheSize { + state := livekit.ParticipantInfo_ACTIVE + if i == cacheSize-1 { + state = livekit.ParticipantInfo_DISCONNECTED + } + require.NoError(t, p.SendParticipantUpdate([]*livekit.ParticipantInfo{{ + Sid: fmt.Sprintf("PA_%d", i), + Identity: fmt.Sprintf("p%d", i), + Version: 1, + State: state, + }})) + } + + disconnected := p.GetRecentlyDisconnectedParticipants() + require.Len(t, disconnected, 1) + require.Equal(t, "PA_127", disconnected[0].Sid) + + // an update for a new participant evicts the least recently updated entry, + // not the disconnect that was just reported + require.NoError(t, p.SendParticipantUpdate([]*livekit.ParticipantInfo{{ + Sid: "PA_new", + Identity: "new", + Version: 1, + State: livekit.ParticipantInfo_ACTIVE, + }})) + + disconnected = p.GetRecentlyDisconnectedParticipants() + require.Len(t, disconnected, 1) + require.Equal(t, "PA_127", disconnected[0].Sid) +} + // after disconnection, things should continue to function and not panic func TestDisconnectTiming(t *testing.T) { t.Run("Negotiate doesn't panic after channel closed", func(t *testing.T) { diff --git a/pkg/rtc/participant_signal.go b/pkg/rtc/participant_signal.go index 9202ebb91..315fbfe99 100644 --- a/pkg/rtc/participant_signal.go +++ b/pkg/rtc/participant_signal.go @@ -268,7 +268,8 @@ func (p *ParticipantImpl) GetRecentlyDisconnectedParticipants() []*livekit.Parti p.updateLock.Lock() keys := p.updateCache.Keys() for i := len(keys) - 1; i >= 0; i-- { - if info, ok := p.updateCache.Get(keys[i]); ok { + // Peek, as Get would move each entry to the front and reverse the eviction order + if info, ok := p.updateCache.Peek(keys[i]); ok { if info.updatedAt.Before(lastSignalAt) { continue }