├── .gitmodules ├── LICENSE ├── README.md ├── cloudbuild.yaml ├── cloudprober ├── cloudprober.cfg ├── go.mod ├── main.go ├── spannerprobers.go └── stackdriverutils.go ├── continuous_load_testing ├── Dockerfile ├── Dockerfile.cloudpath ├── client-go-cloudpath-useast7.yaml ├── client-go-cloudpath.yaml ├── client-go-manual.yaml ├── client-go-useast7.yaml ├── client-go.yaml ├── client.go ├── continuous_load_testing ├── deploy.sh ├── go.mod ├── go.sum ├── grpc-proto-gen.sh └── proto │ └── grpc │ └── testing │ ├── empty │ └── empty.pb.go │ ├── messages │ └── messages.pb.go │ └── test │ ├── test.pb.go │ └── test_grpc.pb.go ├── doc └── gRPC-client-user-guide.md ├── e2e-checksum ├── README.md ├── go.mod ├── go.sum └── main.go ├── e2e-examples ├── echo │ ├── README.md │ ├── echo-client │ │ └── main.go │ └── echo │ │ ├── codegen.sh │ │ ├── echo.pb.go │ │ └── echo.proto ├── gcs │ ├── .gitignore │ ├── README.md │ ├── cloud.google.com │ │ └── go │ │ │ └── storage │ │ │ └── genproto │ │ │ └── apiv2 │ │ │ └── storagepb │ │ │ ├── storage.pb.go │ │ │ └── storage_grpc.pb.go │ └── main.go ├── go.mod └── go.sum ├── examples └── spanner_grpcgcp │ ├── Readme.md │ ├── go.mod │ ├── go.sum │ └── spanner_grpcgcp.go ├── firestore ├── examples │ └── end2end │ │ ├── doc │ │ └── .gitignore │ │ └── src │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── apimethods │ │ ├── batchgetdocuments.go │ │ ├── begintransaction.go │ │ ├── commit.go │ │ ├── createdocument.go │ │ ├── createindex.go │ │ ├── deletedocument.go │ │ ├── deleteindex.go │ │ ├── getdocument.go │ │ ├── getindex.go │ │ ├── listcollectionids.go │ │ ├── listdocuments.go │ │ ├── listindexes.go │ │ ├── rollback.go │ │ ├── runquery.go │ │ ├── updatedocument.go │ │ └── write.go │ │ ├── environment │ │ └── environment.go │ │ ├── fsutils │ │ └── getfsclient.go │ │ ├── gfx │ │ ├── choosefirestoremethod.go │ │ └── drawmenu.go │ │ ├── main.go │ │ └── userutil │ │ ├── drawdocument.go │ │ ├── drawindex.go │ │ └── readfromconsole.go └── go.mod ├── grpcgcp ├── README.md ├── doc.go ├── gcp_balancer.go ├── gcp_balancer_test.go ├── gcp_interceptor.go ├── gcp_interceptor_test.go ├── gcp_logger.go ├── gcp_multiendpoint.go ├── gcp_picker.go ├── gcp_picker_test.go ├── go.mod ├── go.sum ├── grpc_gcp │ ├── codegen.sh │ ├── grpc_gcp.pb.go │ └── grpc_gcp.proto ├── mockgen.sh ├── mocks │ ├── mock_balancer.go │ └── mock_stream.go ├── multiendpoint │ ├── endpoint.go │ ├── multiendpoint.go │ └── multiendpoint_test.go ├── test_config.json └── test_grpc │ ├── gcp_multiendpoint_test.go │ ├── helloworld │ ├── codegen.sh │ ├── helloworld.proto │ └── helloworld │ │ └── helloworld.pb.go │ └── main_test.go ├── grpcgcp_tests ├── go.mod ├── go.sum └── spanner_api_test.go └── spanner_prober ├── Dockerfile ├── Readme.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── prober ├── interceptors.go └── proberlib.go /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/grpc-proto"] 2 | path = third_party/grpc-proto 3 | url = https://github.com/grpc/grpc-proto.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC for GCP extensions 2 | 3 | Copyright 2019 4 | [The gRPC Authors](https://github.com/grpc/grpc/blob/master/AUTHORS) 5 | 6 | ## About This Repository 7 | 8 | This repo is created to support GCP specific extensions for gRPC. To use the extension features, please refer to [grpcgcp](grpcgcp). 9 | 10 | This repo also contains supporting infrastructures such as end2end tests and benchmarks for accessing cloud APIs with gRPC client libraries. 11 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: golang:1.19 3 | dir: 'grpcgcp' 4 | entrypoint: go 5 | args: ['test', '-race', '-v', '-timeout', '600s'] 6 | - name: golang:1.19 7 | dir: 'grpcgcp' 8 | entrypoint: go 9 | args: ['test', '-race', '-v', '-timeout', '600s', 'github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/test_grpc'] 10 | - name: golang:1.19 11 | dir: 'grpcgcp_tests' 12 | env: 13 | - 'GCP_PROJECT_ID=grpc-gcp-testing' 14 | entrypoint: go 15 | args: ['test', '-race', '-v', '-timeout', '600s'] 16 | -------------------------------------------------------------------------------- /cloudprober/cloudprober.cfg: -------------------------------------------------------------------------------- 1 | probe { 2 | type: EXTERNAL 3 | name: "spanner" 4 | interval_msec: 1800000 5 | timeout_msec: 30000 6 | targets { dummy_targets {} } # No targets for external probe 7 | external_probe { 8 | mode: ONCE 9 | command: "./goprober --spanner" 10 | } 11 | } 12 | 13 | surfacer { 14 | type: STACKDRIVER 15 | name: "stackdriver" 16 | stackdriver_surfacer { 17 | monitoring_url: "custom.googleapis.com/cloudprober/" 18 | } 19 | } -------------------------------------------------------------------------------- /cloudprober/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/grpc-gcp-go/cloudprober 2 | 3 | require ( 4 | cloud.google.com/go v0.34.0 5 | github.com/googleapis/gax-go v2.0.2+incompatible // indirect 6 | google.golang.org/api v0.1.0 7 | google.golang.org/genproto v0.0.0-20190111180523-db91494dd46c 8 | google.golang.org/grpc v1.17.0 9 | ) 10 | -------------------------------------------------------------------------------- /cloudprober/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2019 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "os" 23 | 24 | spanner "cloud.google.com/go/spanner/apiv1" 25 | ) 26 | 27 | func parseArgs() []bool { 28 | // Currently we only have spanner probers. May add new features in the future. 29 | vars := []bool{false} 30 | for _, arg := range os.Args { 31 | if arg == "--spanner" { 32 | vars[0] = true 33 | } 34 | } 35 | return vars 36 | } 37 | 38 | type spannerProber func(*spanner.Client, map[string]int64) error 39 | 40 | func executeSpannerProber(p spannerProber, client *spanner.Client, metrics map[string]int64, count *int, util *stackdriverUtil) { 41 | err := p(client, metrics) 42 | if err != nil { 43 | *count = (*count) + 1 44 | util.reportError(err) 45 | } 46 | } 47 | 48 | func executeSpannerProbers() { 49 | metrics := make(map[string]int64) 50 | client := createClient() 51 | failureCount := 0 52 | 53 | util := newStackdriverUtil("Spanner") 54 | defer util.closeErrClient() 55 | 56 | executeSpannerProber(sessionManagementProber, client, metrics, &failureCount, util) 57 | executeSpannerProber(executeSqlProber, client, metrics, &failureCount, util) 58 | executeSpannerProber(readProber, client, metrics, &failureCount, util) 59 | executeSpannerProber(transactionProber, client, metrics, &failureCount, util) 60 | executeSpannerProber(partitionProber, client, metrics, &failureCount, util) 61 | 62 | if failureCount == 0 { 63 | util.setSuccess() 64 | } 65 | util.addMetricsDict(metrics) 66 | util.outputMetrics() 67 | } 68 | 69 | func main() { 70 | vars := parseArgs() 71 | if vars[0] { 72 | executeSpannerProbers() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cloudprober/spannerprobers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 gRPC authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "io" 24 | "log" 25 | "os" 26 | "time" 27 | 28 | "google.golang.org/api/iterator" 29 | 30 | spanner "cloud.google.com/go/spanner/apiv1" 31 | spannerpb "google.golang.org/genproto/googleapis/spanner/v1" 32 | ) 33 | 34 | const ( 35 | database = "projects/grpc-prober-testing/instances/test-instance/databases/test-db" 36 | table = "users" 37 | testUsername = "test_username" 38 | ) 39 | 40 | func createClient() *spanner.Client { 41 | ctx := context.Background() 42 | client, _ := spanner.NewClient(ctx) 43 | if client == nil { 44 | log.Fatal("Fail to create the client.") 45 | os.Exit(1) 46 | } 47 | return client 48 | } 49 | 50 | func sessionManagementProber(client *spanner.Client, metrics map[string]int64) error { 51 | ctx := context.Background() 52 | reqCreate := &spannerpb.CreateSessionRequest{ 53 | Database: database, 54 | } 55 | start := time.Now() 56 | session, err := client.CreateSession(ctx, reqCreate) 57 | if err != nil { 58 | return err 59 | } 60 | if session == nil { 61 | return errors.New("failded to create a new session") 62 | } 63 | metrics["create_session_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 64 | 65 | // DeleteSession 66 | defer func() { 67 | start = time.Now() 68 | reqDelete := &spannerpb.DeleteSessionRequest{ 69 | Name: session.Name, 70 | } 71 | client.DeleteSession(ctx, reqDelete) 72 | metrics["delete_session_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 73 | }() 74 | 75 | // GetSession 76 | reqGet := &spannerpb.GetSessionRequest{ 77 | Name: session.Name, 78 | } 79 | start = time.Now() 80 | respGet, err := client.GetSession(ctx, reqGet) 81 | if err != nil { 82 | return err 83 | } 84 | if reqGet == nil || respGet.Name != session.Name { 85 | return errors.New("fail to get the session") 86 | } 87 | metrics["get_session_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 88 | 89 | // ListSessions 90 | reqList := &spannerpb.ListSessionsRequest{ 91 | Database: database, 92 | } 93 | start = time.Now() 94 | it := client.ListSessions(ctx, reqList) 95 | inList := false 96 | for { 97 | resp, err := it.Next() 98 | if err == iterator.Done { 99 | break 100 | } 101 | if err != nil { 102 | return err 103 | } 104 | if resp.Name == session.Name { 105 | inList = true 106 | break 107 | } 108 | } 109 | metrics["list_sessions_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 110 | if !inList { 111 | return errors.New("list sessions failed") 112 | } 113 | return nil 114 | } 115 | 116 | func executeSqlProber(client *spanner.Client, metrics map[string]int64) error { 117 | ctx := context.Background() 118 | session := createSession(client) 119 | defer deleteSession(client, session) 120 | reqSql := &spannerpb.ExecuteSqlRequest{ 121 | Sql: "select * FROM " + table, 122 | Session: session.Name, 123 | } 124 | 125 | // ExecuteSql 126 | start := time.Now() 127 | respSql, err1 := client.ExecuteSql(ctx, reqSql) 128 | if err1 != nil { 129 | return err1 130 | } 131 | if respSql == nil || len(respSql.Rows) != 1 || respSql.Rows[0].Values[0].GetStringValue() != testUsername { 132 | return errors.New("execute sql failed") 133 | } 134 | metrics["execute_sql_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 135 | 136 | // ExecuteStreamingSql 137 | start = time.Now() 138 | stream, err2 := client.ExecuteStreamingSql(ctx, reqSql) 139 | if err2 != nil { 140 | return err2 141 | } 142 | for { 143 | resp, err := stream.Recv() 144 | if err == io.EOF { 145 | break 146 | } 147 | if err != nil { 148 | return err 149 | } 150 | if resp == nil || resp.Values[0].GetStringValue() != testUsername { 151 | return errors.New("execute streaming sql failed") 152 | } 153 | } 154 | metrics["execute_streaming_sql_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 155 | return nil 156 | } 157 | 158 | func readProber(client *spanner.Client, metrics map[string]int64) error { 159 | ctx := context.Background() 160 | session := createSession(client) 161 | defer deleteSession(client, session) 162 | reqRead := &spannerpb.ReadRequest{ 163 | Session: session.Name, 164 | Table: table, 165 | KeySet: &spannerpb.KeySet{ 166 | All: true, 167 | }, 168 | Columns: []string{"username", "firstname", "lastname"}, 169 | } 170 | 171 | // Read 172 | start := time.Now() 173 | respRead, err1 := client.Read(ctx, reqRead) 174 | if err1 != nil { 175 | return err1 176 | } 177 | if respRead == nil || len(respRead.Rows) != 1 || respRead.Rows[0].Values[0].GetStringValue() != testUsername { 178 | return errors.New("execute read failed") 179 | } 180 | metrics["read_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 181 | 182 | // StreamingRead 183 | start = time.Now() 184 | stream, err2 := client.StreamingRead(ctx, reqRead) 185 | if err2 != nil { 186 | return err2 187 | } 188 | for { 189 | resp, err := stream.Recv() 190 | if err == io.EOF { 191 | break 192 | } 193 | if err != nil { 194 | return err 195 | } 196 | if resp == nil || resp.Values[0].GetStringValue() != testUsername { 197 | return errors.New("streaming read failed") 198 | } 199 | } 200 | metrics["streaming_read_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 201 | return nil 202 | } 203 | 204 | func transactionProber(client *spanner.Client, metrics map[string]int64) error { 205 | ctx := context.Background() 206 | session := createSession(client) 207 | reqBegin := &spannerpb.BeginTransactionRequest{ 208 | Session: session.Name, 209 | Options: &spannerpb.TransactionOptions{ 210 | Mode: &spannerpb.TransactionOptions_ReadWrite_{ 211 | ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, 212 | }, 213 | }, 214 | } 215 | 216 | // BeginTransaction 217 | start := time.Now() 218 | txn, err1 := client.BeginTransaction(ctx, reqBegin) 219 | if err1 != nil { 220 | return err1 221 | } 222 | metrics["begin_transaction_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 223 | 224 | // Commit 225 | reqCommit := &spannerpb.CommitRequest{ 226 | Session: session.Name, 227 | Transaction: &spannerpb.CommitRequest_TransactionId{ 228 | TransactionId: txn.Id, 229 | }, 230 | } 231 | start = time.Now() 232 | _, err2 := client.Commit(ctx, reqCommit) 233 | if err2 != nil { 234 | return err2 235 | } 236 | metrics["commit_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 237 | 238 | // Rollback 239 | txn, err1 = client.BeginTransaction(ctx, reqBegin) 240 | if err1 != nil { 241 | return err1 242 | } 243 | reqRollback := &spannerpb.RollbackRequest{ 244 | Session: session.Name, 245 | TransactionId: txn.Id, 246 | } 247 | start = time.Now() 248 | err2 = client.Rollback(ctx, reqRollback) 249 | if err2 != nil { 250 | return err2 251 | } 252 | metrics["rollback_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 253 | 254 | reqDelete := &spannerpb.DeleteSessionRequest{ 255 | Name: session.Name, 256 | } 257 | client.DeleteSession(ctx, reqDelete) 258 | return nil 259 | } 260 | 261 | func partitionProber(client *spanner.Client, metrics map[string]int64) error { 262 | ctx := context.Background() 263 | session := createSession(client) 264 | defer deleteSession(client, session) 265 | selector := &spannerpb.TransactionSelector{ 266 | Selector: &spannerpb.TransactionSelector_Begin{ 267 | Begin: &spannerpb.TransactionOptions{ 268 | Mode: &spannerpb.TransactionOptions_ReadOnly_{ 269 | ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, 270 | }, 271 | }, 272 | }, 273 | } 274 | 275 | // PartitionQuery 276 | reqQuery := &spannerpb.PartitionQueryRequest{ 277 | Session: session.Name, 278 | Sql: "select * FROM " + table, 279 | Transaction: selector, 280 | } 281 | start := time.Now() 282 | _, err := client.PartitionQuery(ctx, reqQuery) 283 | if err != nil { 284 | return err 285 | } 286 | metrics["partition_query_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 287 | 288 | // PartitionRead 289 | reqRead := &spannerpb.PartitionReadRequest{ 290 | Session: session.Name, 291 | Table: table, 292 | KeySet: &spannerpb.KeySet{ 293 | All: true, 294 | }, 295 | Columns: []string{"username", "firstname", "lastname"}, 296 | Transaction: selector, 297 | } 298 | start = time.Now() 299 | _, err = client.PartitionRead(ctx, reqRead) 300 | if err != nil { 301 | return err 302 | } 303 | metrics["partition_read_latency_ms"] = int64(time.Now().Sub(start) / time.Millisecond) 304 | return nil 305 | } 306 | 307 | func createSession(client *spanner.Client) *spannerpb.Session { 308 | ctx := context.Background() 309 | reqCreate := &spannerpb.CreateSessionRequest{ 310 | Database: database, 311 | } 312 | session, err := client.CreateSession(ctx, reqCreate) 313 | if err != nil { 314 | log.Fatal(err.Error()) 315 | return nil 316 | } 317 | if session == nil { 318 | log.Fatal("Failded to create a new session.") 319 | return nil 320 | } 321 | return session 322 | } 323 | 324 | func deleteSession(client *spanner.Client, session *spannerpb.Session) { 325 | if client == nil { 326 | return 327 | } 328 | ctx := context.Background() 329 | reqDelete := &spannerpb.DeleteSessionRequest{ 330 | Name: session.Name, 331 | } 332 | client.DeleteSession(ctx, reqDelete) 333 | } 334 | -------------------------------------------------------------------------------- /cloudprober/stackdriverutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2019 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "os" 25 | 26 | "cloud.google.com/go/errorreporting" 27 | ) 28 | 29 | const ( 30 | projectID = "grpc-prober-testing" 31 | ) 32 | 33 | type stackdriverUtil struct { 34 | metrics map[string]int64 35 | apiName string 36 | success bool 37 | errClient *errorreporting.Client 38 | } 39 | 40 | func newStackdriverUtil(name string) *stackdriverUtil { 41 | m := make(map[string]int64) 42 | ctx := context.Background() 43 | errorClient, err := errorreporting.NewClient(ctx, projectID, errorreporting.Config{ 44 | ServiceName: "grpc-go-cloudprober", 45 | OnError: func(err error) { 46 | fmt.Fprintln(os.Stderr, "Could not log error: %v", err) 47 | }, 48 | }) 49 | if err != nil { 50 | fmt.Fprintln(os.Stderr, err.Error()) 51 | return &stackdriverUtil{m, name, false, nil} 52 | 53 | } 54 | return &stackdriverUtil{m, name, false, errorClient} 55 | } 56 | 57 | func (util *stackdriverUtil) closeErrClient() { 58 | if util.errClient != nil { 59 | util.errClient.Close() 60 | } 61 | } 62 | 63 | func (util *stackdriverUtil) reportError(err error) { 64 | // Report to the stderr. 65 | fmt.Fprintln(os.Stderr, err.Error()) 66 | 67 | //Report to the stackdriver error log. 68 | util.errClient.Report(errorreporting.Entry{ 69 | Error: err, 70 | }) 71 | } 72 | 73 | func (util *stackdriverUtil) addMetric(key string, value int64) { 74 | (util.metrics)[key] = value 75 | } 76 | 77 | func (util *stackdriverUtil) setSuccess() { 78 | util.success = true 79 | } 80 | 81 | func (util *stackdriverUtil) addMetricsDict(metrics map[string]int64) { 82 | for key, value := range metrics { 83 | (util.metrics)[key] = value 84 | } 85 | } 86 | 87 | func (util *stackdriverUtil) outputMetrics() { 88 | if util.success { 89 | fmt.Printf("%s_success 1\n", util.apiName) 90 | } else { 91 | fmt.Printf("%s_success 0\n", util.apiName) 92 | } 93 | for key, value := range util.metrics { 94 | fmt.Printf("%s %d\n", key, value) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /continuous_load_testing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | COPY continuous_load_testing /app/bin/continuous_load_testing 3 | CMD /app/bin/continuous_load_testing --methods=EmptyCall --concurrency=10 -------------------------------------------------------------------------------- /continuous_load_testing/Dockerfile.cloudpath: -------------------------------------------------------------------------------- 1 | FROM golang 2 | COPY continuous_load_testing /app/bin/continuous_load_testing 3 | CMD /app/bin/continuous_load_testing --methods=EmptyCall --concurrency=10 --disable_directpath=true -------------------------------------------------------------------------------- /continuous_load_testing/client-go-cloudpath-useast7.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: client-go-cloudpath-us-east7 6 | name: client-go-cloudpath-us-east7 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: client-go-cloudpath-us-east7 13 | template: 14 | metadata: 15 | labels: 16 | app: client-go-cloudpath-us-east7 17 | spec: 18 | containers: 19 | - image: "us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-cloudpath-useast7" 20 | imagePullPolicy: Always 21 | name: client-go-cloudpath-us-east7 22 | env: 23 | - name: POD_NAME 24 | valueFrom: 25 | fieldRef: 26 | fieldPath: metadata.name 27 | - name: NAMESPACE_NAME 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: CONTAINER_NAME 32 | value: client-go-cloudpath-us-east7 33 | - name: OTEL_RESOURCE_ATTRIBUTES 34 | value: k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(NAMESPACE_NAME),k8s.container.name=$(CONTAINER_NAME) 35 | resources: 36 | requests: 37 | cpu: "2" 38 | memory: "256Mi" 39 | limits: 40 | cpu: "2" 41 | memory: "1024Mi" -------------------------------------------------------------------------------- /continuous_load_testing/client-go-cloudpath.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: client-go-cloudpath 6 | name: client-go-cloudpath 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: client-go-cloudpath 13 | template: 14 | metadata: 15 | labels: 16 | app: client-go-cloudpath 17 | spec: 18 | containers: 19 | - image: "us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-cloudpath" 20 | imagePullPolicy: Always 21 | name: client-go-cloudpath 22 | env: 23 | - name: POD_NAME 24 | valueFrom: 25 | fieldRef: 26 | fieldPath: metadata.name 27 | - name: NAMESPACE_NAME 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: CONTAINER_NAME 32 | value: client-go-cloudpath 33 | - name: OTEL_RESOURCE_ATTRIBUTES 34 | value: k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(NAMESPACE_NAME),k8s.container.name=$(CONTAINER_NAME) 35 | resources: 36 | requests: 37 | cpu: "2" 38 | memory: "256Mi" 39 | limits: 40 | cpu: "2" 41 | memory: "1024Mi" 42 | -------------------------------------------------------------------------------- /continuous_load_testing/client-go-manual.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: client-go-manual 6 | name: client-go-manual 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: client-go-manual 13 | template: 14 | metadata: 15 | labels: 16 | app: client-go-manual 17 | spec: 18 | containers: 19 | - image: "us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-manual" 20 | imagePullPolicy: Always 21 | name: client-go-manual 22 | env: 23 | - name: POD_NAME 24 | valueFrom: 25 | fieldRef: 26 | fieldPath: metadata.name 27 | - name: NAMESPACE_NAME 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: CONTAINER_NAME 32 | value: client-go-manual 33 | - name: OTEL_RESOURCE_ATTRIBUTES 34 | value: k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(NAMESPACE_NAME),k8s.container.name=$(CONTAINER_NAME) 35 | resources: 36 | requests: 37 | cpu: "2" 38 | memory: "256Mi" 39 | limits: 40 | cpu: "2" 41 | memory: "1024Mi" -------------------------------------------------------------------------------- /continuous_load_testing/client-go-useast7.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: client-go-us-east7 6 | name: client-go-us-east7 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: client-go-us-east7 13 | template: 14 | metadata: 15 | labels: 16 | app: client-go-us-east7 17 | spec: 18 | containers: 19 | - image: "us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-useast7" 20 | imagePullPolicy: Always 21 | name: client-go-us-east7 22 | env: 23 | - name: POD_NAME 24 | valueFrom: 25 | fieldRef: 26 | fieldPath: metadata.name 27 | - name: NAMESPACE_NAME 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: CONTAINER_NAME 32 | value: client-go-us-east7 33 | - name: OTEL_RESOURCE_ATTRIBUTES 34 | value: k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(NAMESPACE_NAME),k8s.container.name=$(CONTAINER_NAME) 35 | resources: 36 | requests: 37 | cpu: "2" 38 | memory: "256Mi" 39 | limits: 40 | cpu: "2" 41 | memory: "1024Mi" -------------------------------------------------------------------------------- /continuous_load_testing/client-go.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: client-go 6 | name: client-go 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: client-go 13 | template: 14 | metadata: 15 | labels: 16 | app: client-go 17 | spec: 18 | containers: 19 | - image: "us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go" 20 | imagePullPolicy: Always 21 | name: client-go 22 | env: 23 | - name: POD_NAME 24 | valueFrom: 25 | fieldRef: 26 | fieldPath: metadata.name 27 | - name: NAMESPACE_NAME 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: CONTAINER_NAME 32 | value: client-go 33 | - name: OTEL_RESOURCE_ATTRIBUTES 34 | value: k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(NAMESPACE_NAME),k8s.container.name=$(CONTAINER_NAME) 35 | resources: 36 | requests: 37 | cpu: "2" 38 | memory: "256Mi" 39 | limits: 40 | cpu: "2" 41 | memory: "1024Mi" 42 | -------------------------------------------------------------------------------- /continuous_load_testing/continuous_load_testing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/grpc-gcp-go/6a9ee9d2228727861a4e351c90cce9f33262479d/continuous_load_testing/continuous_load_testing -------------------------------------------------------------------------------- /continuous_load_testing/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | WORKING_DIR=$(pwd) 3 | ROOT_DIR=$(dirname $(dirname $(pwd))) 4 | echo $WORKING_DIR 5 | echo $ROOT_DIR 6 | 7 | 8 | go generate && go build 9 | kubectl delete deployment client-go-manual 10 | docker system prune -af 11 | docker build --progress=plain --no-cache -t directpathgrpctesting-client-go-manual . 12 | docker tag directpathgrpctesting-client-go-manual us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-manual 13 | gcloud artifacts docker images delete us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-manual --delete-tags -q 14 | docker push us-docker.pkg.dev/directpathgrpctesting-client/directpathgrpctesting-client/directpathgrpctesting-client-go-manual 15 | gcloud container clusters get-credentials cluster-1 --region us-west1 --project directpathgrpctesting-client 16 | kubectl apply -f client-go-manual.yaml -------------------------------------------------------------------------------- /continuous_load_testing/go.mod: -------------------------------------------------------------------------------- 1 | module continuous_load_testing 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 7 | go.opentelemetry.io/contrib/detectors/gcp v1.31.0 8 | go.opentelemetry.io/otel v1.31.0 9 | go.opentelemetry.io/otel/sdk v1.31.0 10 | go.opentelemetry.io/otel/sdk/metric v1.31.0 11 | google.golang.org/grpc v1.69.4 12 | google.golang.org/protobuf v1.36.2 13 | ) 14 | 15 | require ( 16 | cel.dev/expr v0.16.2 // indirect 17 | cloud.google.com/go/auth v0.14.0 // indirect 18 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 19 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 20 | cloud.google.com/go/monitoring v1.22.1 // indirect 21 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 // indirect 22 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect 23 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 24 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 26 | github.com/envoyproxy/go-control-plane v0.13.1 // indirect 27 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 28 | github.com/go-logr/logr v1.4.2 // indirect 29 | github.com/go-logr/stdr v1.2.2 // indirect 30 | github.com/google/s2a-go v0.1.9 // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 33 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 34 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 35 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 36 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 38 | golang.org/x/crypto v0.32.0 // indirect 39 | golang.org/x/net v0.34.0 // indirect 40 | golang.org/x/oauth2 v0.25.0 // indirect 41 | golang.org/x/sync v0.10.0 // indirect 42 | golang.org/x/sys v0.29.0 // indirect 43 | golang.org/x/text v0.21.0 // indirect 44 | golang.org/x/time v0.9.0 // indirect 45 | google.golang.org/api v0.216.0 // indirect 46 | google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 // indirect 47 | google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect 48 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect 49 | ) 50 | 51 | replace ( 52 | grpc.io/grpc/testing/empty => ./proto/grpc.io/grpc/testing/empty 53 | grpc.io/grpc/testing/messages => ./proto/grpc.io/grpc/testing/messages 54 | grpc.io/grpc/testing/test => ./proto/grpc.io/grpc/testing/test 55 | ) 56 | -------------------------------------------------------------------------------- /continuous_load_testing/go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.16.2 h1:RwRhoH17VhAu9U5CMvMhH1PDVgf0tuz9FT+24AfMLfU= 2 | cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8= 3 | cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ= 4 | cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM= 5 | cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= 6 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= 7 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= 8 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 9 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 10 | cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= 11 | cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= 12 | cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg= 13 | cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= 14 | cloud.google.com/go/monitoring v1.22.1 h1:KQbnAC4IAH+5x3iWuPZT5iN9VXqKMzzOgqcYB6fqPDE= 15 | cloud.google.com/go/monitoring v1.22.1/go.mod h1:AuZZXAoN0WWWfsSvET1Cpc4/1D8LXq8KRDU87fMS6XY= 16 | cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= 17 | cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= 18 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 h1:cZpsGsWTIFKymTA0je7IIvi1O7Es7apb9CF3EQlOcfE= 19 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= 20 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= 21 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= 22 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0 h1:nNMpRpnkWDAaqcpxMJvxa/Ud98gjbYwayJY4/9bdjiU= 23 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= 24 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= 25 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= 26 | github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= 27 | github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= 28 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 29 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 30 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= 31 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 32 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= 35 | github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= 36 | github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= 37 | github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= 38 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 39 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 40 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 41 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 42 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 43 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 44 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 45 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 46 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 47 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 48 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 49 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 50 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 51 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 52 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 53 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 54 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 55 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 56 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 57 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 58 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 59 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 60 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 61 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 62 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 63 | go.opentelemetry.io/contrib/detectors/gcp v1.31.0 h1:G1JQOreVrfhRkner+l4mrGxmfqYCAuy76asTDAo0xsA= 64 | go.opentelemetry.io/contrib/detectors/gcp v1.31.0/go.mod h1:tzQL6E1l+iV44YFTkcAeNQqzXUiekSYP9jjJjXwEd00= 65 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= 66 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= 67 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 68 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 69 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 70 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 71 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 72 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 73 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 74 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 75 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 76 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 77 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 78 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 79 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 80 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 81 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 82 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 83 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 84 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 85 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 86 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 87 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 88 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 89 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 90 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 91 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 92 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 93 | google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= 94 | google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= 95 | google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 h1:6GUHKGv2huWOHKmDXLMNE94q3fBDlEHI+oTRIZSebK0= 96 | google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= 97 | google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= 98 | google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= 99 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= 100 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= 101 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 102 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 103 | google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= 104 | google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 105 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 106 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 107 | -------------------------------------------------------------------------------- /continuous_load_testing/grpc-proto-gen.sh: -------------------------------------------------------------------------------- 1 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 2 | export PATH="$PATH:$(go env GOPATH)/bin" 3 | mkdir -p proto 4 | protoc --proto_path=../third_party/grpc-proto \ 5 | --go_out=proto \ 6 | --go_opt=module=continuous_load_testing/proto \ 7 | --go_opt=Mgrpc/testing/empty.proto=continuous_load_testing/proto/grpc/testing/empty \ 8 | --go_opt=Mgrpc/testing/messages.proto=continuous_load_testing/proto/grpc/testing/messages \ 9 | --go_opt=Mgrpc/testing/test.proto=continuous_load_testing/proto/grpc/testing/test \ 10 | --go-grpc_out=proto \ 11 | --go-grpc_opt=module=continuous_load_testing/proto \ 12 | --go-grpc_opt=Mgrpc/testing/empty.proto=continuous_load_testing/proto/grpc/testing/empty \ 13 | --go-grpc_opt=Mgrpc/testing/messages.proto=continuous_load_testing/proto/grpc/testing/messages \ 14 | --go-grpc_opt=Mgrpc/testing/test.proto=continuous_load_testing/proto/grpc/testing/test \ 15 | grpc/testing/empty.proto \ 16 | grpc/testing/messages.proto \ 17 | grpc/testing/test.proto 18 | -------------------------------------------------------------------------------- /continuous_load_testing/proto/grpc/testing/empty/empty.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.36.5 18 | // protoc v4.25.6 19 | // source: grpc/testing/empty.proto 20 | 21 | package empty 22 | 23 | import ( 24 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 25 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 26 | reflect "reflect" 27 | sync "sync" 28 | unsafe "unsafe" 29 | ) 30 | 31 | const ( 32 | // Verify that this generated code is sufficiently up-to-date. 33 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 34 | // Verify that runtime/protoimpl is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 36 | ) 37 | 38 | // An empty message that you can re-use to avoid defining duplicated empty 39 | // messages in your project. A typical example is to use it as argument or the 40 | // return value of a service API. For instance: 41 | // 42 | // service Foo { 43 | // rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; 44 | // }; 45 | type Empty struct { 46 | state protoimpl.MessageState `protogen:"open.v1"` 47 | unknownFields protoimpl.UnknownFields 48 | sizeCache protoimpl.SizeCache 49 | } 50 | 51 | func (x *Empty) Reset() { 52 | *x = Empty{} 53 | mi := &file_grpc_testing_empty_proto_msgTypes[0] 54 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 55 | ms.StoreMessageInfo(mi) 56 | } 57 | 58 | func (x *Empty) String() string { 59 | return protoimpl.X.MessageStringOf(x) 60 | } 61 | 62 | func (*Empty) ProtoMessage() {} 63 | 64 | func (x *Empty) ProtoReflect() protoreflect.Message { 65 | mi := &file_grpc_testing_empty_proto_msgTypes[0] 66 | if x != nil { 67 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 68 | if ms.LoadMessageInfo() == nil { 69 | ms.StoreMessageInfo(mi) 70 | } 71 | return ms 72 | } 73 | return mi.MessageOf(x) 74 | } 75 | 76 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 77 | func (*Empty) Descriptor() ([]byte, []int) { 78 | return file_grpc_testing_empty_proto_rawDescGZIP(), []int{0} 79 | } 80 | 81 | var File_grpc_testing_empty_proto protoreflect.FileDescriptor 82 | 83 | var file_grpc_testing_empty_proto_rawDesc = string([]byte{ 84 | 0x0a, 0x18, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x65, 85 | 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x67, 0x72, 0x70, 0x63, 86 | 0x2e, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 87 | 0x79, 0x42, 0x2a, 0x0a, 0x1b, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x74, 0x65, 0x73, 88 | 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 89 | 0x42, 0x0b, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, 90 | 0x72, 0x6f, 0x74, 0x6f, 0x33, 91 | }) 92 | 93 | var ( 94 | file_grpc_testing_empty_proto_rawDescOnce sync.Once 95 | file_grpc_testing_empty_proto_rawDescData []byte 96 | ) 97 | 98 | func file_grpc_testing_empty_proto_rawDescGZIP() []byte { 99 | file_grpc_testing_empty_proto_rawDescOnce.Do(func() { 100 | file_grpc_testing_empty_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_testing_empty_proto_rawDesc), len(file_grpc_testing_empty_proto_rawDesc))) 101 | }) 102 | return file_grpc_testing_empty_proto_rawDescData 103 | } 104 | 105 | var file_grpc_testing_empty_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 106 | var file_grpc_testing_empty_proto_goTypes = []any{ 107 | (*Empty)(nil), // 0: grpc.testing.Empty 108 | } 109 | var file_grpc_testing_empty_proto_depIdxs = []int32{ 110 | 0, // [0:0] is the sub-list for method output_type 111 | 0, // [0:0] is the sub-list for method input_type 112 | 0, // [0:0] is the sub-list for extension type_name 113 | 0, // [0:0] is the sub-list for extension extendee 114 | 0, // [0:0] is the sub-list for field type_name 115 | } 116 | 117 | func init() { file_grpc_testing_empty_proto_init() } 118 | func file_grpc_testing_empty_proto_init() { 119 | if File_grpc_testing_empty_proto != nil { 120 | return 121 | } 122 | type x struct{} 123 | out := protoimpl.TypeBuilder{ 124 | File: protoimpl.DescBuilder{ 125 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 126 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_testing_empty_proto_rawDesc), len(file_grpc_testing_empty_proto_rawDesc)), 127 | NumEnums: 0, 128 | NumMessages: 1, 129 | NumExtensions: 0, 130 | NumServices: 0, 131 | }, 132 | GoTypes: file_grpc_testing_empty_proto_goTypes, 133 | DependencyIndexes: file_grpc_testing_empty_proto_depIdxs, 134 | MessageInfos: file_grpc_testing_empty_proto_msgTypes, 135 | }.Build() 136 | File_grpc_testing_empty_proto = out.File 137 | file_grpc_testing_empty_proto_goTypes = nil 138 | file_grpc_testing_empty_proto_depIdxs = nil 139 | } 140 | -------------------------------------------------------------------------------- /doc/gRPC-client-user-guide.md: -------------------------------------------------------------------------------- 1 | # Instructions for create a gRPC client for google cloud services 2 | 3 | ## Overview 4 | 5 | This instruction includes a step by step guide for creating a gRPC 6 | client to test the google cloud service from an empty linux 7 | VM, using GCE ubuntu 16.04 TLS instance. 8 | 9 | The main steps are followed as steps below: 10 | 11 | - Environment prerequisite 12 | - Install gRPC-go, plugin, protobuf and oauth2 13 | - Generate client API from .proto files 14 | - Create the client and send/receive RPC. 15 | 16 | ## Environment Prerequisite 17 | 18 | **Golang** 19 | ```sh 20 | $ wget https://dl.google.com/go/go1.9.3.linux-amd64.tar.gz 21 | $ [sudo] tar -C /usr/local -xzf go1.9.3.linux-amd64.tar.gz 22 | $ echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc 23 | $ source ~/.bashrc 24 | $ mkdir $HOME/go 25 | ``` 26 | 27 | ## Install gRPC-go, plugin, protobuf and oauth2 28 | - gRPC-go, plugin and pauth2 29 | ```sh 30 | $ cd $HOME/go 31 | $ go get -u google.golang.org/grpc 32 | $ go get -u github.com/golang/protobuf/protoc-gen-go 33 | $ go get golang.org/x/oauth2/google 34 | ``` 35 | - protobuf 36 | ```sh 37 | $ cd $HOME 38 | $ git clone https://github.com/google/protobuf.git 39 | $ cd $HOME/protobuf 40 | $ ./autogen.sh && ./configure && make -j8 41 | $ [sudo] make install 42 | $ [sudo] ldconfig 43 | ``` 44 | 45 | ## Generate client API from .proto files 46 | Please check files under `$HOME/go/src/google.golang.org/genproto/googleapis` to see whether 47 | you service client API **has already been generated**. 48 | If they are already there, you can skip this step. 49 | For most google cloud APIs, all client APIs are already generated in the 50 | [go-genproto repo](https://github.com/google/go-genproto) under `googleapis/`. 51 | 52 | **If they have not already been generated**, 53 | the common way to use the plugin looks like 54 | ```sh 55 | $ mkdir $HOME/go/src/project-golang && cd $HOME/go/src/project-golang 56 | $ protoc --proto_path=/path/to/proto_dir --go_out=./\ 57 | path/to/your/proto_dependency_directory1/*.proto \ 58 | path/to/your/proto_dependency_directory2/*.proto \ 59 | path/to/your/proto_service_directory/*.proto 60 | ``` 61 | 62 | Assume that you don't need to generate pb files 63 | because you find them generated under `$HOME/go/src/google.golang.org/genproto/googleapis`. 64 | They are installing during installing the gRPC. 65 | Take [`Firestore`](https://github.com/googleapis/googleapis/blob/master/google/firestore/v1beta1/firestore.proto) 66 | as example, the Client API is under 67 | `$HOME/go/src/google.golang.org/genproto/googleapis/firestore/v1beta1/firestore.pb.go` depends on your 68 | package namespace inside .proto file. An easy way to find your client is 69 | ```sh 70 | $ cd $HOME/go 71 | $ find ./ -name [service_name: eg, firestore, cluster_service]* 72 | ``` 73 | The one under `genproto` directory is what you need. 74 | 75 | ## Create the client and send/receive RPC. 76 | Now it's time to use the client API to send and receive RPCs. 77 | 78 | Here I assume that you don't need to generate pb files from the last step 79 | and use files under `$HOME/go/src/google.golang.org/genproto/googleapis` directly. 80 | If you generate them by your own, the difference is change the import path. 81 | 82 | **Set credentials file** 83 | 84 | This is important otherwise your RPC response will be a permission error. 85 | ``` sh 86 | $ vim $HOME/key.json 87 | ## Paste you credential file downloaded from your cloud project 88 | ## which you can find in APIs&Services => credentials => create credentials 89 | ## => Service account key => your credentials 90 | $ export GOOGLE_APPLICATION_CREDENTIALS=$HOME/key.json 91 | ``` 92 | 93 | **Implement Service Client** 94 | 95 | Take a unary-unary RPC `listDocument` from `FirestoreClient` as example. 96 | Create a file name `$HOME/src/main.go`. 97 | - Import library 98 | ``` 99 | package main 100 | 101 | import ( 102 | "context" 103 | "fmt" 104 | "log" 105 | "os" 106 | 107 | "google.golang.org/grpc" 108 | "google.golang.org/grpc/credentials" 109 | "google.golang.org/grpc/credentials/oauth" 110 | 111 | firestore "google.golang.org/genproto/googleapis/firestore/v1beta1" 112 | ) 113 | ``` 114 | - Set Google Auth. Please see the referece for 115 | [authenticate with Google using an Oauth2 token](https://grpc.io/docs/guides/auth.html#authenticate-with-google) for the use of 'googleauth' library. 116 | ``` 117 | keyFile := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 118 | perRPC, err := oauth.NewServiceAccountFromFile(keyFile, "https://www.googleapis.com/auth/datastore") 119 | if err != nil { 120 | log.Fatalf("Failed to create credentials: %v", err) 121 | } 122 | address := "firestore.googleapis.com:443" 123 | conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), grpc.WithPerRPCCredentials(perRPC)) 124 | defer conn.Close() 125 | ``` 126 | - Create Client 127 | ``` 128 | client := firestore.NewFirestoreClient(conn) 129 | ``` 130 | - Make and receive RPC call 131 | ``` 132 | listDocsRequest := firestore.ListDocumentsRequest{ 133 | Parent: "projects/ddyihai-firestore/databases/(default)", 134 | } 135 | resp, err := client.ListDocuments(context.Background(), &listDocsRequest) 136 | if err != nil { 137 | fmt.Println(err) 138 | } 139 | ``` 140 | - Print RPC response 141 | ``` 142 | for _, doc := range resp.Documents { 143 | fmt.Printf("%+v\n", doc) 144 | } 145 | ``` 146 | - Run the script 147 | ```sh 148 | $ cd $HOME/go 149 | $ go run src/main.go 150 | ``` 151 | 152 | For different kinds of RPC(unary-unary, unary-stream, stream-unary, stream-stream), 153 | please check [grpc.io Golang part](https://grpc.io/docs/tutorials/basic/go.html#simple-rpcc) 154 | for reference. 155 | 156 | 157 | -------------------------------------------------------------------------------- /e2e-checksum/README.md: -------------------------------------------------------------------------------- 1 | # End to End Checksum Client 2 | 3 | This is an example datastore client applying end to end checksum for data 4 | integrity. 5 | 6 | ## Usage 7 | 8 | For this client, you can choose to use the production datastore target 9 | ("datastore.googleapis.com"), or use a test server running behind GFE. 10 | 11 | ## Run client 12 | 13 | Override datastore endpoint. 14 | 15 | ```sh 16 | export DATASTORE_EMULATOR_HOST=my-test-service.sandbox.googleapis.com:443 17 | ``` 18 | 19 | Run: 20 | 21 | ```sh 22 | go run main.go 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /e2e-checksum/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/grpc-gcp-go/e2e-checksum 2 | 3 | go 1.19 4 | 5 | require ( 6 | cloud.google.com/go/datastore v1.11.0 7 | github.com/golang/protobuf v1.5.3 8 | google.golang.org/api v0.114.0 9 | google.golang.org/grpc v1.56.3 10 | ) 11 | 12 | require ( 13 | cloud.google.com/go v0.110.0 // indirect 14 | cloud.google.com/go/compute v1.19.1 // indirect 15 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 16 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 17 | github.com/google/go-cmp v0.5.9 // indirect 18 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 19 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 20 | go.opencensus.io v0.24.0 // indirect 21 | golang.org/x/net v0.17.0 // indirect 22 | golang.org/x/oauth2 v0.7.0 // indirect 23 | golang.org/x/sys v0.13.0 // indirect 24 | golang.org/x/text v0.13.0 // indirect 25 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 26 | google.golang.org/appengine v1.6.7 // indirect 27 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 28 | google.golang.org/protobuf v1.30.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /e2e-checksum/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "hash/crc32" 6 | "log" 7 | 8 | "cloud.google.com/go/datastore" 9 | "github.com/golang/protobuf/proto" 10 | "google.golang.org/api/option" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials" 13 | "google.golang.org/grpc/encoding" 14 | protoCodec "google.golang.org/grpc/encoding/proto" 15 | ) 16 | 17 | const ( 18 | checksumField = 2047 19 | checksumWireType = 5 // wire type is a 32-bit 20 | ) 21 | 22 | type myCodec struct { 23 | protoCodec encoding.Codec 24 | } 25 | 26 | func (c *myCodec) Marshal(v interface{}) ([]byte, error) { 27 | bytes, err := c.protoCodec.Marshal(v) 28 | if err != nil { 29 | return bytes, err 30 | } 31 | crc32c := crc32.MakeTable(crc32.Castagnoli) 32 | checksum := crc32.Checksum(bytes, crc32c) 33 | 34 | buffer := proto.NewBuffer([]byte{}) 35 | 36 | // calculate checksum tag (field & wire type) 37 | tag := (checksumField << 3) | checksumWireType 38 | 39 | if err = buffer.EncodeVarint(uint64(tag)); err != nil { 40 | return bytes, err 41 | } 42 | 43 | if err = buffer.EncodeFixed32(uint64(checksum)); err != nil { 44 | return bytes, err 45 | } 46 | 47 | log.Printf("encoded checksum field and value: %+v\n", buffer.Bytes()) 48 | newBytes := append(buffer.Bytes(), bytes...) // prepend 49 | //newBytes := append(bytes, buffer.Bytes()...) // append 50 | log.Printf("Marshalled bytes: %+v\n", bytes) 51 | return newBytes, err 52 | } 53 | 54 | func (c *myCodec) Unmarshal(data []byte, v interface{}) error { 55 | return c.protoCodec.Unmarshal(data, v) 56 | } 57 | 58 | func (c *myCodec) String() string { 59 | return "MyCodec" 60 | } 61 | 62 | func main() { 63 | type Entity struct { 64 | Firstname string 65 | Lastname string 66 | } 67 | 68 | ctx := context.Background() 69 | projectID := "grpc-gcp" 70 | opts := []option.ClientOption{ 71 | option.WithGRPCDialOption(grpc.WithCodec(&myCodec{protoCodec: encoding.GetCodec(protoCodec.Name)})), 72 | option.WithGRPCDialOption(grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))), 73 | //option.WithEndpoint("stubby-e2e-generic-service-test.sandbox.googleapis.com:443"), 74 | } 75 | client, err := datastore.NewClient(ctx, projectID, opts...) 76 | if err != nil { 77 | log.Fatalf("Failed to create firestore client: %v", err) 78 | } 79 | kind := "Person" 80 | name := "weiranf" 81 | key := datastore.NameKey(kind, name, nil) 82 | 83 | e := Entity{ 84 | Firstname: "Weiran", 85 | Lastname: "Fang", 86 | } 87 | 88 | if _, err := client.Put(ctx, key, &e); err != nil { 89 | log.Fatalf("client.Put failed: %v", err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /e2e-examples/echo/README.md: -------------------------------------------------------------------------------- 1 | # Echo Client for e2e test 2 | 3 | Simple echo client for e2e testing against the echo service defined in 4 | [echo.proto](echo/echo.proto). 5 | 6 | ## Usage 7 | 8 | The client sends out `numRpcs` number of Ping-Pong unary requests sequentially 9 | with request size specified by `reqSize` in KB, and response size specified by 10 | `rspSize` in KB, test result will be printed in console. 11 | 12 | ## Generate protobuf code 13 | 14 | If we need to regenerate pb code for echo grpc service, run: 15 | 16 | ```sh 17 | ./echo/codegen.sh 18 | ``` 19 | 20 | ## Run client 21 | 22 | Example command for endpoint `some.test.service` with 100 RPCs and 100KB 23 | response size: 24 | 25 | ```sh 26 | go run echo-client/main.go -numRpcs=100 -rspSize=100 27 | ``` 28 | 29 | Example test result 30 | 31 | ```sh 32 | [Number of RPCs: 100, Request size: 1KB, Response size: 100KB] 33 | Avg Min p50 p90 p99 Max 34 | Time(ms) 76 74 76 78 109 109 35 | ``` 36 | -------------------------------------------------------------------------------- /e2e-examples/echo/echo-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "sort" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | pb "github.com/GoogleCloudPlatform/grpc-gcp-go/e2e-examples/echo/echo" 13 | "google.golang.org/grpc" 14 | "google.golang.org/grpc/credentials" 15 | ) 16 | 17 | var ( 18 | addr = flag.String("addr", "staging-grpc-cfe-benchmarks.googleapis.com:443", "server address") 19 | numRpcs = flag.Int("numRpcs", 1, "number of blocking unary calls") 20 | warmup = flag.Int("warmup", 5, "number of warmup calls before test") 21 | rspSize = flag.Int("rspSize", 1, "response size in KB") 22 | reqSize = flag.Int("reqSize", 1, "request size in KB") 23 | async = flag.Bool("async", false, "use async echo calls") 24 | ) 25 | 26 | func printRsts(numRpcs int, rspSize int, reqSize int, rsts []int) { 27 | sort.Ints(rsts) 28 | n := len(rsts) 29 | sum := 0 30 | for _, r := range rsts { 31 | sum += r 32 | } 33 | log.Printf( 34 | "\n[Number of RPCs: %v, Request size: %vKB, Response size: %vKB]\n"+ 35 | "\t\tAvg\tMin\tp50\tp90\tp99\tMax\n"+ 36 | "Time(ms)\t%v\t%v\t%v\t%v\t%v\t%v\n", 37 | numRpcs, reqSize, rspSize, 38 | sum/n, 39 | rsts[0], 40 | rsts[int(float64(n)*0.5)], 41 | rsts[int(float64(n)*0.9)], 42 | rsts[int(float64(n)*0.99)], 43 | rsts[n-1], 44 | ) 45 | } 46 | 47 | func main() { 48 | flag.Parse() 49 | conn, err := grpc.Dial( 50 | *addr, 51 | grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), 52 | grpc.WithBlock()) 53 | if err != nil { 54 | log.Fatalf("did not connect: %v", err) 55 | } 56 | defer conn.Close() 57 | 58 | client := pb.NewGrpcCloudapiClient(conn) 59 | msg := strings.Repeat("x", *reqSize*1024) 60 | req := &pb.EchoWithResponseSizeRequest{EchoMsg: msg, ResponseSize: int32(*rspSize * 1024)} 61 | 62 | // begin warmup calls 63 | for i := 0; i < *warmup; i++ { 64 | _, err := client.EchoWithResponseSize(context.Background(), req) 65 | if err != nil { 66 | log.Fatalf("EchoWithResponseSize failed with error during warmup: %v", err) 67 | } 68 | } 69 | 70 | rsts := []int{} 71 | 72 | if !*async { 73 | // begin tests 74 | for i := 0; i < *numRpcs; i++ { 75 | start := time.Now() 76 | _, err := client.EchoWithResponseSize(context.Background(), req) 77 | if err != nil { 78 | log.Fatalf("EchoWithResponseSize failed with error: %v", err) 79 | } 80 | rsts = append(rsts, int(time.Since(start).Milliseconds())) 81 | } 82 | printRsts(*numRpcs, *rspSize, *reqSize, rsts) 83 | } else { 84 | var wg sync.WaitGroup 85 | wg.Add(*numRpcs) 86 | 87 | for i := 0; i < *numRpcs; i++ { 88 | go func(r *pb.EchoWithResponseSizeRequest, n int) { 89 | defer wg.Done() 90 | _, err := client.EchoWithResponseSize(context.Background(), r) 91 | if err != nil { 92 | log.Fatalf("EchoWithResponseSize failed with error: %v", err) 93 | } 94 | log.Printf("Done %vth request", n) 95 | }(req, i) 96 | } 97 | wg.Wait() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /e2e-examples/echo/echo/codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$0")" 3 | 4 | #protoc --plugin=$GOPATH/bin/protoc-gen-go --proto_path=./ --go_out=./ ./echo.proto 5 | 6 | protoc --go_out=plugins=grpc:. *.proto 7 | 8 | -------------------------------------------------------------------------------- /e2e-examples/echo/echo/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e_service; 4 | 5 | option java_package = "io.grpc.echo"; 6 | option go_package = "echo"; 7 | 8 | //import "google/api/annotations.proto"; 9 | 10 | // Request message type for simple echo. 11 | message EchoRequest { 12 | string string_to_echo = 1; 13 | } 14 | 15 | // Response message type for simple echo. 16 | message EchoResponse { 17 | string echoed_string = 1; 18 | } 19 | 20 | message EchoWithResponseSizeRequest { 21 | string echo_msg = 1; 22 | int32 response_size = 2; 23 | } 24 | 25 | message StreamEchoRequest { 26 | int32 message_count = 1; 27 | int32 message_interval = 2; 28 | } 29 | 30 | // A simple service to test and debug in an E2E environment 31 | // TODO(qixuanl): implement or change to a more complicated service 32 | service GrpcCloudapi { 33 | // A simple echo RPC returns the input string 34 | rpc Echo(EchoRequest) returns (EchoResponse) { 35 | // option (google.api.http) = { 36 | // get: "/v1/{string_to_echo}" 37 | // }; 38 | } 39 | 40 | // A simple echo RPC receives a custom response size 41 | rpc EchoWithResponseSize(EchoWithResponseSizeRequest) returns (EchoResponse) { 42 | // option (google.api.http) = { 43 | // get: "/v1/{response_size}" 44 | // }; 45 | } 46 | 47 | // A simple stream endpoint 48 | rpc EchoStream(StreamEchoRequest) returns (stream EchoResponse) { 49 | // option (google.api.http) = { 50 | // get: "/v1/stream/{message_count}/{message_interval}" 51 | // }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /e2e-examples/gcs/.gitignore: -------------------------------------------------------------------------------- 1 | /googleapis/ 2 | -------------------------------------------------------------------------------- /e2e-examples/gcs/README.md: -------------------------------------------------------------------------------- 1 | ## Regenerating protos 2 | 3 | ```sh 4 | git clone git@github.com:googleapis/googleapis.git 5 | cd googleapis 6 | protoc --go_out=.. --go-grpc_out=.. google/storage/v2/*.proto 7 | cd .. 8 | ``` 9 | 10 | ## Example commands 11 | 12 | Use grpc client to write a 128KiB file to a GCS bucket 13 | 14 | ```sh 15 | go run main.go --method=write --size=128 16 | ``` 17 | 18 | Use grpc client to read a 128KiB file from a GCS bucket 19 | 20 | ```sh 21 | go run main.go --method=read --size=128 22 | ``` 23 | -------------------------------------------------------------------------------- /e2e-examples/gcs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "hash/crc32" 8 | "io" 9 | "os" 10 | "time" 11 | 12 | gcspb "github.com/GoogleCloudPlatform/grpc-gcp-go/e2e-examples/gcs/cloud.google.com/go/storage/genproto/apiv2/storagepb" 13 | 14 | "google.golang.org/grpc" 15 | _ "google.golang.org/grpc/balancer/rls" 16 | "google.golang.org/grpc/credentials/google" 17 | "google.golang.org/grpc/metadata" 18 | "google.golang.org/grpc/resolver" 19 | _ "google.golang.org/grpc/xds/googledirectpath" 20 | ) 21 | 22 | var ( 23 | dp = flag.Bool("dp", true, "whether use directpath") 24 | td = flag.Bool("td", true, "whether use traffic director") 25 | host = flag.String("host", "storage.googleapis.com", "gcs backend hostname") 26 | clientType = flag.String("client", "go-grpc", "type of client") 27 | objectFormat = flag.String("obj_format", "write/128KiB/128KiB", "gcs object name") 28 | bucketName = flag.String("bkt", "gcs-grpc-team-dp-test-us-central1", "gcs bucket name") 29 | numCalls = flag.Int("calls", 1, "num of calls") 30 | numWarmups = flag.Int("warmups", 1, "num of warmup calls") 31 | numThreads = flag.Int("threads", 1, "num of threads") 32 | method = flag.String("method", "write", "method names") 33 | size = flag.Int("size", 128, "write size in kb") 34 | ) 35 | 36 | func getBucketName() string { 37 | return fmt.Sprintf("projects/_/buckets/%s", *bucketName) 38 | } 39 | 40 | func addBucketMetadataToCtx(ctx context.Context) context.Context { 41 | return metadata.AppendToOutgoingContext(ctx, 42 | "x-goog-request-params", 43 | fmt.Sprintf("bucket=%s", getBucketName())) 44 | } 45 | 46 | func getGrpcClient() gcspb.StorageClient { 47 | resolver.SetDefaultScheme("dns") 48 | endpoint := fmt.Sprintf("google-c2p:///%s", *host) 49 | 50 | var grpcOpts []grpc.DialOption 51 | grpcOpts = []grpc.DialOption{ 52 | grpc.WithCredentialsBundle( 53 | google.NewComputeEngineCredentials(), 54 | ), 55 | } 56 | conn, err := grpc.Dial(endpoint, grpcOpts...) 57 | if err != nil { 58 | fmt.Println("Failed to create clientconn: %v", err) 59 | os.Exit(1) 60 | } 61 | return gcspb.NewStorageClient(conn) 62 | } 63 | 64 | func readRequest(client gcspb.StorageClient) { 65 | ctx := context.Background() 66 | req := gcspb.ReadObjectRequest{ 67 | Bucket: getBucketName(), 68 | Object: *objectFormat, 69 | } 70 | ctx = addBucketMetadataToCtx(ctx) 71 | start := time.Now() 72 | stream, err := client.ReadObject(ctx, &req) 73 | if err != nil { 74 | fmt.Println("ReadObject got error: ", err) 75 | os.Exit(1) 76 | } 77 | for { 78 | resp, err := stream.Recv() 79 | if err == io.EOF { 80 | fmt.Println("Done reading object.") 81 | break 82 | } 83 | if err != nil { 84 | fmt.Println("ReadObject Recv error: ", err) 85 | os.Exit(1) 86 | } 87 | len := len(resp.GetChecksummedData().GetContent()) 88 | fmt.Printf("bytes read: %d\n", len) 89 | } 90 | total := time.Since(start).Milliseconds() 91 | fmt.Println("total time in ms for read: ", total) 92 | } 93 | 94 | func getWriteRequest(isFirst bool, isLast bool, offset int64, data []byte) *gcspb.WriteObjectRequest { 95 | crc32c := crc32.MakeTable(crc32.Castagnoli) 96 | checksum := crc32.Checksum(data, crc32c) 97 | 98 | req := &gcspb.WriteObjectRequest{} 99 | if isFirst { 100 | req.FirstMessage = &gcspb.WriteObjectRequest_WriteObjectSpec{ 101 | WriteObjectSpec: &gcspb.WriteObjectSpec{ 102 | Resource: &gcspb.Object{ 103 | Bucket: getBucketName(), 104 | Name: *objectFormat, 105 | }, 106 | }, 107 | } 108 | } 109 | req.WriteOffset = offset 110 | req.Data = &gcspb.WriteObjectRequest_ChecksummedData{ 111 | ChecksummedData: &gcspb.ChecksummedData{ 112 | Content: data, 113 | Crc32C: &checksum, 114 | }, 115 | } 116 | if isLast { 117 | req.FinishWrite = true 118 | } 119 | return req 120 | } 121 | 122 | func writeRequest(client gcspb.StorageClient) { 123 | ctx := context.Background() 124 | ctx = addBucketMetadataToCtx(ctx) 125 | 126 | totalBytes := *size * 1024 127 | offset := 0 128 | isFirst := true 129 | isLast := false 130 | 131 | start := time.Now() 132 | stream, err := client.WriteObject(ctx) 133 | if err != nil { 134 | fmt.Println("WriteObject got error: ", err) 135 | os.Exit(1) 136 | } 137 | 138 | for offset < totalBytes { 139 | var add int 140 | if offset+int(gcspb.ServiceConstants_MAX_WRITE_CHUNK_BYTES) <= totalBytes { 141 | add = int(gcspb.ServiceConstants_MAX_WRITE_CHUNK_BYTES) 142 | } else { 143 | add = totalBytes - offset 144 | } 145 | if offset+add == totalBytes { 146 | isLast = true 147 | } 148 | data := make([]byte, add, add) 149 | req := getWriteRequest(isFirst, isLast, int64(offset), data) 150 | fmt.Printf("writing %d bytes\n", add) 151 | if err := stream.Send(req); err != nil { 152 | fmt.Println("stream.Send got error: ", err) 153 | } 154 | isFirst = false 155 | offset += add 156 | } 157 | stream.CloseAndRecv() 158 | total := time.Since(start).Milliseconds() 159 | fmt.Println("total time in ms for write: ", total) 160 | } 161 | 162 | func main() { 163 | flag.Parse() 164 | if !*dp { 165 | fmt.Println("only directpath is supported for now") 166 | os.Exit(1) 167 | } 168 | if !*td { 169 | fmt.Println("only traffic director is supported for now") 170 | os.Exit(1) 171 | } 172 | 173 | var client gcspb.StorageClient 174 | switch *clientType { 175 | case "go-grpc": 176 | client = getGrpcClient() 177 | default: 178 | fmt.Printf("Unsupported --client=%s\n", *clientType) 179 | os.Exit(1) 180 | } 181 | 182 | switch *method { 183 | case "write": 184 | writeRequest(client) 185 | case "read": 186 | readRequest(client) 187 | default: 188 | fmt.Printf("Unsupported --method=%s\n", *method) 189 | os.Exit(1) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /e2e-examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/grpc-gcp-go/e2e-examples 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | cloud.google.com/go/iam v1.4.1 7 | cloud.google.com/go/storage v1.51.0 8 | github.com/golang/protobuf v1.5.4 9 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb 10 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb 11 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb 12 | google.golang.org/grpc v1.71.0 13 | google.golang.org/protobuf v1.36.5 14 | ) 15 | 16 | require ( 17 | cel.dev/expr v0.19.2 // indirect 18 | cloud.google.com/go v0.118.3 // indirect 19 | cloud.google.com/go/auth v0.15.0 // indirect 20 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 21 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 22 | cloud.google.com/go/monitoring v1.24.0 // indirect 23 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect 24 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect 25 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect 26 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 27 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect 28 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 29 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/go-logr/logr v1.4.2 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/google/s2a-go v0.1.9 // indirect 34 | github.com/google/uuid v1.6.0 // indirect 35 | github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect 36 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 37 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 38 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 39 | go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect 40 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect 41 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 42 | go.opentelemetry.io/otel v1.34.0 // indirect 43 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 44 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 45 | go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect 46 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 47 | golang.org/x/crypto v0.35.0 // indirect 48 | golang.org/x/net v0.35.0 // indirect 49 | golang.org/x/oauth2 v0.28.0 // indirect 50 | golang.org/x/sync v0.12.0 // indirect 51 | golang.org/x/sys v0.30.0 // indirect 52 | golang.org/x/text v0.22.0 // indirect 53 | golang.org/x/time v0.10.0 // indirect 54 | google.golang.org/api v0.224.0 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /e2e-examples/go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= 2 | cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= 3 | cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= 4 | cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= 5 | cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= 6 | cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= 7 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= 8 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= 9 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 10 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 11 | cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM= 12 | cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM= 13 | cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= 14 | cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= 15 | cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= 16 | cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= 17 | cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= 18 | cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= 19 | cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= 20 | cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= 21 | cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= 22 | cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= 23 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= 24 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= 25 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= 26 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= 27 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= 28 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= 30 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= 31 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 32 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 33 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= 34 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= 38 | github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= 39 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= 40 | github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= 41 | github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= 42 | github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= 43 | github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= 44 | github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= 45 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 46 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 47 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 48 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 49 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 50 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 51 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 52 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 53 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 54 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 55 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 56 | github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= 57 | github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= 58 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 59 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 60 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 61 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 62 | github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= 63 | github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 64 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 65 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 66 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 67 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 71 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 72 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 73 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 74 | go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= 75 | go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= 76 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= 77 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= 78 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= 79 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= 80 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 81 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 82 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= 83 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= 84 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 85 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 86 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 87 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 88 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 89 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 90 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 91 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 92 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 93 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 94 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 95 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 96 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 97 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 98 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 99 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 100 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 101 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 102 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 103 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 104 | golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= 105 | golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 106 | google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= 107 | google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= 108 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= 109 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= 110 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= 111 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= 112 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= 113 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 114 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 115 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 116 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 117 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 118 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 119 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | -------------------------------------------------------------------------------- /examples/spanner_grpcgcp/Readme.md: -------------------------------------------------------------------------------- 1 | ### Spanner client using gRPC-GCP channel pool example 2 | 3 | You'll need a Spanner database to test connectivity via gRPC-GCP channel pool. 4 | 5 | Run 6 | 7 | ``` 8 | go run spanner_grpcgcp.go --project=your-gcp-project --instance=your-spanner-instance --database=your-spanner-database 9 | ``` 10 | 11 | The output should look like: 12 | 13 | ``` 14 | 2023/01/18 19:28:10 Returned: 1 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/spanner_grpcgcp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/grpc-gcp-go/examples/spanner_grpcgcp 2 | 3 | go 1.20 4 | 5 | require ( 6 | cloud.google.com/go/spanner v1.45.0 7 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.3.0 8 | google.golang.org/api v0.114.0 9 | google.golang.org/grpc v1.56.3 10 | google.golang.org/protobuf v1.30.0 11 | ) 12 | 13 | require ( 14 | cloud.google.com/go v0.110.0 // indirect 15 | cloud.google.com/go/compute v1.19.1 // indirect 16 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 17 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect 20 | github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect 21 | github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f // indirect 22 | github.com/envoyproxy/protoc-gen-validate v0.10.1 // indirect 23 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 24 | github.com/golang/protobuf v1.5.3 // indirect 25 | github.com/google/go-cmp v0.5.9 // indirect 26 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 27 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 28 | go.opencensus.io v0.24.0 // indirect 29 | golang.org/x/net v0.17.0 // indirect 30 | golang.org/x/oauth2 v0.7.0 // indirect 31 | golang.org/x/sys v0.13.0 // indirect 32 | golang.org/x/text v0.13.0 // indirect 33 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 34 | google.golang.org/appengine v1.6.7 // indirect 35 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /examples/spanner_grpcgcp/spanner_grpcgcp.go: -------------------------------------------------------------------------------- 1 | // Test program to try Spanner client with gRPC-GCP channel pool. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "cloud.google.com/go/spanner" 12 | "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp" 13 | gtransport "google.golang.org/api/transport/grpc" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/credentials" 16 | "google.golang.org/grpc/credentials/oauth" 17 | "google.golang.org/protobuf/encoding/protojson" 18 | 19 | gpb "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/grpc_gcp" 20 | ) 21 | 22 | const ( 23 | // Spanner client uses a fixed-size channel pool and relies on the pool size 24 | // when issuing BatchCreateSessions calls to create new sessions. 25 | // This constant helps keep in line our replacement channel pool size 26 | // and what is communicated to the Spanner client. 27 | poolSize = 4 28 | endpoint = "spanner.googleapis.com:443" 29 | scope = "https://www.googleapis.com/auth/cloud-platform" 30 | ) 31 | 32 | var ( 33 | project = flag.String("project", "", "GCP project for Cloud Spanner.") 34 | instance_name = flag.String("instance", "test1", "Target instance") 35 | database_name = flag.String("database", "test1", "Target database") 36 | 37 | grpcGcpConfig = &gpb.ApiConfig{ 38 | ChannelPool: &gpb.ChannelPoolConfig{ 39 | // Creates a fixed-size gRPC-GCP channel pool. 40 | MinSize: poolSize, 41 | MaxSize: poolSize, 42 | // This option repeats(preserves) the strategy used by the Spanner 43 | // client to distribute BatchCreateSessions calls across channels. 44 | BindPickStrategy: gpb.ChannelPoolConfig_ROUND_ROBIN, 45 | // When issuing RPC call within Spanner session fallback to a ready 46 | // channel if the channel mapped to the session is not ready. 47 | FallbackToReady: true, 48 | // Establish a new connection for a channel where 49 | // no response/messages were received within last 1 second and 50 | // at least 3 RPC calls (started after the last response/message 51 | // received) timed out (deadline_exceeded). 52 | UnresponsiveDetectionMs: 1000, 53 | UnresponsiveCalls: 3, 54 | }, 55 | // Configuration for all Spanner RPCs that create, use or remove 56 | // Spanner sessions. gRPC-GCP channel pool uses this configuration 57 | // to provide session to channel affinity. If Spanner introduces any new 58 | // method that creates/uses/removes sessions, it must be added here. 59 | Method: []*gpb.MethodConfig{ 60 | { 61 | Name: []string{"/google.spanner.v1.Spanner/CreateSession"}, 62 | Affinity: &gpb.AffinityConfig{ 63 | Command: gpb.AffinityConfig_BIND, 64 | AffinityKey: "name", 65 | }, 66 | }, 67 | { 68 | Name: []string{"/google.spanner.v1.Spanner/BatchCreateSessions"}, 69 | Affinity: &gpb.AffinityConfig{ 70 | Command: gpb.AffinityConfig_BIND, 71 | AffinityKey: "session.name", 72 | }, 73 | }, 74 | { 75 | Name: []string{"/google.spanner.v1.Spanner/DeleteSession"}, 76 | Affinity: &gpb.AffinityConfig{ 77 | Command: gpb.AffinityConfig_UNBIND, 78 | AffinityKey: "name", 79 | }, 80 | }, 81 | { 82 | Name: []string{"/google.spanner.v1.Spanner/GetSession"}, 83 | Affinity: &gpb.AffinityConfig{ 84 | Command: gpb.AffinityConfig_BOUND, 85 | AffinityKey: "name", 86 | }, 87 | }, 88 | { 89 | Name: []string{ 90 | "/google.spanner.v1.Spanner/BeginTransaction", 91 | "/google.spanner.v1.Spanner/Commit", 92 | "/google.spanner.v1.Spanner/ExecuteBatchDml", 93 | "/google.spanner.v1.Spanner/ExecuteSql", 94 | "/google.spanner.v1.Spanner/ExecuteStreamingSql", 95 | "/google.spanner.v1.Spanner/PartitionQuery", 96 | "/google.spanner.v1.Spanner/PartitionRead", 97 | "/google.spanner.v1.Spanner/Read", 98 | "/google.spanner.v1.Spanner/Rollback", 99 | "/google.spanner.v1.Spanner/StreamingRead", 100 | }, 101 | Affinity: &gpb.AffinityConfig{ 102 | Command: gpb.AffinityConfig_BOUND, 103 | AffinityKey: "session", 104 | }, 105 | }, 106 | }, 107 | } 108 | ) 109 | 110 | // ConnPool wrapper for gRPC-GCP channel pool. gtransport.ConnPool is the 111 | // interface Spanner client accepts as a replacement channel pool. 112 | type grpcGcpConnPool struct { 113 | gtransport.ConnPool 114 | 115 | cc *grpc.ClientConn 116 | size int 117 | } 118 | 119 | func (cp *grpcGcpConnPool) Conn() *grpc.ClientConn { 120 | return cp.cc 121 | } 122 | 123 | // Spanner client uses this function to get channel pool size. 124 | func (cp *grpcGcpConnPool) Num() int { 125 | return cp.size 126 | } 127 | 128 | func (cp *grpcGcpConnPool) Close() error { 129 | return cp.cc.Close() 130 | } 131 | 132 | func databaseURI() string { 133 | return fmt.Sprintf("projects/%s/instances/%s/databases/%s", *project, *instance_name, *database_name) 134 | } 135 | 136 | func spannerClient() (*spanner.Client, error) { 137 | var perRPC credentials.PerRPCCredentials 138 | var err error 139 | keyFile := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 140 | if keyFile == "" { 141 | perRPC, err = oauth.NewApplicationDefault(context.Background(), scope) 142 | } else { 143 | perRPC, err = oauth.NewServiceAccountFromFile(keyFile, scope) 144 | } 145 | 146 | // Converting gRPC-GCP config to JSON because grpc.Dial only accepts JSON 147 | // config for configuring load balancers. 148 | grpcGcpJsonConfig, err := protojson.Marshal(grpcGcpConfig) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | conn, err := grpc.Dial( 154 | // Spanner endpoint. Replace this with your custom endpoint if not using 155 | // the default endpoint. 156 | endpoint, 157 | // Application default or service account credentials set up above. 158 | grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), 159 | grpc.WithPerRPCCredentials(perRPC), 160 | // Do not look up load balancer (gRPC-GCP) config via DNS. 161 | grpc.WithDisableServiceConfig(), 162 | // Instead use this static config. 163 | grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":%s}]}`, grpcgcp.Name, string(grpcGcpJsonConfig))), 164 | // gRPC-GCP interceptors required for proper operation of gRPC-GCP 165 | // channel pool. Add your interceptors as next arguments if needed. 166 | grpc.WithChainUnaryInterceptor(grpcgcp.GCPUnaryClientInterceptor), 167 | grpc.WithChainStreamInterceptor(grpcgcp.GCPStreamClientInterceptor), 168 | // Add more DialOption options if needed but make sure not to overwrite 169 | // the interceptors above. 170 | ) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | pool := &grpcGcpConnPool{ 176 | cc: conn, 177 | // Set the pool size on ConnPool to communicate it to Spanner client. 178 | size: poolSize, 179 | } 180 | 181 | // Create spanner client. gtransport.WithConnPool is one of the available 182 | // Spanner client options. Keep other Spanner-related options here if any 183 | // and move channel pool and dial option to the grpc.Dial above. 184 | return spanner.NewClient(context.Background(), databaseURI(), gtransport.WithConnPool(pool)) 185 | } 186 | 187 | func main() { 188 | flag.Parse() 189 | client, err := spannerClient() 190 | if err != nil { 191 | log.Fatalf("Could not create Spanner client: %v", err) 192 | } 193 | 194 | stmt := spanner.Statement{ 195 | SQL: `select 1`, 196 | } 197 | iter := client.Single().Query(context.Background(), stmt) 198 | defer iter.Stop() 199 | row, err := iter.Next() 200 | if err != nil { 201 | log.Fatalf("Could not execute query: %v", err) 202 | } 203 | 204 | var num int64 205 | if err := row.Columns(&num); err != nil { 206 | log.Fatalf("Could not parse result: %v", err) 207 | } 208 | 209 | // Should print "Returned: 1". 210 | log.Printf("Returned: %v\n", num) 211 | } 212 | -------------------------------------------------------------------------------- /firestore/examples/end2end/doc/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/grpc-gcp-go/6a9ee9d2228727861a4e351c90cce9f33262479d/firestore/examples/end2end/doc/.gitignore -------------------------------------------------------------------------------- /firestore/examples/end2end/src/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/grpc-gcp-go/6a9ee9d2228727861a4e351c90cce9f33262479d/firestore/examples/end2end/src/.gitignore -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/batchgetdocuments.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | "io" 8 | "userutil" 9 | 10 | firestore "go-genproto/googleapis/firestore/v1beta1" 11 | ) 12 | 13 | //BatchGetDocuments ... Retrieve all documents from database 14 | func BatchGetDocuments() { 15 | fmt.Println("\n:: Batch Retreive Documents ::\n") 16 | 17 | client, conn := fsutils.MakeFSClient() 18 | 19 | defer conn.Close() 20 | 21 | docList := []string{} 22 | 23 | for { 24 | fmt.Print("Enter Document Id (blank when finished): ") 25 | inp := userutil.ReadFromConsole() 26 | if inp != "" { 27 | docList = append(docList, "projects/firestoretestclient/databases/(default)/documents/GrpcTestData/"+inp) 28 | } else { 29 | break 30 | } 31 | } 32 | 33 | batchGetDocsReq := firestore.BatchGetDocumentsRequest{ 34 | Database: "projects/firestoretestclient/databases/(default)", 35 | Documents: docList, 36 | } 37 | 38 | stream, err := client.BatchGetDocuments(context.Background(), &batchGetDocsReq) 39 | 40 | if err != nil { 41 | fmt.Println(err) 42 | return 43 | } 44 | for { 45 | resp, err := stream.Recv() 46 | if err == io.EOF { 47 | break 48 | } 49 | if err != nil { 50 | fmt.Println(err) 51 | } 52 | userutil.DrawDocument(*resp.GetFound()) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/begintransaction.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "environment" 6 | "fmt" 7 | "fsutils" 8 | firestore "go-genproto/googleapis/firestore/v1beta1" 9 | ) 10 | 11 | //BeginTransaction ... Use BeginTransaction API 12 | func BeginTransaction() { 13 | fmt.Println("\n:: Starting New Transaction ::\n") 14 | 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | options := firestore.TransactionOptions{} 20 | beginTransactionReq := firestore.BeginTransactionRequest{ 21 | Database: "projects/firestoretestclient/databases/(default)", 22 | Options: &options, 23 | } 24 | resp, err := client.BeginTransaction(context.Background(), &beginTransactionReq) 25 | 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | transactionId := resp.GetTransaction() 31 | fmt.Println("Successfully began new transaction", transactionId) 32 | environment.SetEnvironment(transactionId) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/commit.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "environment" 6 | "fmt" 7 | "fsutils" 8 | 9 | firestore "go-genproto/googleapis/firestore/v1beta1" 10 | ) 11 | 12 | //Commit ... Hitting Commit API 13 | func Commit() { 14 | fmt.Println("\n:: Hitting Commit API ::\n") 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | env := environment.GetEnvironment() 20 | 21 | if env == nil { 22 | fmt.Println("\nNo transaction to commit, returning...") 23 | return 24 | } 25 | 26 | transactionId := env.TransactionId 27 | 28 | commitReq := firestore.CommitRequest{ 29 | Database: "projects/firestoretestclient/databases/(default)", 30 | Transaction: transactionId, 31 | } 32 | resp, err := client.Commit(context.Background(), &commitReq) 33 | 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | environment.ClearEnvironment() 39 | fmt.Println("Successful commit! ", resp) 40 | } 41 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/createdocument.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //CreateDocument ... Create a New Document 12 | func CreateDocument() { 13 | fmt.Println("\n:: Creating a New Document ::\n") 14 | 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | fields := make(map[string]*firestore.Value) 20 | 21 | fmt.Print("Enter Document Name: ") 22 | docId := userutil.ReadFromConsole() 23 | 24 | for { 25 | fmt.Print("Enter Field Name (blank when finished): ") 26 | fieldName := userutil.ReadFromConsole() 27 | if fieldName != "" { 28 | fmt.Print("Enter Field Value: ") 29 | fieldValString := userutil.ReadFromConsole() 30 | fields[fieldName] = &firestore.Value{ 31 | ValueType: &firestore.Value_StringValue{ 32 | StringValue: fieldValString, 33 | }, 34 | } 35 | } else { 36 | break 37 | } 38 | } 39 | 40 | doc := firestore.Document{ 41 | Fields: fields, 42 | } 43 | 44 | createDocRequest := firestore.CreateDocumentRequest{ 45 | Parent: "projects/firestoretestclient/databases/(default)/documents", 46 | CollectionId: "GrpcTestData", 47 | DocumentId: docId, 48 | Document: &doc, 49 | } 50 | 51 | resp, err := client.CreateDocument(context.Background(), &createDocRequest) 52 | if err != nil { 53 | fmt.Println(err) 54 | return 55 | } 56 | 57 | fmt.Println("Successfully created document!") 58 | userutil.DrawDocument(*resp) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/createindex.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | admin "go-genproto/googleapis/firestore/admin/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //CreateIndex ... Create a New Index 12 | func CreateIndex() { 13 | fmt.Println("\n:: Creating a New Index ::\n") 14 | 15 | client, conn := fsutils.MakeFSAdminClient() 16 | 17 | defer conn.Close() 18 | 19 | indexFields := []*admin.IndexField{} 20 | 21 | for { 22 | fmt.Print("Enter Field Name (blank when finished): ") 23 | indFieldName := userutil.ReadFromConsole() 24 | if indFieldName != "" { 25 | fmt.Print("Enter Mode (*ASCENDING*/DESCENDING): ") 26 | indFieldMode := userutil.ReadFromConsole() 27 | 28 | indexFields = append(indexFields, &admin.IndexField{ 29 | FieldPath: indFieldName, 30 | Mode: chooseIndexMode(indFieldMode), 31 | }) 32 | 33 | } else { 34 | break 35 | } 36 | } 37 | 38 | newIndex := admin.Index{ 39 | CollectionId: "GrpcTestData", 40 | Fields: indexFields, 41 | } 42 | 43 | createIndexReq := admin.CreateIndexRequest{ 44 | Parent: "projects/firestoretestclient/databases/(default)", 45 | Index: &newIndex, 46 | } 47 | 48 | _, err := client.CreateIndex(context.Background(), &createIndexReq) 49 | 50 | if err != nil { 51 | fmt.Println(err) 52 | return 53 | } 54 | 55 | fmt.Println("Successfully created index! ") 56 | 57 | } 58 | 59 | func chooseIndexMode(indexMode string) admin.IndexField_Mode { 60 | if indexMode == "ASCENDING" || (indexMode != "ASCENDING" && indexMode != "DESCENDING") { 61 | return admin.IndexField_ASCENDING 62 | } 63 | return admin.IndexField_DESCENDING 64 | } 65 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/deletedocument.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //DeleteDocument ... Delete a document from database 12 | func DeleteDocument() { 13 | fmt.Println("\n:: Deleting a Document ::\n") 14 | client, conn := fsutils.MakeFSClient() 15 | 16 | defer conn.Close() 17 | 18 | fmt.Print("Enter Document Name: ") 19 | docName := "projects/firestoretestclient/databases/(default)/documents/GrpcTestData/" + userutil.ReadFromConsole() 20 | 21 | delDocRequest := firestore.DeleteDocumentRequest{ 22 | Name: docName, 23 | } 24 | 25 | _, err := client.DeleteDocument(context.Background(), &delDocRequest) 26 | 27 | if err != nil { 28 | fmt.Println(err) 29 | return 30 | } 31 | 32 | fmt.Println("Successfully Deleted ", docName) 33 | } 34 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/deleteindex.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | admin "go-genproto/googleapis/firestore/admin/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //DeleteIndex ... Delete an Index from the database 12 | func DeleteIndex() { 13 | fmt.Println("\n:: Deleting an Index ::\n") 14 | 15 | client, conn := fsutils.MakeFSAdminClient() 16 | 17 | defer conn.Close() 18 | 19 | fmt.Print("Enter Index Name: ") 20 | indexName := "projects/firestoretestclient/databases/(default)/indexes/" + userutil.ReadFromConsole() 21 | 22 | delIndexReq := admin.DeleteIndexRequest{ 23 | Name: indexName, 24 | } 25 | 26 | _, err := client.DeleteIndex(context.Background(), &delIndexReq) 27 | 28 | if err != nil { 29 | fmt.Println(err) 30 | return 31 | } 32 | 33 | fmt.Println("\nFinished deleting index!\n") 34 | 35 | } 36 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/getdocument.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //GetDocument ... Retrieve a specific Document 12 | func GetDocument() { 13 | fmt.Println("\n:: Getting A Document ::\n") 14 | 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | fmt.Print("Enter Document Name: ") 20 | docName := "projects/firestoretestclient/databases/(default)/documents/GrpcTestData/" + userutil.ReadFromConsole() 21 | 22 | getDocRequest := firestore.GetDocumentRequest{ 23 | Name: docName, 24 | } 25 | 26 | resp, err := client.GetDocument(context.Background(), &getDocRequest) 27 | 28 | if err != nil { 29 | fmt.Println(err) 30 | } 31 | 32 | userutil.DrawDocument(*resp) 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/getindex.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | admin "go-genproto/googleapis/firestore/admin/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //GetIndex ... Get a Specific Index 12 | func GetIndex() { 13 | fmt.Println("\n:: Getting a Specfic Index ::]\n") 14 | 15 | client, conn := fsutils.MakeFSAdminClient() 16 | 17 | defer conn.Close() 18 | 19 | fmt.Print("Enter Index Name: ") 20 | indexName := "projects/firestoretestclient/databases/(default)/indexes/" + userutil.ReadFromConsole() 21 | 22 | getIndexReq := admin.GetIndexRequest{ 23 | Name: indexName, 24 | } 25 | 26 | resp, err := client.GetIndex(context.Background(), &getIndexReq) 27 | 28 | if err != nil { 29 | fmt.Println(err) 30 | return 31 | } 32 | 33 | userutil.DrawIndex(*resp) 34 | 35 | fmt.Println("\nFinished getting index!\n") 36 | 37 | } 38 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/listcollectionids.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | ) 9 | 10 | //ListCollectionIds ... List all collection IDs from a database 11 | func ListCollectionIds() { 12 | fmt.Println("\n:: Listing All Collection Ids ::\n") 13 | 14 | client, conn := fsutils.MakeFSClient() 15 | 16 | defer conn.Close() 17 | 18 | listCollectionIdReq := firestore.ListCollectionIdsRequest{ 19 | Parent: "projects/firestoretestclient/databases/(default)", 20 | } 21 | 22 | resp, err := client.ListCollectionIds(context.Background(), &listCollectionIdReq) 23 | 24 | if err != nil { 25 | fmt.Println(err) 26 | return 27 | } 28 | 29 | for _, cid := range resp.CollectionIds { 30 | fmt.Println(cid) 31 | } 32 | 33 | return 34 | 35 | } 36 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/listdocuments.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //ListDocuments ... List all Documents from a Database 12 | func ListDocuments() { 13 | fmt.Println("\n:: Listing All Documents ::\n") 14 | 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | listDocsRequest := firestore.ListDocumentsRequest{ 20 | Parent: "projects/firestoretestclient/databases/(default)", 21 | } 22 | 23 | //bgd_client, err := client.BatchGetDocuments(context.Background(), &getDocsRequest, grpc.FailFast(true)) 24 | resp, err := client.ListDocuments(context.Background(), &listDocsRequest) 25 | if err != nil { 26 | fmt.Println(err) 27 | } 28 | for _, doc := range resp.Documents { 29 | userutil.DrawDocument(*doc) 30 | } 31 | 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/listindexes.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | admin "go-genproto/googleapis/firestore/admin/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //ListIndexes ... List All indexes in a Database 12 | func ListIndexes() { 13 | fmt.Println("\n:: Listing All Indexes ::\n") 14 | 15 | client, conn := fsutils.MakeFSAdminClient() 16 | 17 | defer conn.Close() 18 | 19 | listIndexesReq := admin.ListIndexesRequest{ 20 | Parent: "projects/firestoretestclient/databases/(default)", 21 | } 22 | 23 | resp, err := client.ListIndexes(context.Background(), &listIndexesReq) 24 | if err != nil { 25 | fmt.Println(err) 26 | return 27 | } 28 | 29 | for _, ind := range resp.Indexes { 30 | userutil.DrawIndex(*ind) 31 | } 32 | 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/rollback.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "environment" 6 | "fmt" 7 | "fsutils" 8 | firestore "go-genproto/googleapis/firestore/v1beta1" 9 | ) 10 | 11 | //Rollback ... Call the Rollback API 12 | func Rollback() { 13 | fmt.Println("\n:: Calling Rollback API ::\n") 14 | client, conn := fsutils.MakeFSClient() 15 | 16 | defer conn.Close() 17 | 18 | env := environment.GetEnvironment() 19 | 20 | if env == nil { 21 | fmt.Println("\nNo transaction to rollback, returning...") 22 | return 23 | } 24 | 25 | transactionId := env.TransactionId 26 | 27 | rollbackReq := firestore.RollbackRequest{ 28 | Database: "projects/firestoretestclient/databases/(default)", 29 | Transaction: transactionId, 30 | } 31 | resp, err := client.Rollback(context.Background(), &rollbackReq) 32 | 33 | if err != nil { 34 | fmt.Println(err) 35 | return 36 | } 37 | environment.ClearEnvironment() 38 | fmt.Println("Successful rollback! ", resp) 39 | } 40 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/runquery.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "io" 9 | "userutil" 10 | ) 11 | 12 | //RunQuery ... Run a Structured Query 13 | func RunQuery() { 14 | fmt.Println("\n:: Running a Query ::\n") 15 | 16 | client, conn := fsutils.MakeFSClient() 17 | 18 | defer conn.Close() 19 | 20 | fmt.Print("Enter field to query: ") 21 | fieldPath := userutil.ReadFromConsole() 22 | 23 | runQueryQuery := firestore.StructuredQuery{ 24 | Select: &firestore.StructuredQuery_Projection{ 25 | Fields: []*firestore.StructuredQuery_FieldReference{ 26 | &firestore.StructuredQuery_FieldReference{ 27 | FieldPath: fieldPath, 28 | }, 29 | }, 30 | }, 31 | } 32 | 33 | runStructQueryReq := firestore.RunQueryRequest_StructuredQuery{ 34 | StructuredQuery: &runQueryQuery, 35 | } 36 | 37 | runQueryReq := firestore.RunQueryRequest{ 38 | Parent: "projects/firestoretestclient/databases/(default)/documents", 39 | QueryType: &runStructQueryReq, 40 | } 41 | 42 | call, err := client.RunQuery(context.Background(), &runQueryReq) 43 | 44 | if err != nil { 45 | fmt.Println("Error getting QueryClient: ", err) 46 | } 47 | 48 | for { 49 | results, err := call.Recv() 50 | if err == io.EOF { 51 | break 52 | } 53 | if err != nil { 54 | fmt.Println("Error received from QueryClient: ", err) 55 | } 56 | userutil.DrawDocument(*results.Document) 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/updatedocument.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //UpdateDocument ... Update a Document in the Database 12 | func UpdateDocument() { 13 | fmt.Println("\n:: Updating a Document ::\n") 14 | 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | fields := make(map[string]*firestore.Value) 20 | fieldPaths := []string{} 21 | 22 | fmt.Print("Enter Document Name: ") 23 | docId := userutil.ReadFromConsole() 24 | 25 | docName := "projects/firestoretestclient/databases/(default)/documents/GrpcTestData/" + docId 26 | 27 | getDocRequest := firestore.GetDocumentRequest{ 28 | Name: docName, 29 | } 30 | 31 | doc, err := client.GetDocument(context.Background(), &getDocRequest) 32 | 33 | if err != nil { 34 | fmt.Println(err) 35 | return 36 | } 37 | 38 | for { 39 | fmt.Print("Enter Field Name (blank when finished): ") 40 | fieldName := userutil.ReadFromConsole() 41 | if fieldName != "" { 42 | fieldPaths = append(fieldPaths, fieldName) 43 | fmt.Print("Enter Field Value: ") 44 | fieldValString := userutil.ReadFromConsole() 45 | fields[fieldName] = &firestore.Value{ 46 | ValueType: &firestore.Value_StringValue{ 47 | StringValue: fieldValString, 48 | }, 49 | } 50 | } else { 51 | break 52 | } 53 | } 54 | 55 | doc.Fields = fields 56 | 57 | docMask := firestore.DocumentMask{ 58 | FieldPaths: fieldPaths, 59 | } 60 | 61 | updateDocRequest := firestore.UpdateDocumentRequest{ 62 | Document: doc, 63 | UpdateMask: &docMask, 64 | } 65 | 66 | resp, err := client.UpdateDocument(context.Background(), &updateDocRequest) 67 | 68 | if err != nil { 69 | fmt.Println(err) 70 | return 71 | } 72 | 73 | fmt.Println("Successfully updated document!") 74 | 75 | userutil.DrawDocument(*resp) 76 | 77 | } 78 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/apimethods/write.go: -------------------------------------------------------------------------------- 1 | package apimethods 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "fsutils" 7 | firestore "go-genproto/googleapis/firestore/v1beta1" 8 | "userutil" 9 | ) 10 | 11 | //Write ... Stream writes to a document 12 | func Write() { 13 | fmt.Println("\n:: Streaming Writes to a Document ::\n") 14 | 15 | client, conn := fsutils.MakeFSClient() 16 | 17 | defer conn.Close() 18 | 19 | // Get initial stream ID and stream token 20 | 21 | writeReq := firestore.WriteRequest{ 22 | Database: "projects/firestoretestclient/databases/(default)", 23 | } 24 | writeClient, err := client.Write(context.Background()) 25 | 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | 31 | err = writeClient.Send(&writeReq) 32 | 33 | if err != nil { 34 | fmt.Println(err) 35 | return 36 | } 37 | 38 | writeResp, err := writeClient.Recv() 39 | 40 | streamId := writeResp.GetStreamId() 41 | streamToken := writeResp.GetStreamToken() 42 | fieldPaths := []string{} 43 | fields := make(map[string]*firestore.Value) 44 | 45 | fmt.Print("Enter Document Name: ") 46 | docId := userutil.ReadFromConsole() 47 | 48 | docName := "projects/firestoretestclient/databases/(default)/documents/GrpcTestData/" + docId 49 | 50 | getDocRequest := firestore.GetDocumentRequest{ 51 | Name: docName, 52 | } 53 | 54 | doc, err := client.GetDocument(context.Background(), &getDocRequest) 55 | 56 | fmt.Println("\n.... Streaming writes to " + docId + "\n") 57 | 58 | for { 59 | fmt.Print("Enter Field Name (blank when finished): ") 60 | fieldName := userutil.ReadFromConsole() 61 | 62 | if fieldName != "" { 63 | fieldPaths := append(fieldPaths, fieldName) 64 | fmt.Print("Enter Field Value: ") 65 | fieldValString := userutil.ReadFromConsole() 66 | fields[fieldName] = &firestore.Value{ 67 | ValueType: &firestore.Value_StringValue{ 68 | StringValue: fieldValString, 69 | }, 70 | } 71 | doc.Fields = fields 72 | docMask := firestore.DocumentMask{ 73 | FieldPaths: fieldPaths, 74 | } 75 | fsWrites := []*firestore.Write{} 76 | fsWrites = append(fsWrites, &firestore.Write{ 77 | Operation: &firestore.Write_Update{ 78 | Update: doc, 79 | }, 80 | UpdateMask: &docMask, 81 | }) 82 | writeReq = firestore.WriteRequest{ 83 | Database: "projects/firestoretestclient/databases/(default)", 84 | Writes: fsWrites, //Write 85 | StreamId: streamId, 86 | StreamToken: streamToken, 87 | } 88 | 89 | err = writeClient.Send(&writeReq) 90 | if err != nil { 91 | fmt.Println(err) 92 | return 93 | } 94 | writeResp, err = writeClient.Recv() 95 | if err != nil { 96 | fmt.Println(err) 97 | return 98 | } 99 | streamToken = writeResp.GetStreamToken() 100 | 101 | } else { 102 | break 103 | } 104 | } //for 105 | 106 | err = writeClient.CloseSend() 107 | 108 | if err != nil { 109 | fmt.Println(err) 110 | return 111 | } 112 | 113 | fmt.Println("\nFinished writing stream!") 114 | 115 | } 116 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/environment/environment.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | //Env ... Current environment 4 | // Contents: 5 | // transactionId - track transaction ids for commits and rollbacks 6 | // 7 | type Env struct { 8 | TransactionId []byte 9 | } 10 | 11 | var ( 12 | env *Env 13 | ) 14 | 15 | //SetEnvironment ... Update current environment 16 | func SetEnvironment(transactionId []byte) *Env { 17 | 18 | env = &Env{ 19 | TransactionId: transactionId, 20 | } 21 | return env 22 | } 23 | 24 | //GetEnvironment ... Return current environment 25 | func GetEnvironment() *Env { 26 | if env != nil { 27 | return env 28 | } 29 | return nil 30 | } 31 | 32 | //ClearEnvironment ... Clean up environment 33 | func ClearEnvironment() *Env { 34 | env = nil 35 | return env 36 | } 37 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/fsutils/getfsclient.go: -------------------------------------------------------------------------------- 1 | package fsutils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials" 9 | "google.golang.org/grpc/credentials/oauth" 10 | 11 | admin "go-genproto/googleapis/firestore/admin/v1beta1" 12 | firestore "go-genproto/googleapis/firestore/v1beta1" 13 | ) 14 | 15 | // MakeFSClient ... Create a new Firestore Client using JWT credentials 16 | func MakeFSClient() (firestore.FirestoreClient, grpc.ClientConn) { 17 | 18 | keyFile := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 19 | jwtCreds, err := oauth.NewServiceAccountFromFile(keyFile, "https://www.googleapis.com/auth/datastore") 20 | address := "firestore.googleapis.com:443" 21 | 22 | if err != nil { 23 | log.Fatalf("Failed to create JWT credentials: %v", err) 24 | } 25 | conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), grpc.WithPerRPCCredentials(jwtCreds)) 26 | if err != nil { 27 | log.Fatalf("did not connect: %v", err) 28 | } 29 | 30 | client := firestore.NewFirestoreClient(conn) 31 | 32 | return client, *conn 33 | 34 | } 35 | 36 | // MakeFSAdminClient ... Create a new Firestore Admin Client using JWT credentials 37 | func MakeFSAdminClient() (admin.FirestoreAdminClient, grpc.ClientConn) { 38 | 39 | keyFile := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 40 | jwtCreds, err := oauth.NewServiceAccountFromFile(keyFile, "https://www.googleapis.com/auth/datastore") 41 | address := "firestore.googleapis.com:443" 42 | 43 | if err != nil { 44 | log.Fatalf("Failed to create JWT credentials: %v", err) 45 | } 46 | conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), grpc.WithPerRPCCredentials(jwtCreds)) 47 | if err != nil { 48 | log.Fatalf("did not connect: %v", err) 49 | } 50 | 51 | client := admin.NewFirestoreAdminClient(conn) 52 | 53 | return client, *conn 54 | 55 | } 56 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/gfx/choosefirestoremethod.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "apimethods" 5 | "fmt" 6 | ) 7 | 8 | //ChooseAPIMethod ... Pick a method call based on menu entry 9 | func ChooseAPIMethod(api string) { 10 | 11 | switch api { 12 | 13 | case "batchgetdocuments": 14 | fallthrough 15 | case "1": 16 | apimethods.BatchGetDocuments() 17 | 18 | case "begintransaction": 19 | fallthrough 20 | case "2": 21 | apimethods.BeginTransaction() 22 | 23 | case "commit": 24 | fallthrough 25 | case "3": 26 | apimethods.Commit() 27 | 28 | case "createdocument": 29 | fallthrough 30 | case "4": 31 | apimethods.CreateDocument() 32 | 33 | case "deletedocument": 34 | fallthrough 35 | case "5": 36 | apimethods.DeleteDocument() 37 | 38 | case "getdocument": 39 | fallthrough 40 | case "6": 41 | apimethods.GetDocument() 42 | 43 | case "listcollectionids": 44 | fallthrough 45 | case "7": 46 | apimethods.ListCollectionIds() 47 | 48 | case "listdocuments": 49 | fallthrough 50 | case "8": 51 | apimethods.ListDocuments() 52 | 53 | case "rollback": 54 | fallthrough 55 | case "9": 56 | apimethods.Rollback() 57 | 58 | case "runquery": 59 | fallthrough 60 | case "10": 61 | apimethods.RunQuery() 62 | 63 | case "updatedocument": 64 | fallthrough 65 | case "11": 66 | apimethods.UpdateDocument() 67 | 68 | case "write": 69 | fallthrough 70 | case "12": 71 | apimethods.Write() 72 | 73 | case "createindex": 74 | fallthrough 75 | case "13": 76 | apimethods.CreateIndex() 77 | 78 | case "deleteindex": 79 | fallthrough 80 | case "14": 81 | apimethods.DeleteIndex() 82 | 83 | case "getindex": 84 | fallthrough 85 | case "15": 86 | apimethods.GetIndex() 87 | 88 | case "listindexes": 89 | fallthrough 90 | case "16": 91 | apimethods.ListIndexes() 92 | 93 | default: 94 | fmt.Println("Unknown option '", api, "'") 95 | 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/gfx/drawmenu.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "userutil" 7 | ) 8 | 9 | // DrawMenu ... Draw the system menu 10 | func DrawMenu() { 11 | 12 | fmt.Println("\n Google Firestore RPC Menu\n") 13 | fmt.Println("1|batchgetdocuments ......... BatchGetDocuments") 14 | fmt.Println("2|begintransaction ......... BeginTransaction") 15 | fmt.Println("3|commit .................... Commit") 16 | fmt.Println("4|createdocument ............ CreateDocument") 17 | fmt.Println("5|deletedocument ............ DeleteDocument") 18 | fmt.Println("6|getdocument ............... GetDocument") 19 | fmt.Println("7|listcollectionids ......... ListCollectionIds") 20 | fmt.Println("8|listdocuments ............. ListDocuments") 21 | fmt.Println("9|rollback .................. Rollback") 22 | fmt.Println("10|runquery ................. RunQuery") 23 | fmt.Println("11|updatedocument ........... UpdateDocument") 24 | fmt.Println("12|write .................... Write") 25 | fmt.Println("\n Firestore Admin RPC's \n") 26 | fmt.Println("13|createindex .............. CreateIndex") 27 | fmt.Println("14|deleteindex .............. DeleteIndex") 28 | fmt.Println("15|getindex ................. GetIndex") 29 | fmt.Println("16|listindexes .............. ListIndex") 30 | fmt.Print("\n\nEnter an option ('quit' to exit): ") 31 | 32 | text := userutil.ReadFromConsole() 33 | 34 | if text == "quit" { 35 | os.Exit(0) 36 | } else { 37 | ChooseAPIMethod(text) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"apimethods" 5 | "gfx" 6 | ) 7 | 8 | var ( 9 | transactionId []byte 10 | ) 11 | 12 | func main() { 13 | 14 | for { 15 | gfx.DrawMenu() 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/userutil/drawdocument.go: -------------------------------------------------------------------------------- 1 | package userutil 2 | 3 | import ( 4 | "fmt" 5 | 6 | firestore "go-genproto/googleapis/firestore/v1beta1" 7 | ) 8 | 9 | //DrawDocument ... Draw Document Name and Fields 10 | func DrawDocument(doc firestore.Document) { 11 | 12 | fmt.Println("\n\nDocument Name:", doc.Name) 13 | fmt.Println(" Fields: ") 14 | for field, value := range doc.Fields { 15 | fmt.Printf(" %v : %v\n", field, value.GetStringValue()) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/userutil/drawindex.go: -------------------------------------------------------------------------------- 1 | package userutil 2 | 3 | import ( 4 | "fmt" 5 | admin "go-genproto/googleapis/firestore/admin/v1beta1" 6 | ) 7 | 8 | //DrawIndex ... Draw a single index instance 9 | func DrawIndex(ind admin.Index) { 10 | 11 | fmt.Println("Index Name: " + ind.Name + "\nIndex State: " + ind.State.String()) 12 | for _, indFields := range ind.Fields { 13 | fmt.Println(" Field: " + indFields.FieldPath + " Mode: " + indFields.Mode.String()) 14 | 15 | } 16 | fmt.Print("\n") 17 | 18 | } 19 | -------------------------------------------------------------------------------- /firestore/examples/end2end/src/userutil/readfromconsole.go: -------------------------------------------------------------------------------- 1 | package userutil 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // ReadFromConsole ... Read user input from console 11 | func ReadFromConsole() string { 12 | reader := bufio.NewReader(os.Stdin) 13 | text, err := reader.ReadString('\n') 14 | text = strings.TrimSpace(text) 15 | 16 | if err != nil { 17 | fmt.Println(err) 18 | return "" 19 | } 20 | return text 21 | 22 | } 23 | -------------------------------------------------------------------------------- /firestore/go.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/grpc-gcp-go/6a9ee9d2228727861a4e351c90cce9f33262479d/firestore/go.mod -------------------------------------------------------------------------------- /grpcgcp/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## How to test Spanner integration 3 | 4 | 1. Set GCP project id with GCP_PROJECT_ID environment variable. 5 | 6 | export GCP_PROJECT_ID=test-project 7 | 8 | 1. Set service key credentials file using GOOGLE_APPLICATION_CREDENTIALS env variable. 9 | 10 | export GOOGLE_APPLICATION_CREDENTIALS=/service/account/credentials.json 11 | 12 | 1. Run the tests. 13 | 14 | go test -v 15 | 16 | To skip Spanner setup run 17 | 18 | SKIP_SPANNER=true go test -v 19 | -------------------------------------------------------------------------------- /grpcgcp/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2019 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | /* 20 | Package grpcgcp provides grpc supports for Google Cloud APIs. 21 | For now it provides connection management with affinity support. 22 | 23 | Note: "channel" is analagous to "connection" in our context. 24 | 25 | Usage: 26 | 27 | 1. First, initialize the api configuration. There are two ways: 28 | 29 | 1a. Create a json file defining the configuration and read it. 30 | 31 | // Create some_api_config.json 32 | { 33 | "channelPool": { 34 | "maxSize": 4, 35 | "maxConcurrentStreamsLowWatermark": 50 36 | }, 37 | "method": [ 38 | { 39 | "name": [ "/some.api.v1/Method1" ], 40 | "affinity": { 41 | "command": "BIND", 42 | "affinityKey": "key1" 43 | } 44 | }, 45 | { 46 | "name": [ "/some.api.v1/Method2" ], 47 | "affinity": { 48 | "command": "BOUND", 49 | "affinityKey": "key2" 50 | } 51 | }, 52 | { 53 | "name": [ "/some.api.v1/Method3" ], 54 | "affinity": { 55 | "command": "UNBIND", 56 | "affinityKey": "key3" 57 | } 58 | } 59 | ] 60 | } 61 | 62 | jsonFile, err := ioutil.ReadFile("some_api_config.json") 63 | if err != nil { 64 | t.Fatalf("Failed to read config file: %v", err) 65 | } 66 | jsonCfg := string(jsonFile) 67 | 68 | 1b. Create apiConfig directly and convert it to json. 69 | 70 | // import ( 71 | // configpb "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/grpc_gcp" 72 | // ) 73 | 74 | apiConfig := &configpb.ApiConfig{ 75 | ChannelPool: &configpb.ChannelPoolConfig{ 76 | MaxSize: 4, 77 | MaxConcurrentStreamsLowWatermark: 50, 78 | }, 79 | Method: []*configpb.MethodConfig{ 80 | &configpb.MethodConfig{ 81 | Name: []string{"/some.api.v1/Method1"}, 82 | Affinity: &configpb.AffinityConfig{ 83 | Command: configpb.AffinityConfig_BIND, 84 | AffinityKey: "key1", 85 | }, 86 | }, 87 | &configpb.MethodConfig{ 88 | Name: []string{"/some.api.v1/Method2"}, 89 | Affinity: &configpb.AffinityConfig{ 90 | Command: configpb.AffinityConfig_BOUND, 91 | AffinityKey: "key2", 92 | }, 93 | }, 94 | &configpb.MethodConfig{ 95 | Name: []string{"/some.api.v1/Method3"}, 96 | Affinity: &configpb.AffinityConfig{ 97 | Command: configpb.AffinityConfig_UNBIND, 98 | AffinityKey: "key3", 99 | }, 100 | }, 101 | }, 102 | } 103 | 104 | c, err := protojson.Marshal(apiConfig) 105 | if err != nil { 106 | t.Fatalf("cannot json encode config: %v", err) 107 | } 108 | jsonCfg := string(c) 109 | 110 | 2. Make ClientConn with specific DialOptions to enable grpc_gcp load balancer 111 | with provided configuration. And specify gRPC-GCP interceptors. 112 | 113 | conn, err := grpc.Dial( 114 | target, 115 | // Register and specify the grpc-gcp load balancer. 116 | grpc.WithDisableServiceConfig(), 117 | grpc.WithDefaultServiceConfig( 118 | fmt.Sprintf( 119 | `{"loadBalancingConfig": [{"%s":%s}]}`, 120 | grpcgcp.Name, 121 | jsonCfg, 122 | ), 123 | ), 124 | // Set grpcgcp interceptors. 125 | grpc.WithUnaryInterceptor(grpcgcp.GCPUnaryClientInterceptor), 126 | grpc.WithStreamInterceptor(grpcgcp.GCPStreamClientInterceptor), 127 | ) 128 | */ 129 | package grpcgcp // import "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp" 130 | -------------------------------------------------------------------------------- /grpcgcp/gcp_interceptor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2019 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package grpcgcp 20 | 21 | import ( 22 | "context" 23 | "sync" 24 | 25 | "google.golang.org/grpc" 26 | ) 27 | 28 | type key int 29 | 30 | var gcpKey key 31 | 32 | type gcpContext struct { 33 | // request message used for pre-process of an affinity call 34 | reqMsg interface{} 35 | // response message used for post-process of an affinity call 36 | replyMsg interface{} 37 | } 38 | 39 | // GCPUnaryClientInterceptor intercepts the execution of a unary RPC 40 | // and injects necessary information to be used by the picker. 41 | func GCPUnaryClientInterceptor( 42 | ctx context.Context, 43 | method string, 44 | req interface{}, 45 | reply interface{}, 46 | cc *grpc.ClientConn, 47 | invoker grpc.UnaryInvoker, 48 | opts ...grpc.CallOption, 49 | ) error { 50 | gcpCtx := &gcpContext{ 51 | reqMsg: req, 52 | replyMsg: reply, 53 | } 54 | ctx = context.WithValue(ctx, gcpKey, gcpCtx) 55 | 56 | return invoker(ctx, method, req, reply, cc, opts...) 57 | } 58 | 59 | // GCPStreamClientInterceptor intercepts the execution of a client streaming RPC 60 | // and injects necessary information to be used by the picker. 61 | func GCPStreamClientInterceptor( 62 | ctx context.Context, 63 | desc *grpc.StreamDesc, 64 | cc *grpc.ClientConn, 65 | method string, 66 | streamer grpc.Streamer, 67 | opts ...grpc.CallOption, 68 | ) (grpc.ClientStream, error) { 69 | // This constructor does not create a real ClientStream, 70 | // it only stores all parameters and let SendMsg() to create ClientStream. 71 | cs := &gcpClientStream{ 72 | ctx: ctx, 73 | desc: desc, 74 | cc: cc, 75 | method: method, 76 | streamer: streamer, 77 | opts: opts, 78 | } 79 | cs.cond = sync.NewCond(cs) 80 | return cs, nil 81 | } 82 | 83 | type gcpClientStream struct { 84 | sync.Mutex 85 | grpc.ClientStream 86 | 87 | cond *sync.Cond 88 | initStreamErr error 89 | 90 | ctx context.Context 91 | desc *grpc.StreamDesc 92 | cc *grpc.ClientConn 93 | method string 94 | streamer grpc.Streamer 95 | opts []grpc.CallOption 96 | } 97 | 98 | func (cs *gcpClientStream) SendMsg(m interface{}) error { 99 | cs.Lock() 100 | // Initialize underlying ClientStream when getting the first request. 101 | if cs.ClientStream == nil { 102 | ctx := context.WithValue(cs.ctx, gcpKey, &gcpContext{reqMsg: m}) 103 | realCS, err := cs.streamer(ctx, cs.desc, cs.cc, cs.method, cs.opts...) 104 | if err != nil { 105 | cs.initStreamErr = err 106 | cs.Unlock() 107 | cs.cond.Broadcast() 108 | return err 109 | } 110 | cs.ClientStream = realCS 111 | } 112 | cs.Unlock() 113 | cs.cond.Broadcast() 114 | return cs.ClientStream.SendMsg(m) 115 | } 116 | 117 | func (cs *gcpClientStream) RecvMsg(m interface{}) error { 118 | // If RecvMsg is called before SendMsg, it should wait until cs.ClientStream 119 | // is initialized or the initialization failed. 120 | cs.Lock() 121 | for cs.initStreamErr == nil && cs.ClientStream == nil { 122 | cs.cond.Wait() 123 | } 124 | if cs.initStreamErr != nil { 125 | cs.Unlock() 126 | return cs.initStreamErr 127 | } 128 | cs.Unlock() 129 | return cs.ClientStream.RecvMsg(m) 130 | } 131 | -------------------------------------------------------------------------------- /grpcgcp/gcp_interceptor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2019 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package grpcgcp 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "sync" 25 | "testing" 26 | "time" 27 | 28 | "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/mocks" 29 | "github.com/golang/mock/gomock" 30 | "github.com/google/go-cmp/cmp" 31 | "google.golang.org/grpc" 32 | ) 33 | 34 | func TestGCPUnaryClientInterceptor(t *testing.T) { 35 | ctx := context.TODO() 36 | wantMethod := "someMethod" 37 | wantReq := "requestMessage" 38 | wantRepl := "replyMessage" 39 | wantGCPCtx := &gcpContext{ 40 | reqMsg: wantReq, 41 | replyMsg: wantRepl, 42 | } 43 | wantCC := &grpc.ClientConn{} 44 | wantOpts := []grpc.CallOption{grpc.CallContentSubtype("someSubtype"), grpc.MaxCallRecvMsgSize(42)} 45 | 46 | invCalled := false 47 | var gotCtx context.Context 48 | gotMethod := "" 49 | var gotReq, gotRepl interface{} 50 | var gotCC *grpc.ClientConn 51 | gotOpts := []grpc.CallOption{} 52 | inv := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { 53 | invCalled = true 54 | gotCtx = ctx 55 | gotMethod = method 56 | gotReq = req 57 | gotRepl = reply 58 | gotCC = cc 59 | gotOpts = opts 60 | return nil 61 | } 62 | 63 | if err := GCPUnaryClientInterceptor(ctx, wantMethod, wantReq, wantRepl, wantCC, inv, wantOpts...); err != nil { 64 | t.Fatalf("GCPUnaryClientInterceptor(...) returned error: %v, want: nil", err) 65 | } 66 | if !invCalled { 67 | t.Fatalf("provided grpc.UnaryInvoker function was not called") 68 | } 69 | gotGCPCtx, hasGCPCtx := gotCtx.Value(gcpKey).(*gcpContext) 70 | if !hasGCPCtx { 71 | t.Errorf("provided grpc.UnaryInvoker function was called with context without gcpContext") 72 | } else if diff := cmp.Diff(wantGCPCtx, gotGCPCtx, cmp.AllowUnexported(gcpContext{})); diff != "" { 73 | t.Errorf("provided grpc.UnaryInvoker function was called with unexpected gcpContext (-want, +got):\n%s", diff) 74 | } 75 | if gotMethod != wantMethod { 76 | t.Errorf("provided grpc.UnaryInvoker function was called with unexpected method: %s, want: %s", gotMethod, wantMethod) 77 | } 78 | if gotReq != wantReq { 79 | t.Errorf("provided grpc.UnaryInvoker function was called with unexpected request: %s, want: %s", gotReq, wantReq) 80 | } 81 | if gotRepl != wantRepl { 82 | t.Errorf("provided grpc.UnaryInvoker function was called with unexpected response: %s, want: %s", gotRepl, wantRepl) 83 | } 84 | if gotCC != wantCC { 85 | t.Errorf("provided grpc.UnaryInvoker function was called with unexpected ClientConn: %v, want: %v", gotCC, wantCC) 86 | } 87 | if diff := cmp.Diff(wantOpts, gotOpts); diff != "" { 88 | t.Errorf("provided grpc.UnaryInvoker function was called with unexpected options (-want, +got):\n%s", diff) 89 | } 90 | } 91 | 92 | type strictMatcher struct { 93 | gomock.Matcher 94 | 95 | matchWith interface{} 96 | } 97 | 98 | func (sm *strictMatcher) Matches(x interface{}) bool { 99 | return sm.matchWith == x 100 | } 101 | 102 | func (sm *strictMatcher) String() string { 103 | return fmt.Sprintf("at address %p: %v", &sm.matchWith, sm.matchWith) 104 | } 105 | 106 | type fakeResp struct{} 107 | 108 | func TestGCPStreamClientInterceptor(t *testing.T) { 109 | mockCtrl := gomock.NewController(t) 110 | defer mockCtrl.Finish() 111 | 112 | ctx := context.TODO() 113 | wantMethod := "someMethod" 114 | wantReq := "someRequest" 115 | wantRes := &fakeResp{} 116 | wantGCPCtx := &gcpContext{ 117 | reqMsg: wantReq, 118 | } 119 | wantSD := &grpc.StreamDesc{} 120 | wantCC := &grpc.ClientConn{} 121 | wantOpts := []grpc.CallOption{grpc.CallContentSubtype("someSubtype"), grpc.MaxCallRecvMsgSize(42)} 122 | 123 | streamerCalled := false 124 | streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { 125 | streamerCalled = true 126 | gotGCPCtx, hasGCPCtx := ctx.Value(gcpKey).(*gcpContext) 127 | if !hasGCPCtx { 128 | t.Errorf("grpc.Streamer called with context without gcpContext") 129 | } else if diff := cmp.Diff(wantGCPCtx, gotGCPCtx, cmp.AllowUnexported(gcpContext{})); diff != "" { 130 | t.Errorf("grpc.Streamer called with unexpected gcpContext (-want, +got):\n%s", diff) 131 | } 132 | if desc != wantSD { 133 | t.Errorf("grpc.Streamer called with unexpected StreamDesc: %v, want: %v", desc, wantSD) 134 | } 135 | if cc != wantCC { 136 | t.Errorf("grpc.Streamer called with unexpected ClientConn: %v, want: %v", cc, wantCC) 137 | } 138 | if method != wantMethod { 139 | t.Errorf("grpc.Streamer called with unexpected method: %v, want: %v", method, wantMethod) 140 | } 141 | if diff := cmp.Diff(wantOpts, opts); diff != "" { 142 | t.Errorf("grpc.Streamer called with unexpected options (-want, +got):\n%s", diff) 143 | } 144 | mockCS := mocks.NewMockClientStream(mockCtrl) 145 | mockCS.EXPECT().SendMsg(gomock.Eq(wantReq)).Times(1) 146 | mockCS.EXPECT().RecvMsg(&strictMatcher{matchWith: wantRes}).Times(1) 147 | return mockCS, nil 148 | } 149 | cs, err := GCPStreamClientInterceptor( 150 | ctx, 151 | wantSD, 152 | wantCC, 153 | wantMethod, 154 | streamer, 155 | wantOpts..., 156 | ) 157 | if err != nil { 158 | t.Fatalf("GCPStreamClientInterceptor(...) returned error: %v, want: nil", err) 159 | } 160 | if streamerCalled { 161 | t.Fatalf("GCPStreamClientInterceptor(...) unexpectedly called grpc.Streamer on init") 162 | } 163 | if err := cs.SendMsg(wantReq); err != nil { 164 | t.Fatalf("SendMsg(wantReq) returned error: %v, want: nil", err) 165 | } 166 | if !streamerCalled { 167 | t.Fatalf("SendMsg(wantReq) must have been called grpc.Streamer") 168 | } 169 | if err := cs.RecvMsg(wantRes); err != nil { 170 | t.Fatalf("RecvMsg() returned error: %v, want: nil", err) 171 | } 172 | } 173 | 174 | func TestGCPStreamClientInterceptorCallingReadBeforeSend(t *testing.T) { 175 | mockCtrl := gomock.NewController(t) 176 | defer mockCtrl.Finish() 177 | 178 | ctx := context.TODO() 179 | wantMethod := "someMethod" 180 | wantReq := "someRequest" 181 | wantRes := &fakeResp{} 182 | wantGCPCtx := &gcpContext{ 183 | reqMsg: wantReq, 184 | } 185 | wantSD := &grpc.StreamDesc{} 186 | wantCC := &grpc.ClientConn{} 187 | wantOpts := []grpc.CallOption{grpc.CallContentSubtype("someSubtype"), grpc.MaxCallRecvMsgSize(42)} 188 | 189 | streamerCalled := false 190 | streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { 191 | streamerCalled = true 192 | gotGCPCtx, hasGCPCtx := ctx.Value(gcpKey).(*gcpContext) 193 | if !hasGCPCtx { 194 | t.Errorf("grpc.Streamer called with context without gcpContext") 195 | } else if diff := cmp.Diff(wantGCPCtx, gotGCPCtx, cmp.AllowUnexported(gcpContext{})); diff != "" { 196 | t.Errorf("grpc.Streamer called with unexpected gcpContext (-want, +got):\n%s", diff) 197 | } 198 | if desc != wantSD { 199 | t.Errorf("grpc.Streamer called with unexpected StreamDesc: %v, want: %v", desc, wantSD) 200 | } 201 | if cc != wantCC { 202 | t.Errorf("grpc.Streamer called with unexpected ClientConn: %v, want: %v", cc, wantCC) 203 | } 204 | if method != wantMethod { 205 | t.Errorf("grpc.Streamer called with unexpected method: %v, want: %v", method, wantMethod) 206 | } 207 | if diff := cmp.Diff(wantOpts, opts); diff != "" { 208 | t.Errorf("grpc.Streamer called with unexpected options (-want, +got):\n%s", diff) 209 | } 210 | mockCS := mocks.NewMockClientStream(mockCtrl) 211 | mockCS.EXPECT().SendMsg(gomock.Eq(wantReq)).Times(1) 212 | mockCS.EXPECT().RecvMsg(&strictMatcher{matchWith: wantRes}).Times(1) 213 | return mockCS, nil 214 | } 215 | cs, err := GCPStreamClientInterceptor( 216 | ctx, 217 | wantSD, 218 | wantCC, 219 | wantMethod, 220 | streamer, 221 | wantOpts..., 222 | ) 223 | if err != nil { 224 | t.Fatalf("GCPStreamClientInterceptor(...) returned error: %v, want: nil", err) 225 | } 226 | if streamerCalled { 227 | t.Fatalf("GCPStreamClientInterceptor(...) unexpectedly called grpc.Streamer on init") 228 | } 229 | wg := &sync.WaitGroup{} 230 | wg.Add(1) 231 | received := &sync.WaitGroup{} 232 | received.Add(1) 233 | go func() { 234 | wg.Done() 235 | if err := cs.RecvMsg(wantRes); err != nil { 236 | t.Errorf("RecvMsg() returned error: %v, want: nil", err) 237 | } 238 | received.Done() 239 | }() 240 | wg.Wait() 241 | time.Sleep(time.Millisecond) 242 | if err := cs.SendMsg(wantReq); err != nil { 243 | t.Fatalf("SendMsg(wantReq) returned error: %v, want: nil", err) 244 | } 245 | if !streamerCalled { 246 | t.Fatalf("SendMsg(wantReq) must have been called grpc.Streamer") 247 | } 248 | received.Wait() 249 | } 250 | -------------------------------------------------------------------------------- /grpcgcp/gcp_logger.go: -------------------------------------------------------------------------------- 1 | package grpcgcp 2 | 3 | import ( 4 | "strings" 5 | 6 | "google.golang.org/grpc/grpclog" 7 | ) 8 | 9 | const ( 10 | FINE = 90 11 | FINEST = 99 12 | ) 13 | 14 | var compLogger = grpclog.Component("grpcgcp") 15 | 16 | type gcpLogger struct { 17 | logger grpclog.LoggerV2 18 | prefix string 19 | } 20 | 21 | // Make sure gcpLogger implements grpclog.LoggerV2. 22 | var _ grpclog.LoggerV2 = (*gcpLogger)(nil) 23 | 24 | func NewGCPLogger(logger grpclog.LoggerV2, prefix string) *gcpLogger { 25 | p := prefix 26 | if !strings.HasSuffix(p, " ") { 27 | p = p + " " 28 | } 29 | return &gcpLogger{ 30 | logger: logger, 31 | prefix: p, 32 | } 33 | } 34 | 35 | // Error implements grpclog.LoggerV2. 36 | func (l *gcpLogger) Error(args ...interface{}) { 37 | l.logger.Error(append([]interface{}{l.prefix}, args)...) 38 | } 39 | 40 | // Errorf implements grpclog.LoggerV2. 41 | func (l *gcpLogger) Errorf(format string, args ...interface{}) { 42 | l.logger.Errorf(l.prefix+format, args...) 43 | } 44 | 45 | // Errorln implements grpclog.LoggerV2. 46 | func (l *gcpLogger) Errorln(args ...interface{}) { 47 | l.logger.Errorln(append([]interface{}{l.prefix}, args)...) 48 | } 49 | 50 | // Fatal implements grpclog.LoggerV2. 51 | func (l *gcpLogger) Fatal(args ...interface{}) { 52 | l.logger.Fatal(append([]interface{}{l.prefix}, args)...) 53 | } 54 | 55 | // Fatalf implements grpclog.LoggerV2. 56 | func (l *gcpLogger) Fatalf(format string, args ...interface{}) { 57 | l.logger.Fatalf(l.prefix+format, args...) 58 | } 59 | 60 | // Fatalln implements grpclog.LoggerV2. 61 | func (l *gcpLogger) Fatalln(args ...interface{}) { 62 | l.logger.Fatalln(append([]interface{}{l.prefix}, args)...) 63 | } 64 | 65 | // Info implements grpclog.LoggerV2. 66 | func (l *gcpLogger) Info(args ...interface{}) { 67 | l.logger.Info(append([]interface{}{l.prefix}, args)...) 68 | } 69 | 70 | // Infof implements grpclog.LoggerV2. 71 | func (l *gcpLogger) Infof(format string, args ...interface{}) { 72 | l.logger.Infof(l.prefix+format, args...) 73 | } 74 | 75 | // Infoln implements grpclog.LoggerV2. 76 | func (l *gcpLogger) Infoln(args ...interface{}) { 77 | l.logger.Infoln(append([]interface{}{l.prefix}, args)...) 78 | } 79 | 80 | // V implements grpclog.LoggerV2. 81 | func (l *gcpLogger) V(level int) bool { 82 | return l.logger.V(level) 83 | } 84 | 85 | // Warning implements grpclog.LoggerV2. 86 | func (l *gcpLogger) Warning(args ...interface{}) { 87 | l.logger.Warning(append([]interface{}{l.prefix}, args)...) 88 | } 89 | 90 | // Warningf implements grpclog.LoggerV2. 91 | func (l *gcpLogger) Warningf(format string, args ...interface{}) { 92 | l.logger.Warningf(l.prefix+format, args...) 93 | } 94 | 95 | // Warningln implements grpclog.LoggerV2. 96 | func (l *gcpLogger) Warningln(args ...interface{}) { 97 | l.logger.Warningln(append([]interface{}{l.prefix}, args)...) 98 | } 99 | -------------------------------------------------------------------------------- /grpcgcp/gcp_picker.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2019 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package grpcgcp 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "reflect" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/grpc_gcp" 30 | "google.golang.org/grpc/balancer" 31 | "google.golang.org/grpc/codes" 32 | "google.golang.org/grpc/grpclog" 33 | "google.golang.org/grpc/status" 34 | ) 35 | 36 | // Deadline exceeded gRPC error caused by client-side context reached deadline. 37 | var deErr = status.Error(codes.DeadlineExceeded, context.DeadlineExceeded.Error()) 38 | 39 | func newGCPPicker(readySCRefs []*subConnRef, gb *gcpBalancer) balancer.Picker { 40 | gp := &gcpPicker{ 41 | gb: gb, 42 | scRefs: readySCRefs, 43 | } 44 | gp.log = NewGCPLogger(gb.log, fmt.Sprintf("[gcpPicker %p]", gp)) 45 | return gp 46 | } 47 | 48 | type gcpPicker struct { 49 | gb *gcpBalancer 50 | mu sync.Mutex 51 | scRefs []*subConnRef 52 | log grpclog.LoggerV2 53 | } 54 | 55 | func (p *gcpPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { 56 | if len(p.scRefs) <= 0 { 57 | if p.log.V(FINEST) { 58 | p.log.Info("returning balancer.ErrNoSubConnAvailable as no subconns are available.") 59 | } 60 | return balancer.PickResult{}, balancer.ErrNoSubConnAvailable 61 | } 62 | 63 | ctx := info.Ctx 64 | gcpCtx, hasGCPCtx := ctx.Value(gcpKey).(*gcpContext) 65 | boundKey := "" 66 | locator := "" 67 | var cmd grpc_gcp.AffinityConfig_Command 68 | 69 | if mcfg, ok := p.gb.methodCfg[info.FullMethodName]; ok { 70 | locator = mcfg.GetAffinityKey() 71 | cmd = mcfg.GetCommand() 72 | if hasGCPCtx && (cmd == grpc_gcp.AffinityConfig_BOUND || cmd == grpc_gcp.AffinityConfig_UNBIND) { 73 | a, err := getAffinityKeysFromMessage(locator, gcpCtx.reqMsg) 74 | if err != nil { 75 | return balancer.PickResult{}, fmt.Errorf( 76 | "failed to retrieve affinity key from request message: %v", err) 77 | } 78 | boundKey = a[0] 79 | } 80 | } 81 | 82 | scRef, err := p.getAndIncrementSubConnRef(info.Ctx, boundKey, cmd) 83 | if err != nil { 84 | return balancer.PickResult{}, err 85 | } 86 | if scRef == nil { 87 | if p.log.V(FINEST) { 88 | p.log.Info("returning balancer.ErrNoSubConnAvailable as no SubConn was picked.") 89 | } 90 | return balancer.PickResult{}, balancer.ErrNoSubConnAvailable 91 | } 92 | 93 | callStarted := time.Now() 94 | // define callback for post process once call is done 95 | callback := func(info balancer.DoneInfo) { 96 | scRef.streamsDecr() 97 | p.detectUnresponsive(ctx, scRef, callStarted, info.Err) 98 | if info.Err != nil { 99 | return 100 | } 101 | 102 | switch cmd { 103 | case grpc_gcp.AffinityConfig_BIND: 104 | bindKeys, err := getAffinityKeysFromMessage(locator, gcpCtx.replyMsg) 105 | if err == nil { 106 | for _, bk := range bindKeys { 107 | p.gb.bindSubConn(bk, scRef.subConn) 108 | } 109 | } 110 | case grpc_gcp.AffinityConfig_UNBIND: 111 | p.gb.unbindSubConn(boundKey) 112 | } 113 | } 114 | 115 | if p.log.V(FINEST) { 116 | p.log.Infof("picked SubConn: %p", scRef.subConn) 117 | } 118 | return balancer.PickResult{SubConn: scRef.subConn, Done: callback}, nil 119 | } 120 | 121 | // unresponsiveWindow returns channel pool's unresponsiveDetectionMs multiplied 122 | // by 2^(refresh count since last response) as a time.Duration. This provides 123 | // exponential backoff when RPCs keep deadline exceeded after consecutive reconnections. 124 | func (p *gcpPicker) unresponsiveWindow(scRef *subConnRef) time.Duration { 125 | factor := uint32(1 << scRef.refreshCnt) 126 | return time.Millisecond * time.Duration(factor*p.gb.cfg.GetChannelPool().GetUnresponsiveDetectionMs()) 127 | } 128 | 129 | func (p *gcpPicker) detectUnresponsive(ctx context.Context, scRef *subConnRef, callStarted time.Time, rpcErr error) { 130 | if !p.gb.unresponsiveDetection { 131 | return 132 | } 133 | 134 | // Treat as a response from the server if deadline exceeded was not caused by client side context reached deadline. 135 | if dl, ok := ctx.Deadline(); rpcErr == nil || status.Code(rpcErr) != codes.DeadlineExceeded || 136 | rpcErr.Error() != deErr.Error() || !ok || dl.After(time.Now()) { 137 | scRef.gotResp() 138 | return 139 | } 140 | 141 | if callStarted.Before(scRef.lastResp) { 142 | return 143 | } 144 | 145 | // Increment deadline exceeded calls and check if there were enough deadline 146 | // exceeded calls and enough time passed since last response to trigger refresh. 147 | if scRef.deCallsInc() >= p.gb.cfg.GetChannelPool().GetUnresponsiveCalls() && 148 | scRef.lastResp.Before(time.Now().Add(-p.unresponsiveWindow(scRef))) { 149 | p.gb.refresh(scRef) 150 | } 151 | } 152 | 153 | func (p *gcpPicker) getAndIncrementSubConnRef(ctx context.Context, boundKey string, cmd grpc_gcp.AffinityConfig_Command) (*subConnRef, error) { 154 | if cmd == grpc_gcp.AffinityConfig_BIND && p.gb.cfg.GetChannelPool().GetBindPickStrategy() == grpc_gcp.ChannelPoolConfig_ROUND_ROBIN { 155 | scRef := p.gb.getSubConnRoundRobin(ctx) 156 | if p.log.V(FINEST) { 157 | p.log.Infof("picking SubConn for round-robin bind: %p", scRef.subConn) 158 | } 159 | scRef.streamsIncr() 160 | return scRef, nil 161 | } 162 | 163 | p.mu.Lock() 164 | defer p.mu.Unlock() 165 | scRef, err := p.getSubConnRef(boundKey) 166 | if err != nil { 167 | return nil, err 168 | } 169 | if scRef != nil { 170 | scRef.streamsIncr() 171 | } 172 | return scRef, nil 173 | } 174 | 175 | // getSubConnRef returns the subConnRef object that contains the subconn 176 | // ready to be used by picker. 177 | // Must be called holding the picker mutex lock. 178 | func (p *gcpPicker) getSubConnRef(boundKey string) (*subConnRef, error) { 179 | if boundKey != "" { 180 | if ref, ok := p.gb.getReadySubConnRef(boundKey); ok { 181 | return ref, nil 182 | } 183 | } 184 | 185 | return p.getLeastBusySubConnRef() 186 | } 187 | 188 | // Must be called holding the picker mutex lock. 189 | func (p *gcpPicker) getLeastBusySubConnRef() (*subConnRef, error) { 190 | minScRef := p.scRefs[0] 191 | minStreamsCnt := minScRef.getStreamsCnt() 192 | for _, scRef := range p.scRefs { 193 | if scRef.getStreamsCnt() < minStreamsCnt { 194 | minStreamsCnt = scRef.getStreamsCnt() 195 | minScRef = scRef 196 | } 197 | } 198 | 199 | // If the least busy connection still has capacity, use it 200 | if minStreamsCnt < int32(p.gb.cfg.GetChannelPool().GetMaxConcurrentStreamsLowWatermark()) { 201 | return minScRef, nil 202 | } 203 | 204 | if p.gb.cfg.GetChannelPool().GetMaxSize() == 0 || p.gb.getConnectionPoolSize() < int(p.gb.cfg.GetChannelPool().GetMaxSize()) { 205 | // Ask balancer to create new subconn when all current subconns are busy and 206 | // the connection pool still has capacity (either unlimited or maxSize is not reached). 207 | p.gb.newSubConn() 208 | 209 | // Let this picker return ErrNoSubConnAvailable because it needs some time 210 | // for the subconn to be READY. 211 | return nil, balancer.ErrNoSubConnAvailable 212 | } 213 | 214 | // If no capacity for the pool size and every connection reachs the soft limit, 215 | // Then picks the least busy one anyway. 216 | return minScRef, nil 217 | } 218 | 219 | func keysFromMessage(val reflect.Value, path []string, start int) ([]string, error) { 220 | if val.Kind() == reflect.Pointer || val.Kind() == reflect.Interface { 221 | val = val.Elem() 222 | } 223 | 224 | if len(path) == start { 225 | if val.Kind() != reflect.String { 226 | return nil, fmt.Errorf("cannot get string value from %q which is %q", strings.Join(path, "."), val.Kind()) 227 | } 228 | return []string{val.String()}, nil 229 | } 230 | 231 | if val.Kind() != reflect.Struct { 232 | return nil, fmt.Errorf("path %q traversal error: cannot lookup field %q (index %d in the path) in a %q value", strings.Join(path, "."), path[start], start, val.Kind()) 233 | } 234 | valField := val.FieldByName(strings.Title(path[start])) 235 | 236 | if valField.Kind() != reflect.Slice { 237 | return keysFromMessage(valField, path, start+1) 238 | } 239 | 240 | keys := []string{} 241 | for i := 0; i < valField.Len(); i++ { 242 | kk, err := keysFromMessage(valField.Index(i), path, start+1) 243 | if err != nil { 244 | return keys, err 245 | } 246 | keys = append(keys, kk...) 247 | } 248 | return keys, nil 249 | } 250 | 251 | // getAffinityKeysFromMessage retrieves the affinity key(s) from proto message using 252 | // the key locator defined in the affinity config. 253 | func getAffinityKeysFromMessage( 254 | locator string, 255 | msg interface{}, 256 | ) (affinityKeys []string, err error) { 257 | names := strings.Split(locator, ".") 258 | if len(names) == 0 { 259 | return nil, fmt.Errorf("empty affinityKey locator") 260 | } 261 | 262 | return keysFromMessage(reflect.ValueOf(msg), names, 0) 263 | } 264 | 265 | // NewErrPicker returns a picker that always returns err on Pick(). 266 | func newErrPicker(err error) balancer.Picker { 267 | return &errPicker{err: err} 268 | } 269 | 270 | type errPicker struct { 271 | err error // Pick() always returns this err. 272 | } 273 | 274 | func (p *errPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { 275 | return balancer.PickResult{}, p.err 276 | } 277 | -------------------------------------------------------------------------------- /grpcgcp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | github.com/google/go-cmp v0.5.9 8 | google.golang.org/grpc v1.56.3 9 | google.golang.org/protobuf v1.30.0 10 | ) 11 | -------------------------------------------------------------------------------- /grpcgcp/grpc_gcp/codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$0")" 3 | 4 | rm grpc_gcp.pb.go 5 | protoc --plugin=$(go env GOPATH)/bin/protoc-gen-go --proto_path=./ --go_out=.. ./grpc_gcp.proto 6 | 7 | -------------------------------------------------------------------------------- /grpcgcp/grpc_gcp/grpc_gcp.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | option go_package = "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/grpc_gcp"; 18 | 19 | package grpc.gcp; 20 | 21 | message ApiConfig { 22 | // The channel pool configurations. 23 | ChannelPoolConfig channel_pool = 2; 24 | 25 | // The method configurations. 26 | repeated MethodConfig method = 1001; 27 | } 28 | 29 | // ChannelPoolConfig are options for configuring the channel pool. 30 | // RPCs will be scheduled onto existing channels in the pool until all channels 31 | // have number of streams. At this point 32 | // a new channel is spun out. Once channels have been spun out and 33 | // each has streams, subsequent RPCs will 34 | // hang until any of the in-flight RPCs is finished, freeing up a channel. 35 | message ChannelPoolConfig { 36 | // The max number of channels in the pool. 37 | // Default value is 0, meaning 'unlimited' size. 38 | uint32 max_size = 1; 39 | 40 | // The idle timeout (seconds) of channels without bound affinity sessions. 41 | uint64 idle_timeout = 2; 42 | 43 | // The low watermark of max number of concurrent streams in a channel. 44 | // New channel will be created once it get hit, until we reach the max size of the channel pool. 45 | // Default value is 100. The valid range is [1, 100]. Any value outside the range will be ignored and the default value will be used. 46 | // Note: It is not recommended that users adjust this value, since a single channel should generally have no trouble managing the default (maximum) number of streams. 47 | uint32 max_concurrent_streams_low_watermark = 3; 48 | 49 | // The minimum number of channels in the pool. 50 | uint32 min_size = 4; 51 | 52 | // If a channel mapped to an affinity key is not ready, temporarily fallback 53 | // to another ready channel. 54 | // Enabling this fallback is beneficial in scenarios with short RPC timeouts 55 | // and rather slow connection establishing or during incidents when new 56 | // connections fail but existing connections still operate. 57 | bool fallback_to_ready = 5; 58 | 59 | // Enables per channel unresponsive connection detection if > 0 and unresponsive_calls > 0. 60 | // If enabled and more than unresponsive_detection_ms passed since the last response from the server, 61 | // and >= unresponsive_calls RPC calls (started after last response from the server) timed-out on the client side, 62 | // then the connection of that channel will be gracefully refreshed. I.e., a new connection will be created for 63 | // that channel and after the new connection is ready it will replace the old connection. The calls on the old 64 | // connection will not be interrupted. The unresponsive_detection_ms will be doubled every consecutive refresh 65 | // if no response from the server is received. 66 | uint32 unresponsive_detection_ms = 6; 67 | 68 | // Enables per channel unresponsive connection detection if > 0 and unresponsive_detection_ms > 0. 69 | // If enabled and more than unresponsive_detection_ms passed since the last response from the server, 70 | // and >= unresponsive_calls RPC calls (started after last response from the server) timed-out on the client side, 71 | // then the connection of that channel will be gracefully refreshed. I.e., a new connection will be created for 72 | // that channel and after the new connection is ready it will replace the old connection. The calls on the old 73 | // connection will not be interrupted. The unresponsive_detection_ms will be doubled every consecutive refresh 74 | // if no response from the server is received. 75 | uint32 unresponsive_calls = 7; 76 | 77 | // A selection of strategies for picking a channel for a call with BIND command. 78 | enum BindPickStrategy { 79 | // No preference -- picking a channel for a BIND call will be no different 80 | // than for any other calls. 81 | UNSPECIFIED = 0; 82 | 83 | // A channel with the least active streams at the moment of a BIND call 84 | // initiation will be picked. 85 | LEAST_ACTIVE_STREAMS = 1; 86 | 87 | // Cycle through channels created by the BIND call initiation. I. e. pick 88 | // a channel in a round-robin manner. Note that some channels may be 89 | // skipped during channel pool resize. 90 | ROUND_ROBIN = 2; 91 | } 92 | 93 | // The strategy for picking a channel for a call with BIND command. 94 | BindPickStrategy bind_pick_strategy = 8; 95 | } 96 | 97 | message MethodConfig { 98 | // A fully qualified name of a gRPC method, or a wildcard pattern ending 99 | // with .*, such as foo.bar.A, foo.bar.*. Method configs are evaluated 100 | // sequentially, and the first one takes precedence. 101 | repeated string name = 1; 102 | 103 | // The channel affinity configurations. 104 | AffinityConfig affinity = 1001; 105 | } 106 | 107 | message AffinityConfig { 108 | enum Command { 109 | // The annotated method will be required to be bound to an existing session 110 | // to execute the RPC. The corresponding will be 111 | // used to find the affinity key from the request message. 112 | BOUND = 0; 113 | // The annotated method will establish the channel affinity with the 114 | // channel which is used to execute the RPC. The corresponding 115 | // will be used to find the affinity key from the 116 | // response message. 117 | BIND = 1; 118 | // The annotated method will remove the channel affinity with the 119 | // channel which is used to execute the RPC. The corresponding 120 | // will be used to find the affinity key from the 121 | // request message. 122 | UNBIND = 2; 123 | } 124 | // The affinity command applies on the selected gRPC methods. 125 | Command command = 2; 126 | // The field path of the affinity key in the request/response message. 127 | // For example: "f.a", "f.b.d", etc. 128 | string affinity_key = 3; 129 | } 130 | -------------------------------------------------------------------------------- /grpcgcp/mockgen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$0")" 3 | mockgen -destination=mocks/mock_balancer.go -package=mocks google.golang.org/grpc/balancer ClientConn,SubConn 4 | mockgen -destination=mocks/mock_stream.go -package=mocks google.golang.org/grpc ClientStream 5 | -------------------------------------------------------------------------------- /grpcgcp/mocks/mock_balancer.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: google.golang.org/grpc/balancer (interfaces: ClientConn,SubConn) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | balancer "google.golang.org/grpc/balancer" 12 | resolver "google.golang.org/grpc/resolver" 13 | ) 14 | 15 | // MockClientConn is a mock of ClientConn interface. 16 | type MockClientConn struct { 17 | // The ClientConn interface below is embedded by a manual edit to comply 18 | // with grpc's requirement to embed an existing ClientConn to allow grpc to 19 | // add new methods to the interface easily. 20 | balancer.ClientConn 21 | ctrl *gomock.Controller 22 | recorder *MockClientConnMockRecorder 23 | } 24 | 25 | // MockClientConnMockRecorder is the mock recorder for MockClientConn. 26 | type MockClientConnMockRecorder struct { 27 | mock *MockClientConn 28 | } 29 | 30 | // NewMockClientConn creates a new mock instance. 31 | func NewMockClientConn(ctrl *gomock.Controller) *MockClientConn { 32 | mock := &MockClientConn{ctrl: ctrl} 33 | mock.recorder = &MockClientConnMockRecorder{mock} 34 | return mock 35 | } 36 | 37 | // EXPECT returns an object that allows the caller to indicate expected use. 38 | func (m *MockClientConn) EXPECT() *MockClientConnMockRecorder { 39 | return m.recorder 40 | } 41 | 42 | // NewSubConn mocks base method. 43 | func (m *MockClientConn) NewSubConn(arg0 []resolver.Address, arg1 balancer.NewSubConnOptions) (balancer.SubConn, error) { 44 | m.ctrl.T.Helper() 45 | ret := m.ctrl.Call(m, "NewSubConn", arg0, arg1) 46 | ret0, _ := ret[0].(balancer.SubConn) 47 | ret1, _ := ret[1].(error) 48 | return ret0, ret1 49 | } 50 | 51 | // NewSubConn indicates an expected call of NewSubConn. 52 | func (mr *MockClientConnMockRecorder) NewSubConn(arg0, arg1 interface{}) *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSubConn", reflect.TypeOf((*MockClientConn)(nil).NewSubConn), arg0, arg1) 55 | } 56 | 57 | // RemoveSubConn mocks base method. 58 | func (m *MockClientConn) RemoveSubConn(arg0 balancer.SubConn) { 59 | m.ctrl.T.Helper() 60 | m.ctrl.Call(m, "RemoveSubConn", arg0) 61 | } 62 | 63 | // RemoveSubConn indicates an expected call of RemoveSubConn. 64 | func (mr *MockClientConnMockRecorder) RemoveSubConn(arg0 interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveSubConn", reflect.TypeOf((*MockClientConn)(nil).RemoveSubConn), arg0) 67 | } 68 | 69 | // ResolveNow mocks base method. 70 | func (m *MockClientConn) ResolveNow(arg0 resolver.ResolveNowOptions) { 71 | m.ctrl.T.Helper() 72 | m.ctrl.Call(m, "ResolveNow", arg0) 73 | } 74 | 75 | // ResolveNow indicates an expected call of ResolveNow. 76 | func (mr *MockClientConnMockRecorder) ResolveNow(arg0 interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveNow", reflect.TypeOf((*MockClientConn)(nil).ResolveNow), arg0) 79 | } 80 | 81 | // Target mocks base method. 82 | func (m *MockClientConn) Target() string { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "Target") 85 | ret0, _ := ret[0].(string) 86 | return ret0 87 | } 88 | 89 | // Target indicates an expected call of Target. 90 | func (mr *MockClientConnMockRecorder) Target() *gomock.Call { 91 | mr.mock.ctrl.T.Helper() 92 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Target", reflect.TypeOf((*MockClientConn)(nil).Target)) 93 | } 94 | 95 | // UpdateAddresses mocks base method. 96 | func (m *MockClientConn) UpdateAddresses(arg0 balancer.SubConn, arg1 []resolver.Address) { 97 | m.ctrl.T.Helper() 98 | m.ctrl.Call(m, "UpdateAddresses", arg0, arg1) 99 | } 100 | 101 | // UpdateAddresses indicates an expected call of UpdateAddresses. 102 | func (mr *MockClientConnMockRecorder) UpdateAddresses(arg0, arg1 interface{}) *gomock.Call { 103 | mr.mock.ctrl.T.Helper() 104 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddresses", reflect.TypeOf((*MockClientConn)(nil).UpdateAddresses), arg0, arg1) 105 | } 106 | 107 | // UpdateState mocks base method. 108 | func (m *MockClientConn) UpdateState(arg0 balancer.State) { 109 | m.ctrl.T.Helper() 110 | m.ctrl.Call(m, "UpdateState", arg0) 111 | } 112 | 113 | // UpdateState indicates an expected call of UpdateState. 114 | func (mr *MockClientConnMockRecorder) UpdateState(arg0 interface{}) *gomock.Call { 115 | mr.mock.ctrl.T.Helper() 116 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateState", reflect.TypeOf((*MockClientConn)(nil).UpdateState), arg0) 117 | } 118 | 119 | // MockSubConn is a mock of SubConn interface. 120 | type MockSubConn struct { 121 | // The SubConn interface is added by a manual edit to 122 | // comply with the requirements in the SubConn interface 123 | // doc comment. 124 | balancer.SubConn 125 | ctrl *gomock.Controller 126 | recorder *MockSubConnMockRecorder 127 | } 128 | 129 | // MockSubConnMockRecorder is the mock recorder for MockSubConn. 130 | type MockSubConnMockRecorder struct { 131 | mock *MockSubConn 132 | } 133 | 134 | // NewMockSubConn creates a new mock instance. 135 | func NewMockSubConn(ctrl *gomock.Controller) *MockSubConn { 136 | mock := &MockSubConn{ctrl: ctrl} 137 | mock.recorder = &MockSubConnMockRecorder{mock} 138 | return mock 139 | } 140 | 141 | // EXPECT returns an object that allows the caller to indicate expected use. 142 | func (m *MockSubConn) EXPECT() *MockSubConnMockRecorder { 143 | return m.recorder 144 | } 145 | 146 | // Connect mocks base method. 147 | func (m *MockSubConn) Connect() { 148 | m.ctrl.T.Helper() 149 | m.ctrl.Call(m, "Connect") 150 | } 151 | 152 | // Connect indicates an expected call of Connect. 153 | func (mr *MockSubConnMockRecorder) Connect() *gomock.Call { 154 | mr.mock.ctrl.T.Helper() 155 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockSubConn)(nil).Connect)) 156 | } 157 | 158 | // GetOrBuildProducer mocks base method. 159 | func (m *MockSubConn) GetOrBuildProducer(arg0 balancer.ProducerBuilder) (balancer.Producer, func()) { 160 | m.ctrl.T.Helper() 161 | ret := m.ctrl.Call(m, "GetOrBuildProducer", arg0) 162 | ret0, _ := ret[0].(balancer.Producer) 163 | ret1, _ := ret[1].(func()) 164 | return ret0, ret1 165 | } 166 | 167 | // GetOrBuildProducer indicates an expected call of GetOrBuildProducer. 168 | func (mr *MockSubConnMockRecorder) GetOrBuildProducer(arg0 interface{}) *gomock.Call { 169 | mr.mock.ctrl.T.Helper() 170 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrBuildProducer", reflect.TypeOf((*MockSubConn)(nil).GetOrBuildProducer), arg0) 171 | } 172 | 173 | // Shutdown mocks base method. 174 | func (m *MockSubConn) Shutdown() { 175 | m.ctrl.T.Helper() 176 | m.ctrl.Call(m, "Shutdown") 177 | } 178 | 179 | // Shutdown indicates an expected call of Shutdown. 180 | func (mr *MockSubConnMockRecorder) Shutdown() *gomock.Call { 181 | mr.mock.ctrl.T.Helper() 182 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockSubConn)(nil).Shutdown)) 183 | } 184 | 185 | // UpdateAddresses mocks base method. 186 | func (m *MockSubConn) UpdateAddresses(arg0 []resolver.Address) { 187 | m.ctrl.T.Helper() 188 | m.ctrl.Call(m, "UpdateAddresses", arg0) 189 | } 190 | 191 | // UpdateAddresses indicates an expected call of UpdateAddresses. 192 | func (mr *MockSubConnMockRecorder) UpdateAddresses(arg0 interface{}) *gomock.Call { 193 | mr.mock.ctrl.T.Helper() 194 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddresses", reflect.TypeOf((*MockSubConn)(nil).UpdateAddresses), arg0) 195 | } 196 | -------------------------------------------------------------------------------- /grpcgcp/mocks/mock_stream.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: google.golang.org/grpc (interfaces: ClientStream) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | metadata "google.golang.org/grpc/metadata" 13 | ) 14 | 15 | // MockClientStream is a mock of ClientStream interface. 16 | type MockClientStream struct { 17 | ctrl *gomock.Controller 18 | recorder *MockClientStreamMockRecorder 19 | } 20 | 21 | // MockClientStreamMockRecorder is the mock recorder for MockClientStream. 22 | type MockClientStreamMockRecorder struct { 23 | mock *MockClientStream 24 | } 25 | 26 | // NewMockClientStream creates a new mock instance. 27 | func NewMockClientStream(ctrl *gomock.Controller) *MockClientStream { 28 | mock := &MockClientStream{ctrl: ctrl} 29 | mock.recorder = &MockClientStreamMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockClientStream) EXPECT() *MockClientStreamMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // CloseSend mocks base method. 39 | func (m *MockClientStream) CloseSend() error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "CloseSend") 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // CloseSend indicates an expected call of CloseSend. 47 | func (mr *MockClientStreamMockRecorder) CloseSend() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseSend", reflect.TypeOf((*MockClientStream)(nil).CloseSend)) 50 | } 51 | 52 | // Context mocks base method. 53 | func (m *MockClientStream) Context() context.Context { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "Context") 56 | ret0, _ := ret[0].(context.Context) 57 | return ret0 58 | } 59 | 60 | // Context indicates an expected call of Context. 61 | func (mr *MockClientStreamMockRecorder) Context() *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockClientStream)(nil).Context)) 64 | } 65 | 66 | // Header mocks base method. 67 | func (m *MockClientStream) Header() (metadata.MD, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "Header") 70 | ret0, _ := ret[0].(metadata.MD) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // Header indicates an expected call of Header. 76 | func (mr *MockClientStreamMockRecorder) Header() *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Header", reflect.TypeOf((*MockClientStream)(nil).Header)) 79 | } 80 | 81 | // RecvMsg mocks base method. 82 | func (m *MockClientStream) RecvMsg(arg0 interface{}) error { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "RecvMsg", arg0) 85 | ret0, _ := ret[0].(error) 86 | return ret0 87 | } 88 | 89 | // RecvMsg indicates an expected call of RecvMsg. 90 | func (mr *MockClientStreamMockRecorder) RecvMsg(arg0 interface{}) *gomock.Call { 91 | mr.mock.ctrl.T.Helper() 92 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecvMsg", reflect.TypeOf((*MockClientStream)(nil).RecvMsg), arg0) 93 | } 94 | 95 | // SendMsg mocks base method. 96 | func (m *MockClientStream) SendMsg(arg0 interface{}) error { 97 | m.ctrl.T.Helper() 98 | ret := m.ctrl.Call(m, "SendMsg", arg0) 99 | ret0, _ := ret[0].(error) 100 | return ret0 101 | } 102 | 103 | // SendMsg indicates an expected call of SendMsg. 104 | func (mr *MockClientStreamMockRecorder) SendMsg(arg0 interface{}) *gomock.Call { 105 | mr.mock.ctrl.T.Helper() 106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMsg", reflect.TypeOf((*MockClientStream)(nil).SendMsg), arg0) 107 | } 108 | 109 | // Trailer mocks base method. 110 | func (m *MockClientStream) Trailer() metadata.MD { 111 | m.ctrl.T.Helper() 112 | ret := m.ctrl.Call(m, "Trailer") 113 | ret0, _ := ret[0].(metadata.MD) 114 | return ret0 115 | } 116 | 117 | // Trailer indicates an expected call of Trailer. 118 | func (mr *MockClientStreamMockRecorder) Trailer() *gomock.Call { 119 | mr.mock.ctrl.T.Helper() 120 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Trailer", reflect.TypeOf((*MockClientStream)(nil).Trailer)) 121 | } 122 | -------------------------------------------------------------------------------- /grpcgcp/multiendpoint/endpoint.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2023 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package multiendpoint 20 | 21 | import ( 22 | "fmt" 23 | "time" 24 | ) 25 | 26 | type status int 27 | 28 | // Status of an endpoint. 29 | const ( 30 | unavailable status = iota 31 | available 32 | recovering 33 | ) 34 | 35 | func (s status) String() string { 36 | switch s { 37 | case unavailable: 38 | return "Unavailable" 39 | case available: 40 | return "Available" 41 | case recovering: 42 | return "Recovering" 43 | default: 44 | return fmt.Sprintf("%d", s) 45 | } 46 | } 47 | 48 | type endpoint struct { 49 | id string 50 | priority int 51 | status status 52 | lastChange time.Time 53 | futureChange timerAlike 54 | } 55 | -------------------------------------------------------------------------------- /grpcgcp/multiendpoint/multiendpoint.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2023 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | // Package multiendpoint implements multiendpoint feature. See [MultiEndpoint] 20 | package multiendpoint 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "sync" 26 | "time" 27 | ) 28 | 29 | type timerAlike interface { 30 | Reset(time.Duration) bool 31 | Stop() bool 32 | } 33 | 34 | // To be redefined in tests. 35 | var ( 36 | timeNow = func() time.Time { 37 | return time.Now() 38 | } 39 | timeAfterFunc = func(d time.Duration, f func()) timerAlike { 40 | return time.AfterFunc(d, f) 41 | } 42 | ) 43 | 44 | // MultiEndpoint holds a list of endpoints, tracks their availability and defines the current 45 | // endpoint. An endpoint has a priority defined by its position in the list (first item has top 46 | // priority). 47 | // 48 | // The current endpoint is the highest available endpoint in the list. If no endpoint is available, 49 | // MultiEndpoint sticks to the previously current endpoint. 50 | // 51 | // Sometimes switching between endpoints can be costly, and it is worth waiting for some time 52 | // after current endpoint becomes unavailable. For this case, use 53 | // [MultiEndpointOptions.RecoveryTimeout] to set the recovery timeout. MultiEndpoint will keep the 54 | // current endpoint for up to recovery timeout after it became unavailable to give it some time to 55 | // recover. 56 | // 57 | // The list of endpoints can be changed at any time with [MultiEndpoint.SetEndpoints] function. 58 | // MultiEndpoint will: 59 | // - remove obsolete endpoints; 60 | // - preserve remaining endpoints and their states; 61 | // - add new endpoints; 62 | // - update all endpoints priority according to the new order; 63 | // - change current endpoint if necessary. 64 | // 65 | // After updating the list of endpoints, MultiEndpoint will switch the current endpoint to the 66 | // highest available endpoint in the list. If you have many processes using MultiEndpoint, this may 67 | // lead to immediate shift of all traffic which may be undesired. To smooth this transfer, use 68 | // [MultiEndpointOptions.SwitchingDelay] with randomized value to introduce a jitter. Each 69 | // MultiEndpoint will delay switching from an available endpoint to another endpoint for this amount 70 | // of time. This delay is only applicable when switching from a lower priority available endpoint to 71 | // a higher priority available endpoint. 72 | type MultiEndpoint interface { 73 | // Current returns current endpoint. 74 | // 75 | // Note that the read is not synchronized and in case of a race condition there is a chance of 76 | // getting an outdated current endpoint. 77 | Current() string 78 | 79 | // SetEndpointAvailability informs MultiEndpoint when an endpoint becomes available or unavailable. 80 | // This may change the current endpoint. 81 | SetEndpointAvailability(e string, avail bool) 82 | 83 | // SetEndpoints updates a list of endpoints: 84 | // - remove obsolete endpoints 85 | // - preserve remaining endpoints and their states 86 | // - add new endpoints 87 | // - update all endpoints priority according to the new order 88 | // This may change the current endpoint. 89 | SetEndpoints(endpoints []string) error 90 | } 91 | 92 | // MultiEndpointOptions is used for configuring [MultiEndpoint]. 93 | type MultiEndpointOptions struct { 94 | // A list of endpoints ordered by priority (first endpoint has top priority). 95 | Endpoints []string 96 | // RecoveryTimeout sets the amount of time MultiEndpoint keeps endpoint as current after it 97 | // became unavailable. 98 | RecoveryTimeout time.Duration 99 | // When switching from a lower priority available endpoint to a higher priority available 100 | // endpoint the MultiEndpoint will delay the switch for this duration. 101 | SwitchingDelay time.Duration 102 | } 103 | 104 | // NewMultiEndpoint validates options and creates a new [MultiEndpoint]. 105 | func NewMultiEndpoint(b *MultiEndpointOptions) (MultiEndpoint, error) { 106 | if len(b.Endpoints) == 0 { 107 | return nil, fmt.Errorf("endpoints list cannot be empty") 108 | } 109 | 110 | me := &multiEndpoint{ 111 | recoveryTimeout: b.RecoveryTimeout, 112 | switchingDelay: b.SwitchingDelay, 113 | current: b.Endpoints[0], 114 | } 115 | eMap := make(map[string]*endpoint) 116 | for i, e := range b.Endpoints { 117 | eMap[e] = me.newEndpoint(e, i) 118 | } 119 | me.endpoints = eMap 120 | return me, nil 121 | } 122 | 123 | type multiEndpoint struct { 124 | sync.RWMutex 125 | 126 | endpoints map[string]*endpoint 127 | recoveryTimeout time.Duration 128 | switchingDelay time.Duration 129 | current string 130 | future string 131 | } 132 | 133 | // Current returns current endpoint. 134 | func (me *multiEndpoint) Current() string { 135 | me.RLock() 136 | defer me.RUnlock() 137 | return me.current 138 | } 139 | 140 | // SetEndpoints updates endpoints list: 141 | // - remove obsolete endpoints; 142 | // - preserve remaining endpoints and their states; 143 | // - add new endpoints; 144 | // - update all endpoints priority according to the new order; 145 | // - change current endpoint if necessary. 146 | func (me *multiEndpoint) SetEndpoints(endpoints []string) error { 147 | me.Lock() 148 | defer me.Unlock() 149 | if len(endpoints) == 0 { 150 | return errors.New("endpoints list cannot be empty") 151 | } 152 | newEndpoints := make(map[string]struct{}) 153 | for _, v := range endpoints { 154 | newEndpoints[v] = struct{}{} 155 | } 156 | // Remove obsolete endpoints. 157 | for e := range me.endpoints { 158 | if _, ok := newEndpoints[e]; !ok { 159 | delete(me.endpoints, e) 160 | } 161 | } 162 | // Add new endpoints and update priority. 163 | for i, e := range endpoints { 164 | if _, ok := me.endpoints[e]; !ok { 165 | me.endpoints[e] = me.newEndpoint(e, i) 166 | } else { 167 | me.endpoints[e].priority = i 168 | } 169 | } 170 | 171 | me.maybeUpdateCurrent() 172 | return nil 173 | } 174 | 175 | // Updates current to the top-priority available endpoint unless the current endpoint is 176 | // recovering. 177 | // 178 | // Must be run under me.Lock. 179 | func (me *multiEndpoint) maybeUpdateCurrent() { 180 | c, exists := me.endpoints[me.current] 181 | var topA *endpoint 182 | var top *endpoint 183 | for _, e := range me.endpoints { 184 | if e.status == available && (topA == nil || topA.priority > e.priority) { 185 | topA = e 186 | } 187 | if top == nil || top.priority > e.priority { 188 | top = e 189 | } 190 | } 191 | 192 | if exists && c.status == recovering && (topA == nil || topA.priority > c.priority) { 193 | // Let current endpoint recover while no higher priority endpoints available. 194 | return 195 | } 196 | 197 | // Always prefer top available endpoint. 198 | if topA != nil { 199 | me.switchFromTo(c, topA) 200 | return 201 | } 202 | 203 | // If no current endpoint exists, resort to the top priority endpoint immediately. 204 | if !exists { 205 | me.current = top.id 206 | } 207 | } 208 | 209 | func (me *multiEndpoint) newEndpoint(id string, priority int) *endpoint { 210 | s := unavailable 211 | if me.recoveryTimeout > 0 { 212 | s = recovering 213 | } 214 | e := &endpoint{ 215 | id: id, 216 | priority: priority, 217 | status: s, 218 | } 219 | if e.status == recovering { 220 | me.scheduleUnavailable(e) 221 | } 222 | return e 223 | } 224 | 225 | // Changes or schedules a change of current to the endpoint t. 226 | // 227 | // Must be run under me.Lock. 228 | func (me *multiEndpoint) switchFromTo(f, t *endpoint) { 229 | if me.current == t.id { 230 | return 231 | } 232 | 233 | if me.switchingDelay == 0 || f == nil || f.status == unavailable { 234 | // Switching immediately if no delay or no current or current is unavailable. 235 | me.current = t.id 236 | return 237 | } 238 | 239 | me.future = t.id 240 | timeAfterFunc(me.switchingDelay, func() { 241 | me.Lock() 242 | defer me.Unlock() 243 | if e, ok := me.endpoints[me.future]; ok && e.status == available { 244 | me.current = e.id 245 | } 246 | }) 247 | } 248 | 249 | // SetEndpointAvailability updates the state of an endpoint. 250 | func (me *multiEndpoint) SetEndpointAvailability(e string, avail bool) { 251 | me.Lock() 252 | defer me.Unlock() 253 | me.setEndpointAvailability(e, avail) 254 | me.maybeUpdateCurrent() 255 | } 256 | 257 | // Must be run under me.Lock. 258 | func (me *multiEndpoint) setEndpointAvailability(e string, avail bool) { 259 | ee, ok := me.endpoints[e] 260 | if !ok { 261 | return 262 | } 263 | 264 | if avail { 265 | setState(ee, available) 266 | return 267 | } 268 | 269 | if ee.status != available { 270 | return 271 | } 272 | 273 | if me.recoveryTimeout == 0 { 274 | setState(ee, unavailable) 275 | return 276 | } 277 | 278 | setState(ee, recovering) 279 | me.scheduleUnavailable(ee) 280 | } 281 | 282 | // Change the state of endpoint e to state s. 283 | // 284 | // Must be run under me.Lock. 285 | func setState(e *endpoint, s status) { 286 | if e.futureChange != nil { 287 | e.futureChange.Stop() 288 | } 289 | e.status = s 290 | e.lastChange = timeNow() 291 | } 292 | 293 | // Schedule endpoint e to become unavailable after recoveryTimeout. 294 | func (me *multiEndpoint) scheduleUnavailable(e *endpoint) { 295 | stateChange := e.lastChange 296 | e.futureChange = timeAfterFunc(me.recoveryTimeout, func() { 297 | me.Lock() 298 | defer me.Unlock() 299 | if e.lastChange != stateChange { 300 | // This timer is outdated. 301 | return 302 | } 303 | setState(e, unavailable) 304 | me.maybeUpdateCurrent() 305 | }) 306 | } 307 | -------------------------------------------------------------------------------- /grpcgcp/test_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "channelPool": { 3 | "maxSize": 10, 4 | "maxConcurrentStreamsLowWatermark": 10 5 | }, 6 | "method": [ 7 | { 8 | "name": [ "method1" ], 9 | "affinity": { 10 | "command": "BIND", 11 | "affinityKey": "key1" 12 | } 13 | }, 14 | { 15 | "name": [ "method2" ], 16 | "affinity": { 17 | "command": "BOUND", 18 | "affinityKey": "key2" 19 | } 20 | }, 21 | { 22 | "name": [ "method3" ], 23 | "affinity": { 24 | "command": "UNBIND", 25 | "affinityKey": "key3" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /grpcgcp/test_grpc/helloworld/codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "$0")" 3 | 4 | rm -r helloworld 5 | protoc --plugin=$(go env GOPATH)/bin/protoc-gen-go --proto_path=./ --go_out=plugins=grpc:. ./helloworld.proto 6 | -------------------------------------------------------------------------------- /grpcgcp/test_grpc/helloworld/helloworld.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | option go_package = "./helloworld"; 18 | option java_multiple_files = true; 19 | option java_package = "io.grpc.examples.helloworld"; 20 | option java_outer_classname = "HelloWorldProto"; 21 | 22 | package helloworld; 23 | 24 | // The greeting service definition. 25 | service Greeter { 26 | // Sends a greeting 27 | rpc SayHello (HelloRequest) returns (HelloReply) {} 28 | 29 | rpc RepeatHello (stream HelloRequest) returns (stream HelloReply) {} 30 | 31 | rpc InterruptedHello (stream HelloRequest) returns (stream HelloReply) {} 32 | } 33 | 34 | // The request message containing the user's name. 35 | message HelloRequest { 36 | string name = 1; 37 | } 38 | 39 | // The response message containing the greetings 40 | message HelloReply { 41 | string message = 1; 42 | } 43 | -------------------------------------------------------------------------------- /grpcgcp/test_grpc/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2023 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package test_grpc 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "io" 25 | "log" 26 | "net" 27 | "testing" 28 | "time" 29 | 30 | "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp" 31 | "google.golang.org/grpc" 32 | "google.golang.org/grpc/metadata" 33 | "google.golang.org/protobuf/encoding/protojson" 34 | 35 | configpb "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/grpc_gcp" 36 | pb "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/test_grpc/helloworld/helloworld" 37 | ) 38 | 39 | var ( 40 | port = 50051 41 | s = grpc.NewServer() 42 | apiConfigNoMethods = &configpb.ApiConfig{ 43 | ChannelPool: &configpb.ChannelPoolConfig{ 44 | MaxSize: 4, 45 | MaxConcurrentStreamsLowWatermark: 1, 46 | }, 47 | } 48 | apiConfig = &configpb.ApiConfig{ 49 | ChannelPool: apiConfigNoMethods.ChannelPool, 50 | Method: []*configpb.MethodConfig{ 51 | { 52 | Name: []string{ 53 | "/helloworld.Greeter/SayHello", 54 | "/helloworld.Greeter/InterruptedHello", 55 | "/helloworld.Greeter/RepeatHello", 56 | }, 57 | Affinity: &configpb.AffinityConfig{ 58 | Command: configpb.AffinityConfig_BIND, 59 | AffinityKey: "message", 60 | }, 61 | }, 62 | }, 63 | } 64 | tests = []struct { 65 | name string 66 | apicfg *configpb.ApiConfig 67 | }{ 68 | { 69 | name: "ApiConfig with methods", 70 | apicfg: apiConfig, 71 | }, 72 | { 73 | name: "ApiConfig without methods", 74 | apicfg: apiConfigNoMethods, 75 | }, 76 | } 77 | ) 78 | 79 | func TestMain(m *testing.M) { 80 | defer teardown() 81 | if err := setup(); err != nil { 82 | panic(fmt.Sprintf("Failed to setup: %v\n", err)) 83 | } 84 | m.Run() 85 | } 86 | 87 | func setup() error { 88 | fmt.Println("Setup started.") 89 | go func() { 90 | lis, err := net.Listen("tcp4", fmt.Sprintf(":%d", port)) 91 | if err != nil { 92 | log.Fatalf("failed to listen: %v", err) 93 | } 94 | pb.RegisterGreeterServer(s, &server{}) 95 | log.Printf("server listening at %v", lis.Addr()) 96 | if err := s.Serve(lis); err != nil { 97 | log.Fatalf("failed to serve: %v", err) 98 | } 99 | }() 100 | // Make sure the server is serving. 101 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) 102 | defer cancel() 103 | conn, err := grpc.DialContext(ctx, fmt.Sprintf("localhost:%d", port), grpc.WithInsecure()) 104 | if err != nil { 105 | return err 106 | } 107 | defer conn.Close() 108 | c := pb.NewGreeterClient(conn) 109 | if _, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"}); err != nil { 110 | return err 111 | } 112 | fmt.Println("Setup ended.") 113 | return nil 114 | } 115 | 116 | func teardown() { 117 | fmt.Println("Teardown started.") 118 | s.Stop() 119 | fmt.Println("Teardown ended.") 120 | } 121 | 122 | type server struct { 123 | pb.UnimplementedGreeterServer 124 | } 125 | 126 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 127 | m, _ := metadata.FromIncomingContext(ctx) 128 | header := metadata.Pairs("authority-was", m[":authority"][0]) 129 | grpc.SendHeader(ctx, header) 130 | return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil 131 | } 132 | 133 | func (s *server) RepeatHello(srv pb.Greeter_RepeatHelloServer) error { 134 | ctx := srv.Context() 135 | for { 136 | select { 137 | case <-ctx.Done(): 138 | return ctx.Err() 139 | default: 140 | } 141 | 142 | req, err := srv.Recv() 143 | if err == io.EOF { 144 | return nil 145 | } 146 | if err != nil { 147 | log.Printf("receive error %v", err) 148 | continue 149 | } 150 | 151 | if err := srv.Send(&pb.HelloReply{Message: "Hello " + req.GetName()}); err != nil { 152 | log.Printf("send error %v", err) 153 | } 154 | } 155 | } 156 | 157 | func (s *server) InterruptedHello(srv pb.Greeter_InterruptedHelloServer) error { 158 | ctx := srv.Context() 159 | for { 160 | select { 161 | case <-ctx.Done(): 162 | return ctx.Err() 163 | default: 164 | } 165 | 166 | // Receive data from stream and close the stream immediately. 167 | srv.Recv() 168 | return nil 169 | } 170 | } 171 | 172 | func getConn(config *configpb.ApiConfig, t *testing.T) (*grpc.ClientConn, error) { 173 | t.Helper() 174 | c, err := protojson.Marshal(config) 175 | if err != nil { 176 | t.Fatalf("cannot parse config: %v", err) 177 | } 178 | opts := []grpc.DialOption{ 179 | grpc.WithInsecure(), 180 | grpc.WithDisableServiceConfig(), 181 | grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":%s}]}`, grpcgcp.Name, string(c))), 182 | grpc.WithUnaryInterceptor(grpcgcp.GCPUnaryClientInterceptor), 183 | grpc.WithStreamInterceptor(grpcgcp.GCPStreamClientInterceptor), 184 | } 185 | return grpc.Dial("localhost:50051", opts...) 186 | } 187 | 188 | func TestUnaryCall(t *testing.T) { 189 | for _, test := range tests { 190 | t.Run(test.name, func(t *testing.T) { 191 | conn, err := getConn(test.apicfg, t) 192 | if err != nil { 193 | t.Fatalf("did not connect: %v", err) 194 | } 195 | defer conn.Close() 196 | c := pb.NewGreeterClient(conn) 197 | 198 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 199 | defer cancel() 200 | r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"}) 201 | if err != nil { 202 | t.Fatalf("could not greet: %v", err) 203 | } 204 | if r.GetMessage() != "Hello world" { 205 | t.Errorf("Expected Hello World, got %v", r.GetMessage()) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func TestStreamingCall(t *testing.T) { 212 | for _, test := range tests { 213 | t.Run(test.name, func(t *testing.T) { 214 | conn, err := getConn(test.apicfg, t) 215 | if err != nil { 216 | t.Fatalf("did not connect: %v", err) 217 | } 218 | defer conn.Close() 219 | c := pb.NewGreeterClient(conn) 220 | 221 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 222 | defer cancel() 223 | rhc, err := c.RepeatHello(ctx) 224 | if err != nil { 225 | t.Fatalf("could not start stream for RepeatHello: %v", err) 226 | } 227 | 228 | rhc.Send(&pb.HelloRequest{Name: "stream"}) 229 | 230 | r, err := rhc.Recv() 231 | if err != nil { 232 | t.Fatalf("could not get reply: %v", err) 233 | } 234 | 235 | if r.GetMessage() != "Hello stream" { 236 | t.Errorf("Expected Hello stream, got %v", r.GetMessage()) 237 | } 238 | 239 | if err := rhc.CloseSend(); err != nil { 240 | t.Fatalf("could not CloseSend: %v", err) 241 | } 242 | }) 243 | } 244 | } 245 | 246 | func TestStreamingCallNoResponse(t *testing.T) { 247 | for _, test := range tests { 248 | t.Run(test.name, func(t *testing.T) { 249 | conn, err := getConn(test.apicfg, t) 250 | if err != nil { 251 | t.Fatalf("did not connect: %v", err) 252 | } 253 | defer conn.Close() 254 | c := pb.NewGreeterClient(conn) 255 | 256 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 257 | defer cancel() 258 | rhc, err := c.InterruptedHello(ctx) 259 | if err != nil { 260 | t.Fatalf("could not start stream for InterruptedHello: %v", err) 261 | } 262 | 263 | rhc.Send(&pb.HelloRequest{Name: "stream"}) 264 | 265 | _, err = rhc.Recv() 266 | wantErr := "EOF" 267 | if err == nil || err.Error() != wantErr { 268 | t.Fatalf("Recv() got err %v, want err %v", err, wantErr) 269 | } 270 | }) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /grpcgcp_tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp_tests 2 | 3 | go 1.20 4 | 5 | require ( 6 | cloud.google.com/go/spanner v1.45.0 7 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.2.0 8 | google.golang.org/api v0.114.0 9 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 10 | google.golang.org/grpc v1.56.3 11 | google.golang.org/protobuf v1.30.0 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go v0.110.0 // indirect 16 | cloud.google.com/go/compute v1.19.1 // indirect 17 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 18 | cloud.google.com/go/iam v0.13.0 // indirect 19 | cloud.google.com/go/longrunning v0.4.1 // indirect 20 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect 23 | github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect 24 | github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f // indirect 25 | github.com/envoyproxy/protoc-gen-validate v0.10.1 // indirect 26 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 27 | github.com/golang/protobuf v1.5.3 // indirect 28 | github.com/google/go-cmp v0.5.9 // indirect 29 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 30 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 31 | go.opencensus.io v0.24.0 // indirect 32 | golang.org/x/net v0.17.0 // indirect 33 | golang.org/x/oauth2 v0.7.0 // indirect 34 | golang.org/x/sys v0.13.0 // indirect 35 | golang.org/x/text v0.13.0 // indirect 36 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 37 | google.golang.org/appengine v1.6.7 // indirect 38 | ) 39 | 40 | replace github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.2.0 => ../grpcgcp 41 | -------------------------------------------------------------------------------- /spanner_prober/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 2 | WORKDIR /go/src/github.com/GoogleCloudPlatform/grpc-gcp-go/spanner_prober/ 3 | COPY . . 4 | RUN CGO_ENABLED=0 go build -a -installsuffix cgo -v -o /go/src/github.com/GoogleCloudPlatform/grpc-gcp-go/spanner_prober ./... 5 | 6 | FROM alpine:latest 7 | RUN apk --no-cache add ca-certificates 8 | WORKDIR /spanner_prober/ 9 | COPY --from=0 /go/src/github.com/GoogleCloudPlatform/grpc-gcp-go/spanner_prober/spanner_prober ./ 10 | ENTRYPOINT [ "./spanner_prober" ] 11 | -------------------------------------------------------------------------------- /spanner_prober/Readme.md: -------------------------------------------------------------------------------- 1 | # gRPC-GCP Go Spanner prober 2 | 3 | The prober performs an operation (set by `probe_type`) at a stable rate (`qps`). 4 | 5 | All operations access the "ProbeTarget" table in the `database` in the `instance`. 6 | 7 | The prober will try to create the instance, database, and the table if any of them doesn't exist. 8 | 9 | For each operation, the prober picks a random number from 0 to `num_rows` and performs read and/or write operations with the selected number as the primary key. 10 | 11 | Each write call updates/inserts three columns: a primary key, a `payload_size`-bytes randomly generated payload, and a SHA256 checksum of the payload. 12 | 13 | ## Usage 14 | 15 | Build image: 16 | 17 | docker build -t spanner_prober:latest . 18 | 19 | Run prober: 20 | 21 | docker run --rm -v :/gcp-creds.json --env GOOGLE_APPLICATION_CREDENTIALS=/gcp-creds.json spanner_prober:latest --project= --qps=0.5 --probe_type=read_write 22 | 23 | ## Arguments 24 | 25 | - enable_cloud_ops - Export metrics to Cloud Operations (former Stackdriver). (Default: true) 26 | - project - GCP project for Cloud Spanner. 27 | - ops_project - Cloud Operations project if differs from Spanner project. 28 | - instance - Target instance. (Default: "test1") 29 | - database - Target database. (Default: "test1") 30 | - instance_config - Target instance config (Default: "regional-us-central1") 31 | - node_count - Node count for the prober. If specified, processing_units must be 0. (Default: 1) 32 | - processing_units - Processing units for the prober. If specified, node_count must be 0. (Default: 0) 33 | - qps - QPS to probe per prober [1, 1000]. (Default: 1) 34 | - num_rows - Number of rows in database to be probed. (Default: 1000) 35 | - probe_type - The probe type this prober will run. (Default: "noop") 36 | - max_staleness - Maximum staleness for stale queries. (Default: 15s) 37 | - payload_size - Size of payload to write to the probe database. (Default: 1024) 38 | - probe_deadline - Deadline for probe request. (Default: 10s) 39 | - endpoint - Cloud Spanner Endpoint to send request to. 40 | 41 | ## Disabling automatic resource detection 42 | 43 | If running on GCE/GKE the metrics will be shipped to corresponding "VM Instance"/"Kubernetes Container" resource in Cloud Monitoring. To disable this and use global resource set environment variable `OC_RESOURCE_TYPE=global`. 44 | 45 | ## Probe types 46 | 47 | | probe_type | Description | 48 | | ------------ | -------------------------------------------------------------- | 49 | | noop | no operation. | 50 | | stale_read | read-only txn `Read` with timestamp bound. | 51 | | strong_query | read-only txn `Query`. | 52 | | stale_query | read-only txn `Query` with timestamp bound. | 53 | | dml | read-write txn with `Query` followed by `Update`. | 54 | | read_write | read-write txn with `Read` followed by `BufferWrite` mutation. | 55 | 56 | ## Reported metrics 57 | 58 | All metrics are prefixed with `custom.googleapis.com/opencensus/grpc_gcp_spanner_prober/` 59 | 60 | `op_name` label 61 | 62 | | Metric | Unit | Kind | Value | Decription 63 | | ------------ | ---- | ---------- | ------------ | ---------- 64 | | **Prober specific** 65 | | op_count | 1 | Cumulative | Int64 | Operation count. Labeled by: op_name, result. 66 | | op_latency | ms | Cumulative | Distribution | Operation latency. Labeled by: op_name, result. 67 | | t4t7_latency | ms | Cumulative | Distribution | GFE latency. Labeled by: rpc_type (unary/streaming), grpc_client_method 68 | | **Opencensus default gRPC metrics** | | | | additional prefix `grpc.io/client/` 69 | | completed_rpcs | 1 | Cumulative | Int64 | Count of RPCs by method and status. 70 | | received_bytes_per_rpc | byte | Cumulative | Distribution | Distribution of bytes received per RPC, by method. 71 | | roundtrip_latency | ms | Cumulative | Distribution | Distribution of round-trip latency, by method. 72 | | sent_bytes_per_rpc | byte | Cumulative | Distribution | Distribution of bytes sent per RPC, by method. 73 | | **From Spanner client** | | | | additional prefix `cloud.google.com/go/spanner/` Labels: client_id, instance_id, database, library_version 74 | | max_allowed_sessions | 1 | Gauge | Int64 | The maximum number of sessions allowed. Configurable by the user. 75 | | max_in_use_sessions | 1 | Gauge | Int64 | The maximum number of sessions in use during the last 10 minute interval. 76 | | num_acquired_sessions | 1 | Cumulative | Int64 | The number of sessions acquired from the session pool. 77 | | num_released_sessions | 1 | Cumulative | Int64 | The number of sessions released by the user and pool maintainer. 78 | | num_sessions_in_pool | 1 | Gauge | Int64 | The number of sessions currently in use. Labeled by type. 79 | | open_session_count | 1 | Gauge | Int64 | Number of sessions currently opened. 80 | -------------------------------------------------------------------------------- /spanner_prober/go.mod: -------------------------------------------------------------------------------- 1 | module spanner_prober 2 | 3 | go 1.17 4 | 5 | require ( 6 | cloud.google.com/go/spanner v1.45.0 7 | contrib.go.opencensus.io/exporter/stackdriver v0.13.14 8 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.3.0 9 | github.com/golang/glog v1.1.0 10 | go.opencensus.io v0.24.0 11 | google.golang.org/api v0.114.0 12 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 13 | google.golang.org/grpc v1.56.3 14 | google.golang.org/protobuf v1.30.0 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.110.0 // indirect 19 | cloud.google.com/go/compute v1.19.1 // indirect 20 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 21 | cloud.google.com/go/iam v0.13.0 // indirect 22 | cloud.google.com/go/longrunning v0.4.1 // indirect 23 | cloud.google.com/go/monitoring v1.13.0 // indirect 24 | cloud.google.com/go/trace v1.9.0 // indirect 25 | github.com/aws/aws-sdk-go v1.43.31 // indirect 26 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 28 | github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect 29 | github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect 30 | github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f // indirect 31 | github.com/envoyproxy/protoc-gen-validate v0.10.1 // indirect 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 33 | github.com/golang/protobuf v1.5.3 // indirect 34 | github.com/google/go-cmp v0.5.9 // indirect 35 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 36 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 37 | github.com/jmespath/go-jmespath v0.4.0 // indirect 38 | github.com/prometheus/prometheus v0.35.0 // indirect 39 | golang.org/x/net v0.17.0 // indirect 40 | golang.org/x/oauth2 v0.7.0 // indirect 41 | golang.org/x/sync v0.1.0 // indirect 42 | golang.org/x/sys v0.13.0 // indirect 43 | golang.org/x/text v0.13.0 // indirect 44 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 45 | google.golang.org/appengine v1.6.7 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /spanner_prober/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/signal" 10 | "regexp" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | proberlib "spanner_prober/prober" 16 | 17 | "cloud.google.com/go/spanner" 18 | "contrib.go.opencensus.io/exporter/stackdriver" 19 | "contrib.go.opencensus.io/exporter/stackdriver/monitoredresource" 20 | log "github.com/golang/glog" 21 | "go.opencensus.io/plugin/ocgrpc" 22 | "go.opencensus.io/resource" 23 | "go.opencensus.io/stats/view" 24 | "google.golang.org/grpc/grpclog" 25 | ) 26 | 27 | var ( 28 | enableCloudOps = flag.Bool("enable_cloud_ops", true, "Export metrics to Cloud Operations (former Stackdriver).") 29 | project = flag.String("project", "", "GCP project for Cloud Spanner.") 30 | opsProject = flag.String("ops_project", "", "Cloud Operations project if differs from Spanner project.") 31 | instance_name = flag.String("instance", "test1", "Target instance.") 32 | database_name = flag.String("database", "test1", "Target database.") 33 | instanceConfig = flag.String("instance_config", "regional-us-central1", "Target instance config.") 34 | nodeCount = flag.Int("node_count", 1, "Node count for the prober. If specified, processing_units must be 0.") 35 | processingUnits = flag.Int("processing_units", 0, "Processing units for the prober. If specified, node_count must be 0.") 36 | qps = flag.Float64("qps", 1, "QPS to probe per prober [1, 1000].") 37 | numRows = flag.Int("num_rows", 1000, "Number of rows in database to be probed.") 38 | probeType = flag.String("probe_type", "noop", "The probe type this prober will run.") 39 | maxStaleness = flag.Duration("max_staleness", 15*time.Second, "Maximum staleness for stale queries.") 40 | payloadSize = flag.Int("payload_size", 1024, "Size of payload to write to the probe database.") 41 | probeDeadline = flag.Duration("probe_deadline", 10*time.Second, "Deadline for probe request.") 42 | useGrpcGcp = flag.Bool("grpc_gcp", false, "Use gRPC-GCP library.") 43 | channelPoolSize = flag.Int("channels", 2, "Number of channels.") 44 | endpoint = flag.String("endpoint", "spanner.googleapis.com:443", "Cloud Spanner Endpoint to send request to.") 45 | ) 46 | 47 | func main() { 48 | flag.Parse() 49 | ctx := context.Background() 50 | 51 | errs := validateFlags() 52 | if len(errs) > 0 { 53 | log.Errorf("Flag validation failed with %v errors", len(errs)) 54 | for _, err := range errs { 55 | log.Errorf("%v", err) 56 | } 57 | log.Exit("Flag validation failed... exiting.") 58 | } 59 | 60 | fmt.Printf("Prober started with options:\nenable_cloud_ops: %v\nproject: %q\n"+ 61 | "ops_project: %q\ninstance: %q\ndatabase: %q\ninstance_config: %q\n"+ 62 | "node_count: %d\nprocessing_units: %d\nqps: %0.3f\nnum_rows: %d\n"+ 63 | "probe_type: %q\nmax_staleness: %v\npayload_size: %d\nprobe_deadline: %v\n"+ 64 | "grpc_gcp: %v\nchannels: %v\nendpoint: %q\n", *enableCloudOps, *project, *opsProject, *instance_name, 65 | *database_name, *instanceConfig, *nodeCount, *processingUnits, *qps, *numRows, 66 | *probeType, *maxStaleness, *payloadSize, *probeDeadline, *useGrpcGcp, *channelPoolSize, 67 | *endpoint) 68 | 69 | grpclog.SetLoggerV2(grpclog.NewLoggerV2(ioutil.Discard, /* Discard logs at INFO level */ 70 | os.Stderr, os.Stderr)) 71 | 72 | if *enableCloudOps { 73 | // Set up the stackdriver exporter for sending metrics. 74 | 75 | // Register gRPC views. 76 | if err := view.Register(ocgrpc.DefaultClientViews...); err != nil { 77 | log.Fatalf("Failed to register ocgrpc client views: %v", err) 78 | } 79 | 80 | // Enable all default views for Cloud Spanner. 81 | if err := spanner.EnableStatViews(); err != nil { 82 | log.Errorf("Failed to export stats view: %v", err) 83 | } 84 | 85 | getPrefix := func(name string) string { 86 | if strings.HasPrefix(name, proberlib.MetricPrefix) { 87 | return "" 88 | } 89 | return proberlib.MetricPrefix 90 | } 91 | exporterOptions := stackdriver.Options{ 92 | ProjectID: *project, 93 | BundleDelayThreshold: 60 * time.Second, 94 | BundleCountThreshold: 3000, 95 | GetMetricPrefix: getPrefix, 96 | } 97 | if *opsProject != "" { 98 | exporterOptions.ProjectID = *opsProject 99 | } 100 | if os.Getenv(resource.EnvVarType) == "" { 101 | exporterOptions.MonitoredResource = &MonitoredResource{delegate: monitoredresource.Autodetect()} 102 | } 103 | sd, err := stackdriver.NewExporter(exporterOptions) 104 | 105 | if err != nil { 106 | log.Fatalf("Failed to create the StackDriver exporter: %v", err) 107 | } 108 | defer sd.Flush() 109 | sd.StartMetricsExporter() 110 | defer sd.StopMetricsExporter() 111 | } 112 | 113 | prober, err := proberlib.ParseProbeType(*probeType) 114 | if err != nil { 115 | log.Exitf("Could not create prober due to %v.", err) 116 | } 117 | 118 | opts := proberlib.ProberOptions{ 119 | Project: *project, 120 | Instance: *instance_name, 121 | Database: *database_name, 122 | InstanceConfig: *instanceConfig, 123 | QPS: *qps, 124 | NumRows: *numRows, 125 | Prober: prober, 126 | MaxStaleness: *maxStaleness, 127 | PayloadSize: *payloadSize, 128 | ProbeDeadline: *probeDeadline, 129 | Endpoint: *endpoint, 130 | NodeCount: *nodeCount, 131 | ProcessingUnits: *processingUnits, 132 | UseGrpcGcp: *useGrpcGcp, 133 | ChannelPoolSize: *channelPoolSize, 134 | } 135 | 136 | p, err := proberlib.NewProber(ctx, opts) 137 | if err != nil { 138 | log.Exitf("Failed to initialize the cloud prober, %v", err) 139 | } 140 | p.Start(ctx) 141 | 142 | cancelChan := make(chan os.Signal, 1) 143 | signal.Notify(cancelChan, syscall.SIGTERM, syscall.SIGINT) 144 | 145 | select { 146 | case <-cancelChan: 147 | } 148 | } 149 | 150 | type MonitoredResource struct { 151 | monitoredresource.Interface 152 | 153 | delegate monitoredresource.Interface 154 | } 155 | 156 | func (mr *MonitoredResource) MonitoredResource() (resType string, labels map[string]string) { 157 | dType, dLabels := mr.delegate.MonitoredResource() 158 | resType = dType 159 | labels = make(map[string]string) 160 | for k, v := range dLabels { 161 | if k == "project_id" { 162 | // Overwrite project id to satisfy Cloud Monitoring rule. 163 | labels[k] = *project 164 | if *opsProject != "" { 165 | labels[k] = *opsProject 166 | } 167 | continue 168 | } 169 | labels[k] = v 170 | } 171 | return 172 | } 173 | 174 | func validateFlags() []error { 175 | var errs []error 176 | 177 | projectRegex, err := regexp.Compile(`^[-_:.a-zA-Z0-9]*$`) 178 | if err != nil { 179 | return []error{err} 180 | } 181 | instanceDBRegex, err := regexp.Compile(`^[-_.a-zA-Z0-9]*$`) 182 | if err != nil { 183 | return []error{err} 184 | } 185 | 186 | // We limit qps to < 1000 to ensure we don't overload Spanner accidentally. 187 | if *qps <= 0 || *qps > 1000 { 188 | errs = append(errs, fmt.Errorf("qps must be 1 <= qps <= 1000, was %v", *qps)) 189 | } 190 | 191 | if *numRows <= 0 { 192 | errs = append(errs, fmt.Errorf("num_rows must be > 0, was %v", *numRows)) 193 | } 194 | 195 | if *payloadSize <= 0 { 196 | errs = append(errs, fmt.Errorf("payload_size must be > 0, was %v", *payloadSize)) 197 | } 198 | 199 | if matched := projectRegex.MatchString(*project); !matched { 200 | errs = append(errs, fmt.Errorf("project did not match %v, was %v", projectRegex, *project)) 201 | } 202 | 203 | if matched := projectRegex.MatchString(*opsProject); !matched { 204 | errs = append(errs, fmt.Errorf("ops_project did not match %v, was %v", projectRegex, *opsProject)) 205 | } 206 | 207 | if matched := instanceDBRegex.MatchString(*instance_name); !matched { 208 | errs = append(errs, fmt.Errorf("instance did not match %v, was %v", instanceDBRegex, *instance_name)) 209 | } 210 | 211 | if matched := instanceDBRegex.MatchString(*database_name); !matched { 212 | errs = append(errs, fmt.Errorf("database did not match %v, was %v", instanceDBRegex, *database_name)) 213 | } 214 | 215 | if matched := instanceDBRegex.MatchString(*instanceConfig); !matched { 216 | errs = append(errs, fmt.Errorf("instance_config did not match %v, was %v", instanceDBRegex, *instanceConfig)) 217 | } 218 | 219 | if _, err := proberlib.ParseProbeType(*probeType); err != nil { 220 | errs = append(errs, err) 221 | } 222 | 223 | return errs 224 | } 225 | -------------------------------------------------------------------------------- /spanner_prober/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | proberlib "spanner_prober/prober" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestValidFlags(t *testing.T) { 11 | var tests = []struct { 12 | name string 13 | project string 14 | instance string 15 | database string 16 | instanceConfig string 17 | qps float64 18 | numRows int 19 | probeType string 20 | maxStaleness time.Duration 21 | payloadSize int 22 | expectedErrors int 23 | }{ 24 | { 25 | name: "good flags", 26 | project: "google.com:abc", 27 | instance: "abc", 28 | database: "abc", 29 | instanceConfig: "regional-test-1", 30 | qps: 1, 31 | numRows: 1, 32 | probeType: "noop", 33 | maxStaleness: 5 * time.Second, 34 | payloadSize: 1024, 35 | expectedErrors: 0, 36 | }, 37 | { 38 | name: "uris not names", 39 | project: "projects/google.com:abc", 40 | instance: "projects/google.com:abc/instances/test-instance", 41 | database: "projects/google.com:abc/instances/test-instance/databases/test-database", 42 | instanceConfig: "projects/google.com:abc/instanceConfigs/regional-test-instance", 43 | qps: 1, 44 | numRows: 1, 45 | probeType: "noop", 46 | payloadSize: 1, 47 | expectedErrors: 4, 48 | }, 49 | { 50 | name: "invalid options", 51 | project: "+abc", 52 | instance: "abc!", 53 | database: "abc=", 54 | instanceConfig: "", 55 | qps: 0, 56 | numRows: 0, 57 | probeType: "notaprobe", 58 | payloadSize: -1, 59 | expectedErrors: 7, 60 | }, 61 | } 62 | 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | *project = tt.project 66 | *instance_name = tt.instance 67 | *database_name = tt.database 68 | *instanceConfig = tt.instanceConfig 69 | *qps = tt.qps 70 | *numRows = tt.numRows 71 | *probeType = tt.probeType 72 | *maxStaleness = tt.maxStaleness 73 | *payloadSize = tt.payloadSize 74 | 75 | errs := validateFlags() 76 | if len(errs) != tt.expectedErrors { 77 | t.Errorf("validateFlags() got %v errors, want %v errors: %q", len(errs), tt.expectedErrors, errs) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestParseProbeType(t *testing.T) { 84 | var tests = []struct { 85 | name string 86 | flag string 87 | probeType proberlib.Probe 88 | wantErr bool 89 | }{ 90 | { 91 | name: "good type", 92 | flag: "stale_read", 93 | probeType: proberlib.StaleReadProbe{}, 94 | }, 95 | { 96 | name: "bad type", 97 | flag: "not_a_probe", 98 | probeType: proberlib.NoopProbe{}, 99 | wantErr: true, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | result, err := proberlib.ParseProbeType(tt.flag) 105 | 106 | if (err != nil) != tt.wantErr { 107 | t.Errorf("parseProbeType(%q) = error %v, wantErr %t", tt.flag, err, tt.wantErr) 108 | } 109 | if got, want := reflect.TypeOf(result), reflect.TypeOf(tt.probeType); got != want { 110 | t.Errorf("parseProbeType(%q) = %v, want %v", tt.flag, got, want) 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /spanner_prober/prober/interceptors.go: -------------------------------------------------------------------------------- 1 | // Package prober defines a Cloud Spanner prober with interceptors. 2 | package prober 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "go.opencensus.io/stats" 12 | "go.opencensus.io/stats/view" 13 | "go.opencensus.io/tag" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/metadata" 16 | ) 17 | 18 | const gfeT4T7prefix = "gfet4t7; dur=" 19 | const serverTimingKey = "server-timing" 20 | 21 | var ( 22 | expDistribution = []float64{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288} 23 | 24 | methodTag = tag.MustNewKey("grpc_client_method") 25 | rpcTypeTag = tag.MustNewKey("rpc_type") 26 | 27 | t4t7Latency = stats.Int64( 28 | "t4t7_latency", 29 | "gRPC-GCP Spanner prober GFE latency", 30 | stats.UnitMilliseconds, 31 | ) 32 | t4t7LatencyView = &view.View{ 33 | Name: MetricPrefix + t4t7Latency.Name(), 34 | Measure: t4t7Latency, 35 | Aggregation: view.Distribution(expDistribution...), 36 | TagKeys: []tag.Key{opNameTag, methodTag, rpcTypeTag}, 37 | } 38 | ) 39 | 40 | func init() { 41 | view.Register(t4t7LatencyView) 42 | } 43 | 44 | func recordLatency(ctx context.Context, method string, latency time.Duration) { 45 | stats.RecordWithTags( 46 | ctx, 47 | []tag.Mutator{tag.Insert(methodTag, method)}, 48 | t4t7Latency.M(latency.Milliseconds()), 49 | ) 50 | } 51 | 52 | // parseT4T7Latency parse the headers and trailers for finding the gfet4t7 latency. 53 | func parseT4T7Latency(headers, trailers metadata.MD) (time.Duration, error) { 54 | var serverTiming []string 55 | 56 | if len(headers[serverTimingKey]) > 0 { 57 | serverTiming = headers[serverTimingKey] 58 | } else if len(trailers[serverTimingKey]) > 0 { 59 | serverTiming = trailers[serverTimingKey] 60 | } else { 61 | return 0, fmt.Errorf("server-timing headers not found") 62 | } 63 | for _, entry := range serverTiming { 64 | if !strings.HasPrefix(entry, gfeT4T7prefix) { 65 | continue 66 | } 67 | durationText := strings.TrimPrefix(entry, gfeT4T7prefix) 68 | durationMillis, err := strconv.ParseInt(durationText, 10, 64) 69 | if err != nil { 70 | return 0, fmt.Errorf("failed to parse gfe latency: %v", err) 71 | } 72 | return time.Duration(durationMillis) * time.Millisecond, nil 73 | } 74 | return 0, fmt.Errorf("no gfe latency response available") 75 | } 76 | 77 | // AddGFELatencyUnaryInterceptor intercepts unary client requests (spanner.Commit, spanner.ExecuteSQL) and annotates GFE latency. 78 | func AddGFELatencyUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, 79 | invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 80 | 81 | var headers, trailers metadata.MD 82 | opts = append(opts, grpc.Header(&headers)) 83 | opts = append(opts, grpc.Trailer(&trailers)) 84 | if err := invoker(ctx, method, req, reply, cc, opts...); err != nil { 85 | return err 86 | } 87 | 88 | if gfeLatency, err := parseT4T7Latency(headers, trailers); err == nil { 89 | if ctx, err := tag.New(ctx, tag.Insert(rpcTypeTag, "unary")); err == nil { 90 | recordLatency(ctx, method, gfeLatency) 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // AddGFELatencyStreamingInterceptor intercepts streaming requests StreamingSQL and annotates GFE latency. 97 | func AddGFELatencyStreamingInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { 98 | cs, err := streamer(ctx, desc, cc, method, opts...) 99 | if err != nil { 100 | return cs, err 101 | } 102 | 103 | go func() { 104 | headers, err := cs.Header() 105 | if err != nil { 106 | return 107 | } 108 | trailers := cs.Trailer() 109 | if gfeLatency, err := parseT4T7Latency(headers, trailers); err == nil { 110 | if ctx, err := tag.New(ctx, tag.Insert(rpcTypeTag, "streaming")); err == nil { 111 | recordLatency(ctx, method, gfeLatency) 112 | } 113 | } 114 | }() 115 | 116 | return cs, nil 117 | } 118 | --------------------------------------------------------------------------------