Skip to content

Commit

Permalink
Richer error codes model (#7257)
Browse files Browse the repository at this point in the history
* Adds richer error messages for the state api

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds integration tests

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds richer error for invalid key names

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Corrects imports formatting

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Updates to latest kit changes

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds integration tests for richer errors in http

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Removes commented tests

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Point to kit fork, temporarily

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* More integration tests

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds more integration tests

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Reenables test

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds MultiMaxSize

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds tests for DAPR_STATE_TOO_MANY_TRANSACTIONS

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds integration tests for states that don’t support transactions

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Cleanup

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Removes leftover from merge

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Move to error builders

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Apply suggestions from code review

Co-authored-by: Josh van Leeuwen <me@joshvanl.dev>
Signed-off-by: Elena Kolevska <elena-kolevska@users.noreply.github.com>

* Changes after review

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Caches MultiMaxSize()

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Update to latest kit

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds a readme file - guiide to contributing new errors. Applies comments from reviews.

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Fixes linter warnings

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Points back to dapr/kit instead of Cassie’s fork

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Updates to latest dapr kit main

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Adds whitespace

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Reverts update

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Reverts version update, as per review comment

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Runs modtidy-all

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Addresses Josh’s comment

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Linter

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Uses .FromError instead of .As

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Apply suggestions from code review

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: Elena Kolevska <elena-kolevska@users.noreply.github.com>

* Update pluggable.go

Co-authored-by: Josh van Leeuwen <me@joshvanl.dev>
Signed-off-by: Elena Kolevska <elena-kolevska@users.noreply.github.com>

* Move const to string literal

Signed-off-by: Elena Kolevska <elena@kolevska.com>

* Apply suggestions from code review

Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: Elena Kolevska <elena-kolevska@users.noreply.github.com>

---------

Signed-off-by: Elena Kolevska <elena@kolevska.com>
Signed-off-by: Elena Kolevska <elena-kolevska@users.noreply.github.com>
Co-authored-by: Artur Souza <asouza.pro@gmail.com>
Co-authored-by: Josh van Leeuwen <me@joshvanl.dev>
Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
  • Loading branch information
5 people committed Jan 9, 2024
1 parent c9715e4 commit 3ddf46a
Show file tree
Hide file tree
Showing 23 changed files with 1,837 additions and 254 deletions.
22 changes: 21 additions & 1 deletion dapr/proto/components/v1/state.proto
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,24 @@ message BulkSetRequest {
}

// reserved for future-proof extensibility
message BulkSetResponse {}
message BulkSetResponse {}

// TransactionalStoreMultiMaxSize service provides a gRPC interface for
// compatible transactional state store components which return the maximum
// number of operations that can be performed in a single transaction.
service TransactionalStoreMultiMaxSize {
// MultiMaxSize returns the maximum number of operations that can be performed
// in a single transaction.
rpc MultiMaxSize(MultiMaxSizeRequest) returns (MultiMaxSizeResponse) {}
}

// MultiMaxSizeRequest is the request for MultiMaxSize. It is empty because
// there are no parameters.
message MultiMaxSizeRequest {}

// MultiMaxSizeResponse is the response for MultiMaxSize.
message MultiMaxSizeResponse {
// The maximum number of operations that can be performed in a single
// transaction.
int64 max_size = 1;
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/cenkalti/backoff/v4 v4.2.1
github.com/cloudevents/sdk-go/v2 v2.14.0
github.com/dapr/components-contrib v1.12.1-0.20231219201927-02c6b21ec621
github.com/dapr/kit v0.12.2-0.20231219171931-d7281f0c24d0
github.com/dapr/kit v0.12.2-0.20231220162141-abe711ef620f
github.com/evanphx/json-patch/v5 v5.7.0
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,8 @@ github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuA
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/dapr/components-contrib v1.12.1-0.20231219201927-02c6b21ec621 h1:3ZCVOQ/wy/pxB8XrHaxFbA4h+Y395ZgOYyRBENIF4EA=
github.com/dapr/components-contrib v1.12.1-0.20231219201927-02c6b21ec621/go.mod h1:IDhR653dYqUhPVZJYCwiLe5V9rTIC3/A6eyVCyPPcfA=
github.com/dapr/kit v0.12.2-0.20231219171931-d7281f0c24d0 h1:WrciPxdLy6Bf8PTdkc0JDUuJOx9fxC9dkPSAEjDGv0w=
github.com/dapr/kit v0.12.2-0.20231219171931-d7281f0c24d0/go.mod h1:q3Nhp54MIp6xIl2BznKEavzOnkM6MMIcHa1yEvQDKC4=
github.com/dapr/kit v0.12.2-0.20231220162141-abe711ef620f h1:21ETHtiA6O2hAovtsNDdhuxlKg/wZpkslzAsBH+L9q0=
github.com/dapr/kit v0.12.2-0.20231220162141-abe711ef620f/go.mod h1:kCRdU8av1epsDzfxjpLo2gB2VYTgsNefRauD5gbir3A=
github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
56 changes: 56 additions & 0 deletions pkg/api/errors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
### Error codes in Dapr

#### Introduction

This guide is intended for developers working on the Dapr code base, specifically for those updating the error handling to align with the gRPC richer error model. The goal is to standardize error responses across Dapr's services.

#### Prerequisites

- Familiarity with gRPC and its error model. Refer to the [gRPC Richer Error Model Guide](https://cloud.google.com/apis/design/errors).

#### Step 1: Understanding the Current Error Handling
Currently, error handling in Dapr is a mix of predefined errors and dynamically constructed errors within the code.

- **Predefined Errors**: There are some legacy predefined errors located at [`/pkg/messages/predefined.go`](https://github.com/dapr/dapr/blob/master/pkg/messages/predefined.go), but the new errors are located in [`/pkg/api/errors`](https://github.com/dapr/dapr/blob/master/pkg/api/errors). These errors are standard and reused across various parts of the Dapr codebase. They provide a consistent error handling mechanism for common scenarios.
- **Dynamically Constructed Errors**: These errors are created on the fly within the code, typically to handle specific situations or errors that are not covered by the predefined set.

As we move predefined errors to the rich error model and a new location (`pkg/api/errors`), the first step in updating to the new error model is to familiarize yourself with the existing error handling patterns, both predefined and dynamically constructed. This understanding will be crucial when replacing them with the richer error model aligned with gRPC standards.

#### Step 2: Adding New Errors

1. **Check for existing errors**: Before creating a new error, check if a relevant error file exists in `/pkg/api/errors/`.
2. **Create a new error file**: If no relevant file exists, create a new file following the pattern `<building-block>.go`.

#### Step 3: Designing Rich Error Messages

1. **Understand Rich Error Construction**: Familiarize yourself with the construction of rich error messages. The definition can be found in [`github.com/dapr/kit/errors`](https://github.com/dapr/kit/blob/main/errors/errors.go).
2. **Helper Methods**: Utilize the error builder and the helper methods like `WithErrorInfo`, `WithResourceInfo` to enrich error information.
3. **Reference implementation**: You can check out `pkg/api/errors/state.go` for a reference implementation. The following code snippet shows how to create a new error using the helper methods:

```go
func StateStoreInvalidKeyName(storeName string, key string, msg string) error {
return kiterrors.NewBuilder(
codes.InvalidArgument,
http.StatusBadRequest,
msg,
"ERR_MALFORMED_REQUEST",
).
WithErrorInfo(kiterrors.CodePrefixStateStore+kiterrors.CodeIllegalKey, nil).
WithResourceInfo("state", storeName, "", "").
WithFieldViolation(key, msg).
Build()
}
```
4. **Mandatory and Optional Information**:
- **ErrorInfo**: This is a required field.
- **ResourceInfo and other Details fields**: These are optional but should be used following best practices as outlined in the [Google Cloud Error Model](https://cloud.google.com/apis/design/errors#error_model).

#### Step 4: Implementing the New Error Model

1. **Consistent Use**: Ensure that the new error model is used consistently throughout the codebase.
2. **Refactoring**: Replace existing errors in the code with the newly defined rich errors.

#### Step 5: Testing and Integration

1. **Integration Tests**: Add integration tests for the new error handling mechanism.
2. **Follow Existing Patterns**: Use the existing integration tests for the state API as a reference for structure and best practices. For example, these are the integration tests for the errors in state api: `/tests/integration/suite/daprd/state/grpc/errors.go` and `/tests/integration/suite/daprd/state/http/errors.go`
114 changes: 114 additions & 0 deletions pkg/api/errors/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package errors

import (
"fmt"
"net/http"
"strconv"

"google.golang.org/grpc/codes"

kiterrors "github.com/dapr/kit/errors"
)

func StateStoreNotConfigured() error {
return kiterrors.NewBuilder(
codes.FailedPrecondition,
http.StatusInternalServerError,
"state store is not configured",
"ERR_STATE_STORE_NOT_CONFIGURED",
).
WithErrorInfo(kiterrors.CodePrefixStateStore+kiterrors.CodeNotConfigured, nil).
Build()
}

func StateStoreNotFound(storeName string) error {
return kiterrors.NewBuilder(
codes.InvalidArgument, // TODO We should change this at some point. It should be codes.NotFound, but it will be a breaking change.
http.StatusBadRequest,
fmt.Sprintf("state store %s is not found", storeName),
"ERR_STATE_STORE_NOT_FOUND",
).
WithErrorInfo(kiterrors.CodePrefixStateStore+kiterrors.CodeNotFound, nil).
Build()
}

func StateStoreInvalidKeyName(storeName string, key string, msg string) error {
return kiterrors.NewBuilder(
codes.InvalidArgument,
http.StatusBadRequest,
msg,
"ERR_MALFORMED_REQUEST",
).WithErrorInfo(kiterrors.CodePrefixStateStore+kiterrors.CodeIllegalKey, nil).
WithResourceInfo("state", storeName, "", "").
WithFieldViolation(key, msg).
Build()
}

/**** Transactions ****/

func StateStoreTransactionsNotSupported(storeName string) error {
return kiterrors.NewBuilder(
codes.Unimplemented,
http.StatusInternalServerError,
fmt.Sprintf("state store %s doesn't support transactions", storeName),
"ERR_STATE_STORE_NOT_SUPPORTED", // TODO: @elena-kolevska this code misleading and also used for different things ("query unsupported"); it should be removed in the next major version
).
WithErrorInfo(kiterrors.CodePrefixStateStore+"TRANSACTIONS_NOT_SUPPORTED", nil).
WithResourceInfo("state", storeName, "", "").
WithHelpLink("https://docs.dapr.io/reference/components-reference/supported-state-stores/", "Check the list of state stores and the features they support").
Build()
}

func StateStoreTooManyTransactionalOps(storeName string, count int, max int) error {
return kiterrors.NewBuilder(
codes.InvalidArgument,
http.StatusBadRequest,
fmt.Sprintf("the transaction contains %d operations, which is more than what the state store supports: %d", count, max),
"ERR_STATE_STORE_TOO_MANY_TRANSACTIONS",
).
WithErrorInfo(kiterrors.CodePrefixStateStore+"TOO_MANY_TRANSACTIONS", map[string]string{
"currentOpsTransaction": strconv.Itoa(count),
"maxOpsPerTransaction": strconv.Itoa(max),
}).
WithResourceInfo("state", storeName, "", "").
Build()
}

/**** Query API ****/

func StateStoreQueryUnsupported(storeName string) error {
return kiterrors.NewBuilder(
codes.Internal,
http.StatusInternalServerError,
"state store does not support querying",
"ERR_STATE_STORE_NOT_SUPPORTED",
).
WithErrorInfo(kiterrors.CodePrefixStateStore+"QUERYING_"+kiterrors.CodeNotSupported, nil).
WithResourceInfo("state", storeName, "", "").
Build()
}

func StateStoreQueryFailed(storeName string, detail string) error {
return kiterrors.NewBuilder(
codes.Internal,
http.StatusInternalServerError,
fmt.Sprintf("state store %s query failed: %s", storeName, detail),
"ERR_STATE_QUERY",
).
WithErrorInfo(kiterrors.CodePrefixStateStore+kiterrors.CodePostfixQueryFailed, nil).
WithResourceInfo("state", storeName, "", "").
Build()
}
61 changes: 57 additions & 4 deletions pkg/components/state/pluggable.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"sync"
"time"

"github.com/dapr/kit/ptr"

"github.com/dapr/components-contrib/state"
"github.com/dapr/components-contrib/state/query"
Expand All @@ -45,6 +50,8 @@ var (
GRPCCodeETagMismatch = codes.FailedPrecondition
GRPCCodeETagInvalid = codes.InvalidArgument
GRPCCodeBulkDeleteRowMismatch = codes.Internal

log = logger.NewLogger("state-pluggable-logger")
)

const (
Expand Down Expand Up @@ -144,7 +151,9 @@ var (
type grpcStateStore struct {
*pluggable.GRPCConnector[stateStoreClient]
// features is the list of state store implemented features.
features []state.Feature
features []state.Feature
multiMaxSize *int
lock sync.RWMutex
}

// Init initializes the grpc state passing out the metadata to the grpc component.
Expand Down Expand Up @@ -321,6 +330,48 @@ func (ss *grpcStateStore) Multi(ctx context.Context, request *state.Transactiona
return err
}

// MultiMaxSize returns the maximum number of operations allowed in a transactional request.
func (ss *grpcStateStore) MultiMaxSize() int {
ss.lock.RLock()
multiMaxSize := ss.multiMaxSize
ss.lock.RUnlock()

if multiMaxSize != nil {
return *multiMaxSize
}

ss.lock.Lock()
defer ss.lock.Unlock()

// Check the cached value again in case another goroutine set it
if multiMaxSize != nil {

Check failure on line 347 in pkg/components/state/pluggable.go

View workflow job for this annotation

GitHub Actions / lint & proto validation (linux, amd64)

File is not `gofmt`-ed with `-s` (gofmt)
return *multiMaxSize
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

resp, err := ss.Client.MultiMaxSize(ctx, new(proto.MultiMaxSizeRequest))
if err != nil {
log.Error("failed to get multi max size from state store", err)
ss.multiMaxSize = ptr.Of(-1)
return *ss.multiMaxSize
}

// If the pluggable component is on a 64bit system and the dapr runtime is on a 32bit system,
// the response could be larger than the maximum int32 value.
// In this case, we set the max size to the maximum possible value for a 32bit system.
is32bitSystem := math.MaxInt == math.MaxInt32
if is32bitSystem && resp.GetMaxSize() > int64(math.MaxInt32) {
log.Warnf("multi max size %d is too large for 32bit systems, setting to max possible", resp.GetMaxSize())
ss.multiMaxSize = ptr.Of(math.MaxInt32)
return *ss.multiMaxSize
}

ss.multiMaxSize = ptr.Of(int(resp.GetMaxSize()))
return *ss.multiMaxSize
}

// mappers and helpers.
//
//nolint:nosnakecase
Expand Down Expand Up @@ -526,6 +577,7 @@ type stateStoreClient struct {
proto.StateStoreClient
proto.TransactionalStateStoreClient
proto.QueriableStateStoreClient
proto.TransactionalStoreMultiMaxSizeClient
}

// strNilIfEmpty returns nil if string is empty
Expand All @@ -547,9 +599,10 @@ func strValueIfNotNil(str *string) string {
// newStateStoreClient creates a new stateStore client instance.
func newStateStoreClient(cc grpc.ClientConnInterface) stateStoreClient {
return stateStoreClient{
StateStoreClient: proto.NewStateStoreClient(cc),
TransactionalStateStoreClient: proto.NewTransactionalStateStoreClient(cc),
QueriableStateStoreClient: proto.NewQueriableStateStoreClient(cc),
StateStoreClient: proto.NewStateStoreClient(cc),
TransactionalStateStoreClient: proto.NewTransactionalStateStoreClient(cc),
QueriableStateStoreClient: proto.NewQueriableStateStoreClient(cc),
TransactionalStoreMultiMaxSizeClient: proto.NewTransactionalStoreMultiMaxSizeClient(cc),
}
}

Expand Down
6 changes: 4 additions & 2 deletions pkg/components/state/state_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"os"
"strings"
"sync"

"github.com/dapr/dapr/pkg/api/errors"
)

const (
Expand Down Expand Up @@ -53,7 +55,7 @@ func SaveStateConfiguration(storeName string, metadata map[string]string) error

err := checkKeyIllegal(strategy)
if err != nil {
return err
return errors.StateStoreInvalidKeyName(storeName, strategy, err.Error())
}

statesConfigurationLock.Lock()
Expand All @@ -64,7 +66,7 @@ func SaveStateConfiguration(storeName string, metadata map[string]string) error

func GetModifiedStateKey(key, storeName, appID string) (string, error) {
if err := checkKeyIllegal(key); err != nil {
return "", err
return "", errors.StateStoreInvalidKeyName(storeName, key, err.Error())
}

stateConfiguration := getStateConfiguration(storeName)
Expand Down
7 changes: 4 additions & 3 deletions pkg/grpc/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/dapr/components-contrib/state"
"github.com/dapr/dapr/pkg/actors"
actorerrors "github.com/dapr/dapr/pkg/actors/errors"
apierrors "github.com/dapr/dapr/pkg/api/errors"
stateLoader "github.com/dapr/dapr/pkg/components/state"
"github.com/dapr/dapr/pkg/config"
diag "github.com/dapr/dapr/pkg/diagnostics"
Expand Down Expand Up @@ -831,8 +832,8 @@ func (a *api) ExecuteStateTransaction(ctx context.Context, in *runtimev1pb.Execu
}

transactionalStore, ok := store.(state.TransactionalStore)
if !ok {
err := status.Errorf(codes.Unimplemented, messages.ErrStateStoreNotSupported, in.GetStoreName())
if !ok || !state.FeatureTransactional.IsPresent(store.Features()) {
err := apierrors.StateStoreTransactionsNotSupported(in.GetStoreName())
apiServerLogger.Debug(err)
return &emptypb.Empty{}, err
}
Expand Down Expand Up @@ -897,7 +898,7 @@ func (a *api) ExecuteStateTransaction(ctx context.Context, in *runtimev1pb.Execu
if maxMulti, ok := store.(state.TransactionalStoreMultiMaxSize); ok {
max := maxMulti.MultiMaxSize()
if max > 0 && len(operations) > max {
err := messages.ErrStateTooManyTransactionalOp.WithFormat(len(operations), max)
err := apierrors.StateStoreTooManyTransactionalOps(in.GetStoreName(), len(operations), max)
apiServerLogger.Debug(err)
return &emptypb.Empty{}, err
}
Expand Down

0 comments on commit 3ddf46a

Please sign in to comment.