Skip to content

Commit dd2478c

Browse files
authored
REP-5358 Fix hangs when reading change stream and documents. (#77)
This fixes hangs from the change stream and document reader when contexts are canceled. StartChangeEventHandler doesn’t need to send to the error channel. It now just returns its error. The other channels—writes-off, error, and done—now allow infinite (non-blocking) reads once populated. This works via a new struct, Eventual, that supplies the same semantics as Context’s Done() and Err() methods. The change stream struct’s writes-off and error channels are written to be Eventual. The doneChan remains a channel but now is just closed rather than written to. This also refactors a couple functions for general reuse and fixes TestStartAtTimeNoChanges, which has flapped occasionally.
1 parent 09adbd0 commit dd2478c

File tree

12 files changed

+425
-111
lines changed

12 files changed

+425
-111
lines changed

internal/testutil/testutil.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package testutil
22

33
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/pkg/errors"
48
"go.mongodb.org/mongo-driver/bson"
59
"go.mongodb.org/mongo-driver/mongo"
10+
"go.mongodb.org/mongo-driver/mongo/options"
11+
"go.mongodb.org/mongo-driver/mongo/readconcern"
612
)
713

814
// Marshal wraps `bsonMarshal` with a panic on failure.
@@ -34,3 +40,64 @@ func convertDocsToAnys(docs []bson.D) []any {
3440

3541
return anys
3642
}
43+
44+
func KillApplicationChangeStreams(
45+
ctx context.Context,
46+
t *testing.T,
47+
client *mongo.Client,
48+
appName string,
49+
) error {
50+
// Kill verifier’s change stream.
51+
cursor, err := client.Database(
52+
"admin",
53+
options.Database().SetReadConcern(readconcern.Local()),
54+
).Aggregate(
55+
ctx,
56+
mongo.Pipeline{
57+
{
58+
{"$currentOp", bson.D{
59+
{"idleCursors", true},
60+
}},
61+
},
62+
{
63+
{"$match", bson.D{
64+
{"clientMetadata.application.name", appName},
65+
{"command.collection", "$cmd.aggregate"},
66+
{"cursor.originatingCommand.pipeline.0.$_internalChangeStreamOplogMatch",
67+
bson.D{{"$type", "object"}},
68+
},
69+
}},
70+
},
71+
},
72+
)
73+
if err != nil {
74+
return errors.Wrapf(err, "failed to list %#q's change streams", appName)
75+
}
76+
77+
ops := []struct {
78+
Opid any
79+
}{}
80+
err = cursor.All(ctx, &ops)
81+
if err != nil {
82+
return errors.Wrapf(err, "failed to decode %#q's change streams", appName)
83+
}
84+
85+
for _, op := range ops {
86+
t.Logf("Killing change stream op %+v", op.Opid)
87+
88+
err :=
89+
client.Database("admin").RunCommand(
90+
ctx,
91+
bson.D{
92+
{"killOp", 1},
93+
{"op", op.Opid},
94+
},
95+
).Err()
96+
97+
if err != nil {
98+
return errors.Wrapf(err, "failed to kill change stream with opId %#q", op.Opid)
99+
}
100+
}
101+
102+
return nil
103+
}

internal/util/cluster_time.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package util
2+
3+
import (
4+
"github.com/pkg/errors"
5+
"go.mongodb.org/mongo-driver/bson"
6+
"go.mongodb.org/mongo-driver/bson/primitive"
7+
"go.mongodb.org/mongo-driver/mongo"
8+
)
9+
10+
func GetClusterTimeFromSession(sess mongo.Session) (primitive.Timestamp, error) {
11+
ctStruct := struct {
12+
ClusterTime struct {
13+
ClusterTime primitive.Timestamp `bson:"clusterTime"`
14+
} `bson:"$clusterTime"`
15+
}{}
16+
17+
clusterTimeRaw := sess.ClusterTime()
18+
err := bson.Unmarshal(sess.ClusterTime(), &ctStruct)
19+
if err != nil {
20+
return primitive.Timestamp{}, errors.Wrapf(err, "failed to find clusterTime in session cluster time document (%v)", clusterTimeRaw)
21+
}
22+
23+
return ctStruct.ClusterTime.ClusterTime, nil
24+
}

internal/util/eventual.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package util
2+
3+
import (
4+
"sync"
5+
6+
"github.com/10gen/migration-verifier/option"
7+
)
8+
9+
// Eventual solves the “one writer, many readers” problem: a value gets
10+
// written once, then the readers will see that the value is `Ready()` and
11+
// can then `Get()` it.
12+
//
13+
// It’s like how `context.Context`’s `Done()` and `Err()` methods work, but
14+
// generalized to any data type.
15+
type Eventual[T any] struct {
16+
ready chan struct{}
17+
val option.Option[T]
18+
mux sync.RWMutex
19+
}
20+
21+
// NewEventual creates an Eventual and returns a pointer
22+
// to it.
23+
func NewEventual[T any]() *Eventual[T] {
24+
return &Eventual[T]{
25+
ready: make(chan struct{}),
26+
}
27+
}
28+
29+
// Ready returns a channel that closes once the Eventual’s value is ready.
30+
func (e *Eventual[T]) Ready() <-chan struct{} {
31+
return e.ready
32+
}
33+
34+
// Get returns the Eventual’s value if it’s ready.
35+
// It panics otherwise.
36+
func (e *Eventual[T]) Get() T {
37+
e.mux.RLock()
38+
defer e.mux.RUnlock()
39+
40+
val, has := e.val.Get()
41+
if has {
42+
return val
43+
}
44+
45+
panic("Eventual's Get() called before value was ready.")
46+
}
47+
48+
// Set sets the Eventual’s value. It may be called only once;
49+
// if called again it will panic.
50+
func (e *Eventual[T]) Set(val T) {
51+
e.mux.Lock()
52+
defer e.mux.Unlock()
53+
54+
if e.val.IsSome() {
55+
panic("Tried to set an eventual twice!")
56+
}
57+
58+
// NB: This *must* happen before the close(), or else a fast reader may
59+
// not see this value.
60+
e.val = option.Some(val)
61+
62+
close(e.ready)
63+
}

internal/util/eventual_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package util
2+
3+
import (
4+
"time"
5+
)
6+
7+
func (s *UnitTestSuite) TestEventual() {
8+
eventual := NewEventual[int]()
9+
10+
s.Assert().Panics(
11+
func() { eventual.Get() },
12+
"Get() should panic before the value is set",
13+
)
14+
15+
select {
16+
case <-eventual.Ready():
17+
s.Require().Fail("should not be ready")
18+
case <-time.NewTimer(time.Second).C:
19+
}
20+
21+
eventual.Set(123)
22+
23+
select {
24+
case <-eventual.Ready():
25+
case <-time.NewTimer(time.Second).C:
26+
s.Require().Fail("should be ready")
27+
}
28+
29+
s.Assert().Equal(
30+
123,
31+
eventual.Get(),
32+
"Get() should return the value",
33+
)
34+
}

internal/verifier/change_stream.go

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type DocKey struct {
4444

4545
const (
4646
minChangeStreamPersistInterval = time.Second * 10
47+
maxChangeStreamAwaitTime = time.Second
4748
metadataChangeStreamCollectionName = "changeStream"
4849
)
4950

@@ -68,8 +69,8 @@ type ChangeStreamReader struct {
6869

6970
changeStreamRunning bool
7071
changeEventBatchChan chan []ParsedEvent
71-
writesOffTsChan chan primitive.Timestamp
72-
errChan chan error
72+
writesOffTs *util.Eventual[primitive.Timestamp]
73+
error *util.Eventual[error]
7374
doneChan chan struct{}
7475

7576
startAtTs *primitive.Timestamp
@@ -87,8 +88,8 @@ func (verifier *Verifier) initializeChangeStreamReaders() {
8788
clusterInfo: *verifier.srcClusterInfo,
8889
changeStreamRunning: false,
8990
changeEventBatchChan: make(chan []ParsedEvent),
90-
writesOffTsChan: make(chan primitive.Timestamp),
91-
errChan: make(chan error),
91+
writesOffTs: util.NewEventual[primitive.Timestamp](),
92+
error: util.NewEventual[error](),
9293
doneChan: make(chan struct{}),
9394
lag: msync.NewTypedAtomic(option.None[time.Duration]()),
9495
}
@@ -101,8 +102,8 @@ func (verifier *Verifier) initializeChangeStreamReaders() {
101102
clusterInfo: *verifier.dstClusterInfo,
102103
changeStreamRunning: false,
103104
changeEventBatchChan: make(chan []ParsedEvent),
104-
writesOffTsChan: make(chan primitive.Timestamp),
105-
errChan: make(chan error),
105+
writesOffTs: util.NewEventual[primitive.Timestamp](),
106+
error: util.NewEventual[error](),
106107
doneChan: make(chan struct{}),
107108
lag: msync.NewTypedAtomic(option.None[time.Duration]()),
108109
}
@@ -123,7 +124,6 @@ func (verifier *Verifier) StartChangeEventHandler(ctx context.Context, reader *C
123124
verifier.logger.Trace().Msgf("Verifier is handling a change event batch from %s: %v", reader, batch)
124125
err := verifier.HandleChangeStreamEvents(ctx, batch, reader.readerType)
125126
if err != nil {
126-
reader.errChan <- err
127127
return err
128128
}
129129
}
@@ -268,6 +268,8 @@ func (csr *ChangeStreamReader) readAndHandleOneChangeEventBatch(
268268
eventsRead := 0
269269
var changeEventBatch []ParsedEvent
270270

271+
latestEvent := option.None[ParsedEvent]()
272+
271273
for hasEventInBatch := true; hasEventInBatch; hasEventInBatch = cs.RemainingBatchLength() > 0 {
272274
gotEvent := cs.TryNext(ctx)
273275

@@ -293,7 +295,9 @@ func (csr *ChangeStreamReader) readAndHandleOneChangeEventBatch(
293295
if changeEventBatch[eventsRead].ClusterTime != nil &&
294296
(csr.lastChangeEventTime == nil ||
295297
csr.lastChangeEventTime.Before(*changeEventBatch[eventsRead].ClusterTime)) {
298+
296299
csr.lastChangeEventTime = changeEventBatch[eventsRead].ClusterTime
300+
latestEvent = option.Some(changeEventBatch[eventsRead])
297301
}
298302

299303
eventsRead++
@@ -305,6 +309,12 @@ func (csr *ChangeStreamReader) readAndHandleOneChangeEventBatch(
305309
return nil
306310
}
307311

312+
if event, has := latestEvent.Get(); has {
313+
csr.logger.Trace().
314+
Interface("event", event).
315+
Msg("Updated lastChangeEventTime.")
316+
}
317+
308318
var curTs primitive.Timestamp
309319
curTs, err := extractTimestampFromResumeToken(cs.ResumeToken())
310320
if err == nil {
@@ -355,7 +365,9 @@ func (csr *ChangeStreamReader) iterateChangeStream(
355365
// source writes are ended and the migration tool is finished / committed.
356366
// This means we should exit rather than continue reading the change stream
357367
// since there should be no more events.
358-
case writesOffTs := <-csr.writesOffTsChan:
368+
case <-csr.writesOffTs.Ready():
369+
writesOffTs := csr.writesOffTs.Get()
370+
359371
csr.logger.Debug().
360372
Interface("writesOffTimestamp", writesOffTs).
361373
Msgf("%s thread received writesOff timestamp. Finalizing change stream.", csr)
@@ -408,7 +420,7 @@ func (csr *ChangeStreamReader) iterateChangeStream(
408420
}
409421
// since we have started Recheck, we must signal that we have
410422
// finished the change stream changes so that Recheck can continue.
411-
csr.doneChan <- struct{}{}
423+
close(csr.doneChan)
412424
break
413425
}
414426
}
@@ -430,7 +442,7 @@ func (csr *ChangeStreamReader) createChangeStream(
430442
) (*mongo.ChangeStream, mongo.Session, primitive.Timestamp, error) {
431443
pipeline := csr.GetChangeStreamFilter()
432444
opts := options.ChangeStream().
433-
SetMaxAwaitTime(1 * time.Second).
445+
SetMaxAwaitTime(maxChangeStreamAwaitTime).
434446
SetFullDocument(options.UpdateLookup)
435447

436448
if csr.clusterInfo.VersionArray[0] >= 6 {
@@ -487,11 +499,17 @@ func (csr *ChangeStreamReader) createChangeStream(
487499
// With sharded clusters the resume token might lead the cluster time
488500
// by 1 increment. In that case we need the actual cluster time;
489501
// otherwise we will get errors.
490-
clusterTime, err := getClusterTimeFromSession(sess)
502+
clusterTime, err := util.GetClusterTimeFromSession(sess)
491503
if err != nil {
492504
return nil, nil, primitive.Timestamp{}, errors.Wrap(err, "failed to read cluster time from session")
493505
}
494506

507+
csr.logger.Debug().
508+
Interface("resumeTokenTimestamp", startTs).
509+
Interface("clusterTime", clusterTime).
510+
Stringer("changeStreamReader", csr).
511+
Msg("Using earlier time as start timestamp.")
512+
495513
if startTs.After(clusterTime) {
496514
startTs = clusterTime
497515
}
@@ -542,10 +560,7 @@ func (csr *ChangeStreamReader) StartChangeStream(ctx context.Context) error {
542560
).Run(ctx, csr.logger)
543561

544562
if err != nil {
545-
// NB: This failure always happens after the initial change stream
546-
// creation.
547-
csr.errChan <- err
548-
close(csr.errChan)
563+
csr.error.Set(err)
549564
}
550565
}()
551566

@@ -661,19 +676,3 @@ func extractTimestampFromResumeToken(resumeToken bson.Raw) (primitive.Timestamp,
661676

662677
return resumeTokenTime, nil
663678
}
664-
665-
func getClusterTimeFromSession(sess mongo.Session) (primitive.Timestamp, error) {
666-
ctStruct := struct {
667-
ClusterTime struct {
668-
ClusterTime primitive.Timestamp `bson:"clusterTime"`
669-
} `bson:"$clusterTime"`
670-
}{}
671-
672-
clusterTimeRaw := sess.ClusterTime()
673-
err := bson.Unmarshal(sess.ClusterTime(), &ctStruct)
674-
if err != nil {
675-
return primitive.Timestamp{}, errors.Wrapf(err, "failed to find clusterTime in session cluster time document (%v)", clusterTimeRaw)
676-
}
677-
678-
return ctStruct.ClusterTime.ClusterTime, nil
679-
}

0 commit comments

Comments
 (0)