Skip to content

Commit 647cc40

Browse files
authored
Optimize progress report queries (#167)
Previously, when running queries for the progress reports, verifier fetched all mismatches for all failed tasks. It would only display up to 40 mismatches, though … so even if there were thousands of mismatches, we discarded all but 40. This changeset alters the logic so that 3 queries run in parallel: - fetch first 20 differing-content mismatches - fetch first 20 missing/changed mismatches - count all mismatches, dividing them into differing-content vs. missing/changed Thus, the server only returns & parses up to 40 documents for these queries, which is much lighter than returning every single mismatch for all tasks.
1 parent f99f130 commit 647cc40

File tree

4 files changed

+207
-73
lines changed

4 files changed

+207
-73
lines changed

internal/verifier/migration_verifier_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/10gen/migration-verifier/internal/util"
2828
"github.com/10gen/migration-verifier/mbson"
2929
"github.com/10gen/migration-verifier/mslices"
30+
"github.com/10gen/migration-verifier/option"
3031
"github.com/cespare/permute/v2"
3132
"github.com/rs/zerolog"
3233
"github.com/samber/lo"
@@ -1254,6 +1255,8 @@ func (suite *IntegrationTestSuite) getFailuresForTask(
12541255
suite.Context(),
12551256
verifier.verificationDatabase(),
12561257
mslices.Of(taskID),
1258+
option.None[bson.D](),
1259+
option.None[int64](),
12571260
)
12581261

12591262
require.NoError(suite.T(), err)

internal/verifier/mismatches.go

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ type MismatchInfo struct {
2323
Detail VerificationResult
2424
}
2525

26+
func getMismatchDocMissingAggExpr(docExpr any) bson.D {
27+
return getResultDocMissingAggExpr(
28+
bson.D{{"$getField", bson.D{
29+
{"input", docExpr},
30+
{"field", "detail"},
31+
}}},
32+
)
33+
}
34+
2635
var _ bson.Marshaler = MismatchInfo{}
2736

2837
func (mi MismatchInfo) MarshalBSON() ([]byte, error) {
@@ -72,21 +81,91 @@ func createMismatchesCollection(ctx context.Context, db *mongo.Database) error {
7281
return nil
7382
}
7483

75-
func getMismatchesForTasks(
84+
func countMismatchesForTasks(
7685
ctx context.Context,
7786
db *mongo.Database,
7887
taskIDs []bson.ObjectID,
79-
) (map[bson.ObjectID][]VerificationResult, error) {
80-
cursor, err := db.Collection(mismatchesCollectionName).Find(
88+
filter bson.D,
89+
) (int64, int64, error) {
90+
cursor, err := db.Collection(mismatchesCollectionName).Aggregate(
8191
ctx,
82-
bson.D{
83-
{"task", bson.D{{"$in", taskIDs}}},
92+
mongo.Pipeline{
93+
{{"$match", bson.D{
94+
{"task", bson.D{{"$in", taskIDs}}},
95+
}}},
96+
{{"$group", bson.D{
97+
{"_id", nil},
98+
{"total", bson.D{{"$sum", 1}}},
99+
{"match", bson.D{{"$sum", bson.D{
100+
{"$cond", bson.D{
101+
{"if", filter},
102+
{"then", 1},
103+
{"else", 0},
104+
}},
105+
}}}},
106+
}}},
84107
},
85-
options.Find().SetSort(
108+
)
109+
110+
if err != nil {
111+
return 0, 0, errors.Wrap(err, "sending mismatch-counting query")
112+
}
113+
114+
var got []bson.Raw
115+
if err := cursor.All(ctx, &got); err != nil {
116+
return 0, 0, errors.Wrap(err, "reading mismatch counts")
117+
}
118+
119+
if len(got) != 1 {
120+
return 0, 0, fmt.Errorf("unexpected mismatch count result: %+v", got)
121+
}
122+
123+
totalRV, err := got[0].LookupErr("total")
124+
if err != nil {
125+
return 0, 0, errors.Wrap(err, "getting mismatch count’s total")
126+
}
127+
128+
matchRV, err := got[0].LookupErr("match")
129+
if err != nil {
130+
return 0, 0, errors.Wrap(err, "getting mismatch count’s filter-match count")
131+
}
132+
133+
matched := matchRV.AsInt64()
134+
135+
return matched, totalRV.AsInt64() - matched, nil
136+
}
137+
138+
func getMismatchesForTasks(
139+
ctx context.Context,
140+
db *mongo.Database,
141+
taskIDs []bson.ObjectID,
142+
filter option.Option[bson.D],
143+
limit option.Option[int64],
144+
) (map[bson.ObjectID][]VerificationResult, error) {
145+
findOpts := options.Find().
146+
SetSort(
86147
bson.D{
87148
{"detail.id", 1},
88149
},
89-
),
150+
)
151+
152+
if limit, has := limit.Get(); has {
153+
findOpts.SetLimit(limit)
154+
}
155+
156+
query := bson.D{
157+
{"task", bson.D{{"$in", taskIDs}}},
158+
}
159+
160+
if filter, has := filter.Get(); has {
161+
query = bson.D{
162+
{"$and", []bson.D{query, filter}},
163+
}
164+
}
165+
cursor, err := db.Collection(mismatchesCollectionName).Find(
166+
ctx,
167+
query,
168+
findOpts,
90169
)
91170

92171
if err != nil {

internal/verifier/result.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ func (vr VerificationResult) DocumentIsMissing() bool {
5151
return vr.Details == Missing && vr.Field == ""
5252
}
5353

54+
func getResultDocMissingAggExpr(docExpr any) bson.D {
55+
return bson.D{
56+
{"$and", []bson.D{
57+
{{"$eq", bson.A{
58+
Missing,
59+
bson.D{{"$getField", bson.D{
60+
{"input", docExpr},
61+
{"field", "details"},
62+
}}},
63+
}}},
64+
{{"$eq", bson.A{
65+
"",
66+
bson.D{{"$getField", bson.D{
67+
{"input", docExpr},
68+
{"field", "field"},
69+
}}},
70+
}}},
71+
}},
72+
}
73+
}
74+
5475
var _ bson.Marshaler = VerificationResult{}
5576

5677
func (vr VerificationResult) MarshalBSON() ([]byte, error) {

internal/verifier/summary.go

Lines changed: 97 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import (
1212
"strings"
1313
"time"
1414

15+
"github.com/10gen/migration-verifier/contextplus"
1516
"github.com/10gen/migration-verifier/internal/reportutils"
1617
"github.com/10gen/migration-verifier/internal/types"
18+
"github.com/10gen/migration-verifier/option"
1719
"github.com/olekukonko/tablewriter"
1820
"github.com/pkg/errors"
1921
"github.com/samber/lo"
@@ -63,6 +65,8 @@ func (verifier *Verifier) reportCollectionMetadataMismatches(ctx context.Context
6365
return ft.PrimaryKey
6466
},
6567
),
68+
option.None[bson.D](),
69+
option.None[int64](),
6670
)
6771
if err != nil {
6872
return false, false, errors.Wrapf(
@@ -112,95 +116,127 @@ func (verifier *Verifier) reportDocumentMismatches(ctx context.Context, strBuild
112116
strBuilder.WriteString("\n")
113117

114118
// First present summaries of failures based on present/missing and differing content
115-
failureTypesTable := tablewriter.NewWriter(strBuilder)
116-
failureTypesTable.SetHeader([]string{"Failure Type", "Count"})
119+
countsTable := tablewriter.NewWriter(strBuilder)
120+
countsTable.SetHeader([]string{"Failure Type", "Count"})
121+
122+
failedTaskIDs := lo.Map(
123+
failedTasks,
124+
func(ft VerificationTask, _ int) bson.ObjectID {
125+
return ft.PrimaryKey
126+
},
127+
)
117128

118-
taskDiscrepancies, err := getMismatchesForTasks(
119-
ctx,
120-
verifier.verificationDatabase(),
121-
lo.Map(
122-
failedTasks,
123-
func(ft VerificationTask, _ int) bson.ObjectID {
124-
return ft.PrimaryKey
125-
},
126-
),
129+
var mismatchTaskDiscrepancies, missingOrChangedDiscrepancies map[bson.ObjectID][]VerificationResult
130+
131+
contentMismatchCount := int64(0)
132+
missingOrChangedCount := int64(0)
133+
134+
eg, egCtx := contextplus.ErrGroup(ctx)
135+
eg.Go(
136+
func() error {
137+
var err error
138+
mismatchTaskDiscrepancies, err = getMismatchesForTasks(
139+
egCtx,
140+
verifier.verificationDatabase(),
141+
failedTaskIDs,
142+
option.Some(
143+
bson.D{{"$expr", bson.D{
144+
{"$not", getMismatchDocMissingAggExpr("$$ROOT")},
145+
}}},
146+
),
147+
option.Some(verifier.failureDisplaySize),
148+
)
149+
150+
return errors.Wrapf(
151+
err,
152+
"fetching %d failed tasks’ content-mismatch discrepancies",
153+
len(failedTasks),
154+
)
155+
},
127156
)
128-
if err != nil {
129-
return false, false, errors.Wrapf(
130-
err,
131-
"fetching %d failed tasks' discrepancies",
132-
len(failedTasks),
133-
)
134-
}
135157

136-
contentMismatchCount := 0
137-
missingOrChangedCount := 0
138-
for _, task := range failedTasks {
139-
discrepancies, hasDiscrepancies := taskDiscrepancies[task.PrimaryKey]
140-
if !hasDiscrepancies {
141-
return false, false, errors.Wrapf(
158+
eg.Go(
159+
func() error {
160+
var err error
161+
missingOrChangedCount, contentMismatchCount, err = countMismatchesForTasks(
162+
egCtx,
163+
verifier.verificationDatabase(),
164+
failedTaskIDs,
165+
getMismatchDocMissingAggExpr("$$ROOT"),
166+
)
167+
168+
return errors.Wrapf(
142169
err,
143-
"task %v is marked %#q but has no recorded discrepancies; internal error?",
144-
task.PrimaryKey,
145-
task.Status,
170+
"counting %d failed tasks’ discrepancies",
171+
len(failedTasks),
146172
)
147-
}
173+
},
174+
)
148175

149-
missingCount := lo.CountBy(
150-
discrepancies,
151-
func(d VerificationResult) bool {
152-
return d.DocumentIsMissing()
153-
},
154-
)
176+
eg.Go(
177+
func() error {
178+
var err error
179+
missingOrChangedDiscrepancies, err = getMismatchesForTasks(
180+
egCtx,
181+
verifier.verificationDatabase(),
182+
failedTaskIDs,
183+
option.Some(
184+
bson.D{{"$expr", getMismatchDocMissingAggExpr("$$ROOT")}},
185+
),
186+
option.Some(verifier.failureDisplaySize),
187+
)
155188

156-
contentMismatchCount += len(discrepancies) - missingCount
157-
missingOrChangedCount += missingCount
189+
return errors.Wrapf(
190+
err,
191+
"fetching %d failed tasks' missing/changed discrepancies",
192+
len(failedTasks),
193+
)
194+
},
195+
)
196+
197+
if err := eg.Wait(); err != nil {
198+
return false, false, errors.Wrapf(err, "gathering mismatch data")
158199
}
159200

160-
failureTypesTable.Append([]string{
201+
countsTable.Append([]string{
161202
"Documents With Differing Content",
162-
fmt.Sprintf("%v", reportutils.FmtReal(contentMismatchCount)),
203+
reportutils.FmtReal(contentMismatchCount),
163204
})
164-
failureTypesTable.Append([]string{
205+
countsTable.Append([]string{
165206
"Missing or Changed Documents",
166-
fmt.Sprintf("%v", reportutils.FmtReal(missingOrChangedCount)),
207+
reportutils.FmtReal(missingOrChangedCount),
167208
})
168-
strBuilder.WriteString("Failure summary:\n")
169-
failureTypesTable.Render()
209+
countsTable.Render()
170210

171211
mismatchedDocsTable := tablewriter.NewWriter(strBuilder)
172212
mismatchedDocsTableRows := types.ToNumericTypeOf(0, verifier.failureDisplaySize)
173213
mismatchedDocsTable.SetHeader([]string{"ID", "Cluster", "Field", "Namespace", "Details"})
174214

175-
printAll := int64(contentMismatchCount) < (verifier.failureDisplaySize + int64(0.25*float32(verifier.failureDisplaySize)))
176-
OUTA:
215+
printAll := int64(contentMismatchCount) <= verifier.failureDisplaySize
216+
177217
for _, task := range failedTasks {
178-
for _, d := range taskDiscrepancies[task.PrimaryKey] {
218+
for _, d := range mismatchTaskDiscrepancies[task.PrimaryKey] {
179219
if d.DocumentIsMissing() {
180-
continue
181-
}
182-
183-
if !printAll && mismatchedDocsTableRows >= verifier.failureDisplaySize {
184-
break OUTA
220+
panic(fmt.Sprintf("found missing-type mismatch but expected content-mismatch: %+v", d))
185221
}
186222

187223
mismatchedDocsTableRows++
188224
mismatchedDocsTable.Append([]string{
189225
fmt.Sprintf("%v", d.ID),
190-
fmt.Sprintf("%v", d.Cluster),
191-
fmt.Sprintf("%v", d.Field),
192-
fmt.Sprintf("%v", d.NameSpace),
193-
fmt.Sprintf("%v", d.Details),
226+
d.Cluster,
227+
d.Field,
228+
d.NameSpace,
229+
d.Details,
194230
})
195231
}
196232
}
197233

198234
if mismatchedDocsTableRows > 0 {
199235
strBuilder.WriteString("\n")
200236
if printAll {
201-
strBuilder.WriteString("All documents in tasks in failed status due to differing content:\n")
237+
strBuilder.WriteString("All documents found with differing content:\n")
202238
} else {
203-
fmt.Fprintf(strBuilder, "First %d documents in tasks in failed status due to differing content:\n", verifier.failureDisplaySize)
239+
fmt.Fprintf(strBuilder, "First %d documents found with differing content:\n", verifier.failureDisplaySize)
204240
}
205241
mismatchedDocsTable.Render()
206242
}
@@ -209,23 +245,18 @@ OUTA:
209245
missingOrChangedDocsTableRows := types.ToNumericTypeOf(0, verifier.failureDisplaySize)
210246
missingOrChangedDocsTable.SetHeader([]string{"Document ID", "Source Namespace", "Destination Namespace"})
211247

212-
printAll = int64(missingOrChangedCount) < (verifier.failureDisplaySize + int64(0.25*float32(verifier.failureDisplaySize)))
213-
OUTB:
248+
printAll = int64(missingOrChangedCount) <= verifier.failureDisplaySize
214249
for _, task := range failedTasks {
215-
for _, d := range taskDiscrepancies[task.PrimaryKey] {
250+
for _, d := range missingOrChangedDiscrepancies[task.PrimaryKey] {
216251
if !d.DocumentIsMissing() {
217-
continue
218-
}
219-
220-
if !printAll && missingOrChangedDocsTableRows >= verifier.failureDisplaySize {
221-
break OUTB
252+
panic(fmt.Sprintf("found content-mismatch mismatch but expected missing/changed: %+v", d))
222253
}
223254

224255
missingOrChangedDocsTableRows++
225256
missingOrChangedDocsTable.Append([]string{
226257
fmt.Sprintf("%v", d.ID),
227-
fmt.Sprintf("%v", task.QueryFilter.Namespace),
228-
fmt.Sprintf("%v", task.QueryFilter.To),
258+
task.QueryFilter.Namespace,
259+
task.QueryFilter.To,
229260
})
230261
}
231262
}

0 commit comments

Comments
 (0)