Skip to content

Commit f9d7402

Browse files
committed
add e2e vstreamclient test
Signed-off-by: Derek Perkins <[email protected]>
1 parent ecb3720 commit f9d7402

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package vreplication
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
"slices"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"vitess.io/vitess/go/vt/log"
14+
binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
15+
"vitess.io/vitess/go/vt/vstreamclient"
16+
"vitess.io/vitess/go/vt/vtgate/vtgateconn"
17+
)
18+
19+
// Customer is the concrete type that will be built from the stream
20+
type Customer struct {
21+
ID int64 `vstream:"customer_id"`
22+
Email string `vstream:"email"`
23+
DeletedAt time.Time `vstream:"-"`
24+
}
25+
26+
// To run the tests, this currently expects the local example to be running
27+
// ./101_initial_cluster.sh; mysql < ../common/insert_commerce_data.sql; ./201_customer_tablets.sh; ./202_move_tables.sh; ./203_switch_reads.sh; ./204_switch_writes.sh; ./205_clean_commerce.sh; ./301_customer_sharded.sh; ./302_new_shards.sh; ./303_reshard.sh; ./304_switch_reads.sh; ./305_switch_writes.sh; ./306_down_shard_0.sh; ./307_delete_shard_0.sh
28+
func TestVStreamClient(t *testing.T) {
29+
vc = NewVitessCluster(t, nil)
30+
defer vc.TearDown()
31+
32+
require.NotNil(t, vc)
33+
defaultReplicas = 2
34+
defaultRdonly = 0
35+
36+
defaultCell := vc.Cells[vc.CellNames[0]]
37+
vc.AddKeyspace(t, []*Cell{defaultCell}, "product", "0", initialProductVSchema, initialProductSchema, defaultReplicas, defaultRdonly, 100, nil)
38+
verifyClusterHealth(t, vc)
39+
insertInitialData(t)
40+
41+
ctx := context.Background()
42+
conn, err := vtgateconn.Dial(ctx, fmt.Sprintf("%s:%d", vc.ClusterConfig.hostname, vc.ClusterConfig.vtgateGrpcPort))
43+
if err != nil {
44+
log.Fatal(err)
45+
}
46+
defer conn.Close()
47+
48+
flushCount := 0
49+
gotCustomers := make([]*Customer, 0)
50+
51+
tables := []vstreamclient.TableConfig{{
52+
Keyspace: "customer",
53+
Table: "customer",
54+
MaxRowsPerFlush: 7,
55+
DataType: &Customer{},
56+
FlushFn: func(ctx context.Context, rows []vstreamclient.Row, meta vstreamclient.FlushMeta) error {
57+
flushCount++
58+
59+
fmt.Printf("upserting %d customers\n", len(rows))
60+
for i, row := range rows {
61+
switch {
62+
// delete event
63+
case row.RowChange.After == nil:
64+
customer := row.Data.(*Customer)
65+
customer.DeletedAt = time.Now()
66+
67+
gotCustomers = append(gotCustomers, customer)
68+
fmt.Printf("deleting customer %d: %v\n", i, row)
69+
70+
// insert event
71+
case row.RowChange.Before == nil:
72+
gotCustomers = append(gotCustomers, row.Data.(*Customer))
73+
fmt.Printf("inserting customer %d: %v\n", i, row)
74+
75+
// update event
76+
case row.RowChange.Before != nil:
77+
gotCustomers = append(gotCustomers, row.Data.(*Customer))
78+
fmt.Printf("updating customer %d: %v\n", i, row)
79+
}
80+
}
81+
82+
// a real implementation would do something more meaningful here. For a data warehouse type workload,
83+
// it would probably look like streaming rows into the data warehouse, or for more complex versions,
84+
// write newline delimited json or a parquet file to object storage, then trigger a load job.
85+
return nil
86+
},
87+
}}
88+
89+
t.Run("first vstream run, should succeed", func(t *testing.T) {
90+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
91+
defer cancel()
92+
93+
vstreamClient, err := vstreamclient.New(ctx, "bob", conn, tables,
94+
vstreamclient.WithMinFlushDuration(500*time.Millisecond),
95+
vstreamclient.WithHeartbeatSeconds(1),
96+
vstreamclient.WithStateTable("commerce", "vstreams"),
97+
vstreamclient.WithEventFunc(func(ctx context.Context, ev *binlogdatapb.VEvent) error {
98+
fmt.Printf("** FIELD EVENT: %v\n", ev)
99+
return nil
100+
}, binlogdatapb.VEventType_FIELD),
101+
)
102+
if err != nil {
103+
t.Fatalf("failed to create VStreamClient: %v", err)
104+
}
105+
106+
err = vstreamClient.Run(ctx)
107+
if err != nil && ctx.Err() == nil {
108+
t.Fatalf("failed to run vstreamclient: %v", err)
109+
}
110+
111+
slices.SortFunc(gotCustomers, func(a, b *Customer) int {
112+
return int(a.ID - b.ID)
113+
})
114+
115+
wantCustomers := []*Customer{
116+
{ID: 1, Email: "[email protected]"},
117+
{ID: 2, Email: "[email protected]"},
118+
{ID: 3, Email: "[email protected]"},
119+
{ID: 4, Email: "[email protected]"},
120+
{ID: 5, Email: "[email protected]"},
121+
}
122+
123+
fmt.Printf("got %d customers | flushed %d times\n", len(gotCustomers), flushCount)
124+
if !reflect.DeepEqual(gotCustomers, wantCustomers) {
125+
t.Fatalf("got %d customers, want %d", len(gotCustomers), len(wantCustomers))
126+
}
127+
})
128+
129+
// this should fail because we're going to restart the stream, but with an additional table
130+
t.Run("second vstream run, should fail", func(t *testing.T) {
131+
withAdditionalTable := append(tables, vstreamclient.TableConfig{
132+
Keyspace: "customer",
133+
Table: "corder",
134+
DataType: &Customer{},
135+
})
136+
137+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
138+
defer cancel()
139+
140+
_, err := vstreamclient.New(ctx, "bob", conn, withAdditionalTable,
141+
vstreamclient.WithStateTable("commerce", "vstreams"),
142+
)
143+
if err == nil {
144+
t.Fatalf("expected VStreamClient error, got nil")
145+
} else if err.Error() != "vstreamclient: provided tables do not match stored tables" {
146+
t.Fatalf("expected error 'vstreamclient: provided tables do not match stored tables', got '%v'", err)
147+
}
148+
})
149+
}
150+
151+
func getConn(t *testing.T, ctx context.Context) *vtgateconn.VTGateConn {
152+
t.Helper()
153+
conn, err := vtgateconn.Dial(ctx, "localhost:15991")
154+
if err != nil {
155+
t.Fatal(err)
156+
}
157+
return conn
158+
}

0 commit comments

Comments
 (0)