Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 |
Expand All @@ -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.
Expand Down
47 changes: 46 additions & 1 deletion examples/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading