Skip to content

Commit a67d53d

Browse files
authored
fix(bigquery/storage/managedwriter): correct reconnection logic (#8164)
Signalling for an AppendRows stream when schema changes is predicated on the backend's status for the connection. For a simplex (non-multiplexed) connection, the expectation is the client closes and reconnects to signal there's a change in the schema. For a connection in multiplex mode, no reconnection is necessary and the backend will look at the schema for changes. In managedwriter, we allow a user to specify multiplex at the outset, but for connections that haven't actually sent writes for more than a single stream ID the backend doesn't recognize the multiplex status. This PR expands the interface for send optimizer to signal whether the optimizer has sent writes for multiple connections, and uses it when making the determination about schema-based reconnects. It also augments the schema evolution test to validate using multiple combinations of writer and client options.
1 parent b051b36 commit a67d53d

File tree

3 files changed

+83
-13
lines changed

3 files changed

+83
-13
lines changed

bigquery/storage/managedwriter/connection.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,12 @@ func (co *connection) lockingAppend(pw *pendingWrite) error {
368368
forceReconnect := false
369369
if pw.writer != nil && pw.descVersion != nil && pw.descVersion.isNewer(pw.writer.curDescVersion) {
370370
pw.writer.curDescVersion = pw.descVersion
371-
if !canMultiplex(pw.writeStreamID) {
371+
if co.optimizer == nil {
372372
forceReconnect = true
373+
} else {
374+
if !co.optimizer.isMultiplexing() {
375+
forceReconnect = true
376+
}
373377
}
374378
}
375379

bigquery/storage/managedwriter/integration_test.go

+56-12
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,6 @@ func TestIntegration_ManagedWriter(t *testing.T) {
245245
t.Parallel()
246246
testPendingStream(ctx, t, mwClient, bqClient, dataset)
247247
})
248-
t.Run("SchemaEvolution", func(t *testing.T) {
249-
t.Parallel()
250-
testSchemaEvolution(ctx, t, mwClient, bqClient, dataset)
251-
})
252248
t.Run("SimpleCDC", func(t *testing.T) {
253249
t.Parallel()
254250
testSimpleCDC(ctx, t, mwClient, bqClient, dataset)
@@ -267,6 +263,56 @@ func TestIntegration_ManagedWriter(t *testing.T) {
267263
})
268264
}
269265

266+
func TestIntegration_SchemaEvolution(t *testing.T) {
267+
268+
testcases := []struct {
269+
desc string
270+
clientOpts []option.ClientOption
271+
writerOpts []WriterOption
272+
}{
273+
{
274+
desc: "Simplex_Committed",
275+
writerOpts: []WriterOption{
276+
WithType(CommittedStream),
277+
},
278+
},
279+
{
280+
desc: "Simplex_Default",
281+
writerOpts: []WriterOption{
282+
WithType(DefaultStream),
283+
},
284+
},
285+
{
286+
desc: "Multiplex_Default",
287+
clientOpts: []option.ClientOption{
288+
WithMultiplexing(),
289+
WithMultiplexPoolLimit(2),
290+
},
291+
writerOpts: []WriterOption{
292+
WithType(DefaultStream),
293+
},
294+
},
295+
}
296+
297+
for _, tc := range testcases {
298+
mwClient, bqClient := getTestClients(context.Background(), t, tc.clientOpts...)
299+
defer mwClient.Close()
300+
defer bqClient.Close()
301+
302+
dataset, cleanup, err := setupTestDataset(context.Background(), t, bqClient, "asia-east1")
303+
if err != nil {
304+
t.Fatalf("failed to init test dataset: %v", err)
305+
}
306+
defer cleanup()
307+
308+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
309+
defer cancel()
310+
t.Run(tc.desc, func(t *testing.T) {
311+
testSchemaEvolution(ctx, t, mwClient, bqClient, dataset, tc.writerOpts...)
312+
})
313+
}
314+
}
315+
270316
func testDefaultStream(ctx context.Context, t *testing.T, mwClient *Client, bqClient *bigquery.Client, dataset *bigquery.Dataset) {
271317
testTable := dataset.Table(tableIDs.New())
272318
if err := testTable.Create(ctx, &bigquery.TableMetadata{Schema: testdata.SimpleMessageSchema}); err != nil {
@@ -1094,7 +1140,7 @@ func testInstrumentation(ctx context.Context, t *testing.T, mwClient *Client, bq
10941140
}
10951141
}
10961142

1097-
func testSchemaEvolution(ctx context.Context, t *testing.T, mwClient *Client, bqClient *bigquery.Client, dataset *bigquery.Dataset) {
1143+
func testSchemaEvolution(ctx context.Context, t *testing.T, mwClient *Client, bqClient *bigquery.Client, dataset *bigquery.Dataset, opts ...WriterOption) {
10981144
testTable := dataset.Table(tableIDs.New())
10991145
if err := testTable.Create(ctx, &bigquery.TableMetadata{Schema: testdata.SimpleMessageSchema}); err != nil {
11001146
t.Fatalf("failed to create test table %s: %v", testTable.FullyQualifiedName(), err)
@@ -1104,11 +1150,9 @@ func testSchemaEvolution(ctx context.Context, t *testing.T, mwClient *Client, bq
11041150
descriptorProto := protodesc.ToDescriptorProto(m.ProtoReflect().Descriptor())
11051151

11061152
// setup a new stream.
1107-
ms, err := mwClient.NewManagedStream(ctx,
1108-
WithDestinationTable(TableParentFromParts(testTable.ProjectID, testTable.DatasetID, testTable.TableID)),
1109-
WithSchemaDescriptor(descriptorProto),
1110-
WithType(CommittedStream),
1111-
)
1153+
opts = append(opts, WithDestinationTable(TableParentFromParts(testTable.ProjectID, testTable.DatasetID, testTable.TableID)))
1154+
opts = append(opts, WithSchemaDescriptor(descriptorProto))
1155+
ms, err := mwClient.NewManagedStream(ctx, opts...)
11121156
if err != nil {
11131157
t.Fatalf("NewManagedStream: %v", err)
11141158
}
@@ -1154,7 +1198,7 @@ func testSchemaEvolution(ctx context.Context, t *testing.T, mwClient *Client, bq
11541198
// this subjects us to a possible race, as the backend that services GetWriteStream isn't necessarily the
11551199
// one in charge of the stream, and thus may report ready early.
11561200
for {
1157-
resp, err := ms.AppendRows(ctx, [][]byte{latestRow}, WithOffset(curOffset))
1201+
resp, err := ms.AppendRows(ctx, [][]byte{latestRow})
11581202
if err != nil {
11591203
t.Errorf("got error on dupe append: %v", err)
11601204
break
@@ -1181,7 +1225,7 @@ func testSchemaEvolution(ctx context.Context, t *testing.T, mwClient *Client, bq
11811225
t.Errorf("failed to marshal evolved message: %v", err)
11821226
}
11831227
// Send an append with an evolved schema
1184-
res, err := ms.AppendRows(ctx, [][]byte{b}, WithOffset(curOffset), UpdateSchemaDescriptor(descriptorProto))
1228+
res, err := ms.AppendRows(ctx, [][]byte{b}, UpdateSchemaDescriptor(descriptorProto))
11851229
if err != nil {
11861230
t.Errorf("failed evolved append: %v", err)
11871231
}

bigquery/storage/managedwriter/send_optimizer.go

+22
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type sendOptimizer interface {
3535

3636
// optimizeSend handles possible manipulation of a request, and triggers the send.
3737
optimizeSend(arc storagepb.BigQueryWrite_AppendRowsClient, pw *pendingWrite) error
38+
39+
// isMultiplexing tracks if we've actually sent writes to more than a single stream on this connection.
40+
isMultiplexing() bool
3841
}
3942

4043
// verboseOptimizer is a primarily a testing optimizer that always sends the full request.
@@ -50,6 +53,11 @@ func (vo *verboseOptimizer) optimizeSend(arc storagepb.BigQueryWrite_AppendRowsC
5053
return arc.Send(pw.constructFullRequest(true))
5154
}
5255

56+
func (vo *verboseOptimizer) isMultiplexing() bool {
57+
// we declare this no to ensure we always reconnect on schema changes.
58+
return false
59+
}
60+
5361
// simplexOptimizer is used for connections bearing AppendRowsRequest for only a single stream.
5462
//
5563
// The optimizations here are straightforward:
@@ -80,6 +88,11 @@ func (so *simplexOptimizer) optimizeSend(arc storagepb.BigQueryWrite_AppendRowsC
8088
return err
8189
}
8290

91+
func (so *simplexOptimizer) isMultiplexing() bool {
92+
// A simplex optimizer is not designed for multiplexing.
93+
return false
94+
}
95+
8396
// multiplexOptimizer is used for connections where requests for multiple default streams are sent on a common
8497
// connection. Only default streams can currently be multiplexed.
8598
//
@@ -93,10 +106,12 @@ func (so *simplexOptimizer) optimizeSend(arc storagepb.BigQueryWrite_AppendRowsC
93106
type multiplexOptimizer struct {
94107
prevStream string
95108
prevDescriptorVersion *descriptorVersion
109+
multiplexStreams bool
96110
}
97111

98112
func (mo *multiplexOptimizer) signalReset() {
99113
mo.prevStream = ""
114+
mo.multiplexStreams = false
100115
mo.prevDescriptorVersion = nil
101116
}
102117

@@ -139,11 +154,18 @@ func (mo *multiplexOptimizer) optimizeSend(arc storagepb.BigQueryWrite_AppendRow
139154
mo.prevStream = pw.writeStreamID
140155
mo.prevDescriptorVersion = pw.descVersion
141156
}
157+
// Also, note that we've sent traffic for multiple streams, which means the backend recognizes this
158+
// is a multiplex stream as well.
159+
mo.multiplexStreams = true
142160
}
143161
}
144162
return err
145163
}
146164

165+
func (mo *multiplexOptimizer) isMultiplexing() bool {
166+
return mo.multiplexStreams
167+
}
168+
147169
// getDescriptorFromAppend is a utility method for extracting the deeply nested schema
148170
// descriptor from a request. It returns a nil if the descriptor is not set.
149171
func getDescriptorFromAppend(req *storagepb.AppendRowsRequest) *descriptorpb.DescriptorProto {

0 commit comments

Comments
 (0)