mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
[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:
parent
89a4a3b095
commit
99091db18c
@ -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
|
||||
|
||||
|
@ -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
95
ledger/error/README.md
Normal 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`.
|
@ -346,6 +346,7 @@ object ErrorCategory {
|
||||
|
||||
}
|
||||
|
||||
// TODO error codes: `who` is not used?
|
||||
/** Default retryability information
|
||||
*
|
||||
* Every error category has a default retryability classification.
|
||||
|
123
ledger/error/src/main/scala/com/daml/error/samples/Example.scala
Normal file
123
ledger/error/src/main/scala/com/daml/error/samples/Example.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user