Skip to content

Commit 7e97b79

Browse files
committed
partitioning improvement
if fetchNextRealKey does not find a valid key; try fetchPrevRealKey before deeming the partition as unsplittable. This is useful for narrow ranges
1 parent 6d512e2 commit 7e97b79

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

flow/connectors/mysql/qrep_partition.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ type rangeProber interface {
184184
fetchNextRealKey(
185185
ctx context.Context, tableName string, quotedCol string, midpoint string, start string, end string,
186186
) (string, bool, error)
187+
fetchPrevRealKey(
188+
ctx context.Context, tableName string, quotedCol string, midpoint string, start string, end string,
189+
) (string, bool, error)
187190
}
188191

189192
// buildAdaptiveStringPartitions splits an arbitrary string watermark column
@@ -225,6 +228,15 @@ func buildAdaptiveStringPartitions(
225228
if err != nil {
226229
return nil, fmt.Errorf("failed to fetch next real key for [%s, %s]: %w", p.start, p.end, err)
227230
}
231+
if !found {
232+
// The interpolated midpoint can overshoot every key in the range when
233+
// keys occupy a narrow slice of the character space. So also probe
234+
// backwards before declaring the partition unsplittable.
235+
k, found, err = prober.fetchPrevRealKey(ctx, tableName, quotedCol, mid, p.start, p.end)
236+
if err != nil {
237+
return nil, fmt.Errorf("failed to fetch prev real key for [%s, %s]: %w", p.start, p.end, err)
238+
}
239+
}
228240
if !found {
229241
outputs = append(outputs, p)
230242
continue
@@ -267,6 +279,23 @@ func (c *MySqlConnector) fetchNextRealKey(
267279
query := fmt.Sprintf(
268280
"SELECT %[1]s FROM %[2]s WHERE %[1]s >= '%[3]s' AND %[1]s > '%[4]s' AND %[1]s < '%[5]s' ORDER BY %[1]s LIMIT 1",
269281
quotedCol, tableName, mysql.Escape(midpoint), mysql.Escape(start), mysql.Escape(end))
282+
return c.fetchRealKey(ctx, query)
283+
}
284+
285+
// fetchPrevRealKey returns the largest real value of the watermark column that
286+
// is below the interpolated midpoint and strictly inside (start, end). The
287+
// bounds are enforced by MySQL using its column's collation. Return false when
288+
// no such key exists.
289+
func (c *MySqlConnector) fetchPrevRealKey(
290+
ctx context.Context, tableName string, quotedCol string, midpoint string, start string, end string,
291+
) (string, bool, error) {
292+
query := fmt.Sprintf(
293+
"SELECT %[1]s FROM %[2]s WHERE %[1]s < '%[3]s' AND %[1]s > '%[4]s' AND %[1]s < '%[5]s' ORDER BY %[1]s DESC LIMIT 1",
294+
quotedCol, tableName, mysql.Escape(midpoint), mysql.Escape(start), mysql.Escape(end))
295+
return c.fetchRealKey(ctx, query)
296+
}
297+
298+
func (c *MySqlConnector) fetchRealKey(ctx context.Context, query string) (string, bool, error) {
270299
rs, err := c.Execute(ctx, query)
271300
if err != nil {
272301
return "", false, err
@@ -277,7 +306,7 @@ func (c *MySqlConnector) fetchNextRealKey(
277306
}
278307
key, err := rs.GetString(0, 0)
279308
if err != nil {
280-
return "", false, fmt.Errorf("failed to read next real key: %w", err)
309+
return "", false, fmt.Errorf("failed to read real key: %w", err)
281310
}
282311
// GetString is zero-copy over the resultset's row buffer, which rs.Close()
283312
// recycles into a pool where the next query's response overwrites it; the

flow/connectors/mysql/qrep_partition_test.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,26 @@ func (f *fakeRangeProber) fetchNextRealKey(
263263
_ context.Context, _ string, _ string, midpoint string, start string, end string,
264264
) (string, bool, error) {
265265
for _, k := range f.keys {
266-
if f.less(k, midpoint) {
266+
if f.less(k, midpoint) { // k < midpoint
267+
continue
268+
}
269+
if !f.less(start, k) { // k <= start
270+
continue
271+
}
272+
if !f.less(k, end) { // k >= end
273+
continue
274+
}
275+
return k, true, nil
276+
}
277+
return "", false, nil
278+
}
279+
280+
func (f *fakeRangeProber) fetchPrevRealKey(
281+
_ context.Context, _ string, _ string, midpoint string, start string, end string,
282+
) (string, bool, error) {
283+
for i := len(f.keys) - 1; i >= 0; i-- {
284+
k := f.keys[i]
285+
if !f.less(k, midpoint) { // k >= midpoint
267286
continue
268287
}
269288
if !f.less(start, k) { // k <= start
@@ -386,17 +405,19 @@ func TestBuildAdaptiveStringPartitions_CaseInsensitiveCollation(t *testing.T) {
386405
verifyFullCoverage(t, partitions, keys, caseInsensitiveLess)
387406
}
388407

389-
func TestBuildAdaptiveStringPartitions_NarrowAlphabetLimitation(t *testing.T) {
408+
func TestBuildAdaptiveStringPartitions_NarrowRange(t *testing.T) {
390409
keys := make([]string, 0, 100)
391410
for i := 1; i <= 100; i++ {
392411
keys = append(keys, fmt.Sprintf("key_%04d", i))
393412
}
394413

395-
// demonstrate midpoint is outside min/max range so fetchNextRealKey always returns false
414+
// demonstrate fetchNextRealKey would not return a valid key
415+
// (e.g. "key_00_" sorts after "key_0099") so splitting these
416+
// keys relies on the fetchPrevRealKey fallback
396417
require.Equal(t, "key_00_`", stringMidpoint("key_0001", "key_0100"))
397418

398419
partitions := runAdaptiveStringPartitions(t, keys, binaryLess, 8)
399-
require.Len(t, partitions, 1)
420+
require.Len(t, partitions, 8)
400421
verifyFullCoverage(t, partitions, keys, binaryLess)
401422
}
402423

0 commit comments

Comments
 (0)