[DPP-771][Error codes] Add readme.md and supplement docs with a concrete error handling example. (#11990)

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
pbatko-da 2021-12-08 13:09:21 +01:00 committed by GitHub
parent 89a4a3b095
commit 99091db18c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 324 additions and 7 deletions

View File

@ -54,6 +54,8 @@ TEMPLATES_DIR=$BUILD_DIR/source/_templates
mkdir -p $TEMPLATES_DIR
tar -zxf $BAZEL_BIN/templates/templates-tarball.tar.gz -C $TEMPLATES_DIR --strip-components=1
GEN_ERROR_CODES=false
for arg in "$@"
do
if [ "$arg" = "--pdf" ]; then
@ -62,17 +64,10 @@ do
cp -L ../../bazel-bin/docs/DigitalAssetSDK.pdf $BUILD_DIR/gen/_downloads
fi
if [ "$arg" = "--gen" ]; then
# NOTE:
# $BUILD_DIR/source is a symlink into the versioned controlled directory with source .rst files.
# When generating files into that directory make sure to remove them before this script ends.
# Error codes and error categories
bazel build //docs:generate-docs-error-codes-inventory-into-rst-file
cp -L ../../bazel-bin/docs/error-codes-inventory.rst $BUILD_DIR/source/app-dev/grpc/error-codes-inventory.rst.inc
bazel build //docs:generate-docs-error-categories-inventory-into-rst-file
cp -L ../../bazel-bin/docs/error-categories-inventory.rst $BUILD_DIR/source/app-dev/grpc/error-categories-inventory.rst.inc
# Hoogle
bazel build //compiler/damlc:daml-base-hoogle.txt
mkdir -p $BUILD_DIR/gen/hoogle_db
@ -92,9 +87,21 @@ do
mkdir -p ../source/daml/stdlib
tar xf ../../bazel-bin/compiler/damlc/daml-base-rst.tar.gz \
--strip-components 1 -C ../source/daml/stdlib
fi
if [ "$arg" = "--gen" ] || [ "$arg" = "--gen-error-codes" ]; then
GEN_ERROR_CODES=true
fi
done
if [ "$GEN_ERROR_CODES" = "true" ]; then
# Error codes and error categories
bazel build //docs:generate-docs-error-codes-inventory-into-rst-file
cp -L ../../bazel-bin/docs/error-codes-inventory.rst $BUILD_DIR/source/app-dev/grpc/error-codes-inventory.rst.inc
bazel build //docs:generate-docs-error-categories-inventory-into-rst-file
cp -L ../../bazel-bin/docs/error-categories-inventory.rst $BUILD_DIR/source/app-dev/grpc/error-categories-inventory.rst.inc
fi
DATE=$(date +"%Y-%m-%d")
echo { \"$DATE\" : \"$DATE\" } > $BUILD_DIR/gen/versions.json

View File

@ -157,6 +157,82 @@ but there is no guarantee given that additional information will be preserved ac
Working with Error Codes
======================================
This example shows how a user can extract the relevant error information.
.. code-block:: scala
object SampleClientSide {
import com.google.rpc.ResourceInfo
import com.google.rpc.{ErrorInfo, RequestInfo, RetryInfo}
import io.grpc.StatusRuntimeException
import scala.jdk.CollectionConverters._
def example(): Unit = {
try {
DummmyServer.serviceEndpointDummy()
} catch {
case e: StatusRuntimeException =>
// Converting to a status object.
val status = io.grpc.protobuf.StatusProto.fromThrowable(e)
// Extracting error code id.
assert(status.getCode == 10)
// Extracting error message, both
// machine oriented part: "MY_ERROR_CODE_ID(2,full-cor):",
// and human oriented part: "A user oriented message".
assert(status.getMessage == "MY_ERROR_CODE_ID(2,full-cor): A user oriented message")
// Getting all the details
val rawDetails: Seq[com.google.protobuf.Any] = status.getDetailsList.asScala.toSeq
// Extracting error code id, error category id and optionally additional metadata.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[ErrorInfo]) =>
val v = any.unpack(classOf[ErrorInfo])
assert(v.getReason == "MY_ERROR_CODE_ID")
assert(v.getMetadataMap.asScala.toMap == Map("category" -> "2", "foo" -> "bar"))
}.isDefined
}
// Extracting full correlation id, if present.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[RequestInfo]) =>
val v = any.unpack(classOf[RequestInfo])
assert(v.getRequestId == "full-correlation-id-123456790")
}.isDefined
}
// Extracting retry information if the error is retryable.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[RetryInfo]) =>
val v = any.unpack(classOf[RetryInfo])
assert(v.getRetryDelay.getSeconds == 123)
}.isDefined
}
// Extracting resource if the error pertains to some well defined resource.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[ResourceInfo]) =>
val v = any.unpack(classOf[ResourceInfo])
assert(v.getResourceType == "CONTRACT_ID")
assert(v.getResourceName == "someContractId")
}.isDefined
}
}
}
}
Error Codes Inventory
**********************

95
ledger/error/README.md Normal file
View File

@ -0,0 +1,95 @@
# Table of Contents
1. [Overview](#overview)
1. [Error codes](#error-codes)
1. [Error categories](#error-categories)
1. [Error groups](#error-groups)
# Overview
Error codes are typically returned as part of gRPC calls.
The users mentioned later in this document might be participant operators, application developers or application users.
# Error codes
Base class: `com.daml.error.ErrorCode`
## Error code definition
Example:
```
object SomeErrorGroup extends ErrorGroup() {
@Explanation("This explanation becomes part of the official documentation.")
@Resolution("This resolution becomes part of the official documentation.")
object MyNewErrorCode extends ErrorCode(id = "<THIS_IS_AN_ERROR_CODE_ID>",
category = ErrorCategory.InvalidIndependentOfSystemState){
case class Reject(reason: String)
(implicit loggingContext: ContextualizedErrorLogger)
extends LoggingTransactionErrorImpl(cause = reason)
}
}
```
In the example above `MyNewErrorCode` is a new error code which has its own unique error code `id`
and belongs to some error `category`.
`Reject` is a supplementary class to create a concrete instance of this error code.
Things to note:
1. Each error code should be placed within some error group in order to render it in the correct section in the
official Daml documentation.
1. The descriptions from `Exaplantion` and `Resolution` annotations are used to automatically generate sections
of the official Daml documentation. Make sure they are clear and helpful to the users.
1. Error code ids and category assignment are communicated to the user and we consider them part of the public API.
1. You should consider creating a user migration guide for example when you:
- add, remove or change an error code id,
- change the set of error code ids that is being returned from a particular gRPC endpoint,
- change the category an error code belongs to.
When adding a new error code make sure to add a unit test where you assert on the contents of the error as delivered to the users.
## Usage
Typically you will want to immediately convert an error code into an instance of `io.grpc.StatusRuntimeException`:
```
MyNewErrorCode.ErrorCode("Msg").asGrpcError: StatusRuntimeException
```
In practice you will also need to provide some additional implicit values. Look for concrete examples in the codebase.
# Error categories
Base class: `com.daml.error.ErrorCategory`.
You must not add, remove or change existing error categories unless you have good reasons to do so.
They are a part of the public API.
Any such change is likely a breaking change especially that we encourage the users
to base their error handling logic on error categories rather than bare gRPC status codes or more fine-grained
error codes.
Error categories, similar to error codes, use Scala annotations to provide descriptions that are used to automatically
generate sections of the official Daml documentation. Make sure they are clear and helpful to the users.
NOTE: Currently we have a unique mapping from error categories to gRPC status codes.
This is incidental and might change in the future.
# Error groups
Base class: `com.daml.error.ErrorGroup.ErrorGroup`.
Error groups are NOT part of the public API.
They only influence how we render error codes sections in the official Daml documentation.
# Handling errors on the client side
See `com.daml.error.samples.SampleUserSide`.

View File

@ -346,6 +346,7 @@ object ErrorCategory {
}
// TODO error codes: `who` is not used?
/** Default retryability information
*
* Every error category has a default retryability classification.

View File

@ -0,0 +1,123 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.error.samples
object DummmyServer {
import com.daml.error.{
BaseError,
ContextualizedErrorLogger,
DamlContextualizedErrorLogger,
ErrorCategory,
ErrorCategoryRetry,
ErrorClass,
ErrorCode,
ErrorResource,
}
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import scala.concurrent.duration.Duration
object ErrorCodeFoo
extends ErrorCode(id = "MY_ERROR_CODE_ID", ErrorCategory.ContentionOnSharedResources)(
ErrorClass.root()
) {
case class Error(message: String) extends BaseError.Impl(cause = message) {
override def loggingContext: ContextualizedErrorLogger = new DamlContextualizedErrorLogger(
ContextualizedLogger.get(getClass),
LoggingContext.newLoggingContext(identity),
Some("full-correlation-id-123456790"),
)
override def resources: Seq[(ErrorResource, String)] = Seq(
ErrorResource.ContractId -> "someContractId"
)
override def retryable: Option[ErrorCategoryRetry] = Some(
ErrorCategoryRetry("me", Duration("123 s"))
)
override def context: Map[String, String] = Map("foo" -> "bar")
}
}
def serviceEndpointDummy(): Unit = {
throw ErrorCodeFoo.Error("A user oriented message").asGrpcError
}
}
/** This shows how a user can handle error codes.
* In particular it shows how to extract useful information from the signalled exception with minimal library dependencies.
*
* NOTE: This class is given as an example in the official Daml documentation. If you change it here, change it also in the docs.
*/
object SampleClientSide {
import com.google.rpc.ResourceInfo
import com.google.rpc.{ErrorInfo, RequestInfo, RetryInfo}
import io.grpc.StatusRuntimeException
import scala.jdk.CollectionConverters._
def example(): Unit = {
try {
DummmyServer.serviceEndpointDummy()
} catch {
case e: StatusRuntimeException =>
// Converting to a status object.
val status = io.grpc.protobuf.StatusProto.fromThrowable(e)
// Extracting error code id.
assert(status.getCode == 10)
// Extracting error message, both
// machine oriented part: "MY_ERROR_CODE_ID(2,full-cor):",
// and human oriented part: "A user oriented message".
assert(status.getMessage == "MY_ERROR_CODE_ID(2,full-cor): A user oriented message")
// Getting all the details
val rawDetails: Seq[com.google.protobuf.Any] = status.getDetailsList.asScala.toSeq
// Extracting error code id, error category id and optionally additional metadata.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[ErrorInfo]) =>
val v = any.unpack(classOf[ErrorInfo])
assert(v.getReason == "MY_ERROR_CODE_ID")
assert(v.getMetadataMap.asScala.toMap == Map("category" -> "2", "foo" -> "bar"))
}.isDefined
}
// Extracting full correlation id, if present.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[RequestInfo]) =>
val v = any.unpack(classOf[RequestInfo])
assert(v.getRequestId == "full-correlation-id-123456790")
}.isDefined
}
// Extracting retry information if the error is retryable.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[RetryInfo]) =>
val v = any.unpack(classOf[RetryInfo])
assert(v.getRetryDelay.getSeconds == 123)
}.isDefined
}
// Extracting resource if the error pertains to some well defined resource.
assert {
rawDetails.collectFirst {
case any if any.is(classOf[ResourceInfo]) =>
val v = any.unpack(classOf[ResourceInfo])
assert(v.getResourceType == "CONTRACT_ID")
assert(v.getResourceName == "someContractId")
}.isDefined
}
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.error.samples
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class SampleClientSideSpec extends AnyFlatSpec with Matchers {
it should "run successfully" in {
SampleClientSide.example()
}
}