mirror of
https://github.com/livekit/livekit.git
synced 2026-09-15 08:45:37 +00:00
* Flush pending signal responses before closing the web socket. When the request direction of a signalling connection goes away, the web socket was closed right away. Responses that the participant had already sent were dropped. This loses the leave request on migration. The media node writes leave(RESUME) and closes the signalling connection just after. The close won the race, so the client saw a plain web socket close with code 1000 and never got the leave. It then did a full reconnect instead of a resume. Now the response pump is signalled first and drains what is pending, then the web socket is closed. The producer closes the response source after its last write, so draining till the source is closed is a complete flush. A deadline bounds the case where the source stays open. The web socket is still closed on all paths, so the ping worker does not leak. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Give the response pump a margin over the drain deadline. Both waits used the same timeout and started at about the same time. So when the drain ran to its deadline, the outer wait could give up at the same moment and close the web socket while the pump was still writing. That write failed and the message was lost. It also logged a timeout even though nothing was stuck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
// Copyright 2026 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package service
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/livekit/protocol/livekit"
|
|
|
|
"github.com/livekit/livekit-server/pkg/routing"
|
|
)
|
|
|
|
func leaveResponse(action livekit.LeaveRequest_Action) *livekit.SignalResponse {
|
|
return &livekit.SignalResponse{
|
|
Message: &livekit.SignalResponse_Leave{
|
|
Leave: &livekit.LeaveRequest{
|
|
Action: action,
|
|
Reason: livekit.DisconnectReason_MIGRATION,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestDrainMessageSource(t *testing.T) {
|
|
collect := func(msgs *[]proto.Message) func(proto.Message) bool {
|
|
return func(msg proto.Message) bool {
|
|
*msgs = append(*msgs, msg)
|
|
return true
|
|
}
|
|
}
|
|
|
|
t.Run("drains queued messages of a closed source", func(t *testing.T) {
|
|
source := routing.NewDefaultMessageChannel("CO_test")
|
|
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME)))
|
|
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RECONNECT)))
|
|
source.Close()
|
|
|
|
var got []proto.Message
|
|
require.True(t, drainMessageSource(source, time.Second, collect(&got)))
|
|
require.Len(t, got, 2)
|
|
require.Equal(
|
|
t,
|
|
livekit.LeaveRequest_RESUME,
|
|
got[0].(*livekit.SignalResponse).GetLeave().GetAction(),
|
|
)
|
|
require.Equal(
|
|
t,
|
|
livekit.LeaveRequest_RECONNECT,
|
|
got[1].(*livekit.SignalResponse).GetLeave().GetAction(),
|
|
)
|
|
})
|
|
|
|
t.Run("drains messages written while draining", func(t *testing.T) {
|
|
source := routing.NewDefaultMessageChannel("CO_test")
|
|
// mimics the relay pushing a message that was still in flight when the
|
|
// request direction went away, and closing the source right after
|
|
go func() {
|
|
time.Sleep(20 * time.Millisecond)
|
|
_ = source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME))
|
|
source.Close()
|
|
}()
|
|
|
|
var got []proto.Message
|
|
require.True(t, drainMessageSource(source, time.Second, collect(&got)))
|
|
require.Len(t, got, 1)
|
|
})
|
|
|
|
t.Run("gives up on the deadline when the source stays open", func(t *testing.T) {
|
|
source := routing.NewDefaultMessageChannel("CO_test")
|
|
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME)))
|
|
|
|
var got []proto.Message
|
|
start := time.Now()
|
|
require.False(t, drainMessageSource(source, 50*time.Millisecond, collect(&got)))
|
|
require.GreaterOrEqual(t, time.Since(start), 50*time.Millisecond)
|
|
// what was queued is still flushed
|
|
require.Len(t, got, 1)
|
|
})
|
|
|
|
t.Run("stops when the write fails", func(t *testing.T) {
|
|
source := routing.NewDefaultMessageChannel("CO_test")
|
|
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME)))
|
|
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RECONNECT)))
|
|
source.Close()
|
|
|
|
var got []proto.Message
|
|
require.False(t, drainMessageSource(source, time.Second, func(msg proto.Message) bool {
|
|
got = append(got, msg)
|
|
return false
|
|
}))
|
|
require.Len(t, got, 1)
|
|
})
|
|
}
|