Skip to content

Commit 983efad

Browse files
committed
Put lag in log.
1 parent 25d06df commit 983efad

File tree

5 files changed

+145
-7
lines changed

5 files changed

+145
-7
lines changed

internal/verifier/change_stream.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/10gen/migration-verifier/internal/keystring"
99
"github.com/10gen/migration-verifier/internal/retry"
1010
"github.com/10gen/migration-verifier/internal/util"
11+
"github.com/10gen/migration-verifier/option"
1112
"github.com/pkg/errors"
1213
"github.com/rs/zerolog"
1314
"github.com/samber/mo"
@@ -138,14 +139,14 @@ func (verifier *Verifier) GetChangeStreamFilter() []bson.D {
138139
// is unideal but shouldn’t impede correctness since post-writesOff events
139140
// shouldn’t really happen anyway by definition.
140141
func (verifier *Verifier) readAndHandleOneChangeEventBatch(
141-
ctx context.Context,
142+
sctx mongo.SessionContext,
142143
cs *mongo.ChangeStream,
143144
) error {
144145
eventsRead := 0
145146
var changeEventBatch []ParsedEvent
146147

147148
for hasEventInBatch := true; hasEventInBatch; hasEventInBatch = cs.RemainingBatchLength() > 0 {
148-
gotEvent := cs.TryNext(ctx)
149+
gotEvent := cs.TryNext(sctx)
149150

150151
if cs.Err() != nil {
151152
return errors.Wrap(cs.Err(), "change stream iteration failed")
@@ -170,7 +171,15 @@ func (verifier *Verifier) readAndHandleOneChangeEventBatch(
170171
return nil
171172
}
172173

173-
err := verifier.HandleChangeStreamEvents(ctx, changeEventBatch)
174+
// Update the change stream lag, which we’ll display in the logs.
175+
lag, err := computeChangeStreamLag(sctx, cs)
176+
if err == nil {
177+
verifier.changeStreamLag.Store(option.Some(lag))
178+
} else {
179+
verifier.logger.Debug().Err(err).Msg("Failed to compute change stream lag.")
180+
}
181+
182+
err = verifier.HandleChangeStreamEvents(sctx, changeEventBatch)
174183
if err != nil {
175184
return errors.Wrap(err, "failed to handle change events")
176185
}
@@ -435,6 +444,20 @@ func (verifier *Verifier) loadChangeStreamResumeToken(ctx context.Context) (bson
435444
return token, err
436445
}
437446

447+
func computeChangeStreamLag(
448+
sctx mongo.SessionContext,
449+
cs *mongo.ChangeStream,
450+
) (time.Duration, error) {
451+
token := cs.ResumeToken()
452+
ts, err := extractTimestampFromResumeToken(token)
453+
if err != nil {
454+
return 0, errors.Wrap(err, "failed to extract timestamp from change stream resume token")
455+
}
456+
457+
optime := sctx.OperationTime()
458+
return time.Second * time.Duration(optime.T-ts.T), nil
459+
}
460+
438461
func (verifier *Verifier) persistChangeStreamResumeToken(sctx mongo.SessionContext, cs *mongo.ChangeStream) error {
439462
token := cs.ResumeToken()
440463

@@ -453,10 +476,6 @@ func (verifier *Verifier) persistChangeStreamResumeToken(sctx mongo.SessionConte
453476

454477
if err == nil {
455478
logEvent = addTimestampToLogEvent(ts, logEvent)
456-
457-
optime := sctx.OperationTime()
458-
lag := time.Second * time.Duration(optime.T-ts.T)
459-
logEvent = logEvent.Stringer("lagAmount", lag)
460479
} else {
461480
verifier.logger.Warn().Err(err).
462481
Msg("failed to extract resume token timestamp")

internal/verifier/migration_verifier.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"github.com/10gen/migration-verifier/internal/retry"
2323
"github.com/10gen/migration-verifier/internal/types"
2424
"github.com/10gen/migration-verifier/internal/uuidutil"
25+
"github.com/10gen/migration-verifier/msync"
26+
"github.com/10gen/migration-verifier/option"
2527
"github.com/olekukonko/tablewriter"
2628
"github.com/pkg/errors"
2729
"go.mongodb.org/mongo-driver/bson"
@@ -126,6 +128,7 @@ type Verifier struct {
126128
changeStreamWritesOffTsChan chan primitive.Timestamp
127129
changeStreamErrChan chan error
128130
changeStreamDoneChan chan struct{}
131+
changeStreamLag *msync.TypedAtomic[option.Option[time.Duration]]
129132
lastChangeEventTime *primitive.Timestamp
130133
writesOffTimestamp *primitive.Timestamp
131134

@@ -197,6 +200,7 @@ func NewVerifier(settings VerifierSettings) *Verifier {
197200
changeStreamWritesOffTsChan: make(chan primitive.Timestamp),
198201
changeStreamErrChan: make(chan error),
199202
changeStreamDoneChan: make(chan struct{}),
203+
changeStreamLag: msync.NewTypedAtomic(option.None[time.Duration]()),
200204
readConcernSetting: readConcern,
201205

202206
// This will get recreated once gen0 starts, but we want it

internal/verifier/summary.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,4 +424,10 @@ func (verifier *Verifier) printChangeEventStatistics(builder *strings.Builder) {
424424

425425
builder.WriteString("\nMost frequently-changing namespaces:\n")
426426
table.Render()
427+
428+
if lag, hasLag := verifier.changeStreamLag.Load().Get(); hasLag {
429+
builder.WriteString(
430+
fmt.Sprintf("\nChange stream lag: %s\n", reportutils.DurationToHMS(lag)),
431+
)
432+
}
427433
}

msync/typed_atomic.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package msync
2+
3+
import "sync/atomic"
4+
5+
// TypedAtomic is a type-safe wrapper around the standard-library atomic.Value.
6+
// TypedAtomic serves largely the same purpose as atomic.Pointer but stores
7+
// the value itself rather than a pointer to it. This is often more ergonomic
8+
// than an atomic.Pointer: it can be used to store constants directly (where
9+
// taking a pointer is inconvenient), and it defaults to the type's zero value
10+
// rather than a nil pointer.
11+
type TypedAtomic[T any] struct {
12+
v atomic.Value
13+
}
14+
15+
// NewTypedAtomic returns a new TypedAtomic, initialized to val.
16+
func NewTypedAtomic[T any](val T) *TypedAtomic[T] {
17+
var v atomic.Value
18+
v.Store(val)
19+
return &TypedAtomic[T]{v}
20+
}
21+
22+
// Load returns the value set by the most recent Store. It returns the zero
23+
// value for the type if there has been no call to Store.
24+
func (ta *TypedAtomic[T]) Load() T {
25+
return orZero[T](ta.v.Load())
26+
}
27+
28+
// Store sets the value TypedAtomic to val. Store(nil) panics.
29+
func (ta *TypedAtomic[T]) Store(val T) {
30+
ta.v.Store(val)
31+
}
32+
33+
// Swap stores newVal into the TypedAtomic and returns the previous value. It
34+
// returns the zero value for the type if the value is empty.
35+
func (ta *TypedAtomic[T]) Swap(newVal T) T {
36+
return orZero[T](ta.v.Swap(newVal))
37+
}
38+
39+
// CompareAndSwap executes the compare-and-swap operation for the TypedAtomic.
40+
func (ta *TypedAtomic[T]) CompareAndSwap(oldVal, newVal T) bool {
41+
return ta.v.CompareAndSwap(oldVal, newVal)
42+
}
43+
44+
func orZero[T any](val any) T {
45+
if val == nil {
46+
return *new(T)
47+
}
48+
49+
return val.(T)
50+
}

msync/typed_atomic_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package msync
2+
3+
import (
4+
"sync"
5+
)
6+
7+
func (s *unitTestSuite) TestTypedAtomic() {
8+
ta := NewTypedAtomic(42)
9+
10+
s.Require().Equal(42, ta.Load())
11+
s.Require().False(ta.CompareAndSwap(17, 99))
12+
s.Require().True(ta.CompareAndSwap(42, 99))
13+
s.Require().Equal(99, ta.Load())
14+
s.Require().Equal(99, ta.Swap(42))
15+
s.Require().Equal(42, ta.Load())
16+
17+
ta.Store(17)
18+
s.Require().Equal(17, ta.Load())
19+
20+
// This block is for race detection under -race.
21+
var wg sync.WaitGroup
22+
for i := 0; i < 100; i++ {
23+
wg.Add(1)
24+
go func() {
25+
defer wg.Done()
26+
ta.Load()
27+
ta.Store(i)
28+
}()
29+
}
30+
wg.Wait()
31+
}
32+
33+
func (s *unitTestSuite) TestAtomicZeroValues() {
34+
s.Run("string", func() {
35+
var ta TypedAtomic[string]
36+
s.Require().Equal("", ta.Load())
37+
s.Require().Equal("", ta.Swap("foo"))
38+
s.Require().Equal("foo", ta.Load())
39+
})
40+
41+
s.Run("int", func() {
42+
var ta TypedAtomic[int]
43+
s.Require().Equal(0, ta.Load())
44+
s.Require().Equal(0, ta.Swap(42))
45+
s.Require().Equal(42, ta.Load())
46+
})
47+
48+
s.Run("arbitrary data", func() {
49+
type data struct {
50+
I int
51+
S string
52+
}
53+
54+
var ta TypedAtomic[data]
55+
s.Require().Equal(data{}, ta.Load())
56+
s.Require().Equal(data{}, ta.Swap(data{76, "trombones"}))
57+
s.Require().Equal(data{76, "trombones"}, ta.Load())
58+
})
59+
}

0 commit comments

Comments
 (0)