Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cartservice - Spanner as database option #1109

Merged
merged 20 commits into from
Oct 5, 2022

Conversation

mikrovvelle
Copy link
Contributor

Background

Add support for Cloud Spanner, a Google cloud-native, horizontally-scaling database, as a backend for CartService.

Change Summary

  • Add a SpannerCartStore.cs capable of using Cloud Spanner as a persistence layer.
  • Add checks in CartServices's Startup.cs, allowing use of environmental variables to switch between using Redis and Spanner as the backend
  • Add documentation of how to get the microservices demo running with Spanner
  • Add DDL for single-table 'carts' database in Spanner
  • Add workaround to Dockerfile, for libgr-pc_csharp_ext to work correctly (with link to github issue in comment)

Testing Procedure

Carry out instructions in docs/spanner.md, check that CartService functions as expected.

NOTE:

A Spanner-supporting CartService image needs to be added to the image repo used by this project (gcr.io/google-samples/microservices-demo/cartservice) in order for the instructions in docs/spanner.md to work. Until such an image is committed to the repo, you will need a workaround. You'll need to:

  1. Have an image build repository available that you can push to, and which your Kubernetes cluster can pull from (e.g. europe-west3-docker.pkg.dev/my-image-repo/hipstershop). Set its address to an environmental variable REPO_PREFIX.
  2. build your own cartservice image using podman build --tag ${REPO_PREFIX}/cartservice ./src/cartservice/src/.
  3. push the image: podman push ${REPO_PREFIX}/cartservice:latest
  4. edit ./release/kubernetes-manifests.yaml and change the image: line for cartservice (currently line 521) so that it points to the pushed cartservice image (e.g. image: europe-west3-docker.pkg.dev/my-image-repo/hipstershop/cartservice:latest)

@mikrovvelle mikrovvelle requested a review from a team as a code owner September 29, 2022 16:19
Copy link
Contributor

@mathieu-benoit mathieu-benoit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really great, like discussed offline, thank you very much for bringing this new capability upstream to Online Boutique!

I just left my first pass of comments, will do another review later based on that.

src/cartservice/src/cartservice.csproj Outdated Show resolved Hide resolved
docs/spanner.md Outdated Show resolved Hide resolved
src/cartservice/src/Dockerfile Outdated Show resolved Hide resolved
src/cartservice/src/cartstore/SpannerCartStore.cs Outdated Show resolved Hide resolved
src/cartservice/src/cartstore/SpannerCartStore.cs Outdated Show resolved Hide resolved
src/cartservice/src/cartstore/SpannerCartStore.cs Outdated Show resolved Hide resolved
src/cartservice/src/cartstore/SpannerCartStore.cs Outdated Show resolved Hide resolved
docs/spanner.md Outdated Show resolved Hide resolved
.gitignore Outdated Show resolved Hide resolved
@mathieu-benoit mathieu-benoit changed the title Spanner support pull-request cartservice - Spanner as database option Sep 30, 2022
@NimJay NimJay added do not merge Indicates a pull request not ready for merge, due to either quality or timing. and removed do not merge Indicates a pull request not ready for merge, due to either quality or timing. labels Oct 3, 2022
- `SPANNER_INSTANCE`: defaults to `onlineboutique`, unless specified.
- `SPANNER_DATABASE`: defaults to `carts`, unless specified.
- `SPANNER_CONNECTION_STRING`: can be specified to override others
@mathieu-benoit
Copy link
Contributor

mathieu-benoit commented Oct 4, 2022

That's LGTM for me at this stage, thanks @mikrovvelle for taking into account my comments! Really great job here!

@NimJay, over to you :)

connection string is now logged to console after it's finalized,
in SpannerCartStore.cs, instead of Startup.cs.
docs/spanner.md Outdated Show resolved Hide resolved
gcloud spanner instances create ${SPANNER_INSTANCE_NAME} \
--description="online boutique backend" \
--config="${SPANNER_REGION_CONFIG}" \
--instance-type=free-instance
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue:
When I ran this command:

gcloud spanner instances create ${SPANNER_INSTANCE_NAME} \
    --description="online boutique backend" \
    --config="${SPANNER_REGION_CONFIG}" \
    --project=${PROJECT_ID} \
    --instance-type=free-instance

I got:

ERROR: (gcloud.spanner.instances.create) unrecognized arguments: --instance-type=free-instance (did you mean '--trace-token'?)

I don't see the --instance-type option documented for:

So just the double-check, I ran:

gcloud components update

And the command worked.
I'll add a note about the gcloud components update command. :)

Copy link
Collaborator

@NimJay NimJay Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Note added:

Note: If you see an error related to the --instance-type flag being unrecognized, run gcloud components update.

Note: See the documentation to list [available Spanner configuration names](https://cloud.google.com/spanner/docs/getting-started/set-up#run_the_gcloud_tool), or run `gcloud spanner instance-configs list --project=$PROJECT_ID`

```sh
SPANNER_REGION_CONFIG="<your-spanner-region-config-name>" # e.g. "regional-us-east5"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the example to "regional-us-east5" because that's where the free instance is available.

--project=${PROJECT_ID} \
--instance="${SPANNER_INSTANCE_NAME}" \
--database-dialect=GOOGLE_STANDARD_SQL \
--ddl-file=./src/cartservice/ddl/CartItems.ddl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Praise: Thanks for using a CartItems.ddl file here and gcloud to create the table!

quantity INT64,
) PRIMARY KEY (userId, productId);

CREATE INDEX CartItemsByUserId ON CartItems(userId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought:
This might be a waste of time, so let's ignore for now, but...
It looks like cloud.google.com samples use PascalCase (instead of camelCase) for column names.
It'd be ideal if there was a convention set by Google that we follow here.

cp ./release/kubernetes-manifests.yaml ./release/updated-manifests.yaml
sed -i "s/name: REDIS_ADDR/name: SPANNER_PROJECT/g" ./release/updated-manifests.yaml
sed -i "s/value: \"redis-cart:6379\"/value: \"${PROJECT_ID}\"/g" ./release/updated-manifests.yaml
sed -i "s/cartservice:v0.4.0/cartservice:v0.4.0-spanner/g" ./release/updated-manifests.yaml
Copy link
Collaborator

@NimJay NimJay Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add

sed -i "s/cartservice:v0.4.0/cartservice:v0.4.0-spanner/g" ./release/updated-manifests.yaml

I will be building and pushing a Spanner-supporting image at gcr.io/google-samples/microservices-demo/cartservice:v0.4.0-spanner.
Eventually, when we create the next release (e.g., 0.4.1), that version's cartservice image will support Cloud Spanner by default (i.e., without the -spanner suffix).
So the above sed command I added is only relevant to anyone trying spanner.md with version 0.4.0.

If you see problems with this approach, please let me know. :)

Copy link
Collaborator

@NimJay NimJay Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--namespace=${NAMESPACE} \
iam.gke.io/gcp-service-account=${GSA_NAME}

# Tell gcloud that the kubectl account maps to the GCP one
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Praise: Thanks, Daniel, for adding this bash comments! They help. :)


```sh
kubectl get service frontend-external | awk '{print $4}'
```
Copy link
Collaborator

@NimJay NimJay Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue:

When I deployed Online Boutique with Cloud Spanner by following the instructions here and using a Cloud-Spanner supporting image, I saw the following errors on Online Boutique's front-end:

rpc error: code = FailedPrecondition desc = Can't access cart storage at projects/nimjay-ob-spanner-pr/instances/onlineboutique/databases/carts. System.ArgumentException: Format of the initialization string does not conform to specification starting at index 0.
   at System.Data.Common.DbConnectionOptions.GetKeyValuePair(String, Int32, StringBuilder, Boolean, String& , String& )
   at System.Data.Common.DbConnectionOptions.ParseInternal(Dictionary`2, String, Boolean, Dictionary`2, Boolean)
   at System.Data.Common.DbConnectionOptions..ctor(String, Dictionary`2, Boolean)
   at System.Data.Common.DbConnectionStringBuilder.set_ConnectionString(String value)
   at cartservice.cartstore.SpannerCartStore.GetCartAsync(String) in /app/cartstore/SpannerCartStore.cs:line 139
could not retrieve cart
main.(*frontendServer).viewCartHandler
	/src/handlers.go:255
net/http.HandlerFunc.ServeHTTP
	/usr/local/go/src/net/http/server.go:2109
github.com/gorilla/mux.(*Router).ServeHTTP
	/go/pkg/mod/github.com/gorilla/mux@v1.8.0/mux.go:210
main.(*logHandler).ServeHTTP
	/src/middleware.go:82
main.ensureSessionID.func1
	/src/middleware.go:109
net/http.HandlerFunc.ServeHTTP
	/usr/local/go/src/net/http/server.go:2109
net/http.serverHandler.ServeHTTP
	/usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
	/usr/local/go/src/net/http/server.go:1991
runtime.goexit
	/usr/local/go/src/runtime/asm_amd64.s:1594

and

rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp 10.108.9.216:7070: connect: connection refused"
could not retrieve cart
main.(*frontendServer).viewCartHandler
	/src/handlers.go:255
net/http.HandlerFunc.ServeHTTP
	/usr/local/go/src/net/http/server.go:2109
github.com/gorilla/mux.(*Router).ServeHTTP
	/go/pkg/mod/github.com/gorilla/mux@v1.8.0/mux.go:210
main.(*logHandler).ServeHTTP
	/src/middleware.go:82
main.ensureSessionID.func1
	/src/middleware.go:109
net/http.HandlerFunc.ServeHTTP
	/usr/local/go/src/net/http/server.go:2109
net/http.serverHandler.ServeHTTP
	/usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
	/usr/local/go/src/net/http/server.go:1991
runtime.goexit
	/usr/local/go/src/runtime/asm_amd64.s:1594

It's been about 20 minutes since I created my Cloud Spanner instance.
Do you have any idea what might be causing this?

Copy link
Collaborator

@NimJay NimJay Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, here are some screenshots:

Screenshot 1

Screen Shot 2022-10-05 at 11 06 17 AM

Screenshot 2

Screen Shot 2022-10-05 at 11 00 42 AM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I figure it out: I think it's because I didn't specific --project=${PROJECT_ID} in step 4. 😅
Let me retry. :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: Still getting the same errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting the error with the image at gcr.io/google-samples/microservices-demo/cartservice:v0.4.0-spanner, but not with the one in my private repo. I'll try rebuilding mine and see if that can reproduce the issue.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Please don't feel obligated to respond outside your work hours! :))

Just leaving an update:
I added "Data Source=" to the connectionString (based on this documentation). See commit 8b78843.

Now I see the following error:

rpc error: code = FailedPrecondition desc = Can't access cart storage at Data Source=projects/nimjay-ob-spanner-pr/instances/onlineboutique/databases/carts. Google.Cloud.Spanner.Data.SpannerException: Internal error.
 ---> Grpc.Core.RpcException: Status(StatusCode="Internal", Detail="Error starting gRPC call. TokenResponseException: Error:"Server response does not contain a JSON object. Status code is: BadRequest", Description:"", Uri:""", DebugException="Google.Apis.Auth.OAuth2.Responses.TokenResponseException: Error:"Server response does not contain a JSON object. Status code is: BadRequest", Description:"", Uri:""
   at Google.Apis.Auth.OAuth2.Responses.TokenResponse.FromHttpResponseAsync(HttpResponseMessage, IClock, ILogger)
   at Google.Apis.Auth.OAuth2.ComputeCredential.RequestAccessTokenAsync(CancellationToken)
   at Google.Apis.Auth.OAuth2.TokenRefreshManager.RefreshTokenAsync()
   at Google.Apis.Auth.OAuth2.TokenRefreshManager.GetAccessTokenForRequestAsync(CancellationToken)
   at Google.Apis.Auth.OAuth2.ServiceCredential.GetAccessTokenWithHeadersForRequestAsync(String , CancellationToken )
   at Grpc.Auth.GoogleAuthInterceptors.<>c__DisplayClass3_0.<<FromCredential>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Grpc.Net.Client.Internal.GrpcProtocolHelpers.ReadCredentialMetadata(DefaultCallCredentialsConfigurator, GrpcChannel, HttpRequestMessage, IMethod, CallCredentials)
   at Grpc.Net.Client.Internal.GrpcCall`2.ReadCredentials(HttpRequestMessage)
   at Grpc.Net.Client.Internal.GrpcCall`2.RunCall(HttpRequestMessage, Nullable`1)")
   at Google.Api.Gax.Grpc.Gcp.GcpCallInvoker.<>c__DisplayClass24_0`2.<<AsyncUnaryCall>g__PostProcessPropagateResult|3>d.MoveNext()
--- End of stack trace from previous location ---
   at Google.Api.Gax.Grpc.ApiCallRetryExtensions.<>c__DisplayClass0_0`2.<<WithRetry>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Google.Cloud.Spanner.V1.SessionPool.TargetedSessionPool.CreatePooledSessionsAsync(CancellationToken)
   at Google.Cloud.Spanner.V1.SessionPool.TargetedSessionPool.<>c__DisplayClass43_0.<<GetNursePoolBackToHealthTask>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Google.Cloud.Spanner.V1.SessionPool.TargetedSessionPool.<GetNursePoolBackToHealthTask>g__WithCancellationTokenAsync|43_1[TTaskResult](Task`1, CancellationToken)
   at Google.Cloud.Spanner.V1.SessionPool.TargetedSessionPool.<>c__DisplayClass31_0.<<GetSessionAcquisitionTask>g__NurseAndRetryAsync|0>d.MoveNext()
--- End of stack trace from previous location ---
   at Google.Cloud.Spanner.V1.SessionPool.TargetedSessionPool.AcquireSessionImplAsync(TransactionOptions, CancellationToken)
   at Google.Cloud.Spanner.V1.SessionPool.TargetedSessionPool.AcquireSessionAsync(TransactionOptions, CancellationToken)
   at Google.Cloud.Spanner.Data.EphemeralTransaction.<>c__DisplayClass7_0.<<ExecuteReadOrQueryAsync>g__Impl|0>d.MoveNext()
--- End of stack trace from previous location ---
   at Google.Cloud.Spanner.Data.ExecuteHelper.WithErrorTranslationAndProfiling[T](Func`1, String, Logger)
   --- End of inner exception stack trace ---
   at Google.Cloud.Spanner.Data.ExecuteHelper.WithErrorTranslationAndProfiling[T](Func`1, String, Logger)
   at Google.Cloud.Spanner.Data.SpannerCommand.ExecutableCommand.ExecuteReaderAsync(CommandBehavior, TimestampBound, CancellationToken)
   at Google.Cloud.Spanner.Data.SpannerCommand.ExecutableCommand.ExecuteDbDataReaderAsync(CommandBehavior, TimestampBound, CancellationToken)
   at Google.Cloud.Spanner.Data.SpannerCommand.ExecuteReaderAsync(CommandBehavior, CancellationToken)
   at cartservice.cartstore.SpannerCartStore.GetCartAsync(String) in /app/cartstore/SpannerCartStore.cs:line 116
could not retrieve cart
main.(*frontendServer).homeHandler
	/src/handlers.go:69
net/http.HandlerFunc.ServeHTTP
	/usr/local/go/src/net/http/server.go:2109
github.com/gorilla/mux.(*Router).ServeHTTP
	/go/pkg/mod/github.com/gorilla/mux@v1.8.0/mux.go:210
main.(*logHandler).ServeHTTP
	/src/middleware.go:82
main.ensureSessionID.func1
	/src/middleware.go:109
net/http.HandlerFunc.ServeHTTP
	/usr/local/go/src/net/http/server.go:2109
net/http.serverHandler.ServeHTTP
	/usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
	/usr/local/go/src/net/http/server.go:1991
runtime.goexit
	/usr/local/go/src/runtime/asm_amd64.s:1594

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reran the commands from step 4 (related to service accounts). The above error disappeared. Online Boutique with Spanner is now working for me! 👍

Copy link
Collaborator

@NimJay NimJay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, once again, Daniel (@mikrovvelle), for implementing this!

It worked on my own GCP project. See screenshot below.

Screen Shot 2022-10-05 at 1 36 14 PM

And thank you, @mathieu-benoit, for the review (and connecting the dots with this effort).

Approved. 😄

@NimJay
Copy link
Collaborator

NimJay commented Oct 5, 2022

Merging.
All checks are passing except the snippet-bot-check. But that isn't relevant here anyway because we're not modifying any region tags.

@NimJay NimJay merged commit 079678f into GoogleCloudPlatform:main Oct 5, 2022
neileverette pushed a commit to neileverette/test-microservices-demo that referenced this pull request Sep 5, 2024
* Spanner support pull-request

* Use static `TableName` in queries, not hardcoding

GoogleCloudPlatform/microservices-demo#1109 (comment)
GoogleCloudPlatform/microservices-demo#1109 (comment)
GoogleCloudPlatform/microservices-demo#1109 (comment)

* spanner.md - better workload identity config

change K8s svc account from`default/default` to bash variables

* Finish un-hard-coding 'default' in spanner.md

* Align spanner.md variables w/workload-identity.md

* Default spanner instance: 'onlineboutique'

* Update Google.Cloud.Spanner.Data to 4.1.0

Can also now drop the workaround for libgrpc_csharp_ext

* remove updated-manifests.yaml from .gitignore

* Expose more Spanner connection details

- `SPANNER_INSTANCE`: defaults to `onlineboutique`, unless specified.
- `SPANNER_DATABASE`: defaults to `carts`, unless specified.
- `SPANNER_CONNECTION_STRING`: can be specified to override others

* Cleanup Spanner connection string handling

connection string is now logged to console after it's finalized,
in SpannerCartStore.cs, instead of Startup.cs.

* Specify --project in docs/spanner.md comment

* Update docs/spanner.md

* Update docs/spanner.md

* Update docs/spanner.md

* Update docs/spanner.md

* Use "Data Source=" in SpannerCartStore.cs

* Fix: use SpannerConnectionStringBuilder again

* Update docs/spanner.md

* Update spanner.md

Co-authored-by: Nim Jayawardena <i.am.nim.jay@gmail.com>
Co-authored-by: Nim Jayawardena <nimjay@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants