Skip to content

Commit b09ed6c

Browse files
committed
fix(coordination): prevent spurious reconnect after session start
1 parent 5d37cfc commit b09ed6c

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Fixed coordination sessions spuriously reconnecting immediately after creation, which could cause non-idempotent operations to fail with `operation status is unknown`
2+
13
## v3.149.0
24
* Added `balancers.PreferPrimaryPile` and `balancers.PreferPrimaryPileWithFallback` endpoint selection policies
35

internal/coordination/session.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,10 @@ func (s *session) receiveLoop( //nolint:funlen
442442

443443
return
444444
case *Ydb_Coordination.SessionResponse_SessionStarted_:
445-
sessionStarted <- message.GetSessionStarted()
445+
// mainLoop may enter the keep-alive loop as soon as the notification is received.
446+
// Publish it only after the timestamp used by that loop has been initialized.
446447
s.updateLastGoodResponseTime()
448+
sessionStarted <- message.GetSessionStarted()
447449
case *Ydb_Coordination.SessionResponse_SessionStopped_:
448450
sessionStopped <- message.GetSessionStopped()
449451
s.cancel()

internal/coordination/session_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
11
package coordination
22

33
import (
4+
"context"
5+
"sync"
46
"testing"
7+
"time"
58

69
"github.com/stretchr/testify/require"
10+
"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Coordination"
11+
"go.uber.org/mock/gomock"
12+
13+
"github.com/ydb-platform/ydb-go-sdk/v3/trace"
714
)
815

16+
func TestReceiveLoopUpdatesLastGoodResponseTimeBeforeSessionStarted(t *testing.T) {
17+
ctrl := gomock.NewController(t)
18+
client := NewMockCoordinationService_SessionClient(ctrl)
19+
streamCtx, cancelStream := context.WithCancel(context.Background())
20+
startedResponse := &Ydb_Coordination.SessionResponse_SessionStarted{
21+
SessionId: 42,
22+
}
23+
recvReturned := make(chan struct{})
24+
25+
firstRecv := client.EXPECT().Recv().DoAndReturn(func() (*Ydb_Coordination.SessionResponse, error) {
26+
close(recvReturned)
27+
28+
return &Ydb_Coordination.SessionResponse{
29+
Response: &Ydb_Coordination.SessionResponse_SessionStarted_{
30+
SessionStarted: startedResponse,
31+
},
32+
}, nil
33+
})
34+
secondRecv := client.EXPECT().Recv().DoAndReturn(func() (*Ydb_Coordination.SessionResponse, error) {
35+
<-streamCtx.Done()
36+
37+
return nil, streamCtx.Err()
38+
})
39+
gomock.InOrder(firstRecv.Call, secondRecv.Call)
40+
41+
s := &session{
42+
trace: &trace.Coordination{},
43+
}
44+
sessionStarted := make(chan *Ydb_Coordination.SessionResponse_SessionStarted, 1)
45+
sessionStopped := make(chan *Ydb_Coordination.SessionResponse_SessionStopped, 1)
46+
wg := sync.WaitGroup{}
47+
wg.Add(1)
48+
s.mutex.Lock()
49+
go s.receiveLoop(&wg, client, cancelStream, sessionStarted, sessionStopped)
50+
51+
<-recvReturned
52+
var start *Ydb_Coordination.SessionResponse_SessionStarted
53+
select {
54+
case start = <-sessionStarted:
55+
case <-time.After(100 * time.Millisecond): //nolint:mnd
56+
}
57+
publishedBeforeTimestamp := start != nil
58+
s.mutex.Unlock()
59+
if start == nil {
60+
start = <-sessionStarted
61+
}
62+
63+
cancelStream()
64+
wg.Wait()
65+
require.False(t, publishedBeforeTimestamp, "session started was published before the keep-alive timestamp update")
66+
require.Same(t, startedResponse, start)
67+
require.False(t, s.getLastGoodResponseTime().IsZero())
68+
}
69+
970
func TestNewProtectionKey(t *testing.T) {
1071
key1 := newProtectionKey()
1172
require.NotNil(t, key1)

0 commit comments

Comments
 (0)