Configurable assertions in Ledger API test tool by feature descriptors (#11328)

* ApiVersionService propagates self-service error codes flag.
* ParticipantTestContext is enriched with feature descriptors
* ContractIdIT adapted with assertions for self-service error codes

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
tudor-da 2021-10-25 14:13:44 +02:00 committed by GitHub
parent 96b7b5812f
commit 03cfd1237c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 292 additions and 74 deletions

View File

@ -0,0 +1,23 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package com.daml.ledger.api.v1;
option java_outer_classname = "ExperimentalFeaturesOuterClass";
option java_package = "com.daml.ledger.api.v1";
option csharp_namespace = "Com.Daml.Ledger.Api.V1";
/*
IMPORTANT: in contrast to other parts of the Ledger API, only json-wire backwards
compatibility guarantees are given for the messages in this file.
*/
// See the feature message definitions for descriptions.
message ExperimentalFeatures {
ExperimentalSelfServiceErrorCodes self_service_error_codes = 1;
}
// GRPC self-service error codes are returned by the Ledger API.
message ExperimentalSelfServiceErrorCodes {}

View File

@ -5,6 +5,8 @@ syntax = "proto3";
package com.daml.ledger.api.v1;
import "com/daml/ledger/api/v1/experimental_features.proto";
option java_outer_classname = "VersionServiceOuterClass";
option java_package = "com.daml.ledger.api.v1";
option csharp_namespace = "Com.Daml.Ledger.Api.V1";
@ -26,6 +28,25 @@ message GetLedgerApiVersionRequest {
message GetLedgerApiVersionResponse {
// The version of the ledger API
// The version of the ledger API.
string version = 1;
// The features supported by this Ledger API endpoint.
//
// Daml applications CAN use the feature descriptor on top of
// version constraints on the Ledger API version to determine
// whether a given Ledger API endpoint supports the features
// required to run the application.
//
// See the feature descriptions themselves for the relation between
// Ledger API versions and feature presence.
FeaturesDescriptor features = 2;
}
message FeaturesDescriptor {
// Features under development or features that are used
// for ledger implementation testing purposes only.
//
// Daml applications SHOULD not depend on these in production.
ExperimentalFeatures experimental = 1;
}

View File

@ -216,6 +216,9 @@ class RejectionGenerators(conformanceMode: Boolean) {
}
}
// TODO error codes: Remove with the removal of the compatibility constraint from Canton
object RejectionGenerators extends RejectionGenerators(conformanceMode = false)
sealed trait ErrorCauseExport
object ErrorCauseExport {
final case class DamlLf(error: LfError) extends ErrorCauseExport

View File

@ -0,0 +1,36 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.error.utils
import com.google.protobuf
import com.google.rpc.{ErrorInfo, RequestInfo, ResourceInfo, RetryInfo}
object ErrorDetails {
sealed trait ErrorDetail extends Product with Serializable
final case class ResourceInfoDetail(name: String, typ: String) extends ErrorDetail
final case class ErrorInfoDetail(reason: String) extends ErrorDetail
final case class RetryInfoDetail(retryDelayInSeconds: Long) extends ErrorDetail
final case class RequestInfoDetail(requestId: String) extends ErrorDetail
def from(anys: Seq[protobuf.Any]): Seq[ErrorDetail] = anys.toList.map {
case any if any.is(classOf[ResourceInfo]) =>
val v = any.unpack(classOf[ResourceInfo])
ResourceInfoDetail(v.getResourceType, v.getResourceName)
case any if any.is(classOf[ErrorInfo]) =>
val v = any.unpack(classOf[ErrorInfo])
ErrorInfoDetail(v.getReason)
case any if any.is(classOf[RetryInfo]) =>
val v = any.unpack(classOf[RetryInfo])
RetryInfoDetail(v.getRetryDelay.getSeconds)
case any if any.is(classOf[RequestInfo]) =>
val v = any.unpack(classOf[RequestInfo])
RequestInfoDetail(v.getRequestId)
case any => throw new IllegalStateException(s"Could not unpack value of: |$any|")
}
}

View File

@ -5,11 +5,11 @@ package com.daml.error
import ch.qos.logback.classic.Level
import com.daml.error.ErrorCategory.TransientServerFailure
import com.daml.error.utils.ErrorDetails
import com.daml.error.utils.testpackage.SeriousError
import com.daml.error.utils.testpackage.subpackage.NotSoSeriousError
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.testing.LogCollector
import com.google.rpc.{ErrorInfo, RequestInfo, RetryInfo}
import io.grpc.protobuf.StatusProto
import org.scalatest.BeforeAndAfter
import org.scalatest.flatspec.AnyFlatSpec
@ -71,21 +71,18 @@ class ErrorCodeSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
val actualTrailers = actualGrpcError.getTrailers
val actualRpcStatus = StatusProto.fromStatusAndTrailers(actualStatus, actualTrailers)
val Seq(rawErrorInfo, rawRetryInfo, rawRequestInfo, rawResourceInfo) =
actualRpcStatus.getDetailsList.asScala.toSeq
val actualResourceInfo = rawResourceInfo.unpack(classOf[com.google.rpc.ResourceInfo])
val actualErrorInfo = rawErrorInfo.unpack(classOf[ErrorInfo])
val actualRetryInfo = rawRetryInfo.unpack(classOf[RetryInfo])
val actualRequestInfo = rawRequestInfo.unpack(classOf[RequestInfo])
val errorDetails =
ErrorDetails.from(actualRpcStatus.getDetailsList.asScala.toSeq)
actualStatus.getCode shouldBe NotSoSeriousError.category.grpcCode.get
actualGrpcError.getMessage shouldBe expectedErrorMessage
actualErrorInfo.getReason shouldBe NotSoSeriousError.id
actualRetryInfo.getRetryDelay.getSeconds shouldBe TransientServerFailure.retryable.get.duration.toSeconds
actualRequestInfo.getRequestId shouldBe correlationId
actualResourceInfo.getResourceType shouldBe error.resources.head._1.asString
actualResourceInfo.getResourceName shouldBe error.resources.head._2
errorDetails should contain theSameElementsAs Seq(
ErrorDetails.ErrorInfoDetail(NotSoSeriousError.id),
ErrorDetails.RetryInfoDetail(TransientServerFailure.retryable.get.duration.toSeconds),
ErrorDetails.RequestInfoDetail(correlationId),
ErrorDetails.ResourceInfoDetail(error.resources.head._1.asString, error.resources.head._2),
)
}
private def logSeriousError(

View File

@ -3,6 +3,7 @@
package com.daml
import com.daml.error.utils.ErrorDetails
import com.daml.error.{
ContextualizedErrorLogger,
DamlContextualizedErrorLogger,
@ -13,7 +14,6 @@ import com.daml.lf.data.Ref
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.server.api.validation.ErrorFactories._
import com.google.protobuf
import com.google.rpc._
import io.grpc.Status.Code
import io.grpc.StatusRuntimeException
@ -492,38 +492,3 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope
// TODO error codes: Assert logging
}
}
object ErrorDetails {
sealed trait ErrorDetail
final case class ResourceInfoDetail(name: String, typ: String) extends ErrorDetail
final case class ErrorInfoDetail(reason: String) extends ErrorDetail
final case class RetryInfoDetail(retryDelayInSeconds: Long) extends ErrorDetail
final case class RequestInfoDetail(requestId: String) extends ErrorDetail
def from(anys: Seq[protobuf.Any]): Seq[ErrorDetail] = {
anys.toList.map(from)
}
private def from(any: protobuf.Any): ErrorDetail = {
if (any.is(classOf[ResourceInfo])) {
val v = any.unpack(classOf[ResourceInfo])
ResourceInfoDetail(v.getResourceType, v.getResourceName)
} else if (any.is(classOf[ErrorInfo])) {
val v = any.unpack(classOf[ErrorInfo])
ErrorInfoDetail(v.getReason)
} else if (any.is(classOf[RetryInfo])) {
val v = any.unpack(classOf[RetryInfo])
RetryInfoDetail(v.getRetryDelay.getSeconds)
} else if (any.is(classOf[RequestInfo])) {
val v = any.unpack(classOf[RequestInfo])
RequestInfoDetail(v.getRequestId)
} else {
throw new IllegalStateException(s"Could not unpack value of: |$any|")
}
}
}

View File

@ -91,6 +91,7 @@ da_scala_binary(
"//ledger/test-common:model-tests-%s.scala" % lf_version,
"//ledger/test-common:dar-files-%s-lib" % lf_version,
"//ledger-api/grpc-definitions:ledger_api_proto_scala",
"//ledger/error",
"//ledger/ledger-api-common",
"//libs-scala/build-info",
"//libs-scala/grpc-utils",
@ -124,6 +125,7 @@ da_scala_binary(
":ledger-api-test-tool-%s-lib" % lf_version,
"//daml-lf/data",
"//language-support/scala/bindings",
"//ledger/error",
"//ledger/ledger-api-common",
"//ledger/ledger-resources",
"//libs-scala/resources",

View File

@ -3,15 +3,17 @@
package com.daml.ledger.api.testtool.infrastructure
import java.util.regex.Pattern
import com.daml.error.ErrorCode
import com.daml.error.utils.ErrorDetails
import com.daml.grpc.{GrpcException, GrpcStatus}
import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext
import com.daml.timer.RetryStrategy
import com.google.rpc.ErrorInfo
import io.grpc.Status
import io.grpc.protobuf.StatusProto
import io.grpc.{Status, StatusRuntimeException}
import munit.{ComparisonFailException, Assertions => MUnit}
import java.util.regex.Pattern
import scala.annotation.tailrec
import scala.concurrent.Future
import scala.jdk.CollectionConverters._
@ -44,6 +46,31 @@ object Assertions {
}
}
/** Asserts GRPC error codes depending on the self-service error codes feature in the Ledger API. */
def assertGrpcError(
participant: ParticipantTestContext,
t: Throwable,
expectedCode: Status.Code,
selfServiceErrorCode: ErrorCode,
exceptionMessageSubstring: Option[String],
checkDefiniteAnswerMetadata: Boolean,
): Unit =
if (participant.features.selfServiceErrorCodes)
t match {
case statusRuntimeException: StatusRuntimeException =>
assertSelfServiceErrorCode(statusRuntimeException, selfServiceErrorCode)
case t => fail(s"Throwable $t does not match ErrorCode $selfServiceErrorCode")
}
else {
assertGrpcErrorRegex(
t,
expectedCode,
exceptionMessageSubstring
.map(msgSubstring => Pattern.compile(Pattern.quote(msgSubstring))),
checkDefiniteAnswerMetadata,
)
}
/** A non-regex alternative to [[assertGrpcErrorRegex]] which just does a substring check.
*/
def assertGrpcError(
@ -124,4 +151,44 @@ object Assertions {
/** Allows for assertions with more information in the error messages. */
implicit def futureAssertions[T](future: Future[T]): FutureAssertions[T] =
new FutureAssertions[T](future)
def assertSelfServiceErrorCode(
statusRuntimeException: StatusRuntimeException,
expectedErrorCode: ErrorCode,
): Unit = {
val status = StatusProto.fromThrowable(statusRuntimeException)
val expectedStatusCode = expectedErrorCode.category.grpcCode
.map(_.value())
.getOrElse(
throw new RuntimeException(
s"Errors without grpc code cannot be asserted on the Ledger API. Expected error: $expectedErrorCode"
)
)
val expectedErrorId = expectedErrorCode.id
val expectedRetryabilitySeconds = expectedErrorCode.category.retryable.map(_.duration.toSeconds)
val actualStatusCode = status.getCode
val actualErrorDetails = ErrorDetails.from(status.getDetailsList.asScala.toSeq)
val actualErrorId = actualErrorDetails
.collectFirst { case err: ErrorDetails.ErrorInfoDetail => err.reason }
.getOrElse(fail("Actual error id is not defined"))
val actualRetryabilitySeconds = actualErrorDetails
.collectFirst { case err: ErrorDetails.RetryInfoDetail => err.retryDelayInSeconds }
Assertions.assertEquals(
"gRPC error code mismatch",
actualStatusCode,
expectedStatusCode,
)
if (!actualErrorId.contains(expectedErrorId))
fail(s"Actual error id ($actualErrorId) does not match expected error id ($expectedErrorId}")
Assertions.assertEquals(
s"Error retryability details mismatch",
actualRetryabilitySeconds,
expectedRetryabilitySeconds,
)
}
}

View File

@ -29,6 +29,8 @@ import com.daml.ledger.api.v1.testing.time_service.TimeServiceGrpc
import com.daml.ledger.api.v1.testing.time_service.TimeServiceGrpc.TimeService
import com.daml.ledger.api.v1.transaction_service.TransactionServiceGrpc
import com.daml.ledger.api.v1.transaction_service.TransactionServiceGrpc.TransactionService
import com.daml.ledger.api.v1.version_service.VersionServiceGrpc
import com.daml.ledger.api.v1.version_service.VersionServiceGrpc.VersionService
import io.grpc.{Channel, ClientInterceptor}
import io.grpc.health.v1.health.HealthGrpc
import io.grpc.health.v1.health.HealthGrpc.Health
@ -80,4 +82,6 @@ private[infrastructure] final class LedgerServices(
val time: TimeService =
TimeServiceGrpc.stub(participant)
val version: VersionService =
VersionServiceGrpc.stub(participant)
}

View File

@ -28,6 +28,7 @@ private[infrastructure] final class LedgerSession private (
applicationId,
identifierSuffix,
clientTlsConfiguration,
session.features,
)
}
.map(new LedgerTestContext(_))

View File

@ -162,6 +162,7 @@ final class LedgerTestCasesRunner(
applicationId = "upload-dars",
identifierSuffix = identifierSuffix,
clientTlsConfiguration = clientTlsConfiguration,
features = session.features,
)
_ <- Future.sequence(Dars.resources.map(uploadDar(context, _)))
} yield ()

View File

@ -0,0 +1,30 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.ledger.api.testtool.infrastructure.participant
import com.daml.ledger.api.v1.version_service.GetLedgerApiVersionResponse
object Features {
def fromApiVersionResponse(request: GetLedgerApiVersionResponse): Features = {
val selfServiceErrorCodesFeature = for {
features <- request.features
experimental <- features.experimental
_ <- experimental.selfServiceErrorCodes
} yield SelfServiceErrorCodes
Features(selfServiceErrorCodesFeature.toList)
}
}
case class Features(features: Seq[Feature]) {
val selfServiceErrorCodes: Boolean = SelfServiceErrorCodes.enabled(features)
}
sealed trait Feature
sealed trait ExperimentalFeature extends Feature
case object SelfServiceErrorCodes extends ExperimentalFeature {
def enabled(features: Seq[Feature]): Boolean = features.contains(SelfServiceErrorCodes)
}

View File

@ -13,6 +13,7 @@ import com.daml.ledger.api.testtool.infrastructure.{
import com.daml.ledger.api.tls.TlsConfiguration
import com.daml.ledger.api.v1.ledger_identity_service.GetLedgerIdentityRequest
import com.daml.ledger.api.v1.transaction_service.GetLedgerEndRequest
import com.daml.ledger.api.v1.version_service.GetLedgerApiVersionRequest
import com.daml.timer.RetryStrategy
import io.grpc.ClientInterceptor
import org.slf4j.LoggerFactory
@ -31,18 +32,21 @@ private[infrastructure] final class ParticipantSession private (
// global state of the ledger breaks this assumption, no matter what.
ledgerId: String,
ledgerEndpoint: Endpoint,
val features: Features,
)(implicit val executionContext: ExecutionContext) {
private[testtool] def createInitContext(
applicationId: String,
identifierSuffix: String,
clientTlsConfiguration: Option[TlsConfiguration],
features: Features,
): Future[ParticipantTestContext] =
createTestContext(
"init",
applicationId,
identifierSuffix,
clientTlsConfiguration = clientTlsConfiguration,
features = features,
)
private[testtool] def createTestContext(
@ -50,6 +54,7 @@ private[infrastructure] final class ParticipantSession private (
applicationId: String,
identifierSuffix: String,
clientTlsConfiguration: Option[TlsConfiguration],
features: Features,
): Future[ParticipantTestContext] =
for {
end <- services.transaction.getLedgerEnd(new GetLedgerEndRequest(ledgerId)).map(_.getOffset)
@ -63,6 +68,7 @@ private[infrastructure] final class ParticipantSession private (
partyAllocation = partyAllocation,
ledgerEndpoint = ledgerEndpoint,
clientTlsConfiguration = clientTlsConfiguration,
features = features,
)
}
@ -94,12 +100,23 @@ object ParticipantSession {
.recoverWith { case NonFatal(exception) =>
Future.failed(new Errors.ParticipantConnectionException(exception))
}
features <-
services.version
.getLedgerApiVersion(new GetLedgerApiVersionRequest(ledgerId))
.map(Features.fromApiVersionResponse)
.recover { case failure =>
// TODO feature descriptors: Remove once all Ledger API implementations respond successfully on VersionService endpoint
logger.warn(
s"Failure in retrieving the feature descriptors from the version service: $failure"
)
Features(Seq.empty)
}
} yield new ParticipantSession(
partyAllocation = partyAllocation,
services = services,
ledgerId = ledgerId,
ledgerEndpoint = participant.endpoint,
features = features,
)
}
}

View File

@ -109,6 +109,7 @@ private[testtool] final class ParticipantTestContext private[participant] (
partyAllocation: PartyAllocationConfiguration,
val ledgerEndpoint: Endpoint,
val clientTlsConfiguration: Option[TlsConfiguration],
val features: Features,
)(implicit ec: ExecutionContext) {
import ParticipantTestContext._

View File

@ -3,16 +3,21 @@
package com.daml.ledger.api.testtool.suites
import com.daml.error.definitions.LedgerApiErrors
import com.daml.grpc.{GrpcException, GrpcStatus}
import com.daml.ledger.api.refinements.ApiTypes.Party
import com.daml.ledger.api.testtool.infrastructure.Allocation._
import com.daml.ledger.api.testtool.infrastructure.Assertions.{assertGrpcError, fail}
import com.daml.ledger.api.testtool.infrastructure.Assertions.{
assertGrpcError,
assertSelfServiceErrorCode,
fail,
}
import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite
import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext
import com.daml.ledger.api.v1.value.{Record, RecordField, Value}
import com.daml.ledger.client.binding.Primitive.ContractId
import com.daml.ledger.test.semantic.ContractIdTests._
import io.grpc.Status
import io.grpc.{Status, StatusRuntimeException}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
@ -53,8 +58,10 @@ final class ContractIdIT extends LedgerTestSuite {
case Success(_) if accepted => ()
case Failure(err: Throwable) if !accepted =>
assertGrpcError(
alpha,
err,
Status.Code.INVALID_ARGUMENT,
LedgerApiErrors.PreprocessingErrors.PreprocessingFailed,
Some(s"""Illegal Contract ID "$testedCid""""),
checkDefiniteAnswerMetadata = true,
)
@ -81,13 +88,27 @@ final class ContractIdIT extends LedgerTestSuite {
)
.transformWith(Future.successful)
} yield result match {
// Assert V1 error code
case Failure(GrpcException(GrpcStatus(Status.Code.ABORTED, Some(msg)), _))
if msg.contains(s"Contract could not be found with id ContractId($testedCid).") =>
if !alpha.features.selfServiceErrorCodes && msg.contains(
s"Contract could not be found with id ContractId($testedCid)"
) =>
Success(())
case Success(_) =>
Failure(new UnknownError("Unexpected Success"))
case otherwise =>
otherwise.map(_ => ())
// Assert self-service error code
case Failure(exception: StatusRuntimeException)
if alpha.features.selfServiceErrorCodes &&
Try(
assertSelfServiceErrorCode(
statusRuntimeException = exception,
expectedErrorCode =
LedgerApiErrors.InterpreterErrors.LookupErrors.ContractNotFound,
)
).isSuccess =>
Success(())
case Success(_) => Failure(new UnknownError("Unexpected Success"))
case otherwise => otherwise.map(_ => ())
}
}

View File

@ -162,6 +162,25 @@ conformance_test(
],
)
# WIP Full conformance test asserting the Ledger API returning self-service error codes
conformance_test(
name = "conformance-test-self-service-error-codes",
ports = [6865],
server = ":app",
server_args = [
"--contract-id-seeding=testing-weak",
"--participant=participant-id=example,port=6865",
"--max-deduplication-duration=PT5S",
"--index-append-only-schema",
"--mutable-contract-state-cache",
"--use-self-service-error-codes",
],
test_tool_args = [
# TODO error codes: Assert all suites as from the `conformance-test`
"--include=ContractIdIT:RejectV0,ContractIdIT:AcceptSuffixedV1,ContractIdIT:AcceptNonSuffixedV1",
],
)
# Feature test: --daml-lf-min-version-1.14-unsafe
# This asserts that you can use the 1.14 packages despite their dependencies on a few stable packages in older LF versions.
conformance_test(

View File

@ -154,7 +154,7 @@ private[daml] object ApiServices {
ApiLedgerIdentityService.create(() => identityService.getLedgerId(), errorsVersionsSwitcher)
val apiVersionService =
ApiVersionService.create(errorsVersionsSwitcher)
ApiVersionService.create(enableSelfServiceErrorCodes)
val apiPackageService =
ApiPackageService.create(ledgerId, packagesService, errorsVersionsSwitcher)

View File

@ -110,9 +110,6 @@ private[apiserver] final class ApiSubmissionService private[services] (
with AutoCloseable {
private val logger = ContextualizedLogger.get(this.getClass)
// TODO error codes: review conformance mode usages wherever RejectionGenerators is instantiated
private val rejectionGenerators = new RejectionGenerators(conformanceMode = true)
private val errorFactories = ErrorFactories(errorCodesVersionSwitcher)
override def submit(
@ -335,7 +332,7 @@ private[apiserver] final class ApiSubmissionService private[services] (
errorCodesVersionSwitcher.chooseAsFailedFuture(
v1 = toStatusExceptionV1(error),
v2 = rejectionGenerators
v2 = RejectionGenerators
.commandExecutorError(cause = ErrorCauseExport.fromErrorCause(error)),
)
}

View File

@ -8,8 +8,13 @@ import com.daml.error.{
DamlContextualizedErrorLogger,
ErrorCodesVersionSwitcher,
}
import com.daml.ledger.api.v1.experimental_features.{
ExperimentalFeatures,
ExperimentalSelfServiceErrorCodes,
}
import com.daml.ledger.api.v1.version_service.VersionServiceGrpc.VersionService
import com.daml.ledger.api.v1.version_service.{
FeaturesDescriptor,
GetLedgerApiVersionRequest,
GetLedgerApiVersionResponse,
VersionServiceGrpc,
@ -24,16 +29,15 @@ import scala.io.Source
import scala.util.Try
import scala.util.control.NonFatal
private[apiserver] final class ApiVersionService private (
errorCodesVersionSwitcher: ErrorCodesVersionSwitcher
)(implicit
private[apiserver] final class ApiVersionService private (enableSelfServiceErrorCodes: Boolean)(
implicit
loggingContext: LoggingContext,
ec: ExecutionContext,
) extends VersionService
with GrpcApiService {
private val errorCodesVersionSwitcher = new ErrorCodesVersionSwitcher(enableSelfServiceErrorCodes)
private val logger = ContextualizedLogger.get(this.getClass)
private val errorFactories = ErrorFactories(errorCodesVersionSwitcher)
private implicit val contextualizedErrorLogger: ContextualizedErrorLogger =
new DamlContextualizedErrorLogger(logger, loggingContext, None)
@ -46,12 +50,21 @@ private[apiserver] final class ApiVersionService private (
): Future[GetLedgerApiVersionResponse] =
Future
.fromTry(apiVersion)
.map(GetLedgerApiVersionResponse(_))
.map(apiVersionResponse)
.andThen(logger.logErrorsOnCall[GetLedgerApiVersionResponse])
.recoverWith { case NonFatal(_) =>
internalError
}
private def apiVersionResponse(version: String) =
if (enableSelfServiceErrorCodes)
GetLedgerApiVersionResponse(version).withFeatures(
FeaturesDescriptor().withExperimental(
ExperimentalFeatures().withSelfServiceErrorCodes(ExperimentalSelfServiceErrorCodes())
)
)
else GetLedgerApiVersionResponse(version)
private lazy val internalError: Future[Nothing] =
Future.failed(
errorFactories.versionServiceInternalError(message = "Cannot read Ledger API version")
@ -75,7 +88,7 @@ private[apiserver] final class ApiVersionService private (
private[apiserver] object ApiVersionService {
def create(
errorCodesVersionSwitcher: ErrorCodesVersionSwitcher
enableSelfServiceErrorCodes: Boolean
)(implicit loggingContext: LoggingContext, ec: ExecutionContext): ApiVersionService =
new ApiVersionService(errorCodesVersionSwitcher)
new ApiVersionService(enableSelfServiceErrorCodes)
}

View File

@ -248,7 +248,7 @@ class ApiSubmissionServiceSpec
LfError.Interpretation.DamlException(LfInterpretationError.ContractNotFound("#cid")),
None,
)
) -> ((Status.ABORTED, Status.ABORTED)),
) -> ((Status.ABORTED, Status.NOT_FOUND)),
ErrorCause.DamlLf(
LfError.Interpretation(
LfError.Interpretation.DamlException(