Skip to content

Commit 6817fdd

Browse files
craig[bot]Vidit Bhat
andcommitted
Merge #143220
143220: roachtest-operations: sink selection in changefeed operations r=nameisbhaskar a=vidit-bhat This PR implements probabilistic sink selection based on environment variables. `SINK_CONFIG` defines sink percentages (e.g., `kafka:20,webhook:10,gs:30,null:40`), ensuring they sum to 100%. Each sink's URI is retrieved from `SINK_CONFIG_<SINK_NAME>`. Changefeeds use a weighted random selection to determine the sink. Epic: none Fixes: #138488 Release note: None Co-authored-by: Vidit Bhat <[email protected]>
2 parents 3b39a6d + f1079ec commit 6817fdd

File tree

3 files changed

+183
-3
lines changed

3 files changed

+183
-3
lines changed

pkg/cmd/roachtest/operations/BUILD.bazel

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ go_library(
5555

5656
go_test(
5757
name = "operations_test",
58-
srcs = ["cluster_settings_test.go"],
58+
srcs = [
59+
"cluster_settings_test.go",
60+
"utils_test.go",
61+
],
5962
embed = [":operations"],
60-
deps = ["@com_github_stretchr_testify//require"],
63+
deps = [
64+
"//pkg/cmd/roachtest/operations/changefeeds",
65+
"@com_github_stretchr_testify//require",
66+
],
6167
)

pkg/cmd/roachtest/operations/changefeeds/utils.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
"context"
1010
gosql "database/sql"
1111
"fmt"
12+
"math/rand"
13+
"os"
14+
"strconv"
1215
"strings"
1316
"time"
1417

@@ -39,6 +42,11 @@ type jobDetails struct {
3942
highWaterTimestamp hlc.Timestamp // high watermark timestamp
4043
}
4144

45+
// configSetter defines a callback function used by parseConfigs to apply each
46+
// parsed sink and its associated percentage. This allows custom handling of
47+
// each config entry during parsing (e.g., storing in a map or validating).
48+
type configSetter func(key string, value int) error
49+
4250
// getJobsUpdatedWithPayload fetches additional changefeed payload details for specific jobs from the database.
4351
// This returns a new slice of jobDetails with the updated payload details without mutating the one in input.
4452
func getJobsUpdatedWithPayload(
@@ -338,10 +346,93 @@ func createChangefeed(
338346
return err
339347
}
340348

349+
// ParseConfigs exported for testing
350+
func ParseConfigs(config string, cb configSetter) error {
351+
if config == "" {
352+
return fmt.Errorf("config string cannot be empty")
353+
}
354+
355+
configsArr := strings.Split(config, ",")
356+
totalCount := 0
357+
358+
for _, c := range configsArr {
359+
parts := strings.Split(c, ":")
360+
if len(parts) != 2 {
361+
return fmt.Errorf("invalid config format: %s", c)
362+
}
363+
364+
key := parts[0]
365+
valueStr := parts[1]
366+
367+
value, err := strconv.Atoi(valueStr)
368+
if err != nil {
369+
return fmt.Errorf("invalid percentage value in config '%s'", c)
370+
}
371+
372+
if value < 0 || value > 100 {
373+
return fmt.Errorf("percentage value out of range in config '%s': must be between 0 and 100", c)
374+
}
375+
376+
err = cb(key, value)
377+
if err != nil {
378+
return err
379+
}
380+
381+
totalCount += value
382+
}
383+
384+
if totalCount != 100 {
385+
return fmt.Errorf("all sinks must sum to 100%%, but total is %d%%", totalCount)
386+
}
387+
388+
return nil
389+
}
390+
391+
func selectSink(sinks map[string]int) string {
392+
choice := rand.Intn(100)
393+
sum := 0
394+
for sink, pct := range sinks {
395+
sum += pct
396+
if choice < sum {
397+
return sink
398+
}
399+
}
400+
return "null" // Default fallback if no valid selection
401+
}
402+
341403
// getSinkConfigs returns the sink uri along with the options for creating the changefeed.
342404
// this will be extended later for more sinks
343405
func getSinkConfigs(_ context.Context, _ []*jobDetails) (string, []string, error) {
344-
return "null://", make([]string, 0), nil
406+
sinkConfigEnv := os.Getenv("SINK_CONFIG")
407+
if sinkConfigEnv == "" {
408+
return "null://", []string{}, nil // Default to null sink if env is not set
409+
}
410+
411+
sinks := make(map[string]int)
412+
uris := make(map[string]string)
413+
414+
err := ParseConfigs(sinkConfigEnv, func(sink string, value int) error {
415+
sinks[sink] = value
416+
uriEnv := fmt.Sprintf("SINK_CONFIG_%s", strings.ToUpper(sink))
417+
uri, exists := os.LookupEnv(uriEnv)
418+
if !exists {
419+
return fmt.Errorf("environment variable %s not found for sink %s", uriEnv, sink)
420+
}
421+
uris[sink] = uri
422+
return nil
423+
})
424+
if err != nil {
425+
// Default to null sink on parsing error
426+
return "null://", []string{}, nil //nolint:returnerrcheck
427+
}
428+
429+
selectedSink := selectSink(sinks)
430+
selectedURI, exists := uris[selectedSink]
431+
if !exists {
432+
return "null://", []string{}, nil // Default to null sink if selection fails
433+
}
434+
435+
return selectedURI, []string{}, nil
345436
}
346437

347438
// calculateScanOption determines whether the new changefeed should have an initial scan based on existing jobs.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2024 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
//
6+
7+
package operations
8+
9+
import (
10+
"testing"
11+
12+
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/operations/changefeeds"
13+
)
14+
15+
func TestParseConfigs(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
config string
19+
wantErr bool
20+
wantMap map[string]int
21+
}{
22+
{
23+
name: "valid config",
24+
config: "kafka:20,webhook:30,null:50",
25+
wantErr: false,
26+
wantMap: map[string]int{"kafka": 20, "webhook": 30, "null": 50},
27+
},
28+
{
29+
name: "percentage does not add up to 100",
30+
config: "kafka:20,webhook:30,null:40",
31+
wantErr: true,
32+
},
33+
{
34+
name: "invalid percentage value",
35+
config: "kafka:abc,webhook:30,null:70",
36+
wantErr: true,
37+
},
38+
{
39+
name: "missing colon",
40+
config: "kafka20,webhook:30,null:70",
41+
wantErr: true,
42+
},
43+
{
44+
name: "negative percentage",
45+
config: "kafka:-10,webhook:60,null:50",
46+
wantErr: true,
47+
},
48+
{
49+
name: "empty config",
50+
config: "",
51+
wantErr: true,
52+
},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
gotMap := make(map[string]int)
58+
err := changefeeds.ParseConfigs(tt.config, func(key string, value int) error {
59+
gotMap[key] = value
60+
return nil
61+
})
62+
63+
if (err != nil) != tt.wantErr {
64+
t.Errorf("parseConfigs() error = %v, wantErr %v", err, tt.wantErr)
65+
}
66+
if !tt.wantErr && !equalMaps(gotMap, tt.wantMap) {
67+
t.Errorf("parseConfigs() gotMap = %v, wantMap %v", gotMap, tt.wantMap)
68+
}
69+
})
70+
}
71+
}
72+
73+
func equalMaps(a, b map[string]int) bool {
74+
if len(a) != len(b) {
75+
return false
76+
}
77+
for k, v := range a {
78+
if b[k] != v {
79+
return false
80+
}
81+
}
82+
return true
83+
}

0 commit comments

Comments
 (0)