diff --git a/README.md b/README.md index fe59384..999aa87 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,45 @@ client, err := authzed.NewClient( ) ``` +### Configuring gRPC middleware + +Because [`NewClient()`] accepts arbitrary [DialOptions], any client-side gRPC middleware can be installed by chaining interceptors. This includes the ecosystem of existing middleware for concerns such as retries, timeouts, metrics, logging, and tracing. + +For example, [go-grpc-middleware]'s retry interceptor adds automatic retries with exponential backoff: + +```go +import ( + "time" + + "github.com/authzed/grpcutil" + grpcretry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + "github.com/authzed/authzed-go/v1" +) + +... +retryOpts := []grpcretry.CallOption{ + grpcretry.WithMax(3), + grpcretry.WithBackoff(grpcretry.BackoffExponentialWithJitter(100*time.Millisecond, 0.1)), + grpcretry.WithCodes(codes.Unavailable, codes.ResourceExhausted), +} + +client, err := authzed.NewClient( + "grpc.authzed.com:443", + systemCerts, + grpcutil.WithBearerToken("t_your_token_here_1234567deadbeef"), + grpc.WithChainUnaryInterceptor(grpcretry.UnaryClientInterceptor(retryOpts...)), +) +``` + +Note that retries installed this way apply to every unary call, including writes. A retry after an ambiguous failure (e.g. `Unavailable`) can replay a write that already committed, so make sure your writes are idempotent or disable retries for them per-call with `grpcretry.Disable()`. + +See `Example_retryMiddleware` in the [`examples/`](examples/) directory for a runnable version. + +[go-grpc-middleware]: https://github.com/grpc-ecosystem/go-grpc-middleware + ## Examples The [`examples/`](examples/) directory contains runnable examples demonstrating common operations: @@ -173,6 +212,7 @@ The [`examples/`](examples/) directory contains runnable examples demonstrating |---------|-------------| | `Example_connectToAuthzed` | Connect to Authzed's hosted service | | `Example_connectToSpiceDB` | Connect to a local SpiceDB instance | +| `Example_retryMiddleware` | Configure retry middleware with exponential backoff | | `Example_writeSchema` | Write a permission schema | | `Example_writeRelationships` | Create relationships between objects | | `Example_checkPermission` | Check if a subject has a permission | @@ -193,7 +233,7 @@ docker run --rm -p 50051:50051 authzed/spicedb serve \ --datastore-engine memory # Run local examples (excludes Example_connectToAuthzed which requires a valid token) -go test -v -tags=examples -run 'Example_connectToSpiceDB|Example_write|Example_check|Example_lookup|Example_read|Example_delete|Example_caveat|Example_watch' ./examples/... +go test -v -tags=examples -run 'Example_connectToSpiceDB|Example_retry|Example_write|Example_check|Example_lookup|Example_read|Example_delete|Example_caveat|Example_watch' ./examples/... ``` > **Note:** `Example_connectToAuthzed` requires a valid Authzed API token and connects to the hosted service, not local SpiceDB. diff --git a/examples/example_test.go b/examples/example_test.go index c2707f6..06341f5 100644 --- a/examples/example_test.go +++ b/examples/example_test.go @@ -10,7 +10,7 @@ // // Then run the local examples with the build tag: // -// go test -v -tags=examples -run 'Example_connectToSpiceDB|Example_write|Example_check|Example_lookup|Example_read|Example_delete|Example_caveat|Example_watch' ./examples/... +// go test -v -tags=examples -run 'Example_connectToSpiceDB|Example_retry|Example_write|Example_check|Example_lookup|Example_read|Example_delete|Example_caveat|Example_watch' ./examples/... // // Note: Example_connectToAuthzed requires a valid Authzed API token and connects to the hosted service. package examples @@ -24,7 +24,9 @@ import ( "time" "github.com/authzed/grpcutil" + grpcretry "github.com/grpc-ecosystem/go-grpc-middleware/retry" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/structpb" @@ -68,6 +70,49 @@ func Example_connectToSpiceDB() { fmt.Println("Connected to SpiceDB") } +// Example_retryMiddleware demonstrates how to install client-side gRPC +// middleware, in this case automatic retries with exponential backoff. +// +// NewClient accepts arbitrary gRPC DialOptions, so any client interceptor +// (e.g. for retries, timeouts, metrics, or tracing) can be chained in. +func Example_retryMiddleware() { + retryOpts := []grpcretry.CallOption{ + grpcretry.WithMax(3), + grpcretry.WithBackoff(grpcretry.BackoffExponentialWithJitter(100*time.Millisecond, 0.1)), + grpcretry.WithCodes(codes.Unavailable, codes.ResourceExhausted), + } + + // A grpcretry.StreamClientInterceptor is also available, but it only + // supports retrying server-side streams; with retries enabled it rejects + // client-streaming RPCs such as ImportBulkRelationships. + client, err := authzed.NewClient( + "localhost:50051", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpcutil.WithInsecureBearerToken("somerandomkeyhere"), + grpc.WithChainUnaryInterceptor(grpcretry.UnaryClientInterceptor(retryOpts...)), + ) + if err != nil { + log.Fatalf("failed to create client: %s", err) + } + defer client.Close() + + // Every unary call made with this client is now retried automatically + // when the server returns one of the configured status codes. This + // includes mutating calls: a retry after an ambiguous failure (e.g. + // Unavailable) can replay a write that already committed, so make sure + // your writes are idempotent or disable retries for them per-call with + // grpcretry.Disable(). + _, err = client.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ + Schema: `definition user {}`, + }) + if err != nil { + log.Fatalf("failed to write schema: %s", err) + } + + fmt.Println("Schema written") + // Output: Schema written +} + // Example_writeSchema demonstrates how to write a permission schema to SpiceDB. func Example_writeSchema() { client, err := authzed.NewClient( diff --git a/go.mod b/go.mod index 5cd3286..532bacc 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b github.com/cenkalti/backoff/v4 v4.3.0 github.com/envoyproxy/protoc-gen-validate v1.3.3 + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 github.com/jzelinskie/stringz v0.0.3 github.com/magefile/mage v1.17.1 @@ -24,7 +25,6 @@ require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect