This is an automated email from the ASF dual-hosted git repository. gurwls223 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/spark-connect-go.git
The following commit(s) were added to refs/heads/master by this push: new af02e0e [SPARK-43958] Adding support for Channel Builder af02e0e is described below commit af02e0eda6dd0f6d2d3e39c9822db7b5032eaa82 Author: Martin Grund <martin.gr...@databricks.com> AuthorDate: Sun Jun 4 20:49:09 2023 +0900 [SPARK-43958] Adding support for Channel Builder ### What changes were proposed in this pull request? Add support for parsing the connection string of Spark Connect in the same way was it's done for the other Spark Connect clients. ### Why are the changes needed? Compatibility ### Does this PR introduce _any_ user-facing change? No ### How was this patch tested? UT Closes #8 from grundprinzip/SPARK-43958. Authored-by: Martin Grund <martin.gr...@databricks.com> Signed-off-by: Hyukjin Kwon <gurwls...@apache.org> --- client/channel/channel.go | 141 ++++++++++++++++++++++++ client/channel/channel_test.go | 82 ++++++++++++++ client/sql/sparksession.go | 36 ++++-- cmd/spark-connect-example-spark-session/main.go | 4 +- go.mod | 17 ++- go.sum | 34 ++++-- 6 files changed, 290 insertions(+), 24 deletions(-) diff --git a/client/channel/channel.go b/client/channel/channel.go new file mode 100644 index 0000000..6cf7696 --- /dev/null +++ b/client/channel/channel.go @@ -0,0 +1,141 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package channel + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "golang.org/x/oauth2" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/oauth" +) + +// Reserved header parameters that must not be injected as variables. +var reservedParams = []string{"user_id", "token", "use_ssl"} + +// The ChannelBuilder is used to parse the different parameters of the connection +// string according to the specification documented here: +// +// https://github.com/apache/spark/blob/master/connector/connect/docs/client-connection-string.md +type ChannelBuilder struct { + Host string + Port int + Token string + User string + Headers map[string]string +} + +// Finalizes the creation of the gprc.ClientConn by creating a GRPC channel +// with the necessary options extracted from the connection string. For +// TLS connections, this function will load the system certificates. +func (cb *ChannelBuilder) Build() (*grpc.ClientConn, error) { + var opts []grpc.DialOption + + opts = append(opts, grpc.WithAuthority(cb.Host)) + if cb.Token == "" { + opts = append(opts, grpc.WithInsecure()) + } else { + // Note: On the Windows platform, use of x509.SystemCertPool() requires + // go version 1.18 or higher. + systemRoots, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + cred := credentials.NewTLS(&tls.Config{ + RootCAs: systemRoots, + }) + opts = append(opts, grpc.WithTransportCredentials(cred)) + + t := oauth2.Token{ + AccessToken: cb.Token, + TokenType: "bearer", + } + opts = append(opts, grpc.WithPerRPCCredentials(oauth.NewOauthAccess(&t))) + } + + remote := fmt.Sprintf("%v:%v", cb.Host, cb.Port) + conn, err := grpc.Dial(remote, opts...) + if err != nil { + return nil, fmt.Errorf("failed to connect to remote %s: %w", remote, err) + } + return conn, nil +} + +// Creates a new instance of the ChannelBuilder. This constructor effectively +// parses the connection string and extracts the relevant parameters directly. +func NewBuilder(connection string) (*ChannelBuilder, error) { + + u, err := url.Parse(connection) + if err != nil { + return nil, err + } + + if u.Scheme != "sc" { + return nil, errors.New("URL schema must be set to `sc`.") + } + + var port = 15002 + var host = u.Host + // Check if the host part of the URL contains a port and extract. + if strings.Contains(u.Host, ":") { + hostStr, portStr, err := net.SplitHostPort(u.Host) + if err != nil { + return nil, err + } + host = hostStr + if len(portStr) != 0 { + port, err = strconv.Atoi(portStr) + if err != nil { + return nil, err + } + } + } + + // Validate that the URL path is empty or follows the right format. + if u.Path != "" && !strings.HasPrefix(u.Path, "/;") { + return nil, fmt.Errorf("The URL path (%v) must be empty or have a proper parameter syntax.", u.Path) + } + + cb := &ChannelBuilder{ + Host: host, + Port: port, + Headers: map[string]string{}, + } + + elements := strings.Split(u.Path, ";") + for _, e := range elements { + props := strings.Split(e, "=") + if len(props) == 2 { + if props[0] == "token" { + cb.Token = props[1] + } else if props[0] == "user_id" { + cb.User = props[1] + } else { + cb.Headers[props[0]] = props[1] + } + } + } + return cb, nil +} diff --git a/client/channel/channel_test.go b/client/channel/channel_test.go new file mode 100644 index 0000000..aedceb8 --- /dev/null +++ b/client/channel/channel_test.go @@ -0,0 +1,82 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package channel_test + +import ( + "strings" + "testing" + + "github.com/apache/spark-connect-go/v_3_4/client/channel" + "github.com/stretchr/testify/assert" +) + +const goodChannelURL = "sc://host:15002/;user_id=a;token=b;x-other-header=c" + +func TestBasicChannelBuilder(t *testing.T) { + cb, _ := channel.NewBuilder(goodChannelURL) + if cb == nil { + t.Error("ChannelBuilder must not be null") + } +} + +func TestBasicChannelParsing(t *testing.T) { + _, err := channel.NewBuilder("abc://asdada:1333") + + assert.False(t, strings.Contains(err.Error(), "scheme"), "Channel build should fail with wrong scheme") + cb, err := channel.NewBuilder("sc://empty") + + assert.Nilf(t, err, "Valid path should not fail: %v", err) + assert.Equalf(t, 15002, cb.Port, "Default port must be set, but got %v", cb.Port) + + _, err = channel.NewBuilder("sc://empty:port") + assert.NotNilf(t, err, "Port must be a valid integer %v", err) + + _, err = channel.NewBuilder("sc://empty:9999999999999") + assert.Nilf(t, err, "Port must be a valid number %v", err) + + _, err = channel.NewBuilder("sc://abcd/this") + assert.True(t, strings.Contains(err.Error(), "The URL path"), "URL path elements are not allowed") + + cb, err = channel.NewBuilder(goodChannelURL) + assert.Equal(t, "host", cb.Host) + assert.Equal(t, 15002, cb.Port) + assert.Len(t, cb.Headers, 1) + assert.Equal(t, "c", cb.Headers["x-other-header"]) + assert.Equal(t, "a", cb.User) + assert.Equal(t, "b", cb.Token) + + cb, err = channel.NewBuilder("sc://localhost:443/;token=token;user_id=user_id;cluster_id=a") + assert.Nilf(t, err, "Unexpected error: %v", err) + assert.Equal(t, 443, cb.Port) + assert.Equal(t, "localhost", cb.Host) + assert.Equal(t, "token", cb.Token) + assert.Equal(t, "user_id", cb.User) +} + +func TestChannelBuildConnect(t *testing.T) { + cb, err := channel.NewBuilder("sc://localhost") + assert.Nil(t, err, "Should not have an error for a proper URL.") + conn, err := cb.Build() + assert.Nil(t, err, "no error for proper connection") + assert.NotNil(t, conn) + + cb, err = channel.NewBuilder("sc://localhost:443/;token=abcd;user_id=a") + assert.Nil(t, err, "Should not have an error for a proper URL.") + conn, err = cb.Build() + assert.Nil(t, err, "no error for proper connection") + assert.NotNil(t, conn) +} diff --git a/client/sql/sparksession.go b/client/sql/sparksession.go index 86f4cd1..6a3ca8c 100644 --- a/client/sql/sparksession.go +++ b/client/sql/sparksession.go @@ -19,10 +19,11 @@ package sql import ( "context" "fmt" + + "github.com/apache/spark-connect-go/v_3_4/client/channel" proto "github.com/apache/spark-connect-go/v_3_4/internal/generated" "github.com/google/uuid" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" ) var SparkSession sparkSessionBuilderEntrypoint @@ -47,25 +48,35 @@ func (s SparkSessionBuilder) Remote(connectionString string) SparkSessionBuilder } func (s SparkSessionBuilder) Build() (sparkSession, error) { - opts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), + + cb, err := channel.NewBuilder(s.connectionString) + if err != nil { + return nil, fmt.Errorf("failed to connect to remote %s: %w", s.connectionString, err) } - conn, err := grpc.Dial(s.connectionString, opts...) + conn, err := cb.Build() if err != nil { return nil, fmt.Errorf("failed to connect to remote %s: %w", s.connectionString, err) } + // Add metadata to the request. + meta := metadata.MD{} + for k, v := range cb.Headers { + meta[k] = append(meta[k], v) + } + client := proto.NewSparkConnectServiceClient(conn) return &sparkSessionImpl{ sessionId: uuid.NewString(), client: client, + metadata: meta, }, nil } type sparkSessionImpl struct { sessionId string client proto.SparkConnectServiceClient + metadata metadata.MD } func (s *sparkSessionImpl) Sql(query string) (DataFrame, error) { @@ -109,8 +120,13 @@ func (s *sparkSessionImpl) executePlan(plan *proto.Plan) (proto.SparkConnectServ request := proto.ExecutePlanRequest{ SessionId: s.sessionId, Plan: plan, + UserContext: &proto.UserContext{ + UserId: "na", + }, } - executePlanClient, err := s.client.ExecutePlan(context.TODO(), &request) + // Append the other items to the request. + ctx := metadata.NewOutgoingContext(context.Background(), s.metadata) + executePlanClient, err := s.client.ExecutePlan(ctx, &request) if err != nil { return nil, fmt.Errorf("failed to call ExecutePlan in session %s: %w", s.sessionId, err) } @@ -125,8 +141,14 @@ func (s *sparkSessionImpl) analyzePlan(plan *proto.Plan) (*proto.AnalyzePlanResp Plan: plan, }, }, + UserContext: &proto.UserContext{ + UserId: "na", + }, } - response, err := s.client.AnalyzePlan(context.TODO(), &request) + // Append the other items to the request. + ctx := metadata.NewOutgoingContext(context.Background(), s.metadata) + + response, err := s.client.AnalyzePlan(ctx, &request) if err != nil { return nil, fmt.Errorf("failed to call AnalyzePlan in session %s: %w", s.sessionId, err) } diff --git a/cmd/spark-connect-example-spark-session/main.go b/cmd/spark-connect-example-spark-session/main.go index 0f4a6cc..d7e70bd 100644 --- a/cmd/spark-connect-example-spark-session/main.go +++ b/cmd/spark-connect-example-spark-session/main.go @@ -18,8 +18,9 @@ package main import ( "flag" - "github.com/apache/spark-connect-go/v_3_4/client/sql" "log" + + "github.com/apache/spark-connect-go/v_3_4/client/sql" ) var ( @@ -28,6 +29,7 @@ var ( ) func main() { + flag.Parse() spark, err := sql.SparkSession.Builder.Remote(*remote).Build() if err != nil { log.Fatalf("Failed: %s", err.Error()) diff --git a/go.mod b/go.mod index f04a331..c95c5ec 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,12 @@ require ( google.golang.org/protobuf v1.30.0 ) +require ( + cloud.google.com/go/compute v1.15.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + google.golang.org/appengine v1.6.7 // indirect +) + require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/apache/thrift v0.16.0 // indirect @@ -42,11 +48,12 @@ require ( github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/oauth2 v0.8.0 + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.9.3 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f8c434e..8e4f954 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -12,6 +16,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -53,30 +58,37 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@spark.apache.org For additional commands, e-mail: commits-h...@spark.apache.org