-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
12 changed files
with
425 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package util | ||
|
||
import ( | ||
"github.com/pkg/errors" | ||
"go.mongodb.org/mongo-driver/bson" | ||
"go.mongodb.org/mongo-driver/bson/primitive" | ||
"go.mongodb.org/mongo-driver/mongo" | ||
) | ||
|
||
func GetClusterTimeFromSession(sess mongo.Session) (primitive.Timestamp, error) { | ||
ctStruct := struct { | ||
ClusterTime struct { | ||
ClusterTime primitive.Timestamp `bson:"clusterTime"` | ||
} `bson:"$clusterTime"` | ||
}{} | ||
|
||
clusterTimeRaw := sess.ClusterTime() | ||
err := bson.Unmarshal(sess.ClusterTime(), &ctStruct) | ||
if err != nil { | ||
return primitive.Timestamp{}, errors.Wrapf(err, "failed to find clusterTime in session cluster time document (%v)", clusterTimeRaw) | ||
} | ||
|
||
return ctStruct.ClusterTime.ClusterTime, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package util | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/10gen/migration-verifier/option" | ||
) | ||
|
||
// Eventual solves the “one writer, many readers” problem: a value gets | ||
// written once, then the readers will see that the value is `Ready()` and | ||
// can then `Get()` it. | ||
// | ||
// It’s like how `context.Context`’s `Done()` and `Err()` methods work, but | ||
// generalized to any data type. | ||
type Eventual[T any] struct { | ||
ready chan struct{} | ||
val option.Option[T] | ||
mux sync.RWMutex | ||
} | ||
|
||
// NewEventual creates an Eventual and returns a pointer | ||
// to it. | ||
func NewEventual[T any]() *Eventual[T] { | ||
return &Eventual[T]{ | ||
ready: make(chan struct{}), | ||
} | ||
} | ||
|
||
// Ready returns a channel that closes once the Eventual’s value is ready. | ||
func (e *Eventual[T]) Ready() <-chan struct{} { | ||
return e.ready | ||
} | ||
|
||
// Get returns the Eventual’s value if it’s ready. | ||
// It panics otherwise. | ||
func (e *Eventual[T]) Get() T { | ||
e.mux.RLock() | ||
defer e.mux.RUnlock() | ||
|
||
val, has := e.val.Get() | ||
if has { | ||
return val | ||
} | ||
|
||
panic("Eventual's Get() called before value was ready.") | ||
} | ||
|
||
// Set sets the Eventual’s value. It may be called only once; | ||
// if called again it will panic. | ||
func (e *Eventual[T]) Set(val T) { | ||
e.mux.Lock() | ||
defer e.mux.Unlock() | ||
|
||
if e.val.IsSome() { | ||
panic("Tried to set an eventual twice!") | ||
} | ||
|
||
// NB: This *must* happen before the close(), or else a fast reader may | ||
// not see this value. | ||
e.val = option.Some(val) | ||
|
||
close(e.ready) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package util | ||
|
||
import ( | ||
"time" | ||
) | ||
|
||
func (s *UnitTestSuite) TestEventual() { | ||
eventual := NewEventual[int]() | ||
|
||
s.Assert().Panics( | ||
func() { eventual.Get() }, | ||
"Get() should panic before the value is set", | ||
) | ||
|
||
select { | ||
case <-eventual.Ready(): | ||
s.Require().Fail("should not be ready") | ||
case <-time.NewTimer(time.Second).C: | ||
} | ||
|
||
eventual.Set(123) | ||
|
||
select { | ||
case <-eventual.Ready(): | ||
case <-time.NewTimer(time.Second).C: | ||
s.Require().Fail("should be ready") | ||
} | ||
|
||
s.Assert().Equal( | ||
123, | ||
eventual.Get(), | ||
"Get() should return the value", | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.