diff --git a/sdk/canton/BUILD.bazel b/sdk/canton/BUILD.bazel index 4e12010cef46..a85ac02aa339 100644 --- a/sdk/canton/BUILD.bazel +++ b/sdk/canton/BUILD.bazel @@ -630,7 +630,6 @@ scala_library( "@maven//:dev_optics_monocle_macro_2_13", "@maven//:io_circe_circe_core_2_13", "@maven//:io_grpc_grpc_api", - "@maven//:io_grpc_grpc_netty", "@maven//:io_grpc_grpc_services", "@maven//:io_grpc_grpc_stub", "@maven//:io_netty_netty_handler", diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala index 331ba63ca6d1..57f50be72f9d 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala @@ -6,9 +6,8 @@ package com.digitalasset.canton.admin.api.client import cats.data.EitherT import com.daml.grpc.AuthCallCredentials import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand -import com.digitalasset.canton.lifecycle.OnShutdownRunner import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil +import com.digitalasset.canton.networking.grpc.{CantonGrpcUtil, GrpcClient, GrpcManagedChannel} import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.LoggerUtil import io.grpc.ManagedChannel @@ -22,7 +21,6 @@ import scala.concurrent.{ExecutionContext, Future} class GrpcCtlRunner( maxRequestDebugLines: Int, maxRequestDebugStringLength: Int, - onShutdownRunner: OnShutdownRunner, val loggerFactory: NamedLoggerFactory, ) extends NamedLogging { @@ -32,27 +30,30 @@ class GrpcCtlRunner( def run[Req, Res, Result]( instanceName: String, command: GrpcAdminCommand[Req, Res, Result], - channel: ManagedChannel, + managedChannel: GrpcManagedChannel, token: Option[String], timeout: Duration, )(implicit ec: ExecutionContext, traceContext: TraceContext): EitherT[Future, String, Result] = { - val baseService: command.Svc = command - .createServiceInternal(channel) - .withInterceptors(TraceContextGrpc.clientInterceptor) - - val service = token.fold(baseService)(AuthCallCredentials.authorizingStub(baseService, _)) + def service(channel: ManagedChannel): command.Svc = { + val baseService = command + .createServiceInternal(channel) + .withInterceptors(TraceContextGrpc.clientInterceptor) + token.toList.foldLeft(baseService)(AuthCallCredentials.authorizingStub) + } + val client = GrpcClient.create(managedChannel, service) for { request <- EitherT.fromEither[Future](command.createRequestInternal()) - response <- submitRequest(command)(instanceName, service, request, timeout) + response <- submitRequest(command)(instanceName, client, request, timeout) result <- EitherT.fromEither[Future](command.handleResponseInternal(response)) } yield result } private def submitRequest[Svc <: AbstractStub[Svc], Req, Res, Result]( command: GrpcAdminCommand[Req, Res, Result] - )(instanceName: String, service: command.Svc, request: Req, timeout: Duration)(implicit + )(instanceName: String, service: GrpcClient[command.Svc], request: Req, timeout: Duration)( + implicit ec: ExecutionContext, traceContext: TraceContext, ): EitherT[Future, String, Res] = CantonGrpcUtil.shutdownAsGrpcErrorE( @@ -64,8 +65,7 @@ class GrpcCtlRunner( ), timeout, logger, - onShutdownRunner, - CantonGrpcUtil.silentLogPolicy, // silent log policy, as the ConsoleEnvironment will log the result + CantonGrpcUtil.SilentLogPolicy, // silent log policy, as the ConsoleEnvironment will log the result _ => false, // no retry to optimize for low latency ) .leftMap(_.toString) diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleGrpcAdminCommandRunner.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleGrpcAdminCommandRunner.scala index 643a081a69c1..80e5b5649ddf 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleGrpcAdminCommandRunner.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleGrpcAdminCommandRunner.scala @@ -16,10 +16,13 @@ import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.Port import com.digitalasset.canton.config.{ClientConfig, ConsoleCommandTimeout, NonNegativeDuration} import com.digitalasset.canton.environment.Environment -import com.digitalasset.canton.lifecycle.Lifecycle.CloseableChannel import com.digitalasset.canton.lifecycle.OnShutdownRunner import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.{CantonGrpcUtil, ClientChannelBuilder} +import com.digitalasset.canton.networking.grpc.{ + CantonGrpcUtil, + ClientChannelBuilder, + GrpcManagedChannel, +} import com.digitalasset.canton.tracing.{Spanning, TraceContext} import io.opentelemetry.api.trace.Tracer @@ -49,10 +52,9 @@ class GrpcAdminCommandRunner( private val grpcRunner = new GrpcCtlRunner( environment.config.monitoring.logging.api.maxMessageLines, environment.config.monitoring.logging.api.maxStringLength, - onShutdownRunner = this, loggerFactory, ) - private val channels = TrieMap[(String, String, Port), CloseableChannel]() + private val channels = TrieMap[(String, String, Port), GrpcManagedChannel]() def runCommandAsync[Result]( instanceName: String, @@ -88,7 +90,8 @@ class GrpcAdminCommandRunner( CantonGrpcUtil.checkCantonApiInfo( serverName = instanceName, expectedName = apiName, - channel = ClientChannelBuilder.createChannelToTrustedServer(clientConfig), + channelBuilder = + ClientChannelBuilder.createChannelBuilderToTrustedServer(clientConfig), logger = logger, timeout = commandTimeouts.bounded, onShutdownRunner = this, @@ -97,12 +100,12 @@ class GrpcAdminCommandRunner( ) } } - closeableChannel = getOrCreateChannel(instanceName, clientConfig, callTimeout) + channel = getOrCreateChannel(instanceName, clientConfig) _ = logger.debug(s"Running command $command on $instanceName against $clientConfig") result <- grpcRunner.run( instanceName, command, - closeableChannel.channel, + channel, token, callTimeout.duration, ) @@ -135,16 +138,16 @@ class GrpcAdminCommandRunner( private def getOrCreateChannel( instanceName: String, clientConfig: ClientConfig, - callTimeout: config.NonNegativeDuration, - ): CloseableChannel = + ): GrpcManagedChannel = blocking(synchronized { val addr = (instanceName, clientConfig.address, clientConfig.port) channels.getOrElseUpdate( addr, - new CloseableChannel( - ClientChannelBuilder.createChannelToTrustedServer(clientConfig), - logger, + GrpcManagedChannel( s"ConsoleCommand", + ClientChannelBuilder.createChannelBuilderToTrustedServer(clientConfig).build(), + this, + logger, ), ) }) @@ -153,10 +156,10 @@ class GrpcAdminCommandRunner( override def onFirstClose(): Unit = closeChannels() - def closeChannels(): Unit = { + def closeChannels(): Unit = blocking(synchronized { channels.values.foreach(_.close()) channels.clear() - } + }) } /** A console-specific version of the GrpcAdminCommandRunner that uses the console environment diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala index 7ba26c0b61ee..d8e2701d654e 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala @@ -6,12 +6,14 @@ package com.digitalasset.canton.console.commands import better.files.File import cats.syntax.either.* import cats.syntax.foldable.* +import com.daml.nonempty.NonEmpty import com.digitalasset.canton.admin.api.client.commands.ParticipantAdminCommands import com.digitalasset.canton.admin.participant.v30.ExportAcsResponse import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.config.{ConsoleCommandTimeout, NonNegativeDuration} import com.digitalasset.canton.console.{ AdminCommandRunner, + CommandErrors, ConsoleCommandResult, ConsoleEnvironment, FeatureFlag, @@ -392,17 +394,26 @@ abstract class LocalParticipantRepairAdministration( skipInactive: Boolean = true, batchSize: Int = 100, ): Unit = - runRepairCommand(tc => - access( - _.sync.repairService.changeAssignationAwait( - contractIds, - sourceDomain, - targetDomain, - skipInactive, - PositiveInt.tryCreate(batchSize), - )(tc) - ) - ) + NonEmpty + .from(contractIds.distinct) match { + case Some(contractIds) => + runRepairCommand(tc => + access( + _.sync.repairService.changeAssignationAwait( + contractIds, + sourceDomain, + targetDomain, + skipInactive, + PositiveInt.tryCreate(batchSize), + )(tc) + ) + ) + case None => + consoleEnvironment.run( + CommandErrors + .GenericCommandError("contractIds must be non-empty") + ) + } @Help.Summary("Rollback an unassignment by re-assigning the contract to the source domain.") @Help.Description( diff --git a/sdk/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala b/sdk/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala index 4df98c4ed0c5..a42fef20ceca 100644 --- a/sdk/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala +++ b/sdk/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.admin.api.client import com.digitalasset.canton.BaseTest import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand import com.digitalasset.canton.lifecycle.OnShutdownRunner.PureOnShutdownRunner +import com.digitalasset.canton.networking.grpc.GrpcManagedChannel import io.grpc.stub.AbstractStub import io.grpc.{CallOptions, Channel, ManagedChannel} import org.scalatest.wordspec.AsyncWordSpec @@ -20,8 +21,7 @@ class GrpcCtlRunnerTest extends AsyncWordSpec with BaseTest { val (channel, command) = defaultMocks() "run successfully" in { - val onShutdownRunner = new PureOnShutdownRunner(logger) - new GrpcCtlRunner(1000, 1000, onShutdownRunner, loggerFactory).run( + new GrpcCtlRunner(1000, 1000, loggerFactory).run( "participant1", command, channel, @@ -38,7 +38,7 @@ class GrpcCtlRunnerTest extends AsyncWordSpec with BaseTest { override def build(channel: Channel, callOptions: CallOptions): TestAbstractStub = this } - private def defaultMocks(): (ManagedChannel, GrpcAdminCommand[String, String, String]) = { + private def defaultMocks(): (GrpcManagedChannel, GrpcAdminCommand[String, String, String]) = { val channel = mock[ManagedChannel] val service = new TestAbstractStub(channel) val command = new GrpcAdminCommand[String, String, String] { @@ -53,6 +53,14 @@ class GrpcCtlRunnerTest extends AsyncWordSpec with BaseTest { ) } - (channel, command) + ( + GrpcManagedChannel( + "GrcpCtrlRunnerTestChannel", + channel, + new PureOnShutdownRunner(logger), + logger, + ), + command, + ) } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/CacheConfig.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/CacheConfig.scala index e0efb4fd5638..390752d95cba 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/CacheConfig.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/CacheConfig.scala @@ -38,8 +38,11 @@ final case class CacheConfigWithTimeout( expireAfterTimeout: PositiveFiniteDuration = PositiveFiniteDuration.ofMinutes(10), ) { - def buildScaffeine(): Scaffeine[Any, Any] = - Scaffeine().maximumSize(maximumSize.value).expireAfterWrite(expireAfterTimeout.underlying) + def buildScaffeine()(implicit executionContext: ExecutionContext): Scaffeine[Any, Any] = + Scaffeine() + .maximumSize(maximumSize.value) + .expireAfterWrite(expireAfterTimeout.underlying) + .executor(executionContext.execute(_)) } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/ServerConfig.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/ServerConfig.scala index 43e8c71d76b8..57d7bf779aa9 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/ServerConfig.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/config/ServerConfig.scala @@ -223,7 +223,9 @@ final case class ClientConfig( port: Port, tls: Option[TlsClientConfig] = None, keepAliveClient: Option[KeepAliveClientConfig] = Some(KeepAliveClientConfig()), -) +) { + def endpointAsString: String = address + ":" + port.unwrap +} sealed trait BaseTlsArguments { def certChainFile: ExistingFile diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/logging/LastErrorsAppender.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/logging/LastErrorsAppender.scala index a9b936e0e293..4b38bd819921 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/logging/LastErrorsAppender.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/logging/LastErrorsAppender.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.discard.Implicits.DiscardOps import com.github.blemale.scaffeine.Scaffeine import java.util +import java.util.concurrent.Executors import scala.collection.mutable /** Logback appender that keeps a bounded queue of errors/warnings that have been logged and associated log entries with @@ -33,13 +34,22 @@ class LastErrorsAppender() private var maxErrors = 128 private var lastErrorsFileAppenderName = "" + /** Separate executor for scaffeine - to avoid using ForkJoin common pool */ + private val scaffeineExecutor = Executors.newSingleThreadExecutor() + /** An error/warn event with previous events of the same trace-id */ private case class ErrorWithEvents(error: ILoggingEvent, events: Seq[ILoggingEvent]) private val eventsCache = - Scaffeine().maximumSize(maxTraces.toLong).build[String, BoundedQueue[ILoggingEvent]]() + Scaffeine() + .maximumSize(maxTraces.toLong) + .executor(scaffeineExecutor) + .build[String, BoundedQueue[ILoggingEvent]]() private val errorsCache = - Scaffeine().maximumSize(maxErrors.toLong).build[String, ErrorWithEvents]() + Scaffeine() + .maximumSize(maxErrors.toLong) + .executor(scaffeineExecutor) + .build[String, ErrorWithEvents]() private def isLastErrorsFileAppender(appender: Appender[ILoggingEvent]): Boolean = appender.getName == lastErrorsFileAppenderName diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtil.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtil.scala index 4cf737cd4255..bd127875a6e2 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtil.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtil.scala @@ -11,7 +11,7 @@ import com.digitalasset.canton.concurrent.DirectExecutionContext import com.digitalasset.canton.connection.v30.{ApiInfoServiceGrpc, GetApiInfoRequest} import com.digitalasset.canton.error.CantonErrorGroups.GrpcErrorGroup import com.digitalasset.canton.error.{BaseCantonError, CantonError} -import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, Lifecycle, OnShutdownRunner} +import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, OnShutdownRunner} import com.digitalasset.canton.logging.{ErrorLoggingContext, TracedLogger} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult @@ -111,14 +111,12 @@ object CantonGrpcUtil { * @param retryPolicy invoked after an error to determine whether to retry */ @GrpcServiceInvocationMethod - def sendGrpcRequest[Svc <: AbstractStub[Svc], Res](client: Svc, serverName: String)( + def sendGrpcRequest[Svc <: AbstractStub[Svc], Res](client: GrpcClient[Svc], serverName: String)( send: Svc => Future[Res], requestDescription: String, timeout: Duration, logger: TracedLogger, - onShutdownRunner: OnShutdownRunner, - logPolicy: GrpcError => TracedLogger => TraceContext => Unit = err => - logger => traceContext => err.log(logger)(traceContext), + logPolicy: GrpcLogPolicy = DefaultGrpcLogPolicy, retryPolicy: GrpcError => Boolean = _.retry, )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, GrpcError, Res] = { implicit val ec: ExecutionContext = DirectExecutionContext(logger) @@ -132,19 +130,19 @@ object CantonGrpcUtil { // This deadline is significantly before `requestDeadline`, because we want to avoid DEADLINE_EXCEEDED due to overly short deadlines. val retryDeadline = requestDeadline.offset(-finite.toMillis * 3 / 10, TimeUnit.MILLISECONDS) ( - client.withDeadline(requestDeadline), + client.service.withDeadline(requestDeadline), (backoffMs: Long) => Math.min(backoffMs, retryDeadline.timeRemaining(TimeUnit.MILLISECONDS)), ) case Duration.Inf => - (client, Predef.identity[Long]) + (client.service, Predef.identity[Long]) case _ => logger.error(s"Ignoring unexpected timeout $timeout value.") - (client, Predef.identity[Long]) + (client.service, Predef.identity[Long]) } def go(backoffMs: Long): FutureUnlessShutdown[Either[GrpcError, Res]] = - if (onShutdownRunner.isClosing) FutureUnlessShutdown.abortedDueToShutdown + if (client.onShutdownRunner.isClosing) FutureUnlessShutdown.abortedDueToShutdown else { logger.debug(s"Sending request $requestDescription to $serverName.") val sendF = sendGrpcRequestUnsafe(clientWithDeadline)(send) @@ -154,31 +152,36 @@ object CantonGrpcUtil { FutureUnlessShutdown.pure(Right(value)).unwrap case Failure(e: StatusRuntimeException) => val error = GrpcError(requestDescription, serverName, e) - logPolicy(error)(logger)(traceContext) - if (retryPolicy(error)) { - val effectiveBackoff = calcEffectiveBackoff(backoffMs) - if (effectiveBackoff > 0) { - logger.info(s"Waiting for ${effectiveBackoff}ms before retrying...") - DelayUtil - .delayIfNotClosing( - s"Delay retrying request $requestDescription for $serverName", - FiniteDuration.apply(effectiveBackoff, TimeUnit.MILLISECONDS), - onShutdownRunner, - ) - .flatMap { _ => - logger.info(s"Retrying request $requestDescription for $serverName...") - go(backoffMs * 2) - } - .unwrap + if (client.onShutdownRunner.isClosing) { + logger.info(s"Ignoring gRPC error due to shutdown. Ignored error: $error") + FutureUnlessShutdown.abortedDueToShutdown.unwrap + } else { + logPolicy.log(error, logger) + if (retryPolicy(error)) { + val effectiveBackoff = calcEffectiveBackoff(backoffMs) + if (effectiveBackoff > 0) { + logger.info(s"Waiting for ${effectiveBackoff}ms before retrying...") + DelayUtil + .delayIfNotClosing( + s"Delay retrying request $requestDescription for $serverName", + FiniteDuration.apply(effectiveBackoff, TimeUnit.MILLISECONDS), + client.onShutdownRunner, + ) + .flatMap { _ => + logger.info(s"Retrying request $requestDescription for $serverName...") + go(backoffMs * 2) + } + .unwrap + } else { + logger.warn("Retry timeout has elapsed, giving up.") + FutureUnlessShutdown.pure(Left(error)).unwrap + } } else { - logger.warn("Retry timeout has elapsed, giving up.") + logger.debug( + s"Retry has not been configured for ${error.getClass.getSimpleName}, giving up." + ) FutureUnlessShutdown.pure(Left(error)).unwrap } - } else { - logger.debug( - s"Retry has not been configured for ${error.getClass.getSimpleName}, giving up." - ) - FutureUnlessShutdown.pure(Left(error)).unwrap } case Failure(e) => logger.error( @@ -201,37 +204,72 @@ object CantonGrpcUtil { def sendSingleGrpcRequest[Svc <: AbstractStub[Svc], Res]( serverName: String, requestDescription: String, - channel: ManagedChannel, + channelBuilder: ManagedChannelBuilderProxy, stubFactory: Channel => Svc, timeout: Duration, logger: TracedLogger, - logPolicy: GrpcError => TracedLogger => TraceContext => Unit = err => - logger => traceContext => err.log(logger)(traceContext), onShutdownRunner: OnShutdownRunner, + logPolicy: GrpcLogPolicy = DefaultGrpcLogPolicy, retryPolicy: GrpcError => Boolean, token: Option[String], )( send: Svc => Future[Res] - )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, GrpcError, Res] = { + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, GrpcError, Res] = { + val (_, fut) = sendSingleGrpcRequestInternal( + serverName, + requestDescription, + channelBuilder, + stubFactory, + timeout, + logger, + onShutdownRunner, + logPolicy, + retryPolicy, + token, + )(send) + fut + } - val closeableChannel = Lifecycle.toCloseableChannel(channel, logger, "sendSingleGrpcRequest") - val stub = token.foldLeft(stubFactory(closeableChannel.channel))((stub, token) => - AuthCallCredentials.authorizingStub(stub, token) + @GrpcServiceInvocationMethod + private def sendSingleGrpcRequestInternal[Svc <: AbstractStub[Svc], Res]( + serverName: String, + requestDescription: String, + channelBuilder: ManagedChannelBuilderProxy, + stubFactory: Channel => Svc, + timeout: Duration, + logger: TracedLogger, + onShutdownRunner: OnShutdownRunner, + logPolicy: GrpcLogPolicy, + retryPolicy: GrpcError => Boolean, + token: Option[String], + )(send: Svc => Future[Res])(implicit + traceContext: TraceContext + ): (GrpcManagedChannelHandle, EitherT[FutureUnlessShutdown, GrpcError, Res]) = { + val managedChannel = GrpcManagedChannel( + "sendSingleGrpcRequest", + channelBuilder.build(), + onShutdownRunner, + logger, + ) + val client = GrpcClient.create( + managedChannel, + channel => token.foldLeft(stubFactory(channel))(AuthCallCredentials.authorizingStub), ) - val res = sendGrpcRequest(stub, serverName)( + val res = sendGrpcRequest(client, serverName)( send(_), requestDescription, timeout, logger, - onShutdownRunner, logPolicy, retryPolicy, ) implicit val ec: ExecutionContext = DirectExecutionContext(logger) - res.thereafter { _ => - closeableChannel.close() + managedChannel.handle -> res.thereafter { _ => + managedChannel.close() } } @@ -246,11 +284,25 @@ object CantonGrpcUtil { )(implicit traceContext: TraceContext): Future[Resp] = TraceContextGrpc.withGrpcContext(traceContext)(send(service)) - def silentLogPolicy(error: GrpcError)(logger: TracedLogger)(traceContext: TraceContext): Unit = - // Log an info, if a cause is defined to not discard the cause information - Option(error.status.getCause).foreach { cause => - logger.info(error.toString, cause)(traceContext) - } + trait GrpcLogPolicy { + def log(error: GrpcError, logger: TracedLogger)(implicit + traceContext: TraceContext + ): Unit + } + object DefaultGrpcLogPolicy extends GrpcLogPolicy { + override def log(error: GrpcError, logger: TracedLogger)(implicit + traceContext: TraceContext + ): Unit = error.log(logger) + } + object SilentLogPolicy extends GrpcLogPolicy { + def log(error: GrpcError, logger: TracedLogger)(implicit + traceContext: TraceContext + ): Unit = + // Log an info, if a cause is defined to not discard the cause information + Option(error.status.getCause).foreach { cause => + logger.info(error.toString, cause) + } + } object RetryPolicy { lazy val noRetry: GrpcError => Boolean = _ => false @@ -304,7 +356,7 @@ object CantonGrpcUtil { def checkCantonApiInfo( serverName: String, expectedName: String, - channel: ManagedChannel, + channelBuilder: ManagedChannelBuilderProxy, logger: TracedLogger, timeout: config.NonNegativeDuration, onShutdownRunner: OnShutdownRunner, @@ -312,50 +364,42 @@ object CantonGrpcUtil { )(implicit ec: ExecutionContext, traceContext: TraceContext, - ): EitherT[FutureUnlessShutdown, String, Unit] = + ): EitherT[FutureUnlessShutdown, String, Unit] = { + val (channelHandle, sendF) = CantonGrpcUtil + .sendSingleGrpcRequestInternal( + serverName = s"$serverName/$expectedName", + requestDescription = "GetApiInfo", + channelBuilder = channelBuilder, + stubFactory = ApiInfoServiceGrpc.stub, + timeout = timeout.unwrap, + logger = logger, + logPolicy = CantonGrpcUtil.SilentLogPolicy, + retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, + onShutdownRunner = onShutdownRunner, + token = token, + )(_.getApiInfo(GetApiInfoRequest())) for { - apiInfo <- EitherTUtil - .leftSubflatMap( - CantonGrpcUtil - .sendSingleGrpcRequest( - serverName = s"$serverName/$expectedName", - requestDescription = "GetApiInfo", - channel = channel, - stubFactory = ApiInfoServiceGrpc.stub, - timeout = timeout.unwrap, - logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, - onShutdownRunner = onShutdownRunner, - retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, - token = token, - )(_.getApiInfo(GetApiInfoRequest())) - .map(_.name) - ) { - // TODO(i16458): Remove this special case once we have a stable release - case _: GrpcError.GrpcServiceUnavailable => - logger.debug( - s"Endpoint '$channel' is not providing an API info service, " + - s"will skip the check for '$serverName/$expectedName' " + - "and assume it is running an older version of Canton." - ) - Right(expectedName) - case error => - Left(error.toString) - } - errorMessage = apiInfoErrorMessage(channel, apiInfo, expectedName, serverName) + apiInfo <- EitherTUtil.leftSubflatMap(sendF.map(_.name)) { + // TODO(i16458): Remove this special case once we have a stable release + case _: GrpcError.GrpcServiceUnavailable => + logger.debug( + s"Endpoint '$channelHandle' is not providing an API info service, " + + s"will skip the check for '$serverName/$expectedName' " + + "and assume it is running an older version of Canton." + ) + Right(expectedName) + case error => + Left(error.toString) + } _ <- - EitherTUtil.condUnitET[FutureUnlessShutdown](apiInfo == expectedName, errorMessage) + EitherTUtil.condUnitET[FutureUnlessShutdown]( + apiInfo == expectedName, + s"Endpoint '${channelHandle.toString}' provides '$apiInfo', " + + s"expected '$expectedName'. This message indicates a possible mistake in configuration, " + + s"please check node connection settings for '$serverName'.", + ) } yield () - - private[grpc] def apiInfoErrorMessage( - channel: Channel, - receivedApiName: String, - expectedApiName: String, - serverName: String, - ): String = - s"Endpoint '$channel' provides '$receivedApiName', " + - s"expected '$expectedApiName'. This message indicates a possible mistake in configuration, " + - s"please check node connection settings for '$serverName'." + } object ApiName { val AdminApi: String = "admin-api" diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ClientChannelBuilder.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ClientChannelBuilder.scala index 83dfa3f5741c..95fcef13c16e 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ClientChannelBuilder.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ClientChannelBuilder.scala @@ -15,7 +15,6 @@ import com.digitalasset.canton.tracing.TraceContextGrpc import com.digitalasset.canton.tracing.TracingConfig.Propagation import com.digitalasset.canton.util.ResourceUtil.withResource import com.google.protobuf.ByteString -import io.grpc.ManagedChannel import io.grpc.netty.{GrpcSslContexts, NettyChannelBuilder} import io.netty.handler.ssl.{SslContext, SslContextBuilder} @@ -52,14 +51,12 @@ trait ClientChannelBuilder { .useTransportSecurity() // this is strictly unnecessary as is the default for the channel builder, but can't hurt either // add certificates if provided - trustCertificate - .fold(builder) { certChain => - val sslContext = withResource(certChain.newInput()) { inputStream => - GrpcSslContexts.forClient().trustManager(inputStream).build() - } - builder.sslContext(sslContext) + trustCertificate.foreach { certChain => + val sslContext = withResource(certChain.newInput()) { inputStream => + GrpcSslContexts.forClient().trustManager(inputStream).build() } - .discard + builder.sslContext(sslContext) + } } else builder.usePlaintext().discard @@ -159,16 +156,16 @@ object ClientChannelBuilder { /** Simple channel construction for test and console clients. * `maxInboundMessageSize` is 2GB; so don't use this to connect to an untrusted server. */ - def createChannelToTrustedServer( + def createChannelBuilderToTrustedServer( clientConfig: ClientConfig - )(implicit executor: Executor): ManagedChannel = { + )(implicit executor: Executor): ManagedChannelBuilderProxy = { val baseBuilder: NettyChannelBuilder = NettyChannelBuilder .forAddress(clientConfig.address, clientConfig.port.unwrap) .executor(executor) .maxInboundMessageSize(Int.MaxValue) // apply keep alive settings - configureKeepAlive( + val builder = configureKeepAlive( clientConfig.keepAliveClient, // if tls isn't configured assume that it's a plaintext channel clientConfig.tls @@ -177,6 +174,7 @@ object ClientChannelBuilder { .useTransportSecurity() .sslContext(sslContext(tls)) }, - ).build() + ) + ManagedChannelBuilderProxy(builder) } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcClient.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcClient.scala new file mode 100644 index 000000000000..837516832265 --- /dev/null +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcClient.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.networking.grpc + +import com.digitalasset.canton.lifecycle.OnShutdownRunner +import io.grpc.ManagedChannel +import io.grpc.stub.AbstractStub + +/** Bundles a service stub together with the channel it is using */ +final class GrpcClient[Svc <: AbstractStub[Svc]] private ( + private[grpc] val channel: GrpcManagedChannel, + val service: Svc, +) { + def onShutdownRunner: OnShutdownRunner = channel.associatedShutdownRunner +} + +object GrpcClient { + def create[Svc <: AbstractStub[Svc]]( + channel: GrpcManagedChannel, + serviceFactory: ManagedChannel => Svc, + ): GrpcClient[Svc] = + new GrpcClient(channel, serviceFactory(channel.channel)) +} diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcManagedChannel.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcManagedChannel.scala new file mode 100644 index 000000000000..f6cdcb148fe8 --- /dev/null +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcManagedChannel.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.networking.grpc + +import com.digitalasset.canton.lifecycle.{Lifecycle, OnShutdownRunner} +import com.digitalasset.canton.logging.TracedLogger +import io.grpc.ManagedChannel + +/** Bundles a gRPC managed channel together with the shutdown runner of the component the channel belongs to */ +final case class GrpcManagedChannel( + name: String, + channel: ManagedChannel, + associatedShutdownRunner: OnShutdownRunner, + override protected val logger: TracedLogger, +) extends AutoCloseable + with OnShutdownRunner { + + // TODO(#21278): Change this so that the channel is force-closed immediately + override protected def onFirstClose(): Unit = + Lifecycle.close(Lifecycle.toCloseableChannel(channel, logger, name))(logger) + + override def close(): Unit = super.close() + + def handle: GrpcManagedChannelHandle = new GrpcManagedChannelHandle(channel) +} + +class GrpcManagedChannelHandle(private val channel: ManagedChannel) { + override def toString: String = channel.toString +} diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ManagedChannelBuilderProxy.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ManagedChannelBuilderProxy.scala new file mode 100644 index 000000000000..0af1565c4e3c --- /dev/null +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/ManagedChannelBuilderProxy.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.networking.grpc + +import io.grpc.{ManagedChannel, ManagedChannelBuilder} + +/** Proxy for a [[io.grpc.ManagedChannelBuilder]] with unknown type argument type + * + * Hides the wildcard type argument to `ManagedChannelBuilder` + * so that type inference will not infer the more correct argument `A forSome { type A <: ManagedChannelBuilder[A] }`, + * which is an existential type that cannot be expressed by wildcards. + * This works only because Scala does not check the bounds of wildcard type arguments. + * + * See https://stackoverflow.com/a/5520212 for some background on existential types + */ +class ManagedChannelBuilderProxy(private val builder: ManagedChannelBuilder[?]) extends AnyVal { + def build(): ManagedChannel = builder.build() +} + +object ManagedChannelBuilderProxy { + def apply(builder: ManagedChannelBuilder[?]): ManagedChannelBuilderProxy = + new ManagedChannelBuilderProxy(builder) +} diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/BftSender.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/BftSender.scala index 055866a32778..3f493615dacd 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/BftSender.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/BftSender.scala @@ -16,7 +16,7 @@ import com.digitalasset.canton.lifecycle.{ import com.digitalasset.canton.logging.{ErrorLoggingContext, TracedLogger} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.Thereafter.syntax.ThereafterOps -import com.digitalasset.canton.util.{ErrorUtil, FutureUtil} +import com.digitalasset.canton.util.{ErrorUtil, FutureUnlessShutdownUtil} import java.util.concurrent.atomic.AtomicInteger import scala.collection.concurrent.TrieMap @@ -134,7 +134,7 @@ object BftSender { } operators.foreach { case (operatorId, operator) => - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( performRequest(operator).value .thereafter(addResult(operatorId, _)) .map(_ => ()), diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/SequencerConnection.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/SequencerConnection.scala index 975cf341bf73..9a3c3a8dafb1 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/SequencerConnection.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/SequencerConnection.scala @@ -8,13 +8,12 @@ import cats.syntax.traverse.* import com.daml.nonempty.NonEmpty import com.digitalasset.canton.admin.domain.v30 import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.networking.grpc.ClientChannelBuilder +import com.digitalasset.canton.networking.grpc.{ClientChannelBuilder, ManagedChannelBuilderProxy} import com.digitalasset.canton.networking.{Endpoint, UrlValidator} import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.tracing.TracingConfig.Propagation import com.digitalasset.canton.{ProtoDeserializationError, SequencerAlias} import com.google.protobuf.ByteString -import io.grpc.netty.NettyChannelBuilder import java.net.URI import java.util.concurrent.Executor @@ -65,9 +64,11 @@ final case class GrpcSequencerConnection( def mkChannelBuilder(clientChannelBuilder: ClientChannelBuilder, tracePropagation: Propagation)( implicit executor: Executor - ): NettyChannelBuilder = - clientChannelBuilder - .create(endpoints, transportSecurity, executor, customTrustCertificates, tracePropagation) + ): ManagedChannelBuilderProxy = + ManagedChannelBuilderProxy( + clientChannelBuilder + .create(endpoints, transportSecurity, executor, customTrustCertificates, tracePropagation) + ) override def toProtoV30: v30.SequencerConnection = v30.SequencerConnection( diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala index 17a20766e066..9c2ab188303a 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala @@ -583,7 +583,7 @@ abstract class SequencerClientImpl( logger.debug( s"Scheduling amplification for message ID $messageId after $patience" ) - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( clock.scheduleAfter(_ => maybeResendAfterPatience(), patience.asJava).flatten, s"Submission request amplification failed for message ID $messageId", ) @@ -1691,7 +1691,7 @@ class SequencerClientImplPekko[E: Pretty]( } val ((killSwitch, subscriptionDoneF, health), completion) = - PekkoUtil.runSupervised(logger.error("Sequencer subscription failed", _), stream) + PekkoUtil.runSupervised(stream, errorLogMessagePrefix = "Sequencer subscription failed") val handle = SubscriptionHandle(killSwitch, subscriptionDoneF, completion) subscriptionHandle.getAndSet(Some(handle)).foreach { _ => // TODO(#13789) Clean up the error logging. diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala index 741a46de7fee..f32a1452e697 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala @@ -12,7 +12,7 @@ import com.digitalasset.canton.config.* import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.crypto.{Crypto, SyncCryptoApi, SyncCryptoClient} import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLoggingContext} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, NamedLoggingContext} import com.digitalasset.canton.metrics.SequencerClientMetrics import com.digitalasset.canton.networking.Endpoint import com.digitalasset.canton.networking.grpc.ClientChannelBuilder @@ -78,11 +78,12 @@ object SequencerClientFactory { metrics: SequencerClientMetrics, loggingConfig: LoggingConfig, exitOnTimeout: Boolean, - loggerFactory: NamedLoggerFactory, + namedLoggerFactory: NamedLoggerFactory, supportedProtocolVersions: Seq[ProtocolVersion], minimumProtocolVersion: Option[ProtocolVersion], ): SequencerClientFactory & SequencerClientTransportFactory = - new SequencerClientFactory with SequencerClientTransportFactory { + new SequencerClientFactory with SequencerClientTransportFactory with NamedLogging { + override protected def loggerFactory: NamedLoggerFactory = namedLoggerFactory override def create( member: Member, @@ -146,7 +147,7 @@ object SequencerClientFactory { ], Option[TrafficState]]( s"Retrieving traffic state from domain for $member at $ts", futureSupervisor, - loggerFactory.getTracedLogger(this.getClass), + logger, sequencerTransportsMap.forgetNE, sequencerConnections.sequencerTrustThreshold, _.getTrafficStateForMember( @@ -373,6 +374,5 @@ object SequencerClientFactory { domainParameters.protocolVersion, ) } - } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientFactory.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientFactory.scala index 8d1630d51f6f..55ae7dbbeb10 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientFactory.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientFactory.scala @@ -97,7 +97,9 @@ final class SequencerChannelClientFactory( member: Member, )(implicit executionContext: ExecutionContextExecutor - ): SequencerChannelClientTransport = + ): SequencerChannelClientTransport = { + val loggerFactoryWithSequencerId = + SequencerClient.loggerFactoryWithSequencerId(loggerFactory, sequencerId) conn match { case connection: GrpcSequencerConnection => val channel = createChannel(connection) @@ -106,9 +108,10 @@ final class SequencerChannelClientFactory( channel, auth, processingTimeout, - loggerFactory.append("sequencerId", sequencerId.uid.toString), + loggerFactoryWithSequencerId, ) } + } private def grpcSequencerClientAuth( connection: GrpcSequencerConnection, diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientTransport.scala index 54b524d2470e..20c88f6bb5dd 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/channel/SequencerChannelClientTransport.scala @@ -6,10 +6,9 @@ package com.digitalasset.canton.sequencing.client.channel import cats.data.EitherT import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.domain.api.v30 -import com.digitalasset.canton.lifecycle.Lifecycle.CloseableChannel import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, Lifecycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.CantonGrpcUtil +import com.digitalasset.canton.networking.grpc.{CantonGrpcUtil, GrpcClient, GrpcManagedChannel} import com.digitalasset.canton.sequencing.client.transports.{ GrpcClientTransportHelpers, GrpcSequencerClientAuth, @@ -31,12 +30,20 @@ private[channel] final class SequencerChannelClientTransport( with FlagCloseable with NamedLogging { - private val grpcStub: v30.SequencerChannelServiceGrpc.SequencerChannelServiceStub = clientAuth( - new v30.SequencerChannelServiceGrpc.SequencerChannelServiceStub( - channel, - options = CallOptions.DEFAULT, + private val managedChannel: GrpcManagedChannel = + GrpcManagedChannel("grpc-sequencer-channel-transport", channel, this, logger) + + private val grpcClient: GrpcClient[v30.SequencerChannelServiceGrpc.SequencerChannelServiceStub] = + GrpcClient.create( + managedChannel, + channel => + clientAuth( + new v30.SequencerChannelServiceGrpc.SequencerChannelServiceStub( + channel, + options = CallOptions.DEFAULT, + ) + ), ) - ) /** Issue the GRPC request to connect to a sequencer channel. * @@ -49,7 +56,7 @@ private[channel] final class SequencerChannelClientTransport( channelEndpoint: SequencerChannelClientEndpoint )(implicit traceContext: TraceContext): Unit = { val requestObserver = - grpcStub.connectToSequencerChannel(channelEndpoint.observer) + grpcClient.service.connectToSequencerChannel(channelEndpoint.observer) // Only now that the GRPC request has provided the "request" GRPC StreamObserver, // pass on the request observer to the channel endpoint. This enables the sequencer @@ -63,13 +70,11 @@ private[channel] final class SequencerChannelClientTransport( traceContext: TraceContext ): EitherT[FutureUnlessShutdown, String, Unit] = { val sendAtMostOnce = retryPolicy(retryOnUnavailable = false) - val response = CantonGrpcUtil.sendGrpcRequest(grpcStub, "sequencer-channel")( - stub => stub.ping(v30.PingRequest()), + val response = CantonGrpcUtil.sendGrpcRequest(grpcClient, "sequencer-channel")( + _.ping(v30.PingRequest()), requestDescription = "ping", timeout = timeouts.network.duration, logger = logger, - logPolicy = noLoggingShutdownErrorsLogPolicy, - onShutdownRunner = this, retryPolicy = sendAtMostOnce, ) response.bimap(_.toString, _ => ()) @@ -79,6 +84,6 @@ private[channel] final class SequencerChannelClientTransport( override protected def onClosed(): Unit = Lifecycle.close( clientAuth, - new CloseableChannel(channel, logger, "grpc-sequencer-channel-transport"), + managedChannel, )(logger) } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala index 593fc999d645..9ea0e014854b 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala @@ -11,16 +11,16 @@ import com.digitalasset.canton.ProtoDeserializationError.ProtoDeserializationFai import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.domain.api.v30 import com.digitalasset.canton.domain.api.v30.SequencerServiceGrpc.SequencerServiceStub -import com.digitalasset.canton.lifecycle.Lifecycle.CloseableChannel import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, Lifecycle} -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.metrics.SequencerClientMetrics -import com.digitalasset.canton.networking.grpc.GrpcError.{ - GrpcClientGaveUp, - GrpcServerError, - GrpcServiceUnavailable, +import com.digitalasset.canton.networking.grpc.GrpcError.GrpcServiceUnavailable +import com.digitalasset.canton.networking.grpc.{ + CantonGrpcUtil, + GrpcClient, + GrpcError, + GrpcManagedChannel, } -import com.digitalasset.canton.networking.grpc.{CantonGrpcUtil, GrpcError} import com.digitalasset.canton.sequencing.SerializedEventHandler import com.digitalasset.canton.sequencing.client.SendAsyncClientError.SendAsyncClientResponseError import com.digitalasset.canton.sequencing.client.{ @@ -63,8 +63,12 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( ): EitherT[FutureUnlessShutdown, Status, Unit] = clientAuth.logout() - protected val sequencerServiceClient: SequencerServiceStub = clientAuth( - new SequencerServiceStub(channel, options = callOptions) + private val managedChannel: GrpcManagedChannel = + GrpcManagedChannel("grpc-sequencer-transport", channel, this, logger) + + protected val sequencerServiceClient: GrpcClient[SequencerServiceStub] = GrpcClient.create( + managedChannel, + channel => clientAuth(new SequencerServiceStub(channel, options = callOptions)), ) override def sendAsyncSigned( @@ -85,8 +89,6 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( requestDescription = s"$endpoint/$messageId", timeout = timeout, logger = logger, - logPolicy = noLoggingShutdownErrorsLogPolicy, - onShutdownRunner = this, retryPolicy = sendAtMostOnce, ) response.biflatMap( @@ -154,8 +156,6 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( requestDescription = s"get-traffic-state/${request.member}", timeout = timeouts.network.duration, logger = logger, - logPolicy = noLoggingShutdownErrorsLogPolicy, - onShutdownRunner = this, retryPolicy = retryPolicy(retryOnUnavailable = true), ) .map { res => @@ -180,8 +180,6 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( requestDescription = s"acknowledge-signed/$timestamp", timeout = timeouts.network.duration, logger = logger, - logPolicy = noLoggingShutdownErrorsLogPolicy, - onShutdownRunner = this, retryPolicy = retryPolicy(retryOnUnavailable = false), ) .map { _ => @@ -201,7 +199,10 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( logger.debug("Downloading topology state for initialization") ClientAdapter - .serverStreaming(request.toProtoV30, sequencerServiceClient.downloadTopologyStateForInit) + .serverStreaming( + request.toProtoV30, + sequencerServiceClient.service.downloadTopologyStateForInit, + ) .map(TopologyStateForInitResponse.fromProtoV30(_)) .flatMapConcat { parsingResult => parsingResult.fold( @@ -225,23 +226,12 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( override protected def onClosed(): Unit = Lifecycle.close( clientAuth, - new CloseableChannel(channel, logger, "grpc-sequencer-transport"), + managedChannel, )(logger) } trait GrpcClientTransportHelpers { this: FlagCloseable & NamedLogging => - protected val noLoggingShutdownErrorsLogPolicy - : GrpcError => TracedLogger => TraceContext => Unit = - err => - logger => - implicit traceContext => - err match { - case _: GrpcClientGaveUp | _: GrpcServerError | _: GrpcServiceUnavailable => - // avoid logging client errors that typically happen during shutdown (such as grpc context cancelled) - if (!isClosing) err.log(logger) - case _ => err.log(logger) - } /** Retry policy to retry once for authentication failures to allow re-authentication and optionally retry when unavailable. */ protected def retryPolicy( @@ -312,7 +302,7 @@ class GrpcSequencerClientTransport( context.run(() => TraceContextGrpc.withGrpcContext(traceContext) { - sequencerServiceClient.subscribeVersioned( + sequencerServiceClient.service.subscribeVersioned( subscriptionRequest.toProtoV30, subscription.observer, ) diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransportPekko.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransportPekko.scala index bb47fef6b4c9..41bfade2efe1 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransportPekko.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransportPekko.scala @@ -122,7 +122,7 @@ class GrpcSequencerClientTransportPekko( ) } - val subscriber = sequencerServiceClient.subscribeVersioned _ + val subscriber = sequencerServiceClient.service.subscribeVersioned _ mkSubscription(subscriber)(SubscriptionResponse.fromVersionedProtoV30(protocolVersion)(_)(_)) } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala index ff615ac89ddd..732a47fa3ea9 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala @@ -235,7 +235,10 @@ abstract class ReplayingSendsSequencerClientTransportCommon( .mapAsyncUnordered(sendParallelism)(replaySubmit(_).unwrap) .toMat(Sink.fold(SendReplayReport()(sendDuration))(_.update(_)))(Keep.right) - PekkoUtil.runSupervised(logger.error("Failed to run submission replay", _), submissionReplay) + PekkoUtil.runSupervised( + submissionReplay, + errorLogMessagePrefix = "Failed to run submission replay", + ) } override def waitForIdle( diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala index 0fedd533329c..6e29bb17c0d7 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala @@ -26,7 +26,7 @@ import com.digitalasset.canton.time.{Clock, DomainTimeTracker} import com.digitalasset.canton.topology.{DomainId, Member} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.{ErrorUtil, FutureUtil} +import com.digitalasset.canton.util.{ErrorUtil, FutureUnlessShutdownUtil} import com.digitalasset.canton.version.ProtocolVersion import scala.concurrent.ExecutionContext @@ -223,7 +223,10 @@ class TrafficPurchasedSubmissionHandler( show"The traffic balance request submission timed out after sequencing time $time has elapsed" ) } - FutureUtil.doNotAwaitUnlessShutdown(logOutcomeF, "Traffic balance request submission failed") + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( + logOutcomeF, + "Traffic balance request submission failed", + ) } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala index 359bf6e38e98..bb866b9eeecc 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.sequencing.protocol.{ import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.{DomainId, Member} import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.FutureUtil +import com.digitalasset.canton.util.FutureUnlessShutdownUtil import com.digitalasset.canton.version.ProtocolVersion import java.util.concurrent.atomic.AtomicReference @@ -110,7 +110,7 @@ class TrafficStateController( def tickStateAt(sequencingTimestamp: CantonTimestamp)(implicit executionContext: ExecutionContext, traceContext: TraceContext, - ): Unit = FutureUtil.doNotAwaitUnlessShutdown( + ): Unit = FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( for { topology <- topologyClient.awaitSnapshotUS(sequencingTimestamp) snapshot = topology.ipsSnapshot diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/IndexedStringStore.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/IndexedStringStore.scala index f3adc4fdd6ae..d6dd67626cd9 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/IndexedStringStore.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/IndexedStringStore.scala @@ -14,7 +14,6 @@ import com.digitalasset.canton.store.db.DbIndexedStringStore import com.digitalasset.canton.store.memory.InMemoryIndexedStringStore import com.digitalasset.canton.topology.DomainId import com.digitalasset.canton.tracing.{TraceContext, TracedAsyncLoadingCache, TracedScaffeine} -import com.github.blemale.scaffeine.Scaffeine import com.google.common.annotations.VisibleForTesting import slick.jdbc.{PositionedParameters, SetParameter} @@ -166,9 +165,7 @@ class IndexedStringCache( private val str2Index : TracedAsyncLoadingCache[FutureUnlessShutdown, (String300, IndexedStringType), Int] = TracedScaffeine.buildTracedAsync[FutureUnlessShutdown, (String300, IndexedStringType), Int]( - cache = Scaffeine() - .maximumSize(config.maximumSize.value) - .expireAfterAccess(config.expireAfterAccess.underlying), + cache = config.buildScaffeine(), loader = implicit tc => { case (str, typ) => parent .getOrCreateIndex(typ, str) @@ -184,9 +181,7 @@ class IndexedStringCache( : TracedAsyncLoadingCache[FutureUnlessShutdown, (Int, IndexedStringType), Option[String300]] = TracedScaffeine .buildTracedAsync[FutureUnlessShutdown, (Int, IndexedStringType), Option[String300]]( - cache = Scaffeine() - .maximumSize(config.maximumSize.value) - .expireAfterAccess(config.expireAfterAccess.underlying), + cache = config.buildScaffeine(), loader = implicit tc => { case (idx, typ) => parent .getForIndex(typ, idx) diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/SessionKeyStore.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/SessionKeyStore.scala index 5f9379ef44d2..b01ec978025b 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/SessionKeyStore.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/store/SessionKeyStore.scala @@ -22,7 +22,9 @@ sealed trait SessionKeyStore { * Otherwise, if the 'global' session store is disabled, we create a local cache that is valid only for a single * transaction. */ - def convertStore: ConfirmationRequestSessionKeyStore = + def convertStore(implicit + executionContext: ExecutionContext + ): ConfirmationRequestSessionKeyStore = this match { case SessionKeyStoreDisabled => new SessionKeyStoreWithNoEviction() case cache: SessionKeyStoreWithInMemoryCache => cache @@ -91,8 +93,9 @@ sealed trait ConfirmationRequestSessionKeyStore { object SessionKeyStoreDisabled extends SessionKeyStore -final class SessionKeyStoreWithInMemoryCache(sessionKeysCacheConfig: SessionKeyCacheConfig) - extends SessionKeyStore +final class SessionKeyStoreWithInMemoryCache(sessionKeysCacheConfig: SessionKeyCacheConfig)(implicit + executionContext: ExecutionContext +) extends SessionKeyStore with ConfirmationRequestSessionKeyStore { /** This cache keeps track of the session key information for each recipient tree, which is then used to encrypt @@ -134,20 +137,23 @@ final class SessionKeyStoreWithInMemoryCache(sessionKeysCacheConfig: SessionKeyC * However, in this implementation, the session keys have neither a size limit nor an eviction time. * Therefore, this cache MUST only be used when it is local to each transaction and short-lived. */ -final class SessionKeyStoreWithNoEviction extends ConfirmationRequestSessionKeyStore { +final class SessionKeyStoreWithNoEviction(implicit executionContext: ExecutionContext) + extends ConfirmationRequestSessionKeyStore { override protected lazy val sessionKeysCacheSender: Cache[RecipientGroup, SessionKeyInfo] = - Scaffeine().build() + Scaffeine().executor(executionContext.execute(_)).build() override protected lazy val sessionKeysCacheReceiver : Cache[AsymmetricEncrypted[SecureRandomness], SecureRandomness] = - Scaffeine().build() + Scaffeine().executor(executionContext.execute(_)).build() } object SessionKeyStore { - def apply(sessionKeyCacheConfig: SessionKeyCacheConfig): SessionKeyStore = + def apply( + sessionKeyCacheConfig: SessionKeyCacheConfig + )(implicit executionContext: ExecutionContext): SessionKeyStore = if (sessionKeyCacheConfig.enabled) new SessionKeyStoreWithInMemoryCache(sessionKeyCacheConfig) else SessionKeyStoreDisabled diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala index 3be0b07b44bb..57274c10695f 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala @@ -20,9 +20,21 @@ import com.digitalasset.canton.lifecycle.{ SyncCloseable, UnlessShutdown, } -import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.logging.{ + ErrorLoggingContext, + NamedLoggerFactory, + NamedLogging, + TracedLogger, +} +import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcLogPolicy import com.digitalasset.canton.networking.grpc.GrpcError.GrpcServiceUnavailable -import com.digitalasset.canton.networking.grpc.{CantonGrpcUtil, ClientChannelBuilder} +import com.digitalasset.canton.networking.grpc.{ + CantonGrpcUtil, + ClientChannelBuilder, + GrpcClient, + GrpcError, + GrpcManagedChannel, +} import com.digitalasset.canton.time.Clock.SystemClockRunningBackwards import com.digitalasset.canton.topology.admin.v30.{ CurrentTimeRequest, @@ -403,7 +415,10 @@ class RemoteClock( loggerFactory.threadName + "-remoteclock", noTracingLogger, ) - private val service = IdentityInitializationServiceGrpc.stub(channel) + + private val managedChannel = + GrpcManagedChannel("channel to remote clock server", channel, this, logger) + private val service = GrpcClient.create(managedChannel, IdentityInitializationServiceGrpc.stub) private val updating = new AtomicReference[Option[CantonTimestamp]](None) @@ -477,6 +492,17 @@ class RemoteClock( ) } + // Do not log warnings for unavailable servers as that can happen on cancellation of the grpc channel on shutdown. + private object LogPolicy extends GrpcLogPolicy { + override def log(err: GrpcError, logger: TracedLogger)(implicit + traceContext: TraceContext + ): Unit = + err match { + case unavailable: GrpcServiceUnavailable => logger.info(unavailable.toString) + case _ => err.log(logger) + } + } + private def getCurrentRemoteTimeOnce(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, String, CantonTimestamp] = @@ -487,17 +513,10 @@ class RemoteClock( "fetch remote time", timeouts.network.duration, logger, - this, // We retry at a higher level indefinitely and not here at all because we want a fairly short connection timeout here. retryPolicy = _ => false, // Do not log warnings for unavailable servers as that can happen on cancellation of the grpc channel on shutdown. - logPolicy = err => - logger => - implicit traceContext => - err match { - case unavailable: GrpcServiceUnavailable => logger.info(unavailable.toString) - case _ => err.log(logger) - }, + logPolicy = LogPolicy, ) .bimap(_.toString, _.currentTime) timestamp <- EitherT.fromEither[FutureUnlessShutdown]( @@ -513,7 +532,7 @@ class RemoteClock( Lifecycle.close( // stopping the scheduler before the channel, so we don't get a failed call on shutdown SyncCloseable("remote clock scheduler", scheduler.shutdown()), - Lifecycle.toCloseableChannel(channel, logger, "channel to remote clock server"), + managedChannel, )(logger) } @@ -524,7 +543,7 @@ object RemoteClock { loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContextExecutor): RemoteClock = new RemoteClock( - ClientChannelBuilder.createChannelToTrustedServer(config), + ClientChannelBuilder.createChannelBuilderToTrustedServer(config).build(), timeouts, loggerFactory, ) diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUnlessShutdownUtil.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUnlessShutdownUtil.scala new file mode 100644 index 000000000000..a8a83af6b800 --- /dev/null +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUnlessShutdownUtil.scala @@ -0,0 +1,45 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.util + +import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} +import com.digitalasset.canton.logging.ErrorLoggingContext +import org.slf4j.event.Level + +object FutureUnlessShutdownUtil { + + /** If the future fails, log the associated error and re-throw. The returned future completes after logging. + * @param logPassiveInstanceAtInfo: If true, log [[PassiveInstanceException]] at INFO instead of ERROR level. Default is false. + */ + def logOnFailureUnlessShutdown[T]( + future: FutureUnlessShutdown[T], + failureMessage: => String, + onFailure: Throwable => Unit = _ => (), + level: => Level = Level.ERROR, + closeContext: Option[CloseContext] = None, + logPassiveInstanceAtInfo: Boolean = false, + )(implicit loggingContext: ErrorLoggingContext): FutureUnlessShutdown[T] = + FutureUnlessShutdown( + FutureUtil.logOnFailure( + future.unwrap, + failureMessage, + onFailure, + level, + closeContext, + logPassiveInstanceAtInfo, + ) + ) + + /** [[FutureUtil.doNotAwait]] but for FUS + */ + def doNotAwaitUnlessShutdown[A]( + future: FutureUnlessShutdown[A], + failureMessage: => String, + onFailure: Throwable => Unit = _ => (), + level: => Level = Level.ERROR, + closeContext: Option[CloseContext] = None, + )(implicit loggingContext: ErrorLoggingContext): Unit = + FutureUtil.doNotAwait(future.unwrap, failureMessage, onFailure, level, closeContext) + +} diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUtil.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUtil.scala index 9afeb2f4e67a..7f51a8e5a92c 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUtil.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/FutureUtil.scala @@ -4,7 +4,7 @@ package com.digitalasset.canton.util import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} +import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.resource.DbStorage.PassiveInstanceException import org.slf4j.event.Level @@ -68,28 +68,6 @@ object FutureUtil { } } - /** If the future fails, log the associated error and re-throw. The returned future completes after logging. - * @param logPassiveInstanceAtInfo: If true, log [[PassiveInstanceException]] at INFO instead of ERROR level. Default is false. - */ - def logOnFailureUnlessShutdown[T]( - future: FutureUnlessShutdown[T], - failureMessage: => String, - onFailure: Throwable => Unit = _ => (), - level: => Level = Level.ERROR, - closeContext: Option[CloseContext] = None, - logPassiveInstanceAtInfo: Boolean = false, - )(implicit loggingContext: ErrorLoggingContext): FutureUnlessShutdown[T] = - FutureUnlessShutdown( - logOnFailure( - future.unwrap, - failureMessage, - onFailure, - level, - closeContext, - logPassiveInstanceAtInfo, - ) - ) - /** Discard `future` and log an error if it does not complete successfully. * This is useful to document that a `Future` is intentionally not being awaited upon. * @param logPassiveInstanceAtInfo: If true, log [[PassiveInstanceException]] at INFO instead of ERROR level. Default is false. @@ -106,17 +84,6 @@ object FutureUtil { logOnFailure(future, failureMessage, onFailure, level, closeContext, logPassiveInstanceAtInfo) } - /** [[doNotAwait]] but for FUS - */ - def doNotAwaitUnlessShutdown[A]( - future: FutureUnlessShutdown[A], - failureMessage: => String, - onFailure: Throwable => Unit = _ => (), - level: => Level = Level.ERROR, - closeContext: Option[CloseContext] = None, - )(implicit loggingContext: ErrorLoggingContext): Unit = - doNotAwait(future.unwrap, failureMessage, onFailure, level, closeContext) - /** Variant of [[doNotAwait]] that also catches non-fatal errors thrown while constructing the future. */ def catchAndDoNotAwait( future: => Future[?], diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala index de944f26e639..c85f279bd074 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala @@ -21,7 +21,12 @@ import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.UnlessShutdown.{AbortedDueToShutdown, Outcome} import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.pretty.Pretty -import com.digitalasset.canton.logging.{HasLoggerName, NamedLoggerFactory, NamedLoggingContext} +import com.digitalasset.canton.logging.{ + ErrorLoggingContext, + HasLoggerName, + NamedLoggerFactory, + NamedLoggingContext, +} import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.SingletonTraverse.syntax.* import com.digitalasset.canton.util.Thereafter.syntax.* @@ -76,22 +81,42 @@ object PekkoUtil extends HasLoggerName { * * By default, an Pekko flow will discard exceptions. Use this method to avoid discarding exceptions. */ - def runSupervised[T]( - reporter: Throwable => Unit, - graph: RunnableGraph[T], + def runSupervised[MaterializedValueT]( + graph: RunnableGraph[MaterializedValueT], + errorLogMessagePrefix: String, + isDone: MaterializedValueT => Boolean = (_: MaterializedValueT) => false, debugLogging: Boolean = false, - )(implicit - mat: Materializer - ): T = { - val tmp = graph + )(implicit mat: Materializer, loggingContext: ErrorLoggingContext): MaterializedValueT = { + val materializedValueCell = new SingleUseCell[MaterializedValueT] + + val graphWithSupervisionStrategy = graph .addAttributes(ActorAttributes.supervisionStrategy { ex => - reporter(ex) + val materializedValue = materializedValueCell.getOrElse( + throw new IllegalStateException( + "Internal invariant violation: materialized value should always be set at this point", + ex, // Pass the original error as well so that we don't lose it + ) + ) + // Avoid errors on shutdown + if (isDone(materializedValue)) { + loggingContext + .info(s"$errorLogMessagePrefix (encountered after the graph is completed)", ex) + } else { + loggingContext.error(errorLogMessagePrefix, ex) + } Supervision.Stop }) - (if (debugLogging) - tmp.addAttributes(ActorAttributes.debugLogging(true)) - else tmp) - .run() + .mapMaterializedValue { materializedValue => + materializedValueCell.putIfAbsent(materializedValue).discard + materializedValue + } + + val materializedValue = + (if (debugLogging) + graphWithSupervisionStrategy.addAttributes(ActorAttributes.debugLogging(true)) + else graphWithSupervisionStrategy).run() + + materializedValue } /** Create an Actor system using the existing execution context `ec` diff --git a/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml b/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml index d7617e48d1c9..e26cda59c34e 100644 --- a/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml +++ b/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: CantonExamples diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/GrpcSequencerConnectClient.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/GrpcSequencerConnectClient.scala index 5e7a1df6dc12..70f5be74cac6 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/GrpcSequencerConnectClient.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/GrpcSequencerConnectClient.scala @@ -62,7 +62,7 @@ class GrpcSequencerConnectClient( .checkCantonApiInfo( domainAlias.unwrap, CantonGrpcUtil.ApiName.SequencerPublicApi, - builder.build(), + builder, logger, timeouts.network, onShutdownRunner = this, @@ -73,11 +73,11 @@ class GrpcSequencerConnectClient( .sendSingleGrpcRequest( serverName = domainAlias.unwrap, requestDescription = "get domain id and sequencer id", - channel = builder.build(), + channelBuilder = builder, stubFactory = v30.SequencerConnectServiceGrpc.stub, timeout = timeouts.network.unwrap, logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, + logPolicy = CantonGrpcUtil.SilentLogPolicy, onShutdownRunner = this, retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, token = None, @@ -107,11 +107,11 @@ class GrpcSequencerConnectClient( .sendSingleGrpcRequest( serverName = domainIdentifier, requestDescription = "get domain parameters", - channel = builder.build(), + channelBuilder = builder, stubFactory = v30.SequencerConnectServiceGrpc.stub, timeout = timeouts.network.unwrap, logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, + logPolicy = CantonGrpcUtil.SilentLogPolicy, onShutdownRunner = this, retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, token = None, @@ -133,11 +133,11 @@ class GrpcSequencerConnectClient( .sendSingleGrpcRequest( serverName = domainIdentifier, requestDescription = "get domain id", - channel = builder.build(), + channelBuilder = builder, stubFactory = v30.SequencerConnectServiceGrpc.stub, timeout = timeouts.network.unwrap, logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, + logPolicy = CantonGrpcUtil.SilentLogPolicy, onShutdownRunner = this, retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, token = None, @@ -163,11 +163,11 @@ class GrpcSequencerConnectClient( .sendSingleGrpcRequest( serverName = domainAlias.unwrap, requestDescription = "handshake", - channel = builder.build(), + channelBuilder = builder, stubFactory = v30.SequencerConnectServiceGrpc.stub, timeout = timeouts.network.unwrap, logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, + logPolicy = CantonGrpcUtil.SilentLogPolicy, onShutdownRunner = this, retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, token = None, @@ -203,13 +203,13 @@ class GrpcSequencerConnectClient( .sendSingleGrpcRequest( serverName = domainAlias.unwrap, requestDescription = "verify active", - channel = builder.build(), + channelBuilder = builder, stubFactory = channel => v30.SequencerConnectServiceGrpc .stub(ClientInterceptors.intercept(channel, interceptor)), timeout = timeouts.network.unwrap, logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, + logPolicy = CantonGrpcUtil.SilentLogPolicy, onShutdownRunner = this, retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, token = None, @@ -241,12 +241,12 @@ class GrpcSequencerConnectClient( .sendSingleGrpcRequest( serverName = domainAlias.unwrap, requestDescription = "register-onboarding-topology-transactions", - channel = builder.build(), + channelBuilder = builder, stubFactory = channel => v30.SequencerConnectServiceGrpc.stub(ClientInterceptors.intercept(channel, interceptor)), timeout = timeouts.network.unwrap, logger = logger, - logPolicy = CantonGrpcUtil.silentLogPolicy, + logPolicy = CantonGrpcUtil.SilentLogPolicy, onShutdownRunner = this, retryPolicy = CantonGrpcUtil.RetryPolicy.noRetry, token = None, diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoader.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoader.scala index f5d4f82135ca..552ad4e4c23f 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoader.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoader.scala @@ -35,7 +35,7 @@ import com.digitalasset.canton.tracing.{TraceContext, TracingConfig} import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.retry.NoExceptionRetryPolicy -import com.digitalasset.canton.util.{FutureUtil, LoggerUtil, retry} +import com.digitalasset.canton.util.{FutureUnlessShutdownUtil, LoggerUtil, retry} import com.digitalasset.canton.version.ProtocolVersion import com.google.common.annotations.VisibleForTesting import io.opentelemetry.api.trace.Tracer @@ -318,7 +318,7 @@ class SequencerInfoLoader( ) // Note that we tested using HasFlushFuture.addToFlush, but the complexity and risk of delaying shutdown // wasn't worth the questionable benefit of tracking "dangling threads" without ownership of the netty channel. - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( getInfo(connection).transform( { case Outcome(res) => diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/data/TaskScheduler.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/data/TaskScheduler.scala index 159d20f35e4d..7255ad1a2873 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/data/TaskScheduler.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/data/TaskScheduler.scala @@ -23,7 +23,12 @@ import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, import com.digitalasset.canton.time.Clock import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.{ErrorUtil, FutureUtil, SimpleExecutionQueue} +import com.digitalasset.canton.util.{ + ErrorUtil, + FutureUnlessShutdownUtil, + FutureUtil, + SimpleExecutionQueue, +} import com.digitalasset.canton.{SequencerCounter, SequencerCounterDiscriminator} import com.google.common.annotations.VisibleForTesting @@ -138,7 +143,7 @@ class TaskScheduler[Task <: TaskScheduler.TimedTask]( scheduleNextCheck(alertAfter) private def scheduleNextCheck(after: JDuration): Unit = - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( clock.scheduleAfter(_ => checkIfBlocked(), after), "The check for missing ticks has failed unexpectedly", )(errorLoggingContext(TraceContext.empty)) diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala index c906a768f9e1..100ff23643ef 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala @@ -90,7 +90,7 @@ import com.digitalasset.canton.topology.transaction.{ TopologyMapping, } import com.digitalasset.canton.tracing.{NoTracing, TraceContext, TracerProvider} -import com.digitalasset.canton.util.{FutureUtil, SimpleExecutionQueue} +import com.digitalasset.canton.util.{FutureUnlessShutdownUtil, SimpleExecutionQueue} import com.digitalasset.canton.version.{ProtocolVersion, ReleaseProtocolVersion} import com.digitalasset.canton.watchdog.WatchdogService import io.grpc.ServerServiceDefinition @@ -685,7 +685,7 @@ abstract class CantonNodeBootstrapImpl[ // because all stages run on a sequential queue. // - Topology transactions added during the resumption do not deadlock // because the topology processor runs all notifications and topology additions on a sequential queue. - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( resumeIfCompleteStage().value, s"Checking whether new topology transactions completed stage $description failed ", ) diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/DomainOutboxDispatchHelper.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/DomainOutboxDispatchHelper.scala index 7b7d8f4e67e1..ebc92335e87a 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/DomainOutboxDispatchHelper.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/DomainOutboxDispatchHelper.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.Ge import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.FutureInstances.* import com.digitalasset.canton.util.retry.AllExceptionRetryPolicy -import com.digitalasset.canton.util.{FutureUtil, retry} +import com.digitalasset.canton.util.{FutureUnlessShutdownUtil, retry} import com.digitalasset.canton.version.ProtocolVersion import scala.concurrent.duration.* @@ -179,7 +179,7 @@ trait DomainOutboxDispatch extends NamedLogging with FlagCloseable { logger.debug( s"Attempting to push ${transactions.size} topology transactions to $domain: $transactions" ) - FutureUtil.logOnFailureUnlessShutdown( + FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( handle.submit(transactions), s"Pushing topology transactions to $domain", ) diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/QueueBasedDomainOutbox.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/QueueBasedDomainOutbox.scala index 0d86edb29992..971950779e3a 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/QueueBasedDomainOutbox.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/QueueBasedDomainOutbox.scala @@ -22,7 +22,7 @@ import com.digitalasset.canton.topology.store.{TopologyStore, TopologyStoreId} import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.retry.AllExceptionRetryPolicy -import com.digitalasset.canton.util.{DelayUtil, EitherTUtil, FutureUtil, retry} +import com.digitalasset.canton.util.{DelayUtil, EitherTUtil, FutureUnlessShutdownUtil, retry} import com.digitalasset.canton.version.ProtocolVersion import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} @@ -305,7 +305,7 @@ class QueueBasedDomainOutbox( s"Attempting to push ${transactions.size} topology transactions to $domain, specifically: $transactions" ) } - FutureUtil.logOnFailureUnlessShutdown( + FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( handle.submit(transactions), s"Pushing topology transactions to $domain", ) diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtilTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtilTest.scala index eaf70c7d85b9..822a4d089167 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtilTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/networking/grpc/CantonGrpcUtilTest.scala @@ -50,13 +50,21 @@ object CantonGrpcUtilTest { registry.addService(helloServiceDefinition) registry.addService(apiInfoServiceDefinition) - val channel: ManagedChannel = InProcessChannelBuilder - .forName(channelName) - .intercept(TraceContextGrpc.clientInterceptor) - .build() - val client: HelloServiceGrpc.HelloServiceStub = HelloServiceGrpc.stub(channel) - val onShutdownRunner = new PureOnShutdownRunner(logger) + val channelBuilder: ManagedChannelBuilderProxy = ManagedChannelBuilderProxy( + InProcessChannelBuilder + .forName(channelName) + .intercept(TraceContextGrpc.clientInterceptor) + ) + val managedChannel: GrpcManagedChannel = + GrpcManagedChannel( + "channel-to-broken-client", + channelBuilder.build(), + onShutdownRunner, + logger, + ) + val client: GrpcClient[HelloServiceStub] = + GrpcClient.create(managedChannel, HelloServiceGrpc.stub) def sendRequest( timeoutMs: Long = 2000 @@ -70,14 +78,11 @@ object CantonGrpcUtilTest { "command", Duration(timeoutMs, TimeUnit.MILLISECONDS), logger, - onShutdownRunner, ) .onShutdown(throw new IllegalStateException("Unexpected shutdown")) def close(): Unit = { - channel.shutdown() - channel.awaitTermination(2, TimeUnit.SECONDS) - assert(channel.isTerminated) + managedChannel.close() server.shutdown() server.awaitTermination() @@ -231,7 +236,7 @@ class CantonGrpcUtilTest extends FixtureAnyWordSpec with BaseTest with HasExecut val err = Await.result(requestF, 5.seconds).left.value err shouldBe a[GrpcServiceUnavailable] err.status.getCode shouldBe UNAVAILABLE - channel.getState(false) shouldBe ConnectivityState.TRANSIENT_FAILURE + managedChannel.channel.getState(false) shouldBe ConnectivityState.TRANSIENT_FAILURE } } @@ -374,8 +379,9 @@ class CantonGrpcUtilTest extends FixtureAnyWordSpec with BaseTest with HasExecut import env.* // Create a mocked client - val brokenClient = - mock[HelloServiceStub](withSettings.useConstructor(channel, CallOptions.DEFAULT)) + val brokenClient = mock[HelloServiceStub]( + withSettings.useConstructor(managedChannel.channel, CallOptions.DEFAULT) + ) when(brokenClient.build(*[Channel], *[CallOptions])).thenReturn(brokenClient) // Make the client fail with an embedded cause @@ -383,15 +389,16 @@ class CantonGrpcUtilTest extends FixtureAnyWordSpec with BaseTest with HasExecut val status = Status.INTERNAL.withDescription("test description").withCause(cause) when(brokenClient.hello(request)).thenReturn(Future.failed(status.asRuntimeException())) + val brokenGrpcClient = GrpcClient.create(managedChannel, _ => brokenClient) + // Send the request val requestF = loggerFactory.assertLogs( CantonGrpcUtil - .sendGrpcRequest(brokenClient, "serverName")( + .sendGrpcRequest(brokenGrpcClient, "serverName")( _.hello(request), "command", Duration(2000, TimeUnit.MILLISECONDS), logger, - onShutdownRunner, ) .value .failOnShutdown, @@ -424,7 +431,7 @@ class CantonGrpcUtilTest extends FixtureAnyWordSpec with BaseTest with HasExecut .checkCantonApiInfo( "server-name", "correct-api", - channel, + channelBuilder, logger, timeouts.network, onShutdownRunner, @@ -442,7 +449,7 @@ class CantonGrpcUtilTest extends FixtureAnyWordSpec with BaseTest with HasExecut .checkCantonApiInfo( "server-name", "other-api", - channel, + channelBuilder, logger, timeouts.network, onShutdownRunner, @@ -452,12 +459,7 @@ class CantonGrpcUtilTest extends FixtureAnyWordSpec with BaseTest with HasExecut val resultE = requestET.value.futureValue inside(resultE) { case Left(message) => - message shouldBe CantonGrpcUtil.apiInfoErrorMessage( - channel = channel, - receivedApiName = "correct-api", - expectedApiName = "other-api", - serverName = "server-name", - ) + message should include regex """Endpoint '.*' provides 'correct-api', expected 'other-api'\. This message indicates a possible mistake in configuration, please check node connection settings for 'server-name'\.""" } } } diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorTest.scala index be2ac6e9aa6b..f80fd3d624f3 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorTest.scala @@ -79,7 +79,7 @@ class BatchAggregatorTest case class CacheWithAggregator(aggregator: BatchAggregator[K, V])(implicit traceContext: TraceContext ) { - private val cache = Scaffeine().buildAsync[K, V]() + private val cache = Scaffeine().executor(executorService).buildAsync[K, V]() def get(key: K): Future[V] = cache.getFuture(key, key => aggregator.run(key)) } diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorUSTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorUSTest.scala index d045e8cdf6e3..8587fa6d8329 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorUSTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/util/BatchAggregatorUSTest.scala @@ -84,7 +84,7 @@ class BatchAggregatorUSTest case class CacheWithAggregator(aggregator: BatchAggregatorUS[K, V])(implicit traceContext: TraceContext ) { - private val cache = Scaffeine().buildAsync[K, V]() + private val cache = Scaffeine().executor(executorService).buildAsync[K, V]() def get(key: K): FutureUnlessShutdown[V] = FutureUnlessShutdown.outcomeF(cache.get(key, key => aggregator.run(key).futureValueUS)) diff --git a/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml b/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml index 58cbf963df1a..7e89357f9cfb 100644 --- a/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: ai-analysis diff --git a/sdk/canton/community/demo/src/main/daml/bank/daml.yaml b/sdk/canton/community/demo/src/main/daml/bank/daml.yaml index eccf6c5e7d89..7edad683de76 100644 --- a/sdk/canton/community/demo/src/main/daml/bank/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/bank/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: bank diff --git a/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml b/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml index e01117986cbc..344bb56807c9 100644 --- a/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: doctor diff --git a/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml b/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml index 7e00ff95b304..e25d62359406 100644 --- a/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: health-insurance diff --git a/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml b/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml index a49b4c22c210..c9ec3295903c 100644 --- a/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: medical-records diff --git a/sdk/canton/community/demo/src/main/scala/com/digitalasset/canton/demo/DemoUI.scala b/sdk/canton/community/demo/src/main/scala/com/digitalasset/canton/demo/DemoUI.scala index b959ca54c396..f17c1ec058a1 100644 --- a/sdk/canton/community/demo/src/main/scala/com/digitalasset/canton/demo/DemoUI.scala +++ b/sdk/canton/community/demo/src/main/scala/com/digitalasset/canton/demo/DemoUI.scala @@ -258,8 +258,9 @@ class ParticipantTab( } private def subscribeToChannel(offset: Long): Unit = { - val channel = - ClientChannelBuilder.createChannelToTrustedServer(participant.config.clientLedgerApi) + val channel = ClientChannelBuilder + .createChannelBuilderToTrustedServer(participant.config.clientLedgerApi) + .build() logger.debug(s"Subscribing ${participant.name} at $offset") val current = currentChannel.getAndUpdate { cur => // store channel and set subscribed offset to None unless it has changed in the meantime diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/ConfirmationRequestAndResponseProcessor.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/ConfirmationRequestAndResponseProcessor.scala index 6fec71e86669..a88976e23f78 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/ConfirmationRequestAndResponseProcessor.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/ConfirmationRequestAndResponseProcessor.scala @@ -852,7 +852,7 @@ private[mediator] class ConfirmationRequestAndResponseProcessor( protected def doNotAwait(requestId: RequestId, f: => FutureUnlessShutdown[Any])(implicit tc: TraceContext ): Future[Unit] = { - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( performUnlessClosingUSF("send-result-if-done")(f), s"send-result-if-done failed for request $requestId", level = Level.WARN, diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/Mediator.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/Mediator.scala index e8461d0d0eb1..f261b802c606 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/Mediator.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/Mediator.scala @@ -45,8 +45,8 @@ import com.digitalasset.canton.topology.{ } import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.util.EitherUtil.RichEither -import com.digitalasset.canton.util.FutureUtil import com.digitalasset.canton.util.ShowUtil.* +import com.digitalasset.canton.util.{FutureUnlessShutdownUtil, FutureUtil} import com.digitalasset.canton.version.ProtocolVersion import com.google.common.annotations.VisibleForTesting import io.opentelemetry.api.trace.Tracer @@ -323,7 +323,7 @@ private[mediator] class Mediator( logger.debug(s"Processing ${tracedOpenEvents.size} events for the mediator") - val result = FutureUtil.logOnFailureUnlessShutdown( + val result = FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( eventsProcessor.handle(tracedOpenEvents), "Failed to handle Mediator events", closeContext = Some(closeContext), diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencer.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencer.scala index 714760126bbf..0335d90c9159 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencer.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencer.scala @@ -10,8 +10,8 @@ import cats.syntax.either.* import cats.syntax.option.* import com.daml.nameof.NameOf.functionFullName import com.digitalasset.canton.SequencerCounter +import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, NonNegativeLong, PositiveInt} -import com.digitalasset.canton.config.{CachingConfigs, ProcessingTimeout} import com.digitalasset.canton.crypto.DomainSyncCryptoClient import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.domain.metrics.SequencerMetrics @@ -70,10 +70,8 @@ object DatabaseSequencer { topologyClientMember: Member, protocolVersion: ProtocolVersion, cryptoApi: DomainSyncCryptoClient, - cachingConfigs: CachingConfigs, metrics: SequencerMetrics, loggerFactory: NamedLoggerFactory, - runtimeReady: FutureUnlessShutdown[Unit], )(implicit ec: ExecutionContext, tracer: Tracer, @@ -110,11 +108,9 @@ object DatabaseSequencer { trafficConsumedStore = None, protocolVersion, cryptoApi, - cachingConfigs, metrics, loggerFactory, blockSequencerMode = false, - runtimeReady, ) } } @@ -138,11 +134,9 @@ class DatabaseSequencer( trafficConsumedStore: Option[TrafficConsumedStore], protocolVersion: ProtocolVersion, cryptoApi: DomainSyncCryptoClient, - cachingConfigs: CachingConfigs, metrics: SequencerMetrics, loggerFactory: NamedLoggerFactory, blockSequencerMode: Boolean, - runtimeReady: FutureUnlessShutdown[Unit], )(implicit ec: ExecutionContext, tracer: Tracer, materializer: Materializer) extends BaseSequencer( loggerFactory, @@ -164,10 +158,8 @@ class DatabaseSequencer( eventSignaller, protocolVersion, loggerFactory, - blockSequencerMode = blockSequencerMode, - sequencerMember = topologyClientMember, - cachingConfigs = cachingConfigs, - metrics = metrics, + blockSequencerMode, + metrics, ) private lazy val storageForAdminChanges: Storage = exclusiveStorage.getOrElse( diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignaller.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignaller.scala index c52fdb89f6b9..af724943f4fe 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignaller.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignaller.scala @@ -41,14 +41,16 @@ class LocalSequencerStateEventSignaller( with FlagCloseableAsync with NamedLogging { - private val (queue, notificationsHubSource) = + private val (queue, notificationsHubSource) = { + implicit val traceContext: TraceContext = TraceContext.empty PekkoUtil.runSupervised( - logger.error("LocalStateEventSignaller flow failed", _)(TraceContext.empty), Source .queue[WriteNotification](1, OverflowStrategy.backpressure) .conflate(_ union _) .toMat(BroadcastHub.sink(1))(Keep.both), + errorLogMessagePrefix = "LocalStateEventSignaller flow failed", ) + } override def notifyOfLocalWrite( notification: WriteNotification diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerFactory.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerFactory.scala index e8a1882016ae..c826872fd38e 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerFactory.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerFactory.scala @@ -129,10 +129,8 @@ class CommunityDatabaseSequencerFactory( sequencerId, sequencerProtocolVersion, domainSyncCryptoApi, - nodeParameters.cachingConfigs, metrics, loggerFactory, - runtimeReady, ) Future.successful(config.testingInterceptor.map(_(clock)(sequencer)(ec)).getOrElse(sequencer)) diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriter.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriter.scala index d31aa26aee5a..c5d1caa0ee5c 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriter.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriter.scala @@ -11,20 +11,22 @@ import cats.syntax.option.* import cats.syntax.parallel.* import com.daml.nameof.NameOf.functionFullName import com.digitalasset.canton.config +import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.config.{CachingConfigs, ProcessingTimeout} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.domain.metrics.SequencerMetrics import com.digitalasset.canton.domain.sequencing.admin.data.SequencerHealthStatus -import com.digitalasset.canton.domain.sequencing.sequencer.SequencerWriter.ResetWatermark +import com.digitalasset.canton.domain.sequencing.sequencer.SequencerWriter.{ + ResetWatermark, + SequencerWriterFlowFactory, +} import com.digitalasset.canton.domain.sequencing.sequencer.WriterStartupError.FailedToInitializeFromSnapshot import com.digitalasset.canton.domain.sequencing.sequencer.store.* import com.digitalasset.canton.lifecycle.* -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} +import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.resource.Storage import com.digitalasset.canton.sequencing.protocol.{SendAsyncError, SubmissionRequest} import com.digitalasset.canton.time.{Clock, NonNegativeFiniteDuration, SimClock} -import com.digitalasset.canton.topology.Member import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.tracing.TraceContext.withNewTraceContext import com.digitalasset.canton.util.FutureInstances.* @@ -126,10 +128,7 @@ object SequencerWriterStoreFactory { */ class SequencerWriter( writerStoreFactory: SequencerWriterStoreFactory, - createWriterFlow: ( - SequencerWriterStore, - TraceContext, - ) => RunningSequencerWriterFlow, + writerFlowFactory: SequencerWriterFlowFactory, storage: Storage, val generalStore: SequencerStore, clock: Clock, @@ -396,10 +395,8 @@ class SequencerWriter( )(implicit traceContext: TraceContext): Unit = // if these actions fail we want to ensure that the store is closed try { - val writerFlow = createWriterFlow(store, traceContext) - + val writerFlow = writerFlowFactory.create(store) setupWriterRecovery(writerFlow.done) - runningWriterRef.set(RunningWriter(writerFlow, store).some) } catch { case NonFatal(ex) => @@ -488,18 +485,19 @@ object SequencerWriter { protocolVersion: ProtocolVersion, loggerFactory: NamedLoggerFactory, blockSequencerMode: Boolean, - sequencerMember: Member, - cachingConfigs: CachingConfigs, metrics: SequencerMetrics, )(implicit materializer: Materializer, executionContext: ExecutionContext): SequencerWriter = { - val logger = TracedLogger(SequencerWriter.getClass, loggerFactory) + implicit val loggingContext: ErrorLoggingContext = ErrorLoggingContext( + loggerFactory.getTracedLogger(SequencerWriter.getClass), + loggerFactory.properties, + TraceContext.empty, + ) - def createWriterFlow(store: SequencerWriterStore)(implicit - traceContext: TraceContext - ): RunningSequencerWriterFlow = - PekkoUtil.runSupervised( - logger.error(s"Sequencer writer flow error", _)(TraceContext.empty), - SequencerWriterSource( + object DefaultSequencerWriterFlowFactory extends SequencerWriterFlowFactory { + override def create( + store: SequencerWriterStore + )(implicit traceContext: TraceContext): RunningSequencerWriterFlow = { + val runnableGraph = SequencerWriterSource( writerConfig, totalNodeCount, keepAliveInterval, @@ -510,15 +508,22 @@ object SequencerWriter { protocolVersion, metrics, processingTimeout, - blockSequencerMode = blockSequencerMode, + blockSequencerMode, ) .toMat(Sink.ignore)(Keep.both) - .mapMaterializedValue(m => new RunningSequencerWriterFlow(m._1, m._2.void)), - ) + .mapMaterializedValue(m => new RunningSequencerWriterFlow(m._1, m._2.void)) + + PekkoUtil.runSupervised( + runnableGraph, + errorLogMessagePrefix = "Sequencer writer flow error", + isDone = (runningFlow: RunningSequencerWriterFlow) => runningFlow.done.isCompleted, + ) + } + } new SequencerWriter( writerStorageFactory, - createWriterFlow(_)(_), + DefaultSequencerWriterFlowFactory, storage, sequencerStore, clock, @@ -533,4 +538,9 @@ object SequencerWriter { final case object ResetWatermarkToClockNow extends ResetWatermark final case class ResetWatermarkToTimestamp(timestamp: CantonTimestamp) extends ResetWatermark + trait SequencerWriterFlowFactory { + def create(store: SequencerWriterStore)(implicit + traceContext: TraceContext + ): RunningSequencerWriterFlow + } } diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencer.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencer.scala index 819af1d1240d..5bad9846a33d 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencer.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencer.scala @@ -6,8 +6,8 @@ package com.digitalasset.canton.domain.sequencing.sequencer.block import cats.data.EitherT import cats.syntax.either.* import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt} -import com.digitalasset.canton.config.{CachingConfigs, ProcessingTimeout} import com.digitalasset.canton.crypto.{DomainSyncCryptoClient, HashPurpose, Signature} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.domain.api.v30.TrafficControlErrorReason @@ -75,7 +75,6 @@ class BlockSequencer( protocolVersion: ProtocolVersion, blockRateLimitManager: SequencerRateLimitManager, orderingTimeFixMode: OrderingTimeFixMode, - cachingConfigs: CachingConfigs, processingTimeouts: ProcessingTimeout, logEventDetails: Boolean, prettyPrinter: CantonPrettyPrinter, @@ -107,11 +106,9 @@ class BlockSequencer( Some(blockRateLimitManager.trafficConsumedStore), protocolVersion, cryptoApi, - cachingConfigs, metrics, loggerFactory, blockSequencerMode = true, - runtimeReady, ) with DatabaseSequencerIntegration with NamedLogging @@ -152,16 +149,16 @@ class BlockSequencer( memberValidator = memberValidator, )(CloseContext(cryptoApi)) + implicit val traceContext: TraceContext = TraceContext.empty + val driverSource = Source .futureSource(runtimeReady.unwrap.map { case UnlessShutdown.AbortedDueToShutdown => - logger.debug("Not initiating subscription to block source due to shutdown")( - TraceContext.empty - ) + noTracingLogger.debug("Not initiating subscription to block source due to shutdown") Source.empty.viaMat(KillSwitches.single)(Keep.right) case UnlessShutdown.Outcome(_) => - logger.debug("Subscribing to block source")(TraceContext.empty) - blockOrderer.subscribe()(TraceContext.empty) + noTracingLogger.debug("Subscribing to block source") + blockOrderer.subscribe() }) // Explicit async to make sure that the block processing runs in parallel with the block retrieval .async @@ -173,8 +170,8 @@ class BlockSequencer( metrics.block.delay.updateValue((clock.now - lastTs.value).toMillis) } PekkoUtil.runSupervised( - ex => logger.error("Fatally failed to handle state changes", ex)(TraceContext.empty), driverSource.toMat(Sink.ignore)(Keep.both), + errorLogMessagePrefix = "Fatally failed to handle state changes", ) } diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/DriverBlockSequencerFactory.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/DriverBlockSequencerFactory.scala index 1dabed4e5c85..3cb192f378dc 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/DriverBlockSequencerFactory.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/DriverBlockSequencerFactory.scala @@ -115,7 +115,6 @@ class DriverBlockSequencerFactory[C]( protocolVersion, rateLimitManager, orderingTimeFixMode, - nodeParameters.cachingConfigs, nodeParameters.processingTimeouts, nodeParameters.loggingConfig.eventDetails, nodeParameters.loggingConfig.api.printer, diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/bftordering/core/driver/BftSequencerFactory.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/bftordering/core/driver/BftSequencerFactory.scala index 913eaea0e736..0e36d72f5fa3 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/bftordering/core/driver/BftSequencerFactory.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/bftordering/core/driver/BftSequencerFactory.scala @@ -121,7 +121,6 @@ class BftSequencerFactory( protocolVersion, rateLimitManager, orderingTimeFixMode, - nodeParameters.cachingConfigs, nodeParameters.processingTimeouts, nodeParameters.loggingConfig.eventDetails, nodeParameters.loggingConfig.api.printer, diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/store/SequencerMemberCache.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/store/SequencerMemberCache.scala index 653398b34f57..0fdb9271a75a 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/store/SequencerMemberCache.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/store/SequencerMemberCache.scala @@ -17,6 +17,7 @@ class SequencerMemberCache(populate: Traced[Member] => Future[Option[RegisteredM ) { // Using a AsyncLoadingCache seemed to be problematic with ScalaTest and it would rarely not read-through even if empty private val cache: Cache[Member, RegisteredMember] = Scaffeine() + .executor(executionContext.execute(_)) .recordStats() .build() diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/DirectSequencerSubscription.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/DirectSequencerSubscription.scala index 39f8c6bfc39d..2c5135adaff4 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/DirectSequencerSubscription.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/DirectSequencerSubscription.scala @@ -49,7 +49,6 @@ private[service] class DirectSequencerSubscription[E]( new SingleUseCell[SubscriptionCloseReason[E]]() private val ((killSwitch, sourceDone), done) = PekkoUtil.runSupervised( - logger.error("Fatally failed to handle event", _), source .mapAsync(1) { eventOrError => externalCompletionRef.get match { @@ -68,6 +67,7 @@ private[service] class DirectSequencerSubscription[E]( } .take(1) .toMat(Sink.headOption)(Keep.both), + errorLogMessagePrefix = "Fatally failed to handle event", ) FutureUtil.doNotAwait( diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerApiTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerApiTest.scala index c687e47c64e5..fd4745148eb4 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerApiTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerApiTest.scala @@ -9,7 +9,6 @@ import com.digitalasset.canton.crypto.DomainSyncCryptoClient import com.digitalasset.canton.domain.metrics.SequencerMetrics import com.digitalasset.canton.domain.sequencing.sequencer.Sequencer as CantonSequencer import com.digitalasset.canton.domain.sequencing.sequencer.store.SequencerStore -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.protocol.DynamicDomainParameters import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.time.SimClock @@ -58,10 +57,8 @@ abstract class DatabaseSequencerApiTest extends SequencerApiTest { sequencerId, testedProtocolVersion, crypto, - CachingConfigs(), metrics, loggerFactory, - runtimeReady = FutureUnlessShutdown.unit, )(executorService, tracer, materializer) } diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerSnapshottingTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerSnapshottingTest.scala index 870e0f9d02c2..f9172b40b1a3 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerSnapshottingTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/DatabaseSequencerSnapshottingTest.scala @@ -11,7 +11,6 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.domain.metrics.SequencerMetrics import com.digitalasset.canton.domain.sequencing.sequencer.Sequencer as CantonSequencer import com.digitalasset.canton.domain.sequencing.sequencer.store.SequencerStore -import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.protocol.DynamicDomainParameters import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.sequencing.protocol.{Recipients, SubmissionRequest} @@ -65,10 +64,8 @@ class DatabaseSequencerSnapshottingTest extends SequencerApiTest { sequencerId, testedProtocolVersion, crypto, - CachingConfigs(), metrics, loggerFactory, - runtimeReady = FutureUnlessShutdown.unit, )(executorService, tracer, materializer) } diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/FetchLatestEventsFlowTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/FetchLatestEventsFlowTest.scala index 55378113b2f0..f4294ebfeb21 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/FetchLatestEventsFlowTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/FetchLatestEventsFlowTest.scala @@ -73,7 +73,6 @@ class FetchLatestEventsFlowTest sink: Sink[Event, Mat2], ): (Mat1, Mat2) = PekkoUtil.runSupervised( - logger.error("LatestEventsFlowTest failed", _), source .via( FetchLatestEventsFlow[Event, State]( @@ -83,6 +82,7 @@ class FetchLatestEventsFlowTest ) ) .toMat(sink)(Keep.both), + errorLogMessagePrefix = "LatestEventsFlowTest failed", ) def create[Mat1](source: Source[ReadSignal, Mat1]): (Mat1, SinkQueueWithCancel[Event]) = diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignallerTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignallerTest.scala index 3f5f2cbb456e..9dec751bcec8 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignallerTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/LocalSequencerStateEventSignallerTest.scala @@ -89,13 +89,13 @@ class LocalSequencerStateEventSignallerTest // regardless of all of the prior events that were pummeled only a single signal is produced when subscribed // what's past is prologue aliceSignals <- PekkoUtil.runSupervised( - logger.error("writer updates", _), signaller .readSignalsForMember(alice, aliceId) .takeWithin( 200.millis ) // crude wait to receive more signals if some were going to be produced .toMat(Sink.seq)(Keep.right), + errorLogMessagePrefix = "writer updates", ) } yield { aliceSignals should have size (2) // there is a one item buffer on both the source queue and broadcast hub diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerTest.scala index 2f64bf24b20a..64ac5671ddac 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerTest.scala @@ -122,10 +122,8 @@ class SequencerTest extends FixtureAsyncWordSpec with BaseTest with HasExecution topologyClientMember, testedProtocolVersion, crypto, - CachingConfigs(), metrics, loggerFactory, - runtimeReady = FutureUnlessShutdown.unit, )(parallelExecutionContext, tracer, materializer) def readAsSeq( diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterSourceTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterSourceTest.scala index 88842ef5fe57..2a6418d7e621 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterSourceTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterSourceTest.scala @@ -133,7 +133,6 @@ class SequencerWriterSourceTest // explicitly pass a real execution context so shutdowns don't deadlock while Await'ing completion of the done // future while still finishing up running tasks that require an execution context val (writer, doneF) = PekkoUtil.runSupervised( - logger.error("Writer flow failed", _), SequencerWriterSource( testWriterConfig, totalNodeCount = PositiveInt.tryCreate(1), @@ -148,6 +147,7 @@ class SequencerWriterSourceTest blockSequencerMode = true, )(executorService, implicitly[TraceContext]) .toMat(Sink.ignore)(Keep.both), + errorLogMessagePrefix = "Writer flow failed", ) def completeFlow(): Future[Unit] = { diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterTest.scala index 946d56c56dd8..f7317514fb26 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerWriterTest.scala @@ -79,7 +79,7 @@ class SequencerWriterTest extends FixtureAsyncWordSpec with BaseTest { val writer = new SequencerWriter( storageFactory, - createWriterFlow, + TestSequencerWriterFlowFactory, storage, store, clock, @@ -101,13 +101,14 @@ class SequencerWriterTest extends FixtureAsyncWordSpec with BaseTest { def close(): Unit = () - private def createWriterFlow( - store: SequencerWriterStore, - traceContext: TraceContext, - ): RunningSequencerWriterFlow = { - val mockFlow = new MockRunningWriterFlow - runningFlows.append(mockFlow) - mockFlow.writerFlow + object TestSequencerWriterFlowFactory extends SequencerWriter.SequencerWriterFlowFactory { + override def create( + store: SequencerWriterStore + )(implicit traceContext: TraceContext): RunningSequencerWriterFlow = { + val mockFlow = new MockRunningWriterFlow + runningFlows.append(mockFlow) + mockFlow.writerFlow + } } } diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencerTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencerTest.scala index 810d4730bf49..c591fdecb8fb 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencerTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/block/BlockSequencerTest.scala @@ -181,7 +181,6 @@ class BlockSequencerTest protocolVersion = testedProtocolVersion, blockRateLimitManager = defaultRateLimiter, orderingTimeFixMode = OrderingTimeFixMode.MakeStrictlyIncreasing, - cachingConfigs = CachingConfigs(), processingTimeouts = BlockSequencerTest.this.timeouts, logEventDetails = true, prettyPrinter = new CantonPrettyPrinter( diff --git a/sdk/canton/community/drivers/reference/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/reference/ReferenceSequencerDriver.scala b/sdk/canton/community/drivers/reference/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/reference/ReferenceSequencerDriver.scala index 316ec0e6da08..dbb5c1dafa14 100644 --- a/sdk/canton/community/drivers/reference/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/reference/ReferenceSequencerDriver.scala +++ b/sdk/canton/community/drivers/reference/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/reference/ReferenceSequencerDriver.scala @@ -74,9 +74,10 @@ class ReferenceSequencerDriver( with NamedLogging with FlagCloseableAsync { - private lazy val (sendQueue, done) = + private lazy val (sendQueue, done) = { + implicit val traceContext: TraceContext = TraceContext.empty + PekkoUtil.runSupervised( - ex => logger.error("Fatally failed to handle state changes", ex)(TraceContext.empty), Source .queue[Traced[TimestampedRequest]](bufferSize = 100) .groupedWithin(n = config.maxBlockSize, d = config.maxBlockCutMillis.millis) @@ -91,10 +92,12 @@ class ReferenceSequencerDriver( .map(req => store.insertRequest( BlockFormat.OrderedRequest(req.microsecondsSinceEpoch, req.tag, req.body) - )(TraceContext.empty) + ) ) .toMat(Sink.ignore)(Keep.both), + errorLogMessagePrefix = "Fatally failed to handle state changes", ) + } override def subscribe()(implicit traceContext: TraceContext diff --git a/sdk/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala b/sdk/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala index f75885df9328..679f422ad929 100644 --- a/sdk/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala +++ b/sdk/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerSubscription.scala @@ -38,8 +38,16 @@ object LedgerSubscription { import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.* + override val loggerFactory: NamedLoggerFactory = + if (subscriptionName.isEmpty) + namedLoggerFactory + else + namedLoggerFactory.appendUnnamedKey( + "subscription", + subscriptionName, + ) + val (killSwitch, completed) = PekkoUtil.runSupervised( - logger.error("Fatally failed to handle transaction", _), source // we place the kill switch before the map operator, such that // we can shut down the operator quickly and signal upstream to cancel further sending @@ -48,15 +56,8 @@ object LedgerSubscription { // and we get the Future[Done] as completed from the sink so we know when the last message // was processed .toMat(Sink.ignore)(Keep.both), + errorLogMessagePrefix = "Fatally failed to handle transaction", ) - override val loggerFactory: NamedLoggerFactory = - if (subscriptionName.isEmpty) - namedLoggerFactory - else - namedLoggerFactory.appendUnnamedKey( - "subscription", - subscriptionName, - ) override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = { import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.* diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml index 97074c78a9e3..d13c7167754e 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --enable-interfaces=yes name: carbonv1-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml index 8b431c1f89a8..ce41577b2839 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --enable-interfaces=yes name: carbonv2-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml index 3b4cbb574e36..77afccda4d44 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf name: experimental-tests source: . version: 3.1.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml index 73ea62945dd0..93f0bf7ed443 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --enable-interfaces=yes name: model-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/ongoing_stream_package_upload/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/ongoing_stream_package_upload/daml.yaml index 3ecde66b03d8..582fc70792f8 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/ongoing_stream_package_upload/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/ongoing_stream_package_upload/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf name: ongoing-stream-package-upload-tests source: . version: 3.1.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml index 80c90d491034..c29257807266 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf name: package-management-tests source: . version: 3.1.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml index 3d6bb47f4dc0..071fcb68410c 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --enable-interfaces=yes name: semantic-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml index 29ad3f50c67c..19cbc189ea65 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf name: upgrade-tests source: . version: 1.0.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml index 1ebd8091b0b3..4b3829d6e596 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf name: upgrade-tests source: . version: 2.0.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml index 0eca80dbe3c6..212cd565e594 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf name: upgrade-tests source: . version: 3.0.0 diff --git a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/dep/daml.yaml b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/dep/daml.yaml index bdaaada41f6c..14bb5d3a43ab 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/dep/daml.yaml +++ b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/dep/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.dev - --enable-interfaces=yes diff --git a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/main/daml.yaml b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/main/daml.yaml index 3e0fb3b7036b..5ab6f9859458 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/main/daml.yaml +++ b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/damldefinitionsservice/main/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.dev - --enable-interfaces=yes diff --git a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml index a444b9043ad5..c96c6379768c 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml +++ b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: JsonEncodingTest diff --git a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml index 1b3ebf12845c..84d5e7af6a27 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml +++ b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.dev name: JsonEncodingTestDev diff --git a/sdk/canton/community/participant/src/main/daml/daml.yaml b/sdk/canton/community/participant/src/main/daml/daml.yaml index 288b007443d0..692e700cfb4f 100644 --- a/sdk/canton/community/participant/src/main/daml/daml.yaml +++ b/sdk/canton/community/participant/src/main/daml/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.3.0-snapshot.20241112.13422.0.vf18106b1 +sdk-version: 3.3.0-snapshot.20241113.13425.0.v3e1a44cf build-options: - --target=2.1 name: AdminWorkflows diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala index 470029322aa1..350c1074906f 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala @@ -36,7 +36,7 @@ class PackageDependencyResolver( Set[PackageId], ] = TracedScaffeine .buildTracedAsync[EitherT[FutureUnlessShutdown, PackageId, *], PackageId, Set[PackageId]]( - cache = Scaffeine().maximumSize(10000).expireAfterAccess(15.minutes), + cache = Scaffeine().maximumSize(10000).expireAfterAccess(15.minutes).executor(ec.execute(_)), loader = implicit tc => loadPackageDependencies _, allLoader = None, )(logger) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantRepairService.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantRepairService.scala index ac1d0822367d..393530a93486 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantRepairService.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcParticipantRepairService.scala @@ -366,19 +366,19 @@ final class GrpcParticipantRepairService( ): Future[RollbackUnassignmentResponse] = TraceContext.withNewTraceContext { implicit traceContext => val res = for { - unassignId <- EitherT.fromEither[Future]( + unassignId <- EitherT.fromEither[FutureUnlessShutdown]( Try(request.unassignId.toLong).toEither.left .map(_ => TimestampConversionError(s"cannot convert ${request.unassignId} into Long")) .flatMap(fromProtoPrimitive) .leftMap(_.message) ) - sourceDomainId <- EitherT.fromEither[Future]( + sourceDomainId <- EitherT.fromEither[FutureUnlessShutdown]( DomainId .fromProtoPrimitive(request.source, "source") .map(Source(_)) .leftMap(_.message) ) - targetDomainId <- EitherT.fromEither[Future]( + targetDomainId <- EitherT.fromEither[FutureUnlessShutdown]( DomainId .fromProtoPrimitive(request.target, "target") .map(Target(_)) @@ -390,9 +390,11 @@ final class GrpcParticipantRepairService( } yield RollbackUnassignmentResponse() - EitherTUtil.toFuture( - res.leftMap(err => io.grpc.Status.CANCELLED.withDescription(err).asRuntimeException()) - ) + EitherTUtil + .toFutureUnlessShutdown( + res.leftMap(err => io.grpc.Status.CANCELLED.withDescription(err).asRuntimeException()) + ) + .asGrpcResponse } } diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala index fbfd55c9f34f..2caee755bbb6 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspection.scala @@ -426,17 +426,21 @@ final class SyncStateInspection( )(implicit traceContext: TraceContext ): Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)] = - timeouts.inspection.await(s"$functionFullName from $start to $end on $domain")( - getOrFail(getPersistentState(domain), domain).acsCommitmentStore - .searchComputedBetween(start, end, counterParticipant.toList) - ) + timeouts.inspection + .awaitUS(s"$functionFullName from $start to $end on $domain")( + getOrFail(getPersistentState(domain), domain).acsCommitmentStore + .searchComputedBetween(start, end, counterParticipant.toList) + ) + .onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) def findLastComputedAndSent( domain: DomainAlias )(implicit traceContext: TraceContext): Option[CantonTimestampSecond] = - timeouts.inspection.await(s"$functionFullName on $domain")( - getOrFail(getPersistentState(domain), domain).acsCommitmentStore.lastComputedAndSent - ) + timeouts.inspection + .awaitUS(s"$functionFullName on $domain")( + getOrFail(getPersistentState(domain), domain).acsCommitmentStore.lastComputedAndSent + ) + .onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) def findReceivedCommitments( domain: DomainAlias, @@ -444,10 +448,12 @@ final class SyncStateInspection( end: CantonTimestamp, counterParticipant: Option[ParticipantId] = None, )(implicit traceContext: TraceContext): Iterable[SignedProtocolMessage[AcsCommitment]] = - timeouts.inspection.await(s"$functionFullName from $start to $end on $domain")( - getOrFail(getPersistentState(domain), domain).acsCommitmentStore - .searchReceivedBetween(start, end, counterParticipant.toList) - ) + timeouts.inspection + .awaitUS(s"$functionFullName from $start to $end on $domain")( + getOrFail(getPersistentState(domain), domain).acsCommitmentStore + .searchReceivedBetween(start, end, counterParticipant.toList) + ) + .onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) def crossDomainSentCommitmentMessages( domainPeriods: Seq[DomainSearchCommitmentPeriod], @@ -455,56 +461,60 @@ final class SyncStateInspection( states: Seq[CommitmentPeriodState], verbose: Boolean, )(implicit traceContext: TraceContext): Either[String, Iterable[SentAcsCommitment]] = - timeouts.inspection.await(functionFullName) { - val searchResult = domainPeriods.map { dp => - for { - domain <- syncDomainPersistentStateManager - .aliasForDomainId(dp.indexedDomain.domainId) - .toRight(s"No domain alias found for ${dp.indexedDomain.domainId}") - - persistentState <- getPersistentStateE(domain) - - result = for { - computed <- persistentState.acsCommitmentStore - .searchComputedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) - received <- - if (verbose) - persistentState.acsCommitmentStore - .searchReceivedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) - .map(iter => iter.map(rec => rec.message)) - else Future.successful(Seq.empty) - outstanding <- persistentState.acsCommitmentStore - .outstanding( - dp.fromExclusive, - dp.toInclusive, - counterParticipants, - includeMatchedPeriods = true, - ) - .map { collection => - collection - .collect { case (period, participant, state) => - val converted = fromIntValidSentPeriodState(state.toInt) - (period, participant, converted) - } - .flatMap { - case (period, participant, Some(state)) => Some((period, participant, state)) - case _ => None - } - } - } yield SentAcsCommitment - .compare(dp.indexedDomain.domainId, computed, received, outstanding, verbose) - .filter(cmt => states.isEmpty || states.contains(cmt.state)) - } yield result - } + timeouts.inspection + .awaitUS(functionFullName) { + val searchResult = domainPeriods.map { dp => + for { + domain <- syncDomainPersistentStateManager + .aliasForDomainId(dp.indexedDomain.domainId) + .toRight(s"No domain alias found for ${dp.indexedDomain.domainId}") + + persistentState <- getPersistentStateE(domain) + + result = for { + computed <- persistentState.acsCommitmentStore + .searchComputedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) + received <- + if (verbose) + persistentState.acsCommitmentStore + .searchReceivedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) + .map(iter => iter.map(rec => rec.message)) + else FutureUnlessShutdown.pure(Seq.empty) + outstanding <- persistentState.acsCommitmentStore + .outstanding( + dp.fromExclusive, + dp.toInclusive, + counterParticipants, + includeMatchedPeriods = true, + ) + .map { collection => + collection + .collect { case (period, participant, state) => + val converted = fromIntValidSentPeriodState(state.toInt) + (period, participant, converted) + } + .flatMap { + case (period, participant, Some(state)) => Some((period, participant, state)) + case _ => None + } + } + } yield SentAcsCommitment + .compare(dp.indexedDomain.domainId, computed, received, outstanding, verbose) + .filter(cmt => states.isEmpty || states.contains(cmt.state)) + } yield result + } - val (lefts, rights) = searchResult.partitionMap(identity) + val (lefts, rights) = searchResult.partitionMap(identity) - NonEmpty.from(lefts) match { - case Some(leftsNe) => Future.successful(Left(leftsNe.head1)) - case None => - Future.sequence(rights).map(seqSentAcsCommitments => Right(seqSentAcsCommitments.flatten)) + NonEmpty.from(lefts) match { + case Some(leftsNe) => FutureUnlessShutdown.pure(Left(leftsNe.head1)) + case None => + FutureUnlessShutdown + .sequence(rights) + .map(seqSentAcsCommitments => Right(seqSentAcsCommitments.flatten)) + } } - } + .onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) def crossDomainReceivedCommitmentMessages( domainPeriods: Seq[DomainSearchCommitmentPeriod], @@ -524,25 +534,20 @@ final class SyncStateInspection( result = for { computed <- if (verbose) - FutureUnlessShutdown.outcomeF( - persistentState.acsCommitmentStore - .searchComputedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) - ) + persistentState.acsCommitmentStore + .searchComputedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) else FutureUnlessShutdown.pure(Seq.empty) - received <- FutureUnlessShutdown.outcomeF( - persistentState.acsCommitmentStore - .searchReceivedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) - .map(iter => iter.map(rec => rec.message)) - ) - outstanding <- FutureUnlessShutdown.outcomeF( - persistentState.acsCommitmentStore - .outstanding( - dp.fromExclusive, - dp.toInclusive, - counterParticipants, - includeMatchedPeriods = true, - ) - ) + received <- persistentState.acsCommitmentStore + .searchReceivedBetween(dp.fromExclusive, dp.toInclusive, counterParticipants) + .map(iter => iter.map(rec => rec.message)) + outstanding <- persistentState.acsCommitmentStore + .outstanding( + dp.fromExclusive, + dp.toInclusive, + counterParticipants, + includeMatchedPeriods = true, + ) + buffered <- persistentState.acsCommitmentStore.queue .peekThrough(dp.toInclusive) // peekThrough takes an upper bound parameter .map(iter => @@ -586,11 +591,11 @@ final class SyncStateInspection( traceContext: TraceContext ): Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)] = { val persistentState = getPersistentState(domain) - timeouts.inspection.await(s"$functionFullName from $start to $end on $domain")( + timeouts.inspection.awaitUS(s"$functionFullName from $start to $end on $domain")( getOrFail(persistentState, domain).acsCommitmentStore .outstanding(start, end, counterParticipant.toList) ) - } + }.onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) def bufferedCommitments( domain: DomainAlias, @@ -609,14 +614,14 @@ final class SyncStateInspection( ): Option[CantonTimestamp] = { val persistentState = getPersistentState(domain) - timeouts.inspection.await(s"$functionFullName on $domain for ts $beforeOrAt")( + timeouts.inspection.awaitUS(s"$functionFullName on $domain for ts $beforeOrAt")( for { result <- getOrFail(persistentState, domain).acsCommitmentStore.noOutstandingCommitments( beforeOrAt ) } yield result ) - } + }.onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) def lookupRequestIndex(domain: DomainAlias)(implicit traceContext: TraceContext @@ -685,25 +690,20 @@ final class SyncStateInspection( syncDomainPersistentStateManager.getAll .filter { case (domain, _) => domains.contains(domain) || domains.isEmpty } } yield for { - lastSent <- FutureUnlessShutdown.outcomeF( - syncDomain.acsCommitmentStore.lastComputedAndSent(traceContext) - ) - lastSentFinal = lastSent.fold(CantonTimestamp.MinValue)(_.forgetRefinement) - outstanding <- FutureUnlessShutdown.outcomeF( - syncDomain.acsCommitmentStore - .outstanding( - CantonTimestamp.MinValue, - lastSentFinal, - participants, - includeMatchedPeriods = true, - ) - ) + lastSent <- syncDomain.acsCommitmentStore.lastComputedAndSent(traceContext) - upToDate <- FutureUnlessShutdown.outcomeF( - syncDomain.acsCommitmentStore.searchReceivedBetween( - lastSentFinal, + lastSentFinal = lastSent.fold(CantonTimestamp.MinValue)(_.forgetRefinement) + outstanding <- syncDomain.acsCommitmentStore + .outstanding( + CantonTimestamp.MinValue, lastSentFinal, + participants, + includeMatchedPeriods = true, ) + + upToDate <- syncDomain.acsCommitmentStore.searchReceivedBetween( + lastSentFinal, + lastSentFinal, ) filteredAllParticipants <- findAllKnownParticipants(domains, participants) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala index 98a382ccc0c0..5979c4467466 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/ChangeAssignation.scala @@ -54,15 +54,14 @@ private final class ChangeAssignation( reassignmentData: ChangeAssignation.Data[ReassignmentData] )(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = { + ): EitherT[FutureUnlessShutdown, String, Unit] = { val contractId = reassignmentData.payload.contract.contractId for { contractStatusAtSource <- EitherT.right( sourcePersistentState.unwrap.activeContractStore .fetchStates(Seq(contractId)) - .failOnShutdownToAbortException("readContractAcsStates") ) - _ <- EitherT.cond[Future]( + _ <- EitherT.cond[FutureUnlessShutdown]( contractStatusAtSource.get(contractId).exists(_.status.isReassignedAway), (), s"Contract $contractId is not unassigned in source domain $sourceDomainAlias. " + @@ -79,7 +78,6 @@ private final class ChangeAssignation( reassignmentData.targetTimeOfChange.unwrap, ) .toEitherT - .mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("completeUnassigned")) _ <- persistAssignments(List(unassignedContract)).toEitherT _ <- EitherT.right( publishAssignmentEvent(unassignedContract, reassignmentData.payload.reassignmentId) @@ -94,12 +92,11 @@ private final class ChangeAssignation( skipInactive: Boolean, )(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = + ): EitherT[FutureUnlessShutdown, String, Unit] = for { contractStatusAtSource <- EitherT.right( sourcePersistentState.unwrap.activeContractStore .fetchStates(contractIds.map(_.payload)) - .failOnShutdownToAbortException("changeAssignation") ) _ = logger.debug(s"Contracts status at source: $contractStatusAtSource") contractsAtSource <- changingContractsAtSource( @@ -112,7 +109,6 @@ private final class ChangeAssignation( contractStatusAtTarget <- EitherT.right( targetPersistentState.unwrap.activeContractStore .fetchStates(sourceContractIds) - .failOnShutdownToAbortException("readContractAcsStates") ) _ = logger.debug(s"Contract status at target: $contractStatusAtTarget") contractIds <- changingContractIds(contractsAtSource, contractStatusAtTarget) @@ -130,7 +126,9 @@ private final class ChangeAssignation( contractIds: Iterable[ChangeAssignation.Data[LfContractId]], source: Map[LfContractId, ContractState], skipInactive: Boolean, - ): EitherT[Future, String, List[ChangeAssignation.Data[(LfContractId, ReassignmentCounter)]]] = { + ): EitherT[FutureUnlessShutdown, String, List[ + ChangeAssignation.Data[(LfContractId, ReassignmentCounter)] + ]] = { def errorUnlessSkipInactive( cid: ChangeAssignation.Data[LfContractId], reason: String, @@ -166,7 +164,9 @@ private final class ChangeAssignation( targetStatus: Map[LfContractId, ContractState], )(implicit traceContext: TraceContext - ): EitherT[Future, String, List[ChangeAssignation.Data[(LfContractId, ReassignmentCounter)]]] = { + ): EitherT[FutureUnlessShutdown, String, List[ + ChangeAssignation.Data[(LfContractId, ReassignmentCounter)] + ]] = { val filteredE = sourceContracts .traverse { case data @ ChangeAssignation.Data((cid, reassignmentCounter), _, _) => @@ -183,7 +183,7 @@ private final class ChangeAssignation( } for { - filtered <- EitherT.fromEither[Future](filteredE) + filtered <- EitherT.fromEither[FutureUnlessShutdown](filteredE) filteredContractIds = filtered.map(_.payload._1) stakeholders <- stakeholdersAtSource(filteredContractIds.toSet) _ <- filteredContractIds.parTraverse_ { contractId => @@ -197,17 +197,18 @@ private final class ChangeAssignation( private def stakeholdersAtSource(contractIds: Set[LfContractId])(implicit traceContext: TraceContext - ): EitherT[Future, String, Map[LfContractId, Set[LfPartyId]]] = + ): EitherT[FutureUnlessShutdown, String, Map[LfContractId, Set[LfPartyId]]] = sourcePersistentState.unwrap.contractStore .lookupStakeholders(contractIds) .leftMap(e => s"Failed to look up stakeholder of contracts in domain $sourceDomainAlias: $e") + .mapK(FutureUnlessShutdown.outcomeK) private def atLeastOneHostedStakeholderAtTarget( contractId: LfContractId, stakeholders: Set[LfPartyId], )(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = + ): EitherT[FutureUnlessShutdown, String, Unit] = EitherT( hostsParties(repairTarget.unwrap.domain.topologySnapshot, stakeholders, participantId).map { hosted => @@ -217,7 +218,7 @@ private final class ChangeAssignation( show"Not allowed to move contract $contractId without at least one stakeholder of $stakeholders existing locally on the target domain asOf=${repairTarget.unwrap.domain.topologySnapshot.timestamp}", ) } - ) + ).mapK(FutureUnlessShutdown.outcomeK) private def readContractsFromSource( contractIdsWithReassignmentCounters: List[ @@ -225,24 +226,25 @@ private final class ChangeAssignation( ] )(implicit traceContext: TraceContext - ): EitherT[Future, String, List[ + ): EitherT[FutureUnlessShutdown, String, List[ (SerializableContract, ChangeAssignation.Data[(LfContractId, ReassignmentCounter)]) ]] = sourcePersistentState.unwrap.contractStore .lookupManyExistingUncached(contractIdsWithReassignmentCounters.map(_.payload._1)) .map(_.map(_.contract).zip(contractIdsWithReassignmentCounters)) .leftMap(contractId => s"Failed to look up contract $contractId in domain $sourceDomainAlias") + .mapK(FutureUnlessShutdown.outcomeK) private def readContract( contractIdWithReassignmentCounter: ChangeAssignation.Data[(LfContractId, ReassignmentCounter)] )(implicit traceContext: TraceContext - ): EitherT[Future, String, ChangeAssignation.Data[Changed]] = { + ): EitherT[FutureUnlessShutdown, String, ChangeAssignation.Data[Changed]] = { val contractId = contractIdWithReassignmentCounter.payload._1 for { contracts <- readContracts(List(contractIdWithReassignmentCounter)) contract <- - EitherT.fromOption[Future]( + EitherT.fromOption[FutureUnlessShutdown]( contracts.find(_.payload.contract.contractId == contractId), s"Cannot read contract $contractId", ) @@ -254,7 +256,7 @@ private final class ChangeAssignation( ChangeAssignation.Data[(LfContractId, ReassignmentCounter)] ] )(implicit traceContext: TraceContext): EitherT[ - Future, + FutureUnlessShutdown, String, List[ChangeAssignation.Data[Changed]], ] = @@ -265,20 +267,22 @@ private final class ChangeAssignation( data @ ChangeAssignation.Data((contractId, reassignmentCounter), _, _), ) => for { - reassignmentCounter <- EitherT.fromEither[Future](Right(reassignmentCounter)) - serializedTargetO <- EitherT.right( + reassignmentCounter <- EitherT.fromEither[FutureUnlessShutdown]( + Right(reassignmentCounter) + ) + serializedTargetO <- EitherTUtil.rightUS( targetPersistentState.unwrap.contractStore .lookupContract(contractId) .value ) _ <- serializedTargetO .map { serializedTarget => - EitherTUtil.condUnitET[Future]( + EitherTUtil.condUnitET[FutureUnlessShutdown]( serializedTarget == serializedSource, s"Contract $contractId already exists in the contract store, but differs from contract to be created. Contract to be created $serializedSource versus existing contract $serializedTarget.", ) } - .getOrElse(EitherT.rightT[Future, String](())) + .getOrElse(EitherT.rightT[FutureUnlessShutdown, String](())) } yield data.copy(payload = Changed(serializedSource, reassignmentCounter, serializedTargetO.isEmpty) ) @@ -289,9 +293,9 @@ private final class ChangeAssignation( contracts: List[ChangeAssignation.Data[Changed]] )(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = + ): EitherT[FutureUnlessShutdown, String, Unit] = for { - _ <- EitherT.right { + _ <- EitherTUtil.rightUS { contracts.parTraverse_ { contract => if (contract.payload.isNew) targetPersistentState.unwrap.contractStore @@ -306,7 +310,7 @@ private final class ChangeAssignation( private def persistAssignments(contracts: List[ChangeAssignation.Data[Changed]])(implicit traceContext: TraceContext - ): CheckedT[Future, String, ActiveContractStore.AcsWarning, Unit] = + ): CheckedT[FutureUnlessShutdown, String, ActiveContractStore.AcsWarning, Unit] = targetPersistentState.unwrap.activeContractStore .assignContracts( contracts.map { contract => @@ -318,14 +322,13 @@ private final class ChangeAssignation( ) } ) - .mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("persistAssignments")) .mapAbort(e => s"Failed to mark contracts as assigned: $e") private def persistUnassignAndAssign( contracts: List[ChangeAssignation.Data[Changed]] )(implicit traceContext: TraceContext - ): CheckedT[Future, String, ActiveContractStore.AcsWarning, Unit] = { + ): CheckedT[FutureUnlessShutdown, String, ActiveContractStore.AcsWarning, Unit] = { val unassignF = sourcePersistentState.unwrap.activeContractStore .unassignContracts( @@ -341,24 +344,26 @@ private final class ChangeAssignation( .mapAbort(e => s"Failed to mark contracts as unassigned: $e") unassignF - .mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("persistUnassignAndAssign")) .flatMap(_ => persistAssignments(contracts)) } private def publishAssignmentEvent( changedContract: ChangeAssignation.Data[Changed], reassignmentId: ReassignmentId, - )(implicit traceContext: TraceContext): Future[Unit] = + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = for { - hostedTargetParties <- repairTarget.traverse(repair => - hostedParties( - repair.domain.topologySnapshot, - changedContract.payload.contract.metadata.stakeholders, - participantId, + hostedTargetParties <- repairTarget + .traverse(repair => + hostedParties( + repair.domain.topologySnapshot, + changedContract.payload.contract.metadata.stakeholders, + participantId, + ) + ) + _ <- FutureUnlessShutdown.outcomeF( + repairIndexer.offer( + assignment(reassignmentId, hostedTargetParties)(traceContext)(changedContract) ) - ) - _ <- repairIndexer.offer( - assignment(reassignmentId, hostedTargetParties)(traceContext)(changedContract) ) } yield () @@ -366,7 +371,7 @@ private final class ChangeAssignation( changedContracts: List[ChangeAssignation.Data[Changed]] )(implicit traceContext: TraceContext - ): Future[Unit] = { + ): FutureUnlessShutdown[Unit] = { val reassignmentId = ReassignmentId(sourceDomainId, repairSource.unwrap.timestamp) val allStakeholders = changedContracts.flatMap(_.payload.contract.metadata.stakeholders).toSet @@ -385,12 +390,14 @@ private final class ChangeAssignation( participantId, ) ) - _ <- MonadUtil.sequentialTraverse_( - Iterator( - unassignment(reassignmentId, hostedSourceParties), - assignment(reassignmentId, hostedTargetParties), - ).flatMap(changedContracts.map) - )(repairIndexer.offer) + _ <- FutureUnlessShutdown.outcomeF( + MonadUtil.sequentialTraverse_( + Iterator( + unassignment(reassignmentId, hostedSourceParties), + assignment(reassignmentId, hostedTargetParties), + ).flatMap(changedContracts.map) + )(repairIndexer.offer) + ) } yield () } @@ -401,11 +408,13 @@ private final class ChangeAssignation( participantId: ParticipantId, )(implicit traceContext: TraceContext - ): Future[Set[LfPartyId]] = - hostsParties( - topologySnapshot, - stakeholders, - participantId, + ): FutureUnlessShutdown[Set[LfPartyId]] = + FutureUnlessShutdown.outcomeF( + hostsParties( + topologySnapshot, + stakeholders, + participantId, + ) ) private def unassignment( diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairRequest.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairRequest.scala index 61f1f6bf4728..320b4b2d7899 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairRequest.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairRequest.scala @@ -4,7 +4,6 @@ package com.digitalasset.canton.participant.admin.repair import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.RequestCounter import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.participant.protocol.ProcessingStartingPoints import com.digitalasset.canton.participant.protocol.RequestJournal.{RequestData, RequestState} @@ -13,6 +12,7 @@ import com.digitalasset.canton.participant.util.TimeOfChange import com.digitalasset.canton.protocol.{StaticDomainParameters, TransactionId} import com.digitalasset.canton.topology.DomainId import com.digitalasset.canton.topology.client.TopologySnapshot +import com.digitalasset.canton.{DomainAlias, RequestCounter} private[repair] final case class RepairRequest( domain: RepairRequest.DomainData, @@ -48,7 +48,7 @@ private[repair] object RepairRequest { final case class DomainData( id: DomainId, - alias: String, + alias: DomainAlias, topologySnapshot: TopologySnapshot, persistentState: SyncDomainPersistentState, parameters: StaticDomainParameters, diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala index dd359728c72f..d374621de880 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/repair/RepairService.scala @@ -38,6 +38,7 @@ import com.digitalasset.canton.logging.{ NamedLogging, NamedLoggingContext, } +import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors import com.digitalasset.canton.participant.ParticipantNodeParameters import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.admin.repair.RepairService.{ContractToAdd, DomainLookup} @@ -133,12 +134,14 @@ final class RepairService( ignoreAlreadyAdded: Boolean, acsState: Option[ActiveContractStore.Status], storedContract: Option[SerializableContract], - )(implicit traceContext: TraceContext): EitherT[Future, String, Option[ContractToAdd]] = { + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, String, Option[ContractToAdd]] = { val contractId = repairContract.contract.contractId def addContract( reassigningFrom: Option[Source[DomainId]] - ): EitherT[Future, String, Option[ContractToAdd]] = Right( + ): EitherT[FutureUnlessShutdown, String, Option[ContractToAdd]] = Right( Option( ContractToAdd( repairContract.contract, @@ -147,7 +150,7 @@ final class RepairService( reassigningFrom, ) ) - ).toEitherT[Future] + ).toEitherT[FutureUnlessShutdown] acsState match { case None => addContract(reassigningFrom = None) @@ -156,10 +159,12 @@ final class RepairService( if (ignoreAlreadyAdded) { logger.debug(s"Skipping contract $contractId because it is already active") for { - contractAlreadyThere <- EitherT.fromEither[Future](storedContract.toRight { - s"Contract ${repairContract.contract.contractId} is active but is not found in the stores" - }) - _ <- EitherTUtil.condUnitET[Future]( + contractAlreadyThere <- EitherT.fromEither[FutureUnlessShutdown]( + storedContract.toRight { + s"Contract ${repairContract.contract.contractId} is active but is not found in the stores" + } + ) + _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( contractAlreadyThere == repairContract.contract, log( s"Contract $contractId exists in domain, but does not match with contract being added. " @@ -219,7 +224,7 @@ final class RepairService( ignoreAlreadyAdded: Boolean, )(implicit traceContext: TraceContext - ): EitherT[Future, String, Option[ContractToAdd]] = + ): EitherT[FutureUnlessShutdown, String, Option[ContractToAdd]] = for { // Able to recompute contract signatories and stakeholders (and sanity check // repairContract metadata otherwise ignored matches real metadata) @@ -234,6 +239,7 @@ final class RepairService( .leftMap(e => log(s"Failed to compute contract ${repairContract.contract.contractId} metadata: $e") ) + .mapK(FutureUnlessShutdown.outcomeK) _ = if (repairContract.contract.metadata.signatories != contractWithMetadata.signatories) { logger.info( s"Contract ${repairContract.contract.contractId} metadata signatories ${repairContract.contract.metadata.signatories} differ from actual signatories ${contractWithMetadata.signatories}" @@ -263,24 +269,30 @@ final class RepairService( // If this repair request succeeds, it will advance the clean request prehead to this time of change. // That's why it is important that there are no inflight validation requests before the repair request. private def readDomainData( - domainId: DomainId - )(implicit traceContext: TraceContext): EitherT[Future, String, RepairRequest.DomainData] = + domainId: DomainId, + domainAlias: DomainAlias, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, String, RepairRequest.DomainData] = for { - obtainedPersistentState <- getPersistentState(domainId) - (persistentState, domainAlias) = obtainedPersistentState + persistentState <- EitherT.fromEither[FutureUnlessShutdown]( + lookUpDomainPersistence(domainId, s"domain $domainAlias") + ) - startingPoints <- EitherT.right( - SyncDomainEphemeralStateFactory.startingPoints( - persistentState.requestJournalStore, - persistentState.sequencedEventStore, - tc => ledgerApiIndexer.value.ledgerApiStore.value.domainIndex(domainId)(tc), + startingPoints <- EitherT + .right( + SyncDomainEphemeralStateFactory.startingPoints( + persistentState.requestJournalStore, + persistentState.sequencedEventStore, + tc => ledgerApiIndexer.value.ledgerApiStore.value.domainIndex(domainId)(tc), + ) ) - ) + .mapK(FutureUnlessShutdown.outcomeK) topologyFactory <- domainLookup .topologyFactoryFor(domainId) .toRight(s"No topology factory for domain $domainAlias") - .toEitherT[Future] + .toEitherT[FutureUnlessShutdown] topologySnapshot = topologyFactory.createTopologySnapshot( startingPoints.processing.prenextTimestamp, @@ -289,6 +301,7 @@ final class RepairService( ) domainParameters <- OptionT(persistentState.parameterStore.lastParameters) .toRight(log(s"No static domains parameters found for $domainAlias")) + .mapK(FutureUnlessShutdown.outcomeK) } yield RepairRequest.DomainData( domainId, domainAlias, @@ -326,104 +339,109 @@ final class RepairService( if (contracts.isEmpty) { Either.right(logger.info("No contracts to add specified")) } else { - runConsecutiveAndAwaitDomainAlias( + runConsecutiveAndAwaitUS( "repair.add", - domainId => - withRepairIndexer { repairIndexer => - for { - domain <- readDomainData(domainId) - - contractStates <- EitherT.right[String]( - readContractAcsStates( - domain.persistentState, - contracts.map(_.contract.contractId), - ) + withRepairIndexer { repairIndexer => + (for { + domainId <- EitherT.fromEither[FutureUnlessShutdown]( + aliasManager.domainIdForAlias(domain).toRight(s"Could not find $domain") + ) + domain <- readDomainData(domainId, domain) + + contractStates <- EitherT.right[String]( + readContractAcsStates( + domain.persistentState, + contracts.map(_.contract.contractId), ) + ) - storedContracts <- fromFuture( - domain.persistentState.contractStore.lookupManyUncached( - contracts.map(_.contract.contractId) - ), - "Unable to lookup contracts in contract store", - ).map { contracts => + storedContracts <- logOnFailureWithInfoLevel( + domain.persistentState.contractStore.lookupManyUncached( + contracts.map(_.contract.contractId) + ), + "Unable to lookup contracts in contract store", + ) + .map { contracts => contracts.view.flatMap(_.map(c => c.contractId -> c.contract)).toMap } - filteredContracts <- contracts.zip(contractStates).parTraverseFilter { - case (contract, acsState) => - readRepairContractCurrentState( - domain, - repairContract = contract, - acsState = acsState, - storedContract = storedContracts.get(contract.contract.contractId), - ignoreAlreadyAdded = ignoreAlreadyAdded, - ) - } + filteredContracts <- contracts.zip(contractStates).parTraverseFilter { + case (contract, acsState) => + readRepairContractCurrentState( + domain, + repairContract = contract, + acsState = acsState, + storedContract = storedContracts.get(contract.contract.contractId), + ignoreAlreadyAdded = ignoreAlreadyAdded, + ) + } - contractsByCreation = filteredContracts - .groupBy(_.contract.ledgerCreateTime) - .toList - .sortBy { case (ledgerCreateTime, _) => ledgerCreateTime } - - _ <- PositiveInt - .create(contractsByCreation.size) - .fold( - _ => EitherT.rightT[Future, String](logger.info("No contract needs to be added")), - groupCount => { - val workflowIds = workflowIdsFromPrefix(workflowIdPrefix, groupCount) - for { - repair <- initRepairRequestAndVerifyPreconditions( - domain = domain, - requestCountersToAllocate = groupCount, + contractsByCreation = filteredContracts + .groupBy(_.contract.ledgerCreateTime) + .toList + .sortBy { case (ledgerCreateTime, _) => ledgerCreateTime } + + _ <- PositiveInt + .create(contractsByCreation.size) + .fold( + _ => + EitherT.rightT[FutureUnlessShutdown, String]( + logger.info("No contract needs to be added") + ), + groupCount => { + val workflowIds = workflowIdsFromPrefix(workflowIdPrefix, groupCount) + for { + repair <- initRepairRequestAndVerifyPreconditions( + domain = domain, + requestCountersToAllocate = groupCount, + ) + + hostedWitnesses <- EitherTUtil.rightUS( + hostsParties( + repair.domain.topologySnapshot, + filteredContracts.flatMap(_.witnesses).toSet, + participantId, ) + ) - hostedWitnesses <- EitherT.right( - hostsParties( - repair.domain.topologySnapshot, - filteredContracts.flatMap(_.witnesses).toSet, - participantId, - ) - ) + _ <- addContractsCheck( + repair, + hostedWitnesses = hostedWitnesses, + ignoreStakeholderCheck = ignoreStakeholderCheck, + filteredContracts, + ) - _ <- addContractsCheck( - repair, - hostedWitnesses = hostedWitnesses, - ignoreStakeholderCheck = ignoreStakeholderCheck, - filteredContracts, - ) + contractsToAdd = repair.timesOfChange.zip(contractsByCreation) - contractsToAdd = repair.timesOfChange.zip(contractsByCreation) + _ = logger.debug(s"Publishing ${filteredContracts.size} added contracts") - _ = logger.debug(s"Publishing ${filteredContracts.size} added contracts") + contractsWithTimeOfChange = contractsToAdd.flatMap { case (toc, (_, cs)) => + cs.map(_ -> toc) + } - contractsWithTimeOfChange = contractsToAdd.flatMap { case (toc, (_, cs)) => - cs.map(_ -> toc) - } + _ <- persistAddContracts( + repair, + contractsToAdd = contractsWithTimeOfChange, + storedContracts = storedContracts, + ) - _ <- persistAddContracts( - repair, - contractsToAdd = contractsWithTimeOfChange, - storedContracts = storedContracts, - ) + _ <- cleanRepairRequests(repair) - _ <- cleanRepairRequests(repair) - - // Publish added contracts upstream as created via the ledger api. - _ <- EitherT.right[String]( - writeContractsAddedEvents( - repair, - hostedWitnesses, - contractsToAdd, - workflowIds, - repairIndexer, - ) + // Publish added contracts upstream as created via the ledger api. + _ <- EitherT.right[String]( + writeContractsAddedEvents( + repair, + hostedWitnesses, + contractsToAdd, + workflowIds, + repairIndexer, ) - } yield () - }, - ) - } yield () - }, - domain, + ) + } yield () + }, + ) + } yield ()).mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("addContracts")) + }, ) } } @@ -455,67 +473,70 @@ final class RepairService( logger.info( s"Purging ${contractIds.length} contracts from $domain with ignoreAlreadyPurged=$ignoreAlreadyPurged" ) - runConsecutiveAndAwaitDomainAlias( + runConsecutiveAndAwaitUS( "repair.purge", - domainId => - withRepairIndexer { repairIndexer => - for { - repair <- initRepairRequestAndVerifyPreconditions(domainId) + withRepairIndexer { repairIndexer => + (for { + domainId <- EitherT.fromEither[FutureUnlessShutdown]( + aliasManager.domainIdForAlias(domain).toRight(s"Could not find $domain") + ) + repair <- initRepairRequestAndVerifyPreconditions(domainId) - contractStates <- EitherT.right( - readContractAcsStates( - repair.domain.persistentState, - contractIds, - ) + contractStates <- EitherT.right[String]( + readContractAcsStates( + repair.domain.persistentState, + contractIds, ) + ) - storedContracts <- fromFuture( + storedContracts <- + logOnFailureWithInfoLevel( repair.domain.persistentState.contractStore.lookupManyUncached(contractIds), "Unable to lookup contracts in contract store", - ).map { contracts => - contracts.view.flatMap(_.map(c => c.contractId -> c.contract)).toMap - } - - operationsE = contractIds - .zip(contractStates) - .foldMapM { case (cid, acsStatus) => - val storedContract = storedContracts.get(cid) - computePurgeOperations(repair, ignoreAlreadyPurged)(cid, acsStatus, storedContract) - .map { case (missingPurge, missingAssignment) => - (storedContract.toList, missingPurge, missingAssignment) - } + ) + .map { contracts => + contracts.view.flatMap(_.map(c => c.contractId -> c.contract)).toMap } - operations <- EitherT.fromEither[Future](operationsE) - (contractsToPublishUpstream, missingPurges, missingAssignments) = operations + operationsE = contractIds + .zip(contractStates) + .foldMapM { case (cid, acsStatus) => + val storedContract = storedContracts.get(cid) + computePurgeOperations(repair, ignoreAlreadyPurged)(cid, acsStatus, storedContract) + .map { case (missingPurge, missingAssignment) => + (storedContract.toList, missingPurge, missingAssignment) + } + } + operations <- EitherT.fromEither[FutureUnlessShutdown](operationsE) - // Update the stores - _ <- repair.domain.persistentState.activeContractStore - .purgeContracts(missingPurges) - .toEitherTWithNonaborts - .leftMap(e => - log(s"Failed to purge contracts $missingAssignments in ActiveContractStore: $e") - ) + (contractsToPublishUpstream, missingPurges, missingAssignments) = operations - _ <- repair.domain.persistentState.activeContractStore - .assignContracts(missingAssignments) - .toEitherTWithNonaborts - .leftMap(e => - log( - s"Failed to assign contracts $missingAssignments in ActiveContractStore: $e" - ) + // Update the stores + _ <- repair.domain.persistentState.activeContractStore + .purgeContracts(missingPurges) + .toEitherTWithNonaborts + .leftMap(e => + log(s"Failed to purge contracts $missingAssignments in ActiveContractStore: $e") + ) + .mapK(FutureUnlessShutdown.outcomeK) + + _ <- repair.domain.persistentState.activeContractStore + .assignContracts(missingAssignments) + .toEitherTWithNonaborts + .leftMap(e => + log( + s"Failed to assign contracts $missingAssignments in ActiveContractStore: $e" ) - .mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("purgeContracts")) + ) - _ <- cleanRepairRequests(repair) + _ <- cleanRepairRequests(repair) - // Publish purged contracts upstream as archived via the ledger api. - _ <- EitherT.right( - writeContractsPurgedEvent(contractsToPublishUpstream, Nil, repair, repairIndexer) - ) - } yield () - }, - domain, + // Publish purged contracts upstream as archived via the ledger api. + _ <- EitherTUtil.rightUS[String, Unit]( + writeContractsPurgedEvent(contractsToPublishUpstream, Nil, repair, repairIndexer) + ) + } yield ()).mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("purgeContracts")) + }, ) } @@ -529,7 +550,7 @@ final class RepairService( * @param batchSize how big the batches should be used during the change assignation process */ def changeAssignationAwait( - contractIds: Seq[LfContractId], + contractIds: NonEmpty[Seq[LfContractId]], sourceDomain: DomainAlias, targetDomain: DomainAlias, skipInactive: Boolean, @@ -538,20 +559,24 @@ final class RepairService( logger.info( s"Change assignation request for ${contractIds.length} contracts from $sourceDomain to $targetDomain with skipInactive=$skipInactive" ) - runConsecutiveAndAwaitDomainPair( + runConsecutiveAndAwaitUS( "repair.change_assignation", - (sourceDomainId, targetDomainId) => { - for { - _ <- changeAssignation( - contractIds, - Source(sourceDomainId), - Target(targetDomainId), - skipInactive, - batchSize, - ) - } yield () - }, - (sourceDomain, targetDomain), + for { + domains <- ( + aliasToDomainId(sourceDomain), + aliasToDomainId(targetDomain), + ).tupled + .mapK(FutureUnlessShutdown.outcomeK) + + (sourceDomainId, targetDomainId) = domains + _ <- changeAssignation( + contractIds, + Source(sourceDomainId), + Target(targetDomainId), + skipInactive, + batchSize, + ) + } yield (), ) } @@ -565,65 +590,63 @@ final class RepairService( * @param skipInactive if true, then the migration will skip contracts in the contractId list that are inactive */ def changeAssignation( - contractIds: Seq[LfContractId], + contractIds: NonEmpty[Seq[LfContractId]], sourceDomainId: Source[DomainId], targetDomainId: Target[DomainId], skipInactive: Boolean, batchSize: PositiveInt, - )(implicit traceContext: TraceContext): EitherT[Future, String, Unit] = - PositiveInt - .create(contractIds.size) - .map { numberOfContracts => - for { - _ <- EitherTUtil.condUnitET[Future]( - sourceDomainId.unwrap != targetDomainId.unwrap, - "Source must differ from target domain!", - ) - _ <- withRepairIndexer { repairIndexer => - for { - repairSource <- sourceDomainId.traverse( - initRepairRequestAndVerifyPreconditions( - _, - numberOfContracts, - ) - ) - repairTarget <- targetDomainId.traverse( - initRepairRequestAndVerifyPreconditions( - _, - numberOfContracts, - ) - ) + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, Unit] = { + val numberOfContracts = PositiveInt.tryCreate(contractIds.size) + for { + _ <- EitherTUtil.condUnitET[FutureUnlessShutdown]( + sourceDomainId.unwrap != targetDomainId.unwrap, + "Source must differ from target domain!", + ) - changeAssignation = new ChangeAssignation( - repairSource, - repairTarget, - participantId, - syncCrypto, - repairIndexer, - loggerFactory, - ) + repairSource <- sourceDomainId.traverse( + initRepairRequestAndVerifyPreconditions( + _, + numberOfContracts, + ) + ) - changeAssignationData <- EitherT.fromEither[Future]( - ChangeAssignation.Data.from(contractIds, changeAssignation) - ) + repairTarget <- targetDomainId.traverse( + initRepairRequestAndVerifyPreconditions( + _, + numberOfContracts, + ) + ) - _ <- cleanRepairRequests(repairTarget.unwrap, repairSource.unwrap) + _ <- withRepairIndexer { repairIndexer => + val changeAssignation = new ChangeAssignation( + repairSource, + repairTarget, + participantId, + syncCrypto, + repairIndexer, + loggerFactory, + ) + (for { + changeAssignationData <- EitherT.fromEither[FutureUnlessShutdown]( + ChangeAssignation.Data.from(contractIds.forgetNE, changeAssignation) + ) - // Note the following purposely fails if any contract fails which results in not all contracts being processed. - _ <- MonadUtil - .batchedSequentialTraverse( - parallelism = threadsAvailableForWriting * PositiveInt.two, - batchSize, - )( - changeAssignationData - )(changeAssignation.changeAssignation(_, skipInactive).map(_ => Seq[Unit]())) - .map(_ => ()) + _ <- cleanRepairRequests(repairTarget.unwrap, repairSource.unwrap) - } yield () - } - } yield () + // Note the following purposely fails if any contract fails which results in not all contracts being processed. + _ <- MonadUtil + .batchedSequentialTraverse( + parallelism = threadsAvailableForWriting * PositiveInt.two, + batchSize, + )( + changeAssignationData + )(changeAssignation.changeAssignation(_, skipInactive).map(_ => Seq[Unit]())) + .map(_ => ()) + + } yield ()).mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("changeAssignation")) } - .getOrElse(EitherT.rightT(())) + } yield () + } def ignoreEvents( domain: DomainId, @@ -652,9 +675,9 @@ final class RepairService( def rollbackUnassignment( reassignmentId: ReassignmentId, target: Target[DomainId], - )(implicit context: TraceContext): EitherT[Future, String, Unit] = + )(implicit context: TraceContext): EitherT[FutureUnlessShutdown, String, Unit] = withRepairIndexer { repairIndexer => - for { + (for { sourceRepairRequest <- reassignmentId.sourceDomain.traverse( initRepairRequestAndVerifyPreconditions(_) ) @@ -663,7 +686,6 @@ final class RepairService( targetRepairRequest.unwrap.domain.persistentState.reassignmentStore .lookup(reassignmentId) .leftMap(_.message) - .mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("rollbackUnassignment")) changeAssignation = new ChangeAssignation( sourceRepairRequest, @@ -684,7 +706,7 @@ final class RepairService( repairIndexer, loggerFactory, ) - contractIdData <- EitherT.fromEither[Future]( + contractIdData <- EitherT.fromEither[FutureUnlessShutdown]( ChangeAssignation.Data .from( reassignmentData.contract.contractId, @@ -693,7 +715,7 @@ final class RepairService( .incrementRequestCounter ) _ <- changeAssignationBack.changeAssignation(Seq(contractIdData), skipInactive = false) - } yield () + } yield ()).mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("rollbackUnassignment")) } private def performIfRangeSuitableForIgnoreOperations[T]( @@ -748,8 +770,10 @@ final class RepairService( private def useComputedContractAndMetadata( inputContract: SerializableContract, computed: ContractWithMetadata, - )(implicit traceContext: TraceContext): EitherT[Future, String, SerializableContract] = - EitherT.fromEither[Future]( + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, String, SerializableContract] = + EitherT.fromEither[FutureUnlessShutdown]( for { rawContractInstance <- SerializableRawContractInstance .create(computed.instance) @@ -771,7 +795,7 @@ final class RepairService( hostedWitnesses: Set[LfPartyId], ignoreStakeholderCheck: Boolean, contracts: Seq[ContractToAdd], - )(implicit traceContext: TraceContext): EitherT[Future, String, Unit] = + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, Unit] = for { // All referenced templates known and vetted _packagesVetted <- contracts @@ -797,12 +821,12 @@ final class RepairService( contractToAdd: ContractToAdd )(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = { + ): EitherT[FutureUnlessShutdown, String, Unit] = { val topologySnapshot = repair.domain.topologySnapshot val contract = contractToAdd.contract val contractId = contractToAdd.cid for { - _warnOnEmptyMaintainers <- EitherT.cond[Future]( + _warnOnEmptyMaintainers <- EitherT.cond[FutureUnlessShutdown]( !contract.metadata.maybeKeyWithMaintainers.exists(_.maintainers.isEmpty), (), log(s"Contract $contractId has key without maintainers."), @@ -817,16 +841,19 @@ final class RepairService( ) } - _witnessesKnownLocally <- EitherT.fromEither[Future](_witnessesKnownLocallyE) + _witnessesKnownLocally <- EitherT.fromEither[FutureUnlessShutdown](_witnessesKnownLocallyE) _ <- - if (ignoreStakeholderCheck) EitherT.rightT[Future, String](()) + if (ignoreStakeholderCheck) EitherT.rightT[FutureUnlessShutdown, String](()) else for { // At least one stakeholder is hosted locally if no witnesses are defined _localStakeholderOrWitnesses <- EitherT( - hostsParties(topologySnapshot, contract.metadata.stakeholders, participantId).map { - localStakeholders => + FutureUnlessShutdown + .outcomeF( + hostsParties(topologySnapshot, contract.metadata.stakeholders, participantId) + ) + .map { localStakeholders => Either.cond( contractToAdd.witnesses.nonEmpty || localStakeholders.nonEmpty, (), @@ -834,7 +861,7 @@ final class RepairService( s"Contract ${contract.contractId} has no local stakeholders ${contract.metadata.stakeholders} and no witnesses defined" ), ) - } + } ) // All stakeholders exist on the domain _ <- topologySnapshot @@ -844,22 +871,27 @@ final class RepairService( s"Domain ${repair.domain.alias} missing stakeholders $missingStakeholders of contract ${contract.contractId}" ) } + .mapK(FutureUnlessShutdown.outcomeK) } yield () // All witnesses exist on the domain - _ <- topologySnapshot.allHaveActiveParticipants(contractToAdd.witnesses).leftMap { - missingWitnesses => + _ <- topologySnapshot + .allHaveActiveParticipants(contractToAdd.witnesses) + .leftMap { missingWitnesses => log( s"Domain ${repair.domain.alias} missing witnesses $missingWitnesses of contract ${contract.contractId}" ) - } + } + .mapK(FutureUnlessShutdown.outcomeK) } yield () } - private def fromFuture[T](f: Future[T], errorMessage: => String)(implicit + private def logOnFailureWithInfoLevel[T](f: Future[T], errorMessage: => String)(implicit traceContext: TraceContext - ): EitherT[Future, String, T] = - EitherT.right(FutureUtil.logOnFailure(f, errorMessage, level = Level.INFO)) + ): EitherT[FutureUnlessShutdown, String, T] = + EitherTUtil.rightUS( + FutureUtil.logOnFailure(f, errorMessage, level = Level.INFO) + ) /** Actual persistence work * @param repair Repair request @@ -872,21 +904,21 @@ final class RepairService( storedContracts: Map[LfContractId, SerializableContract], )(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = + ): EitherT[FutureUnlessShutdown, String, Unit] = for { // We compute first which changes we need to persist missingContracts <- contractsToAdd - .parTraverseFilter[EitherT[Future, String, *], MissingContract] { + .parTraverseFilter[EitherT[FutureUnlessShutdown, String, *], MissingContract] { case (contractToAdd, toc) => storedContracts.get(contractToAdd.cid) match { case None => - EitherT.pure[Future, String]( + EitherT.pure[FutureUnlessShutdown, String]( Some((contractToAdd.contract, toc.rc)) ) case Some(storedContract) => EitherTUtil - .condUnitET[Future]( + .condUnitET[FutureUnlessShutdown]( storedContract == contractToAdd.contract, s"Contract ${contractToAdd.cid} already exists in the contract store, but differs from contract to be created. Contract to be created $contractToAdd versus existing contract $storedContract.", ) @@ -909,7 +941,7 @@ final class RepairService( } // Now, we update the stores - _ <- fromFuture( + _ <- logOnFailureWithInfoLevel( repair.domain.persistentState.contractStore.storeCreatedContracts(missingContracts), "Unable to store missing contracts", ) @@ -922,6 +954,7 @@ final class RepairService( s"Failed to add contracts ${missingAdds.map { case (cid, _, _) => cid }} in ActiveContractStore: $e" ) ) + .mapK(FutureUnlessShutdown.outcomeK) _ <- repair.domain.persistentState.activeContractStore .assignContracts(missingAssignments) @@ -931,7 +964,6 @@ final class RepairService( s"Failed to assign ${missingAssignments.map { case (cid, _, _, _) => cid }} in ActiveContractStore: $e" ) ) - .mapK(FutureUnlessShutdown.failOnShutdownToAbortExceptionK("persistAddContracts")) } yield () /** For the given contract, returns the operations (purge, assignment) to perform @@ -1111,8 +1143,8 @@ final class RepairService( contractsAdded: Seq[(TimeOfChange, (LedgerCreateTime, Seq[ContractToAdd]))], workflowIds: Iterator[Option[LfWorkflowId]], repairIndexer: FutureQueue[Update], - )(implicit traceContext: TraceContext): Future[Unit] = - MonadUtil.sequentialTraverse_(contractsAdded) { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + FutureUnlessShutdown.outcomeF(MonadUtil.sequentialTraverse_(contractsAdded) { case (timeOfChange, (timestamp, contractsToAdd)) => // not waiting for Update.persisted, since CommitRepair anyway will be waited for at the end repairIndexer @@ -1127,17 +1159,17 @@ final class RepairService( ) ) .map(_ => ()) - } + }) private def packageKnown( lfPackageId: LfPackageId - )(implicit traceContext: TraceContext): EitherT[Future, String, Unit] = + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, Unit] = for { - packageDescription <- EitherT.right( + packageDescription <- EitherTUtil.rightUS( packageDependencyResolver.getPackageDescription(lfPackageId) ) _packageVetted <- EitherTUtil - .condUnitET[Future]( + .condUnitET[FutureUnlessShutdown]( packageDescription.nonEmpty, log(s"Failed to locate package $lfPackageId"), ) @@ -1170,9 +1202,11 @@ final class RepairService( ) } } - getPersistentState(domainId) - .mapK(FutureUnlessShutdown.outcomeK) - .flatMap { case (persistentState, _) => + EitherT + .fromEither[FutureUnlessShutdown]( + lookUpDomainPersistence(domainId, s"domain $domainId") + ) + .flatMap { persistentState => EitherT( retry .Pause( @@ -1201,19 +1235,6 @@ final class RepairService( ne <- NonEmpty.from(rcs).toRight("generated an empty collection with PositiveInt length") } yield ne - private def getPersistentState( - domainId: DomainId - )(implicit - traceContext: TraceContext - ): EitherT[Future, String, (SyncDomainPersistentState, String)] = { - val domainAlias = aliasManager.aliasForDomainId(domainId).fold(domainId.filterString)(_.unwrap) - for { - persistentState <- EitherT.fromEither[Future]( - lookUpDomainPersistence(domainId, s"domain $domainAlias") - ) - } yield (persistentState, domainAlias) - } - /** Repair commands are inserted where processing starts again upon reconnection. * * @param domainId The ID of the domain for which the request is valid @@ -1222,9 +1243,14 @@ final class RepairService( private def initRepairRequestAndVerifyPreconditions( domainId: DomainId, requestCountersToAllocate: PositiveInt = PositiveInt.one, - )(implicit traceContext: TraceContext): EitherT[Future, String, RepairRequest] = + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, RepairRequest] = for { - domainData <- readDomainData(domainId) + domainAlias <- EitherT.fromEither[FutureUnlessShutdown]( + aliasManager + .aliasForDomainId(domainId) + .toRight(s"domain alias for $domainId not found") + ) + domainData <- readDomainData(domainId, domainAlias) repairRequest <- initRepairRequestAndVerifyPreconditions( domainData, requestCountersToAllocate, @@ -1234,7 +1260,7 @@ final class RepairService( private def initRepairRequestAndVerifyPreconditions( domain: RepairRequest.DomainData, requestCountersToAllocate: PositiveInt, - )(implicit traceContext: TraceContext): EitherT[Future, String, RepairRequest] = { + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, RepairRequest] = { val rtRepair = RecordTime.fromTimeOfChange( TimeOfChange( domain.startingPoints.processing.nextRequestCounter, @@ -1246,14 +1272,17 @@ final class RepairService( s"Starting repair request on (${domain.persistentState}, ${domain.alias}) at $rtRepair." ) for { - _ <- EitherT.right( - SyncDomainEphemeralStateFactory - .cleanupPersistentState(domain.persistentState, domain.startingPoints) - ) + _ <- EitherT + .right( + SyncDomainEphemeralStateFactory + .cleanupPersistentState(domain.persistentState, domain.startingPoints) + ) + .mapK(FutureUnlessShutdown.outcomeK) + incrementalAcsSnapshotWatermark <- EitherT.right( domain.persistentState.acsCommitmentStore.runningCommitments.watermark ) - _ <- EitherT.cond[Future]( + _ <- EitherT.cond[FutureUnlessShutdown]( rtRepair > incrementalAcsSnapshotWatermark, (), log( @@ -1262,7 +1291,7 @@ final class RepairService( |Reconnect to the domain to reprocess inflight validation requests and retry repair afterwards.""".stripMargin ), ) - requestCounters <- EitherT.fromEither[Future]( + requestCounters <- EitherT.fromEither[FutureUnlessShutdown]( requestCounterSequence( domain.startingPoints.processing.nextRequestCounter, requestCountersToAllocate, @@ -1276,16 +1305,18 @@ final class RepairService( ) // Mark the repair request as pending in the request journal store - _ <- EitherT.right[String]( - repair.requestData.parTraverse_(domain.persistentState.requestJournalStore.insert) - ) + _ <- EitherT + .right[String]( + repair.requestData.parTraverse_(domain.persistentState.requestJournalStore.insert) + ) + .mapK(FutureUnlessShutdown.outcomeK) } yield repair } private def markClean( repair: RepairRequest - )(implicit traceContext: TraceContext): EitherT[Future, String, Unit] = + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, Unit] = repair.requestCounters.forgetNE .parTraverse_( repair.domain.persistentState.requestJournalStore.replace( @@ -1296,10 +1327,11 @@ final class RepairService( ) ) .leftMap(t => log(s"Failed to update request journal store on ${repair.domain.alias}: $t")) + .mapK(FutureUnlessShutdown.outcomeK) private def cleanRepairRequests( repairs: RepairRequest* - )(implicit traceContext: TraceContext): EitherT[Future, String, Unit] = + )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, String, Unit] = for { _ <- repairs.parTraverse_(markClean) } yield () @@ -1313,10 +1345,9 @@ final class RepairService( cids: Seq[LfContractId], )(implicit traceContext: TraceContext - ): Future[Seq[Option[ActiveContractStore.Status]]] = + ): FutureUnlessShutdown[Seq[Option[ActiveContractStore.Status]]] = persistentState.activeContractStore .fetchStates(cids) - .failOnShutdownToAbortException("readContractAcsStates") .map { states => cids.map(cid => states.get(cid).map(_.status)) } @@ -1338,19 +1369,20 @@ final class RepairService( ) } yield dp - private def runConsecutiveAndAwait[B]( + private def runConsecutiveAndAwaitUS[B]( description: String, - code: => EitherT[Future, String, B], + code: => EitherT[FutureUnlessShutdown, String, B], )(implicit traceContext: TraceContext ): Either[String, B] = { logger.info(s"Queuing $description") // repair commands can take an unbounded amount of time - parameters.processingTimeouts.unbounded.await(description)( - runConsecutive(description, code).value - ) - } + parameters.processingTimeouts.unbounded.awaitUS(description) { + logger.info(s"Queuing $description") + executionQueue.executeEUS(code, description).value + } + }.onShutdown(throw GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) private def runConsecutive[B]( description: String, @@ -1367,40 +1399,6 @@ final class RepairService( ) } - private def runConsecutiveAndAwaitDomainAlias[B]( - description: String, - code: DomainId => EitherT[Future, String, B], - domainAlias: DomainAlias, - )(implicit - traceContext: TraceContext - ): Either[String, B] = { - val domainId = EitherT.fromEither[Future]( - aliasManager.domainIdForAlias(domainAlias).toRight(s"Could not find $domainAlias") - ) - - runConsecutiveAndAwait( - description, - domainId.flatMap(code), - ) - } - - private def runConsecutiveAndAwaitDomainPair[B]( - description: String, - code: (DomainId, DomainId) => EitherT[Future, String, B], - domainAliases: (DomainAlias, DomainAlias), - )(implicit - traceContext: TraceContext - ): Either[String, B] = { - val domainIds = ( - aliasToDomainId(domainAliases._1), - aliasToDomainId(domainAliases._2), - ).tupled - runConsecutiveAndAwait[B]( - description, - domainIds.flatMap(Function.tupled(code)), - ) - } - private def log(message: String)(implicit traceContext: TraceContext): String = { // consider errors user errors and log them on the server side as info: logger.info(message) @@ -1411,13 +1409,15 @@ final class RepairService( private def withRepairIndexer(code: FutureQueue[Update] => EitherT[Future, String, Unit])(implicit traceContext: TraceContext - ): EitherT[Future, String, Unit] = + ): EitherT[FutureUnlessShutdown, String, Unit] = if (domainLookup.isConnectedToAnyDomain) { - EitherT.leftT[Future, Unit]( + EitherT.leftT[FutureUnlessShutdown, Unit]( "There are still domains connected. Please disconnect all domains." ) } else { - ledgerApiIndexer.value.withRepairIndexer(code) + ledgerApiIndexer.value + .withRepairIndexer(code) + .mapK(FutureUnlessShutdown.outcomeK) } } diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/StartableStoppableLedgerApiServer.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/StartableStoppableLedgerApiServer.scala index fa9095b0ad6a..baaf80c07a3e 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/StartableStoppableLedgerApiServer.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/StartableStoppableLedgerApiServer.scala @@ -453,7 +453,9 @@ class StartableStoppableLedgerApiServer( for { channel <- ResourceOwner .forReleasable(() => - ClientChannelBuilder.createChannelToTrustedServer(config.serverConfig.clientConfig) + ClientChannelBuilder + .createChannelBuilderToTrustedServer(config.serverConfig.clientConfig) + .build() )(channel => Future(channel.shutdown().discard)) _ <- HttpApiServer( jsonApiConfig, diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala index 679deb23f119..c8c75a897bab 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/AbstractMessageProcessor.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.sequencing.client.{SendCallback, SequencerClientS import com.digitalasset.canton.sequencing.protocol.{Batch, MessageId, Recipients} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.{ErrorUtil, FutureUtil} +import com.digitalasset.canton.util.{ErrorUtil, FutureUnlessShutdownUtil} import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.{RequestCounter, SequencerCounter} @@ -200,7 +200,7 @@ abstract class AbstractMessageProcessor( if (timeoutResult.timedOut) FutureUnlessShutdown.outcomeF(onTimeout) else FutureUnlessShutdown.unit } - FutureUtil.doNotAwaitUnlessShutdown(timeoutF, "Handling timeout failed") + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown(timeoutF, "Handling timeout failed") } } yield () diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala index 866b51013e95..8c25e1a04e0f 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessor.scala @@ -575,7 +575,7 @@ abstract class ProtocolProcessor[ } } - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( removeF, s"Failed to remove the pending submission $submissionId", ) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala index 215764748d2e..14d067acc7ec 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala @@ -597,20 +597,19 @@ class AcsCommitmentProcessor private ( _ = logger.debug( show"Commitment messages for $completedPeriod: ${msgs.fmap(_.commitment)}" ) - _ <- FutureUnlessShutdown.outcomeF(storeCommitments(msgs)) + _ <- storeCommitments(msgs) _ = logger.debug( s"Computed and stored ${msgs.size} commitment messages for period $completedPeriod" ) - _ <- FutureUnlessShutdown.outcomeF( - store.markOutstanding(completedPeriod, msgs.keySet) - ) - _ <- FutureUnlessShutdown.outcomeF(persistRunningCommitments(snapshotRes)) + _ <- store.markOutstanding(completedPeriod, msgs.keySet) + + _ <- persistRunningCommitments(snapshotRes) } yield { sendCommitmentMessages(completedPeriod, msgs) } } else FutureUnlessShutdown.unit - _ <- FutureUnlessShutdown.outcomeF { + _ <- if (catchingUpInProgress) { for { _ <- @@ -624,12 +623,11 @@ class AcsCommitmentProcessor private ( // if a mismatch appears after catch-up runningCmtSnapshotsForCatchUp += completedPeriod -> snapshotRes.active } - } else Future.unit + } else FutureUnlessShutdown.unit // mark the period as processed during catch-up _ = endOfLastProcessedPeriodDuringCatchUp = Some(completedPeriod.toInclusive) } yield () - } else Future.unit - } + } else FutureUnlessShutdown.unit _ <- if (!catchingUpInProgress) { @@ -637,7 +635,7 @@ class AcsCommitmentProcessor private ( indicateReadyForRemote(completedPeriod.toInclusive) for { _ <- processBuffered(completedPeriod.toInclusive, endExclusive = false) - _ <- FutureUnlessShutdown.outcomeF(indicateLocallyProcessed(completedPeriod)) + _ <- indicateLocallyProcessed(completedPeriod) } yield () } else FutureUnlessShutdown.unit @@ -658,7 +656,7 @@ class AcsCommitmentProcessor private ( // Ignore the buffered commitment at the boundary _ <- processBuffered(completedPeriod.toInclusive, endExclusive = true) // *After the above check* (the order matters), mark all reconciliation intervals as locally processed. - _ <- FutureUnlessShutdown.outcomeF(indicateLocallyProcessed(completedPeriod)) + _ <- indicateLocallyProcessed(completedPeriod) // clear the commitment snapshot in memory once we caught up _ = runningCmtSnapshotsForCatchUp.clear() _ = cachedCommitmentsForRetroactiveSends.clear() @@ -839,7 +837,7 @@ class AcsCommitmentProcessor private ( ) } yield () - FutureUtil.logOnFailureUnlessShutdown( + FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( future, failureMessage = s"Failed to process incoming commitment.", onFailure = _ => { @@ -991,7 +989,7 @@ class AcsCommitmentProcessor private ( private def persistRunningCommitments( res: CommitmentSnapshot - )(implicit traceContext: TraceContext): Future[Unit] = + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = store.runningCommitments .update(res.recordTime, res.delta, res.deleted) .map(_ => logger.debug(s"Persisted ACS commitments at ${res.recordTime}")) @@ -1003,7 +1001,7 @@ class AcsCommitmentProcessor private ( */ private def persistCatchUpPeriod(period: CommitmentPeriod)(implicit traceContext: TraceContext - ): Future[Unit] = { + ): FutureUnlessShutdown[Unit] = { val catchUpCmt = mkCommitment(participantId, AcsCommitmentProcessor.emptyCommitment, period) storeCommitments(Map(participantId -> catchUpCmt)) } @@ -1036,7 +1034,7 @@ class AcsCommitmentProcessor private ( @VisibleForTesting private[pruning] def indicateLocallyProcessed( period: CommitmentPeriod - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { endOfLastProcessedPeriod = Some(period.toInclusive) for { // mark that we're done with processing this period; safe to do at any point after the commitment has been sent @@ -1226,7 +1224,7 @@ class AcsCommitmentProcessor private ( // check if we were in a catch-up phase possibleCatchUp <- isCatchUpPeriod(cmt.period) lastPruningTime <- store.pruningStatus - _ <- FutureUnlessShutdown.outcomeF { + _ <- if (matches(cmt, commitments, lastPruningTime.map(_.timestamp), possibleCatchUp)) { store.markSafe( cmt.sender, @@ -1240,7 +1238,7 @@ class AcsCommitmentProcessor private ( sortedReconciliationIntervalsProvider, ) } - } + } yield () } } @@ -1264,11 +1262,9 @@ class AcsCommitmentProcessor private ( ) for { // retrieve commitments computed at catch-up boundary - computed <- FutureUnlessShutdown.outcomeF( - store.searchComputedBetween( - completedPeriod.fromExclusive.forgetRefinement, - completedPeriod.toInclusive.forgetRefinement, - ) + computed <- store.searchComputedBetween( + completedPeriod.fromExclusive.forgetRefinement, + completedPeriod.toInclusive.forgetRefinement, ) intervals <- sortedReconciliationIntervalsProvider.computeReconciliationIntervalsCovering( @@ -1282,12 +1278,10 @@ class AcsCommitmentProcessor private ( s"Processing own commitment $cmt for period $period and counter-participant $counterParticipant" ) for { - counterCommitmentList <- FutureUnlessShutdown.outcomeF( - store.queue.peekOverlapsForCounterParticipant( - period, - counterParticipant, - )(traceContext) - ) + counterCommitmentList <- store.queue.peekOverlapsForCounterParticipant( + period, + counterParticipant, + )(traceContext) lastPruningTime <- store.pruningStatus @@ -1315,17 +1309,15 @@ class AcsCommitmentProcessor private ( ) // we mark safe all matching counter-commitments - _ <- FutureUnlessShutdown.outcomeF { - matching.parTraverse_ { counterCommitment => - logger.debug( - s"Marked as safe commitment $cmt against counterComm $counterCommitment" - ) - store.markSafe( - counterCommitment.sender, - counterCommitment.period, - sortedReconciliationIntervalsProvider, - ) - } + _ <- matching.parTraverse_ { counterCommitment => + logger.debug( + s"Marked as safe commitment $cmt against counterComm $counterCommitment" + ) + store.markSafe( + counterCommitment.sender, + counterCommitment.period, + sortedReconciliationIntervalsProvider, + ) } // if there is a mismatch, send all fine-grained commitments between `lastSentCatchUpCommitmentTimestamp` @@ -1443,7 +1435,7 @@ class AcsCommitmentProcessor private ( } case None => completedPeriodTimestamp } - comm <- FutureUnlessShutdown.outcomeF(store.queue.peekThroughAtOrAfter(catchUpTimestamp)) + comm <- store.queue.peekThroughAtOrAfter(catchUpTimestamp) } yield { if (comm.nonEmpty) catchUpTimestamp else completedPeriodTimestamp @@ -1454,11 +1446,11 @@ class AcsCommitmentProcessor private ( /** Store the computed commitments of the commitment messages */ private def storeCommitments( msgs: Map[ParticipantId, AcsCommitment] - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { val items = msgs.map { case (pid, msg) => AcsCommitmentStore.CommitmentData(pid, msg.period, msg.commitment) } - NonEmpty.from(items.toList).fold(Future.unit)(store.storeComputed(_)) + NonEmpty.from(items.toList).fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) } /** Send the computed commitment messages */ @@ -1484,7 +1476,7 @@ class AcsCommitmentProcessor private ( performUnlessClosingUSF(functionFullName) { def message = s"Failed to send commitment message batch for period $period" val cryptoSnapshot = domainCrypto.currentSnapshotApproximation - FutureUtil.logOnFailureUnlessShutdown( + FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( for { batchForm <- msgs.toSeq.parTraverse { case (participant, commitment) => SignedProtocolMessage @@ -1522,7 +1514,7 @@ class AcsCommitmentProcessor private ( } if (msgs.nonEmpty) { - FutureUtil + FutureUnlessShutdownUtil .logOnFailureUnlessShutdown( clock .scheduleAfter( @@ -1633,7 +1625,7 @@ class AcsCommitmentProcessor private ( case (counterParticipant, cmt) if LtHash16.isNonEmptyCommitment(cmt) => counterParticipant -> mkCommitment(counterParticipant, cmt, period) } - _ <- FutureUnlessShutdown.outcomeF(storeCommitments(msgs)) + _ <- storeCommitments(msgs) // TODO(i15333) batch more commitments and handle the case when we reach the maximum message limit. _ = sendCommitmentMessages(period, msgs) } yield () @@ -1646,10 +1638,10 @@ class AcsCommitmentProcessor private ( Lifecycle.close(dbQueue, publishQueue)(logger) @VisibleForTesting - private[pruning] def flush(): Future[Unit] = + private[pruning] def flush(): FutureUnlessShutdown[Unit] = // flatMap instead of zip because the `publishQueue` pushes tasks into the `queue`, // so we must call `queue.flush()` only after everything in the `publishQueue` has been flushed. - publishQueue.flush().flatMap(_ => dbQueue.flush()) + FutureUnlessShutdown.outcomeF(publishQueue.flush().flatMap(_ => dbQueue.flush())) private[canton] class AcsCommitmentProcessorHealth( override val name: String, @@ -1719,8 +1711,8 @@ object AcsCommitmentProcessor extends HasLoggerName { s"Initialized from stored snapshot at ${runningCommitments.watermark} (might be incomplete)." ) } yield (lastComputed, runningCommitments) - FutureUtil.logOnFailureUnlessShutdown( - FutureUnlessShutdown.outcomeF(executed), + FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( + executed, "Failed to initialize the ACS commitment processor.", logPassiveInstanceAtInfo = true, ) @@ -1771,7 +1763,7 @@ object AcsCommitmentProcessor extends HasLoggerName { @VisibleForTesting private[pruning] def initRunningCommitments( store: AcsCommitmentStore - )(implicit ec: ExecutionContext): Future[RunningCommitments] = + )(implicit ec: ExecutionContext): FutureUnlessShutdown[RunningCommitments] = store.runningCommitments.get()(TraceContext.empty).map { case (rt, snapshot) => new RunningCommitments( rt, diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/PruningProcessor.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/PruningProcessor.scala index aaeb06da481c..d81ee8c8dcb6 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/PruningProcessor.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/PruningProcessor.scala @@ -780,16 +780,10 @@ private[pruning] object PruningProcessor extends HasLoggerName { val commitmentsPruningBound = if (checkForOutstandingCommitments) - CommitmentsPruningBound.Outstanding(ts => - FutureUnlessShutdown.outcomeF { - acsCommitmentStore.noOutstandingCommitments(ts) - } - ) + CommitmentsPruningBound.Outstanding(ts => acsCommitmentStore.noOutstandingCommitments(ts)) else CommitmentsPruningBound.LastComputedAndSent( - FutureUnlessShutdown.outcomeF { - acsCommitmentStore.lastComputedAndSent.map(_.map(_.forgetRefinement)) - } + acsCommitmentStore.lastComputedAndSent.map(_.map(_.forgetRefinement)) ) val earliestInFlightF = inFlightSubmissionStore.lookupEarliest(domainId) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala index 5e5af06f9f03..73756645513c 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/AcsCommitmentStore.scala @@ -20,7 +20,6 @@ import com.digitalasset.canton.topology.ParticipantId import com.digitalasset.canton.tracing.TraceContext import scala.collection.immutable.SortedSet -import scala.concurrent.Future import scala.util.control.Breaks.* /** Read and write interface for ACS commitments. Apart from pruning, should only be used by the ACS commitment processor */ @@ -40,12 +39,12 @@ trait AcsCommitmentStore extends AcsCommitmentLookup with PrunableByTime with Au */ def storeComputed(items: NonEmpty[Seq[AcsCommitmentStore.CommitmentData]])(implicit traceContext: TraceContext - ): Future[Unit] + ): FutureUnlessShutdown[Unit] /** Mark that remote commitments are outstanding for a period */ def markOutstanding(period: CommitmentPeriod, counterParticipants: Set[ParticipantId])(implicit traceContext: TraceContext - ): Future[Unit] + ): FutureUnlessShutdown[Unit] /** Marks a period as processed and thus its end as a safe point for crash-recovery. * @@ -55,7 +54,7 @@ trait AcsCommitmentStore extends AcsCommitmentLookup with PrunableByTime with Au */ def markComputedAndSent(period: CommitmentPeriod)(implicit traceContext: TraceContext - ): Future[Unit] + ): FutureUnlessShutdown[Unit] /** Store a received ACS commitment. To be called by the ACS commitment processor only. * @@ -86,7 +85,7 @@ trait AcsCommitmentStore extends AcsCommitmentLookup with PrunableByTime with Au counterParticipant: ParticipantId, period: CommitmentPeriod, sortedReconciliationIntervalsProvider: SortedReconciliationIntervalsProvider, - )(implicit traceContext: TraceContext): Future[Unit] = + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = markPeriod( counterParticipant, period, @@ -108,7 +107,7 @@ trait AcsCommitmentStore extends AcsCommitmentLookup with PrunableByTime with Au counterParticipant: ParticipantId, period: CommitmentPeriod, sortedReconciliationIntervalsProvider: SortedReconciliationIntervalsProvider, - )(implicit traceContext: TraceContext): Future[Unit] = + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = markPeriod( counterParticipant, period, @@ -128,7 +127,7 @@ trait AcsCommitmentStore extends AcsCommitmentLookup with PrunableByTime with Au period: CommitmentPeriod, sortedReconciliationIntervalsProvider: SortedReconciliationIntervalsProvider, matchingState: CommitmentPeriodState, - )(implicit traceContext: TraceContext): Future[Unit] + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] val runningCommitments: IncrementalCommitmentStore @@ -153,7 +152,7 @@ trait AcsCommitmentLookup { */ def lastComputedAndSent(implicit traceContext: TraceContext - ): Future[Option[CantonTimestampSecond]] + ): FutureUnlessShutdown[Option[CantonTimestampSecond]] /** The latest timestamp before or at the given timestamp for which no commitments are outstanding. * A list of [[com.digitalasset.canton.pruning.ConfigForNoWaitCounterParticipants]] can be given for counter participants that should not be considered. @@ -166,7 +165,7 @@ trait AcsCommitmentLookup { beforeOrAt: CantonTimestamp )(implicit traceContext: TraceContext - ): Future[Option[CantonTimestamp]] + ): FutureUnlessShutdown[Option[CantonTimestamp]] /** Inspection: find periods for which commitments are still outstanding, and from whom. * @@ -179,7 +178,7 @@ trait AcsCommitmentLookup { includeMatchedPeriods: Boolean = false, )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] + ): FutureUnlessShutdown[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] /** Inspection: search computed commitments applicable to the specified period (start is exclusive, end is inclusive) */ def searchComputedBetween( @@ -188,14 +187,16 @@ trait AcsCommitmentLookup { counterParticipants: Seq[ParticipantId] = Seq.empty, )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)]] + ): FutureUnlessShutdown[Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)]] /** Inspection: search received commitments applicable to the specified period (start is exclusive, end is inclusive) */ def searchReceivedBetween( start: CantonTimestamp, end: CantonTimestamp, counterParticipants: Seq[ParticipantId] = Seq.empty, - )(implicit traceContext: TraceContext): Future[Iterable[SignedProtocolMessage[AcsCommitment]]] + )(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Iterable[SignedProtocolMessage[AcsCommitment]]] } @@ -223,14 +224,14 @@ trait IncrementalCommitmentStore { */ def get()(implicit traceContext: TraceContext - ): Future[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] + ): FutureUnlessShutdown[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] /** Return the record time of the latest update. * * Defaults to [[com.digitalasset.canton.participant.event.RecordTime.MinValue]] * if no changes have been added yet. */ - def watermark(implicit traceContext: TraceContext): Future[RecordTime] + def watermark(implicit traceContext: TraceContext): FutureUnlessShutdown[RecordTime] /** Update the commitments. * @@ -242,7 +243,7 @@ trait IncrementalCommitmentStore { rt: RecordTime, updates: Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType], deletes: Set[SortedSet[LfPartyId]], - )(implicit traceContext: TraceContext): Future[Unit] + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] } @@ -271,7 +272,7 @@ trait CommitmentQueue { */ def peekThroughAtOrAfter(timestamp: CantonTimestamp)(implicit traceContext: TraceContext - ): Future[Seq[AcsCommitment]] + ): FutureUnlessShutdown[Seq[AcsCommitment]] /** Returns, if exists, a list containing all commitments originating from the given participant * that overlap the given period. @@ -296,10 +297,12 @@ trait CommitmentQueue { counterParticipant: ParticipantId, )(implicit traceContext: TraceContext - ): Future[Seq[AcsCommitment]] + ): FutureUnlessShutdown[Seq[AcsCommitment]] /** Deletes all commitments whose period ends at or before the given timestamp. */ - def deleteThrough(timestamp: CantonTimestamp)(implicit traceContext: TraceContext): Future[Unit] + def deleteThrough(timestamp: CantonTimestamp)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Unit] } object AcsCommitmentStore { diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala index fc3a164fea5c..21186b118cc6 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbAcsCommitmentStore.scala @@ -120,7 +120,7 @@ class DbAcsCommitmentStore( override def storeComputed( items: NonEmpty[Seq[AcsCommitmentStore.CommitmentData]] - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { // We want to detect if we try to overwrite an existing commitment with a different value, as this signals an error. // Still, for performance reasons, we want to do everything in a single, non-interactive statement. @@ -161,25 +161,26 @@ class DbAcsCommitmentStore( val bulkUpsert = DbStorage.bulkOperation(query, items.toList, storage.profile)(setData) - storage.queryAndUpdate(bulkUpsert, "commitments: insert computed").map { rowCounts => - rowCounts.zip(items.toList).foreach { case (rowCount, item) => - val CommitmentData(counterParticipant, period, commitment) = item - // Underreporting of the affected rows should not matter here as the query is idempotent and updates the row even if the same values had been there before - ErrorUtil.requireState( - rowCount != 0, - s"Commitment for domain $indexedDomain, counterparticipant $counterParticipant and period $period already computed with a different value; refusing to insert $commitment", - ) - } + storage.queryAndUpdateUnlessShutdown(bulkUpsert, "commitments: insert computed").map { + rowCounts => + rowCounts.zip(items.toList).foreach { case (rowCount, item) => + val CommitmentData(counterParticipant, period, commitment) = item + // Underreporting of the affected rows should not matter here as the query is idempotent and updates the row even if the same values had been there before + ErrorUtil.requireState( + rowCount != 0, + s"Commitment for domain $indexedDomain, counterparticipant $counterParticipant and period $period already computed with a different value; refusing to insert $commitment", + ) + } } } override def markOutstanding(period: CommitmentPeriod, counterParticipants: Set[ParticipantId])( implicit traceContext: TraceContext - ): Future[Unit] = { + ): FutureUnlessShutdown[Unit] = { logger.debug( s"Marking $period as outstanding for ${counterParticipants.size} remote participants" ) - if (counterParticipants.isEmpty) Future.unit + if (counterParticipants.isEmpty) FutureUnlessShutdown.unit else { import DbStorage.Implicits.BuilderChain.* @@ -192,13 +193,16 @@ class DbAcsCommitmentStore( ) .intercalate(sql", ") ++ sql" on conflict do nothing").asUpdate - storage.update_(insertOutstanding, operationName = "commitments: storeOutstanding") + storage.updateUnlessShutdown_( + insertOutstanding, + operationName = "commitments: storeOutstanding", + ) } } override def markComputedAndSent( period: CommitmentPeriod - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { val timestamp = period.toInclusive val upsertQuery = storage.profile match { case _: DbStorage.Profile.H2 => @@ -208,7 +212,7 @@ class DbAcsCommitmentStore( on conflict (domain_idx) do update set ts = $timestamp""" } - storage.update_(upsertQuery, operationName = "commitments: markComputedAndSent") + storage.updateUnlessShutdown_(upsertQuery, operationName = "commitments: markComputedAndSent") } override def outstanding( @@ -218,7 +222,7 @@ class DbAcsCommitmentStore( includeMatchedPeriods: Boolean, )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] = { + ): FutureUnlessShutdown[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] = { val participantFilter: SQLActionBuilderChain = counterParticipants match { case Seq() => sql"" case list => @@ -234,7 +238,7 @@ class DbAcsCommitmentStore( and ($includeMatchedPeriods or matching_state != ${CommitmentPeriodState.Matched}) """ ++ participantFilter - storage.query( + storage.queryUnlessShutdown( query .as[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)] .transactionally @@ -281,7 +285,7 @@ class DbAcsCommitmentStore( matchingState: CommitmentPeriodState, )(implicit traceContext: TraceContext - ): Future[Unit] = { + ): FutureUnlessShutdown[Unit] = { def dbQueries( sortedReconciliationIntervals: SortedReconciliationIntervals ): DBIOAction[Unit, NoStream, Effect.All] = { @@ -377,23 +381,17 @@ class DbAcsCommitmentStore( ) } - _ <- FutureUnlessShutdown.outcomeF( - storage.queryAndUpdate( - dbQueries(sortedReconciliationIntervals).transactionally.withTransactionIsolation( - TransactionIsolation.Serializable - ), - operationName = - s"commitments: mark period safe (${period.fromExclusive}, ${period.toInclusive}]", - ) + _ <- storage.queryAndUpdateUnlessShutdown( + dbQueries(sortedReconciliationIntervals).transactionally.withTransactionIsolation( + TransactionIsolation.Serializable + ), + operationName = + s"commitments: mark period safe (${period.fromExclusive}, ${period.toInclusive}]", ) + } yield (), "Run mark period safe DB query", ) - .onShutdown( - logger.debug( - s"Aborted marking period safe (${period.fromExclusive}, ${period.toInclusive}] due to shutdown" - ) - ) } override def doPrune( @@ -416,8 +414,8 @@ class DbAcsCommitmentStore( override def lastComputedAndSent(implicit traceContext: TraceContext - ): Future[Option[CantonTimestampSecond]] = - storage.query( + ): FutureUnlessShutdown[Option[CantonTimestampSecond]] = + storage.queryUnlessShutdown( sql"select ts from par_last_computed_acs_commitments where domain_idx = $indexedDomain" .as[CantonTimestampSecond] .headOption, @@ -428,15 +426,14 @@ class DbAcsCommitmentStore( beforeOrAt: CantonTimestamp )(implicit traceContext: TraceContext - ): Future[Option[CantonTimestamp]] = + ): FutureUnlessShutdown[Option[CantonTimestamp]] = for { computed <- lastComputedAndSent adjustedTsOpt = computed.map(_.forgetRefinement.min(beforeOrAt)) ignores <- acsCounterParticipantConfigStore .getAllActiveNoWaitCounterParticipants(Seq(indexedDomain.domainId), Seq.empty) - .failOnShutdownToAbortException("noOutstandingCommitments") outstandingOpt <- adjustedTsOpt.traverse { ts => - storage.query( + storage.queryUnlessShutdown( sql"select from_exclusive, to_inclusive, counter_participant from par_outstanding_acs_commitments where domain_idx=$indexedDomain and from_exclusive < $ts and matching_state != ${CommitmentPeriodState.Matched}" .as[(CantonTimestamp, CantonTimestamp, ParticipantId)] .withTransactionIsolation(Serializable), @@ -464,7 +461,9 @@ class DbAcsCommitmentStore( counterParticipants: Seq[ParticipantId], )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)]] = { + ): FutureUnlessShutdown[ + Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)] + ] = { val participantFilter: SQLActionBuilderChain = counterParticipants match { case Seq() => sql"" @@ -480,7 +479,7 @@ class DbAcsCommitmentStore( from par_computed_acs_commitments where domain_idx = $indexedDomain and to_inclusive >= $start and from_exclusive < $end""" ++ participantFilter - storage.query( + storage.queryUnlessShutdown( query.as[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)], functionFullName, ) @@ -490,7 +489,9 @@ class DbAcsCommitmentStore( start: CantonTimestamp, end: CantonTimestamp, counterParticipants: Seq[ParticipantId] = Seq.empty, - )(implicit traceContext: TraceContext): Future[Iterable[SignedProtocolMessage[AcsCommitment]]] = { + )(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Iterable[SignedProtocolMessage[AcsCommitment]]] = { val participantFilter: SQLActionBuilderChain = counterParticipants match { case Seq() => sql"" @@ -506,7 +507,7 @@ class DbAcsCommitmentStore( from par_received_acs_commitments where domain_idx = $indexedDomain and to_inclusive >= $start and from_exclusive < $end""" ++ participantFilter - storage.query(query.as[SignedProtocolMessage[AcsCommitment]], functionFullName) + storage.queryUnlessShutdown(query.as[SignedProtocolMessage[AcsCommitment]], functionFullName) } @@ -549,9 +550,9 @@ class DbIncrementalCommitmentStore( override def get()(implicit traceContext: TraceContext - ): Future[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] = + ): FutureUnlessShutdown[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] = for { - res <- storage.query( + res <- storage.queryUnlessShutdown( (for { tsWithTieBreaker <- sql"""select ts, tie_breaker from par_commitment_snapshot_time where domain_idx = $indexedDomain""" @@ -575,13 +576,13 @@ class DbIncrementalCommitmentStore( } } - override def watermark(implicit traceContext: TraceContext): Future[RecordTime] = { + override def watermark(implicit traceContext: TraceContext): FutureUnlessShutdown[RecordTime] = { val query = sql"""select ts, tie_breaker from par_commitment_snapshot_time where domain_idx=$indexedDomain""" .as[(CantonTimestamp, Long)] .headOption storage - .query(query, operationName = "commitments: read snapshot watermark") + .queryUnlessShutdown(query, operationName = "commitments: read snapshot watermark") .map(_.fold(RecordTime.MinValue) { case (ts, tieBreaker) => RecordTime(ts, tieBreaker) }) } @@ -589,7 +590,7 @@ class DbIncrementalCommitmentStore( rt: RecordTime, updates: Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType], deletes: Set[SortedSet[LfPartyId]], - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { def partySetHash(parties: SortedSet[LfPartyId]): String68 = Hash .digest( @@ -650,7 +651,7 @@ class DbIncrementalCommitmentStore( val operations = List(insertRt(rt), storeUpdates(updateList), deleteCommitments(deleteList)) - storage.queryAndUpdate( + storage.queryAndUpdateUnlessShutdown( DBIO.seq(operations*).transactionally.withTransactionIsolation(Serializable), operationName = "commitments: incremental commitments snapshot update", ) @@ -710,9 +711,9 @@ class DbCommitmentQueue( */ override def peekThroughAtOrAfter( timestamp: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Seq[AcsCommitment]] = + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Seq[AcsCommitment]] = storage - .query( + .queryUnlessShutdown( sql"""select sender, counter_participant, from_exclusive, to_inclusive, commitment from par_commitment_queue where domain_idx = $indexedDomain and to_inclusive >= $timestamp""" @@ -725,9 +726,9 @@ class DbCommitmentQueue( counterParticipant: ParticipantId, )(implicit traceContext: TraceContext - ): Future[Seq[AcsCommitment]] = + ): FutureUnlessShutdown[Seq[AcsCommitment]] = storage - .query( + .queryUnlessShutdown( sql"""select sender, counter_participant, from_exclusive, to_inclusive, commitment from par_commitment_queue where domain_idx = $indexedDomain and sender = $counterParticipant @@ -740,8 +741,8 @@ class DbCommitmentQueue( /** Deletes all commitments whose period ends at or before the given timestamp. */ override def deleteThrough( timestamp: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Unit] = - storage.update_( + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + storage.updateUnlessShutdown_( sqlu"delete from par_commitment_queue where domain_idx = $indexedDomain and to_inclusive <= $timestamp", operationName = "delete queued commitments", ) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbParticipantSettingsStore.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbParticipantSettingsStore.scala index 99a43cc1aaee..199495664d98 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbParticipantSettingsStore.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbParticipantSettingsStore.scala @@ -15,7 +15,7 @@ import com.digitalasset.canton.participant.store.ParticipantSettingsStore.Settin import com.digitalasset.canton.resource.{DbStorage, DbStore} import com.digitalasset.canton.time.NonNegativeFiniteDuration import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.{FutureUtil, SimpleExecutionQueue} +import com.digitalasset.canton.util.{FutureUnlessShutdownUtil, SimpleExecutionQueue} import slick.jdbc.{GetResult, SetParameter} import slick.sql.SqlAction @@ -147,7 +147,7 @@ class DbParticipantSettingsStore( performUnlessClosingF(operationName)(storage.update_(query, operationName)).transformWith { res => // Reload cache to make it consistent with the DB. Particularly important in case of concurrent writes. - FutureUtil + FutureUnlessShutdownUtil .logOnFailureUnlessShutdown( refreshCache(), s"An exception occurred while refreshing the cache. Keeping old value $settings.", diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala index 0d0a962bcb96..b5e84d087948 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemoryAcsCommitmentStore.scala @@ -70,7 +70,7 @@ class InMemoryAcsCommitmentStore( override def storeComputed( items: NonEmpty[Seq[AcsCommitmentStore.CommitmentData]] - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { blocking { computed.synchronized { items.toList.foreach { item => @@ -89,7 +89,7 @@ class InMemoryAcsCommitmentStore( } } } - Future.unit + FutureUnlessShutdown.unit } override def getComputed(period: CommitmentPeriod, counterParticipant: ParticipantId)(implicit @@ -120,27 +120,27 @@ class InMemoryAcsCommitmentStore( override def markOutstanding(period: CommitmentPeriod, counterParticipants: Set[ParticipantId])( implicit traceContext: TraceContext - ): Future[Unit] = { + ): FutureUnlessShutdown[Unit] = { if (counterParticipants.nonEmpty) { _outstanding.updateAndGet(os => os ++ counterParticipants.map(cp => (period, cp, CommitmentPeriodState.Outstanding)) ) } - Future.unit + FutureUnlessShutdown.unit } override def markComputedAndSent( period: CommitmentPeriod - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { val timestamp = period.toInclusive lastComputed.set(Some(timestamp)) - Future.unit + FutureUnlessShutdown.unit } override def lastComputedAndSent(implicit traceContext: TraceContext - ): Future[Option[CantonTimestampSecond]] = - Future.successful(lastComputed.get()) + ): FutureUnlessShutdown[Option[CantonTimestampSecond]] = + FutureUnlessShutdown.pure(lastComputed.get()) private def computeOutstanding( counterParticipant: ParticipantId, @@ -218,7 +218,7 @@ class InMemoryAcsCommitmentStore( period: CommitmentPeriod, sortedReconciliationIntervalsProvider: SortedReconciliationIntervalsProvider, matchingState: CommitmentPeriodState, - )(implicit traceContext: TraceContext): Future[Unit] = { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = { val approxInterval = sortedReconciliationIntervalsProvider.approximateReconciliationIntervals val intervals = approxInterval @@ -247,21 +247,15 @@ class InMemoryAcsCommitmentStore( ) () } - .onShutdown( - logger.debug( - s"Aborted marking period safe (${period.fromExclusive}, ${period.toInclusive}] due to shutdown" - ) - ) } override def noOutstandingCommitments( beforeOrAt: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Option[CantonTimestamp]] = + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Option[CantonTimestamp]] = for { ignoredParticipants <- acsCounterParticipantConfigStore .getAllActiveNoWaitCounterParticipants(Seq(domainId), Seq.empty) - .failOnShutdownToAbortException("noOutstandingCommitments") - result <- Future.successful { + result <- FutureUnlessShutdown.pure { for { lastTs <- lastComputed.get adjustedTs = lastTs.forgetRefinement.min(beforeOrAt) @@ -290,8 +284,8 @@ class InMemoryAcsCommitmentStore( includeMatchedPeriods: Boolean, )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] = - Future.successful(_outstanding.get.filter { case (period, participant, state) => + ): FutureUnlessShutdown[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] = + FutureUnlessShutdown.pure(_outstanding.get.filter { case (period, participant, state) => (counterParticipant.isEmpty || counterParticipant.contains(participant)) && period.fromExclusive < end && @@ -305,12 +299,14 @@ class InMemoryAcsCommitmentStore( counterParticipant: Seq[ParticipantId] = Seq.empty, )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)]] = { + ): FutureUnlessShutdown[ + Iterable[(CommitmentPeriod, ParticipantId, AcsCommitment.CommitmentType)] + ] = { val filteredByCounterParty = if (counterParticipant.isEmpty) computed else computed.filter(c => counterParticipant.contains(c._1)) - Future.successful( + FutureUnlessShutdown.pure( filteredByCounterParty.flatMap { case (p, m) => LazyList .continually(p) @@ -328,12 +324,14 @@ class InMemoryAcsCommitmentStore( start: CantonTimestamp, end: CantonTimestamp, counterParticipant: Seq[ParticipantId] = Seq.empty, - )(implicit traceContext: TraceContext): Future[Iterable[SignedProtocolMessage[AcsCommitment]]] = { + )(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Iterable[SignedProtocolMessage[AcsCommitment]]] = { val filteredByCounterParty = (if (counterParticipant.isEmpty) received else received.filter(c => counterParticipant.contains(c._1))).values - Future.successful( + FutureUnlessShutdown.pure( filteredByCounterParty.flatMap(msgs => msgs.filter(msg => start <= msg.message.period.toInclusive && msg.message.period.fromExclusive < end @@ -413,20 +411,20 @@ class InMemoryIncrementalCommitments( override def get()(implicit traceContext: TraceContext - ): Future[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] = { + ): FutureUnlessShutdown[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] = { val rt = watermark_() - Future.successful((rt, snapshot.toMap)) + FutureUnlessShutdown.pure((rt, snapshot.toMap)) } override def update( rt: RecordTime, updates: Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType], deletes: Set[SortedSet[LfPartyId]], - )(implicit traceContext: TraceContext): Future[Unit] = - Future.successful(update_(rt, updates, deletes)) + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + FutureUnlessShutdown.pure(update_(rt, updates, deletes)) - override def watermark(implicit traceContext: TraceContext): Future[RecordTime] = - Future.successful(watermark_()) + override def watermark(implicit traceContext: TraceContext): FutureUnlessShutdown[RecordTime] = + FutureUnlessShutdown.pure(watermark_()) } class InMemoryCommitmentQueue(implicit val ec: ExecutionContext) extends CommitmentQueue { @@ -443,11 +441,6 @@ class InMemoryCommitmentQueue(implicit val ec: ExecutionContext) extends Commitm private object lock - private def syncF[T](v: => T): Future[T] = { - val evaluated = blocking(lock.synchronized(v)) - Future.successful(evaluated) - } - private def syncUS[T](v: => T): FutureUnlessShutdown[T] = { val evaluated = blocking(lock.synchronized(v)) FutureUnlessShutdown.pure(evaluated) @@ -475,8 +468,8 @@ class InMemoryCommitmentQueue(implicit val ec: ExecutionContext) extends Commitm */ override def peekThroughAtOrAfter( timestamp: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Seq[AcsCommitment]] = - syncF { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Seq[AcsCommitment]] = + syncUS { queue.filter(_.period.toInclusive >= timestamp).toSeq } @@ -485,8 +478,8 @@ class InMemoryCommitmentQueue(implicit val ec: ExecutionContext) extends Commitm counterParticipant: ParticipantId, )(implicit traceContext: TraceContext - ): Future[Seq[AcsCommitment]] = - syncF { + ): FutureUnlessShutdown[Seq[AcsCommitment]] = + syncUS { queue .filter(_.period.overlaps(period)) .filter(_.sender == counterParticipant) @@ -496,7 +489,7 @@ class InMemoryCommitmentQueue(implicit val ec: ExecutionContext) extends Commitm /** Deletes all commitments whose period ends at or before the given timestamp. */ override def deleteThrough( timestamp: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Unit] = syncF { + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = syncUS { deleteWhile(queue)(_.period.toInclusive <= timestamp) } } diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala index 76ed7aeb8138..ee85ec2c6619 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala @@ -86,7 +86,7 @@ import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.util.FutureInstances.* import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.{ErrorUtil, FutureUtil, MonadUtil} +import com.digitalasset.canton.util.{ErrorUtil, FutureUnlessShutdownUtil, MonadUtil} import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.daml.lf.engine.Engine import io.grpc.Status @@ -571,8 +571,10 @@ class SyncDomain( // The "suitable point" must ensure that the [[com.digitalasset.canton.participant.store.AcsSnapshotStore]] // receives any partially-applied changes; choosing the timestamp returned by the store is sufficient and optimal // in terms of performance, but any earlier timestamp is also correct - acsChangesReplayStartRt <- liftF(persistent.acsCommitmentStore.runningCommitments.watermark) - .mapK(FutureUnlessShutdown.outcomeK) + acsChangesReplayStartRt <- EitherT.right( + persistent.acsCommitmentStore.runningCommitments.watermark + ) + _ <- loadPendingEffectiveTimesFromTopologyStore(acsChangesReplayStartRt.timestamp) acsChangesToReplay <- if ( @@ -729,7 +731,7 @@ class SyncDomain( logger.debug(s"Started sync domain for $domainId")(initializationTraceContext) ephemeral.markAsRecovered() logger.debug("Sync domain is ready.")(initializationTraceContext) - FutureUtil.doNotAwaitUnlessShutdown( + FutureUnlessShutdownUtil.doNotAwaitUnlessShutdown( completeAssignment, "Failed to complete outstanding assignments on startup. " + "You may have to complete the assignments manually.", diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainMigration.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainMigration.scala index 32ed8aad73c0..1c6a304580d0 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainMigration.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainMigration.scala @@ -10,6 +10,7 @@ import cats.syntax.functor.* import cats.syntax.traverse.* import com.daml.error.{ErrorCategory, ErrorCode, Explanation} import com.daml.nameof.NameOf.functionFullName +import com.daml.nonempty.NonEmpty import com.digitalasset.canton.DomainAlias import com.digitalasset.canton.common.domain.grpc.SequencerInfoLoader import com.digitalasset.canton.config.ProcessingTimeout @@ -298,19 +299,24 @@ class SyncDomainMigration( _ = logger.info( s"Found ${acs.size} contracts in the ACS of $sourceAlias that need to be migrated" ) - // move contracts from one domain to the other domain using repair service in batches of batchSize - _ <- performUnlessClosingEitherU(functionFullName)( - repair.changeAssignation( - acs.keys.toSeq, - source, - target, - skipInactive = true, - batchSize, - ) - ) - .leftMap[SyncDomainMigrationError]( - SyncDomainMigrationError.InternalError.FailedMigratingContracts(sourceAlias.unwrap, _) - ) + _ <- NonEmpty + .from(acs.keys.toSeq.distinct) match { + case None => EitherT.right[SyncDomainMigrationError](FutureUnlessShutdown.unit) + case Some(contractIds) => + // move contracts from one domain to the other domain using repair service in batches of batchSize + performUnlessClosingEitherUSF(functionFullName)( + repair.changeAssignation( + contractIds, + source, + target, + skipInactive = true, + batchSize, + ) + ) + .leftMap[SyncDomainMigrationError]( + SyncDomainMigrationError.InternalError.FailedMigratingContracts(sourceAlias.unwrap, _) + ) + } } yield () } diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/LedgerServerPartyNotifier.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/LedgerServerPartyNotifier.scala index 5a0ff7fd2451..a23c9442af7c 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/LedgerServerPartyNotifier.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/LedgerServerPartyNotifier.scala @@ -22,7 +22,7 @@ import com.digitalasset.canton.topology.transaction.* import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* -import com.digitalasset.canton.util.{FutureUtil, SimpleExecutionQueue} +import com.digitalasset.canton.util.{FutureUnlessShutdownUtil, FutureUtil, SimpleExecutionQueue} import com.digitalasset.canton.{LedgerSubmissionId, SequencerCounter} import scala.collection.concurrent.TrieMap @@ -319,7 +319,7 @@ class LedgerServerPartyNotifier( // note, that if this fails, we have an issue as ledger server will not have // received the event. this is generally an issue with everything we send to the // index server - FutureUtil.logOnFailureUnlessShutdown( + FutureUnlessShutdownUtil.logOnFailureUnlessShutdown( sequentialQueue.execute( updateAndNotify( party, diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspectionTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspectionTest.scala index 6a9a533871ae..eec6e5488310 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspectionTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/inspection/SyncStateInspectionTest.scala @@ -46,7 +46,13 @@ import com.digitalasset.canton.store.IndexedDomain import com.digitalasset.canton.store.db.{DbTest, PostgresTest} import com.digitalasset.canton.time.PositiveSeconds import com.digitalasset.canton.topology.{DomainId, ParticipantId, UniqueIdentifier} -import com.digitalasset.canton.{BaseTest, CloseableTest, DomainAlias, HasExecutionContext} +import com.digitalasset.canton.{ + BaseTest, + CloseableTest, + DomainAlias, + FailOnShutdown, + HasExecutionContext, +} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec @@ -58,6 +64,7 @@ sealed trait SyncStateInspectionTest with BaseTest with CloseableTest with SortedReconciliationIntervalsHelpers + with FailOnShutdown with HasExecutionContext { this: DbTest => @@ -297,7 +304,7 @@ sealed trait SyncStateInspectionTest createDummyReceivedCommitment(domainId, remoteId, testPeriod) for { _ <- store.markOutstanding(testPeriod, Set(remoteId)) - _ <- store.storeReceived(dummyCommitment).failOnShutdown + _ <- store.storeReceived(dummyCommitment) crossDomainReceived = syncStateInspection.crossDomainReceivedCommitmentMessages( Seq(domainSearchPeriod), @@ -305,7 +312,9 @@ sealed trait SyncStateInspectionTest Seq.empty, verbose = false, ) - } yield crossDomainReceived.valueOrFail("crossDomainReceived").toSet shouldBe Set(received) + } yield crossDomainReceived.valueOrFail("crossDomainReceived").toSet shouldBe Set( + received + ) } "fetch a computed commitment if it has been computed" in { @@ -320,11 +329,11 @@ sealed trait SyncStateInspectionTest val (sent, dummyCommitment) = createDummyComputedCommitment(domainId, remoteId, testPeriod) for { - _ <- store.markOutstanding(testPeriod, Set(remoteId)) + _ <- store.markOutstanding(testPeriod, Set(remoteId)).failOnShutdown nonEmpty = NonEmpty .from(Seq(dummyCommitment)) .getOrElse(throw new IllegalStateException("How is this empty?")) - _ <- store.storeComputed(nonEmpty) + _ <- store.storeComputed(nonEmpty).failOnShutdown crossDomainSent = syncStateInspection.crossDomainSentCommitmentMessages( Seq(domainSearchPeriod), @@ -369,7 +378,7 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitment)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store.storeComputed(nonEmpty) - _ <- store.storeReceived(dummyRecCommitment).failOnShutdown + _ <- store.storeReceived(dummyRecCommitment) _ <- store.markSafe(remoteId, testPeriod, srip) crossDomainSent = syncStateInspection.crossDomainSentCommitmentMessages( @@ -466,7 +475,7 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitment)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store.storeComputed(nonEmpty) - _ <- store.storeReceived(dummyRecCommitment).failOnShutdown + _ <- store.storeReceived(dummyRecCommitment) _ <- store.markSafe(remoteId, testPeriod, srip) // same thing, but for the second store @@ -475,7 +484,7 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitment2)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store2.storeComputed(nonEmpty2) - _ <- store2.storeReceived(dummyRecCommitment2).failOnShutdown + _ <- store2.storeReceived(dummyRecCommitment2) _ <- store2.markSafe(remoteId, testPeriod, srip) crossDomainSent = syncStateInspection.crossDomainSentCommitmentMessages( @@ -562,7 +571,7 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitment)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store.storeComputed(nonEmpty) - _ <- store.storeReceived(dummyRecCommitment).failOnShutdown + _ <- store.storeReceived(dummyRecCommitment) _ <- store.markSafe(remoteId, testPeriod, srip) // same thing, but for the second store @@ -571,7 +580,7 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitment2)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store2.storeComputed(nonEmpty2) - _ <- store2.storeReceived(dummyRecCommitment2).failOnShutdown + _ <- store2.storeReceived(dummyRecCommitment2) _ <- store2.markSafe(remoteId, testPeriod, srip) // introduce commitments for remoteId2, since we filter by remoteId then these should not appear @@ -580,7 +589,7 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitmentTrap)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store2.storeComputed(nonEmptyTrap) - _ <- store2.storeReceived(dummyRecCommitmentTrap).failOnShutdown + _ <- store2.storeReceived(dummyRecCommitmentTrap) _ <- store2.markSafe(remoteId2, testPeriod, srip) crossDomainSent = syncStateInspection.crossDomainSentCommitmentMessages( @@ -654,8 +663,8 @@ sealed trait SyncStateInspectionTest .from(Seq(dummySentCommitment, dummySentCommitment2, dummySentCommitment3)) .getOrElse(throw new IllegalStateException("How is this empty?")) _ <- store.storeComputed(nonEmpty) - _ <- store.storeReceived(dummyRecCommitment).failOnShutdown - _ <- store.storeReceived(dummyRecCommitment2).failOnShutdown + _ <- store.storeReceived(dummyRecCommitment) + _ <- store.storeReceived(dummyRecCommitment2) // test period 1 is matched _ <- store.markSafe(remoteId, testPeriod, srip) @@ -786,7 +795,7 @@ sealed trait SyncStateInspectionTest createDummyReceivedCommitment(domainId, remoteId, testPeriod) for { _ <- store.markOutstanding(testPeriod, Set(remoteId)) - _ <- store.storeReceived(dummyCommitment).failOnShutdown + _ <- store.storeReceived(dummyCommitment) crossDomainReceived = syncStateInspection.crossDomainReceivedCommitmentMessages( Seq(domainSearchPeriod, domainSearchPeriod), diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessorTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessorTest.scala index 0de4128b0db5..8633dfec2cc8 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessorTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessorTest.scala @@ -864,7 +864,7 @@ class AcsCommitmentProcessorTest CantonTimestampSecond, Option[PositiveSeconds], ) - ): Future[SignedProtocolMessage[AcsCommitment]] = { + ): FutureUnlessShutdown[SignedProtocolMessage[AcsCommitment]] = { val (remote, contracts, fromExclusive, toInclusive, reconciliationInterval) = params val syncCrypto = @@ -874,7 +874,7 @@ class AcsCommitmentProcessorTest .forOwnerAndDomain(remote) // we assume that the participant has a single stakeholder group val cmt = commitmentsFromStkhdCmts(Seq(stakeholderCommitment(contracts))) - val snapshotF = syncCrypto.snapshot(CantonTimestamp.Epoch) + val snapshotF = syncCrypto.snapshotUS(CantonTimestamp.Epoch) val period = CommitmentPeriod .create( @@ -889,7 +889,6 @@ class AcsCommitmentProcessorTest snapshotF.flatMap { snapshot => SignedProtocolMessage .trySignAndCreate(payload, snapshot, testedProtocolVersion) - .failOnShutdown } } @@ -1272,8 +1271,8 @@ class AcsCommitmentProcessorTest (remoteId2, Map((coid(0, 1), initialReassignmentCounter)), ts(5), ts(10), None), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => ( @@ -1286,7 +1285,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- processChanges(processor, store, changes) computed <- store.searchComputedBetween(CantonTimestamp.Epoch, timeProofs.lastOption.value) @@ -1295,7 +1293,7 @@ class AcsCommitmentProcessorTest sequencerClient.requests.size shouldBe 2 assert(computed.size === 2) assert(received.size === 2) - } + }).failOnShutdown } /* @@ -1360,8 +1358,8 @@ class AcsCommitmentProcessorTest val remoteCommitments = List((remoteId1, Map((coid(0, 0), initialReassignmentCounter)), ts(5), ts(10), None)) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => ( @@ -1374,7 +1372,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- processor.flush() @@ -1415,7 +1412,7 @@ class AcsCommitmentProcessorTest } outstanding shouldBe empty - } + }).failOnShutdown } "prevent pruning when there is no timestamp such that no commitments are outstanding" in { @@ -1428,7 +1425,7 @@ class AcsCommitmentProcessorTest any[TraceContext] ) ) - .thenReturn(Future.successful(None)) + .thenReturn(FutureUnlessShutdown.pure(None)) val inFlightSubmissionStore = new InMemoryInFlightSubmissionStore(loggerFactory) for { @@ -1468,7 +1465,7 @@ class AcsCommitmentProcessorTest ) ) .thenAnswer { (ts: CantonTimestamp) => - Future.successful(Some(ts.min(CantonTimestamp.Epoch))) + FutureUnlessShutdown.pure(Some(ts.min(CantonTimestamp.Epoch))) } val inFlightSubmissionStore = new InMemoryInFlightSubmissionStore(loggerFactory) @@ -1513,7 +1510,7 @@ class AcsCommitmentProcessorTest ) ) .thenAnswer { (ts: CantonTimestamp) => - Future.successful( + FutureUnlessShutdown.pure( Some(ts.min(CantonTimestamp.Epoch.plusSeconds(JDuration.ofDays(200).getSeconds))) ) } @@ -1620,7 +1617,7 @@ class AcsCommitmentProcessorTest ) ) .thenAnswer { (ts: CantonTimestamp) => - Future.successful( + FutureUnlessShutdown.pure( Some(ts.min(CantonTimestamp.Epoch.plusSeconds(JDuration.ofDays(200).getSeconds))) ) } @@ -1685,7 +1682,7 @@ class AcsCommitmentProcessorTest ) ) .thenAnswer { (ts: CantonTimestamp) => - Future.successful( + FutureUnlessShutdown.pure( Some(ts.min(CantonTimestamp.Epoch.plusSeconds(JDuration.ofDays(200).getSeconds))) ) } @@ -2024,9 +2021,9 @@ class AcsCommitmentProcessorTest cantonTimestamp: CantonTimestamp, nrIntervalsToTriggerCatchUp: PositiveInt = PositiveInt.tryCreate(1), catchUpIntervalSkip: PositiveInt = PositiveInt.tryCreate(2), - ): Future[Assertion] = + ): FutureUnlessShutdown[Assertion] = for { - config <- processor.catchUpConfig(cantonTimestamp).failOnShutdown + config <- processor.catchUpConfig(cantonTimestamp) } yield { config match { case Some(cfg) => @@ -2039,9 +2036,9 @@ class AcsCommitmentProcessorTest def checkCatchUpModeCfgDisabled( processor: pruning.AcsCommitmentProcessor, cantonTimestamp: CantonTimestamp, - ): Future[Assertion] = + ): FutureUnlessShutdown[Assertion] = for { - config <- processor.catchUpConfig(cantonTimestamp).failOnShutdown + config <- processor.catchUpConfig(cantonTimestamp) } yield { config match { case Some(cfg) @@ -2120,8 +2117,8 @@ class AcsCommitmentProcessorTest (remoteId2, Map((coid(1, 0), initialReassignmentCounter)), ts(25), ts(30), None), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, timeProofs.head) remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => @@ -2136,7 +2133,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- processChanges(processor, store, changes) outstanding <- store.noOutstandingCommitments(timeProofs.lastOption.value) @@ -2159,7 +2155,7 @@ class AcsCommitmentProcessorTest assert(received.size === 5) // all local commitments were matched and can be pruned assert(outstanding.contains(toc(55).timestamp)) - } + }).failOnShutdown } "catch up parameters overflow causes exception" in { @@ -2239,8 +2235,8 @@ class AcsCommitmentProcessorTest ), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect( processor, testSequences.head, @@ -2266,7 +2262,7 @@ class AcsCommitmentProcessorTest ) } yield { succeed - } + }).failOnShutdown }, // the computed timestamp to catch up to represents an out of bound CantonTimestamp, therefore we log an error LogEntry.assertLogSeq( @@ -2324,8 +2320,8 @@ class AcsCommitmentProcessorTest domainParametersUpdates = List(startConfigWithValidity), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect( processor, testSequences.head, @@ -2355,7 +2351,7 @@ class AcsCommitmentProcessorTest _ = sequencerClient.requests.size shouldBe 9 } yield { succeed - } + }).failOnShutdown } "catch up in correct skip steps scenario2" in { @@ -2402,8 +2398,8 @@ class AcsCommitmentProcessorTest domainParametersUpdates = List(startConfigWithValidity), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect( processor, testSequences.head, @@ -2432,7 +2428,7 @@ class AcsCommitmentProcessorTest _ = sequencerClient.requests.size shouldBe 10 } yield { succeed - } + }).failOnShutdown } "pruning works correctly for a participant ahead of a counter-participant that catches up" in { @@ -2498,8 +2494,8 @@ class AcsCommitmentProcessorTest (remoteId2, Map((coid(1, 0), initialReassignmentCounter)), ts(20), ts(30), None), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, timeProofs.head) remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => @@ -2516,7 +2512,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- processor.flush() outstanding <- store.noOutstandingCommitments(timeProofs.lastOption.value) computed <- store.searchComputedBetween( @@ -2534,7 +2529,7 @@ class AcsCommitmentProcessorTest assert(received.size === 4) // all local commitments were matched and can be pruned assert(outstanding.contains(toc(30).timestamp)) - } + }).failOnShutdown } "send skipped commitments on mismatch during catch-up" in { @@ -2606,8 +2601,8 @@ class AcsCommitmentProcessorTest (remoteId2, Map((coid(1, 0), initialReassignmentCounter)), ts(25), ts(30), None), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, timeProofs.head) remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => @@ -2621,7 +2616,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- loggerFactory.assertLoggedWarningsAndErrorsSeq( { @@ -2667,7 +2661,7 @@ class AcsCommitmentProcessorTest assert(received.size === 5) // cannot prune past the mismatch assert(outstanding.contains(toc(30).timestamp)) - } + }).failOnShutdown } "dynamically change, disable & re-enable catch-up config during a catch-up" in { @@ -2734,8 +2728,8 @@ class AcsCommitmentProcessorTest domainParametersUpdates = List(disabledConfigWithValidity, changedConfigWithValidity), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, testSequences.head.head) _ <- checkCatchUpModeCfgDisabled(processor, testSequences.apply(1).last) _ <- checkCatchUpModeCfgCorrect( @@ -2783,7 +2777,7 @@ class AcsCommitmentProcessorTest _ = sequencerClient.requests.size shouldBe (3 + 5 + 5) } yield { succeed - } + }).failOnShutdown } "disable catch-up config during catch-up mode" in { @@ -2836,8 +2830,8 @@ class AcsCommitmentProcessorTest domainParametersUpdates = List(startConfigWithValidity, disabledConfigWithValidity), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect( processor, testSequences.head, @@ -2867,7 +2861,7 @@ class AcsCommitmentProcessorTest _ = sequencerClient.requests.size shouldBe 6 } yield { succeed - } + }).failOnShutdown } "change catch-up config during catch-up mode" in { @@ -2923,8 +2917,8 @@ class AcsCommitmentProcessorTest domainParametersUpdates = List(startConfigWithValidity, changeConfigWithValidity), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect( processor, testSequences.head, @@ -2965,7 +2959,7 @@ class AcsCommitmentProcessorTest _ = sequencerClient.requests.size shouldBe 5 } yield { succeed - } + }).failOnShutdown } "should mark as unhealthy when not caught up" in { @@ -3009,10 +3003,10 @@ class AcsCommitmentProcessorTest acsCommitmentsCatchUpModeEnabled = true, ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc // we apply any changes (contract deployment) that happens before our windows - _ <- Future.successful( + _ <- FutureUnlessShutdown.pure( changes .filter(a => a._1 < testSequences.head) .foreach { case (ts, tb, change) => @@ -3033,7 +3027,7 @@ class AcsCommitmentProcessorTest _ = assert(processor.healthComponent.isDegraded) } yield { succeed - } + }).failOnShutdown } def testSequence( @@ -3045,7 +3039,7 @@ class AcsCommitmentProcessorTest expectDegradation: Boolean = false, noLogSuppression: Boolean = false, justProcessingNoChecks: Boolean = false, - ): Future[Assertion] = { + ): FutureUnlessShutdown[Assertion] = { val remoteCommitments = sequence .map(i => ( @@ -3077,7 +3071,7 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) + _ <- processChanges( processor, store, @@ -3178,8 +3172,8 @@ class AcsCommitmentProcessorTest (remoteId2, Map((coid(1, 1), initialReassignmentCounter)), ts(25), ts(30), None), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, timeProofs.head) remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => @@ -3194,7 +3188,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- loggerFactory.assertLoggedWarningsAndErrorsSeq( { @@ -3242,7 +3235,7 @@ class AcsCommitmentProcessorTest assert(received.size === 5) // cannot prune past the mismatch 25-30, because there are no commitments that match past this point assert(outstanding.contains(toc(25).timestamp)) - } + }).failOnShutdown } "not report errors about skipped commitments due to catch-up mode" in { @@ -3342,8 +3335,8 @@ class AcsCommitmentProcessorTest ), ) - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, timeProofs.head) remoteFast <- remoteCommitmentsFast.parTraverse(commitmentMsg) deliveredFast = remoteFast.map(cmt => @@ -3359,7 +3352,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ = loggerFactory.assertLogs( { @@ -3388,7 +3380,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- processor.flush() outstanding <- store.noOutstandingCommitments(toc(30).timestamp) @@ -3410,7 +3401,7 @@ class AcsCommitmentProcessorTest assert(received.size === 8) // cannot prune past the mismatch assert(outstanding.contains(toc(25).timestamp)) - } + }).failOnShutdown } "perform match for fine-grained commitments in case of mismatch at catch-up boundary" in { @@ -3524,8 +3515,8 @@ class AcsCommitmentProcessorTest ) loggerFactory.assertLoggedWarningsAndErrorsSeq( - for { - processor <- proc.onShutdown(fail()) + (for { + processor <- proc _ <- checkCatchUpModeCfgCorrect(processor, timeProofs.head) remoteFast <- remoteCommitmentsFast.parTraverse(commitmentMsg) deliveredFast = remoteFast.map(cmt => @@ -3542,7 +3533,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ = changes.foreach { case (ts, tb, change) => processor.publish(RecordTime(ts, tb.v), change, Future.unit) @@ -3566,7 +3556,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) _ <- processor.flush() outstanding <- store.noOutstandingCommitments(toc(30).timestamp) @@ -3588,7 +3577,7 @@ class AcsCommitmentProcessorTest assert(received.size === 9) // cannot prune past the mismatch assert(outstanding.contains(toc(25).timestamp)) - }, + }).failOnShutdown, LogEntry.assertLogSeq( Seq( ( @@ -3637,7 +3626,7 @@ class AcsCommitmentProcessorTest store: AcsCommitmentStore, changes: List[(CantonTimestamp, RequestCounter, AcsChange)], noLogSuppression: Boolean = false, - ): Future[Unit] = { + ): FutureUnlessShutdown[Unit] = { lazy val fut = { changes.foreach { case (ts, tb, change) => processor.publish(RecordTime(ts, tb.v), change, Future.unit) @@ -3645,7 +3634,7 @@ class AcsCommitmentProcessorTest processor.flush() } for { - config <- processor.catchUpConfig(changes.head._1).failOnShutdown + config <- processor.catchUpConfig(changes.head._1) remote <- store.searchReceivedBetween(changes.head._1, changes.last._1) _ <- config match { case _ if remote.isEmpty || noLogSuppression => fut @@ -3683,7 +3672,7 @@ class AcsCommitmentProcessorTest val runningCommitments = initRunningCommitments(inMemoryCommitmentStore) val cachedCommitments = new CachedCommitments() - for { + (for { // init phase rc <- runningCommitments _ = rc.update(rt(2, 0), acsChanges(ts(2))) @@ -3695,7 +3684,6 @@ class AcsCommitmentProcessorTest ts(2), parallelism, ) - .failOnShutdown normalCommitments2 = computeCommitmentsPerParticipant(byParticipant2, cachedCommitments) _ = cachedCommitments.setCachedCommitments( normalCommitments2, @@ -3719,7 +3707,6 @@ class AcsCommitmentProcessorTest ts(4), parallelism, ) - .failOnShutdown computeFromCachedRemoteId1 = cachedCommitments.computeCmtFromCached( remoteId1, @@ -3737,7 +3724,7 @@ class AcsCommitmentProcessorTest // because less than 1/2 of the stakeholder commitments for participant "remoteId2" change, we should // use cached commitments for the computation of remoteId2's commitment assert(computeFromCachedRemoteId2.isDefined) - } + }).failOnShutdown } "yields the same commitments as without caching" in { @@ -3763,7 +3750,7 @@ class AcsCommitmentProcessorTest val runningCommitments = initRunningCommitments(inMemoryCommitmentStore) val cachedCommitments = new CachedCommitments() - for { + (for { rc <- runningCommitments _ = rc.update(rt(2, 0), acsChanges(ts(2))) @@ -3778,7 +3765,6 @@ class AcsCommitmentProcessorTest // behaves as if we don't use caching, because we don't reuse this object for further computation new CachedCommitments(), ) - .failOnShutdown cachedCommitments2 <- AcsCommitmentProcessor .commitments( localId, @@ -3789,7 +3775,6 @@ class AcsCommitmentProcessorTest parallelism, cachedCommitments, ) - .failOnShutdown _ = rc.update(rt(4, 0), acsChanges(ts(4))) normalCommitments4 <- AcsCommitmentProcessor @@ -3803,7 +3788,6 @@ class AcsCommitmentProcessorTest // behaves as if we don't use caching, because we don't reuse this object for further computation new CachedCommitments(), ) - .failOnShutdown cachedCommitments4 <- AcsCommitmentProcessor .commitments( localId, @@ -3814,12 +3798,11 @@ class AcsCommitmentProcessorTest parallelism, cachedCommitments, ) - .failOnShutdown } yield { assert(normalCommitments2 equals cachedCommitments2) assert(normalCommitments4 equals cachedCommitments4) - } + }).failOnShutdown } "handles stakeholder group removal correctly" in { @@ -3836,7 +3819,7 @@ class AcsCommitmentProcessorTest val runningCommitments = initRunningCommitments(inMemoryCommitmentStore) val cachedCommitments = new CachedCommitments() - for { + (for { // init phase // participant "remoteId2" has one stakeholder group in common with "remote1": (alice, bob, charlie) with one contract // participant "remoteId2" has three stakeholder group in common with "local": (alice, bob, charlie) with one contract, @@ -3851,7 +3834,6 @@ class AcsCommitmentProcessorTest ts(2), parallelism, ) - .failOnShutdown normalCommitments2 = computeCommitmentsPerParticipant(byParticipant2, cachedCommitments) _ = cachedCommitments.setCachedCommitments( normalCommitments2, @@ -3869,7 +3851,6 @@ class AcsCommitmentProcessorTest ts(2), parallelism, ) - .failOnShutdown // simulate offboarding party ed from remoteId2 by replacing the commitment for (alice,ed) with an empty commitment byParticipantWithOffboard = byParticipant.updatedWith(localId) { @@ -3892,7 +3873,7 @@ class AcsCommitmentProcessorTest ) } yield { assert(computeFromCachedLocalId1.contains(correctCmts)) - } + }).failOnShutdown } "handles stakeholder group addition correctly" in { @@ -3909,7 +3890,7 @@ class AcsCommitmentProcessorTest val runningCommitments = initRunningCommitments(inMemoryCommitmentStore) val cachedCommitments = new CachedCommitments() - for { + (for { // init phase // participant "remoteId2" has one stakeholder group in common with "remote1": (alice, bob, charlie) with one contract // participant "remoteId2" has three stakeholder group in common with "local": (alice, bob, charlie) with one contract, @@ -3924,7 +3905,6 @@ class AcsCommitmentProcessorTest ts(2), parallelism, ) - .failOnShutdown normalCommitments2 = computeCommitmentsPerParticipant(byParticipant2, cachedCommitments) _ = cachedCommitments.setCachedCommitments( normalCommitments2, @@ -3942,7 +3922,6 @@ class AcsCommitmentProcessorTest ts(2), parallelism, ) - .failOnShutdown // simulate onboarding party ed to remoteId1 by adding remoteId1 as a participant for group (alice, ed) // the commitment for (alice,ed) existed previously, but was not used for remoteId1's commitment @@ -3968,7 +3947,7 @@ class AcsCommitmentProcessorTest ) } yield { assert(computeFromCachedRemoteId1.contains(correctCmts)) - } + }).failOnShutdown } } @@ -4017,7 +3996,7 @@ class AcsCommitmentProcessorTest (remoteId1, Map((coid(0, 0), initialReassignmentCounter)), ts(15), ts(20), None), ) - for { + (for { _ <- configStore .createOrUpdateCounterParticipantConfigs( Seq( @@ -4036,11 +4015,11 @@ class AcsCommitmentProcessorTest ) ), ) - .failOnShutdown("Aborted due to shutdown.") - processor <- proc.onShutdown(fail()) - _ <- processor.indicateLocallyProcessed( - new CommitmentPeriod(ts(0), PositiveSeconds.tryOfSeconds(20)) - ) + processor <- proc + _ <- processor + .indicateLocallyProcessed( + new CommitmentPeriod(ts(0), PositiveSeconds.tryOfSeconds(20)) + ) remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => ( @@ -4054,7 +4033,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) } yield { val intervalAsMicros = interval.duration.toMillis * 1000 @@ -4062,7 +4040,7 @@ class AcsCommitmentProcessorTest ParticipantTestMetrics.domain.commitments.largestCounterParticipantLatency.getValue shouldBe 3 * intervalAsMicros // remoteId3 is 2 intervals behind remoteId1 ParticipantTestMetrics.domain.commitments.largestDistinguishedCounterParticipantLatency.getValue shouldBe 2 * intervalAsMicros - } + }).failOnShutdown } "Report latency metrics for the specified counter participants" in { @@ -4108,7 +4086,7 @@ class AcsCommitmentProcessorTest (remoteId1, Map((coid(0, 0), initialReassignmentCounter)), ts(15), ts(20), None), ) - for { + (for { _ <- configStore .createOrUpdateCounterParticipantConfigs( Seq( @@ -4133,11 +4111,11 @@ class AcsCommitmentProcessorTest ) ), ) - .failOnShutdown("Aborted due to shutdown.") - processor <- proc.onShutdown(fail()) - _ <- processor.indicateLocallyProcessed( - new CommitmentPeriod(ts(0), PositiveSeconds.tryOfSeconds(20)) - ) + processor <- proc + _ <- processor + .indicateLocallyProcessed( + new CommitmentPeriod(ts(0), PositiveSeconds.tryOfSeconds(20)) + ) remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => ( @@ -4151,7 +4129,6 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) } yield { val intervalAsMicros = interval.duration.toMillis * 1000 @@ -4163,7 +4140,7 @@ class AcsCommitmentProcessorTest ParticipantTestMetrics.domain.commitments .counterParticipantLatency(remoteId3) .getValue shouldBe 2 * intervalAsMicros - } + }).failOnShutdown } "Only report latency if above thresholds" in { @@ -4209,7 +4186,7 @@ class AcsCommitmentProcessorTest (remoteId1, Map((coid(0, 0), initialReassignmentCounter)), ts(15), ts(20), None), ) - for { + (for { _ <- configStore .createOrUpdateCounterParticipantConfigs( Seq( @@ -4228,9 +4205,8 @@ class AcsCommitmentProcessorTest ) ), ) - .failOnShutdown("Aborted due to shutdown.") - processor <- proc.onShutdown(fail()) + processor <- proc remote <- remoteCommitments.parTraverse(commitmentMsg) delivered = remote.map(cmt => ( @@ -4244,14 +4220,13 @@ class AcsCommitmentProcessorTest .parTraverse_ { case (ts, batch) => processor.processBatchInternal(ts.forgetRefinement, batch) } - .onShutdown(fail()) } yield { // remoteId3 is 2 intervals behind remoteId1, but threshold is 5 so nothing should be reported ParticipantTestMetrics.domain.commitments.largestDistinguishedCounterParticipantLatency.getValue shouldBe 0 // remoteId2 is 3 intervals behind remoteId1, but threshold is 5 so nothing should be reported ParticipantTestMetrics.domain.commitments.largestCounterParticipantLatency.getValue shouldBe 0 - } + }).failOnShutdown } } } diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/AcsCommitmentStoreTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/AcsCommitmentStoreTest.scala index d52cc790d39c..81c1245e2535 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/AcsCommitmentStoreTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/AcsCommitmentStoreTest.scala @@ -28,6 +28,7 @@ import com.digitalasset.canton.topology.{DomainId, ParticipantId, UniqueIdentifi import com.digitalasset.canton.{ BaseTest, CloseableTest, + FailOnShutdown, HasExecutionContext, LfPartyId, ProtocolVersionChecksAsyncWordSpec, @@ -36,10 +37,11 @@ import com.google.protobuf.ByteString import org.scalatest.wordspec.AsyncWordSpec import scala.collection.immutable.SortedSet -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext trait CommitmentStoreBaseTest extends AsyncWordSpec + with FailOnShutdown with BaseTest with CloseableTest with HasExecutionContext { @@ -154,13 +156,13 @@ trait AcsCommitmentStoreTest for { _ <- NonEmpty .from(List(CommitmentData(remoteId, period(0, 1), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) _ <- NonEmpty .from(List(CommitmentData(remoteId, period(1, 2), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) - found1 <- store.getComputed(period(0, 1), remoteId).failOnShutdown - found2 <- store.getComputed(period(0, 2), remoteId).failOnShutdown - found3 <- store.getComputed(period(0, 1), remoteId2).failOnShutdown + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) + found1 <- store.getComputed(period(0, 1), remoteId) + found2 <- store.getComputed(period(0, 2), remoteId) + found3 <- store.getComputed(period(0, 1), remoteId2) } yield { found1.toList shouldBe List(period(0, 1) -> dummyCommitment) found2.toList shouldBe List( @@ -411,19 +413,19 @@ trait AcsCommitmentStoreTest limitNoIgnore <- store.noOutstandingCommitments(endOfTime) _ <- store.acsCounterParticipantConfigStore .addNoWaitCounterParticipant(Seq(configRemoteId1)) - .failOnShutdown limitIgnoreRemoteId <- store.noOutstandingCommitments(endOfTime) - _ <- store.acsCounterParticipantConfigStore - .addNoWaitCounterParticipant(Seq(configRemoteId2)) - .failOnShutdown + + _ <- store.acsCounterParticipantConfigStore.addNoWaitCounterParticipant( + Seq(configRemoteId2) + ) limitOnlyRemoteId3 <- store.noOutstandingCommitments(endOfTime) - _ <- store.acsCounterParticipantConfigStore - .addNoWaitCounterParticipant(Seq(configRemoteId3)) - .failOnShutdown + + _ <- store.acsCounterParticipantConfigStore.addNoWaitCounterParticipant( + Seq(configRemoteId3) + ) limitIgnoreAll <- store.noOutstandingCommitments(endOfTime) _ <- store.acsCounterParticipantConfigStore .removeNoWaitCounterParticipant(Seq(domainId), Seq(remoteId, remoteId2, remoteId3)) - .failOnShutdown } yield { limitNoIgnore shouldBe Some(ts(2)) // remoteId stopped after ts(2) // remoteId is ignored @@ -535,21 +537,23 @@ trait AcsCommitmentStoreTest for { _ <- NonEmpty .from(List(CommitmentData(remoteId, period(0, 1), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) _ <- NonEmpty .from(List(CommitmentData(remoteId2, period(1, 2), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) _ <- NonEmpty .from(List(CommitmentData(remoteId, period(1, 2), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) _ <- NonEmpty .from(List(CommitmentData(remoteId, period(2, 3), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) found1 <- store.searchComputedBetween(ts(0), ts(1), Seq(remoteId)) + found2 <- store.searchComputedBetween(ts(0), ts(2)) found3 <- store.searchComputedBetween(ts(1), ts(1)) found4 <- store.searchComputedBetween(ts(0), ts(0)) found5 <- store.searchComputedBetween(ts(2), ts(2), Seq(remoteId, remoteId2)) + } yield { found1.toSet shouldBe Set((period(0, 1), remoteId, dummyCommitment)) found2.toSet shouldBe Set( @@ -615,7 +619,6 @@ trait AcsCommitmentStoreTest start <- store.noOutstandingCommitments(endOfTime) _ <- store.markOutstanding(period(0, 10), Set(remoteId, remoteId2)) _ <- store.markComputedAndSent(period(0, 10)) - _ <- store.markSafe(remoteId, period(0, 2), srip) _ <- store.markSafe(remoteId, period(2, 4), srip) _ <- store.markSafe(remoteId, period(4, 6), srip) @@ -624,9 +627,9 @@ trait AcsCommitmentStoreTest _ <- store.markUnsafe(remoteId2, period(2, 4), srip) _ <- store.markUnsafe(remoteId2, period(4, 6), srip) - _ <- store.prune(ts(3)).failOnShutdown + _ <- store.prune(ts(3)) prune1 <- store.outstanding(ts(0), ts(10), includeMatchedPeriods = true) - _ <- store.prune(ts(6)).failOnShutdown + _ <- store.prune(ts(6)) prune2 <- store.outstanding(ts(0), ts(10), includeMatchedPeriods = true) } yield { start shouldBe None @@ -689,10 +692,10 @@ trait AcsCommitmentStoreTest for { _ <- NonEmpty .from(List(CommitmentData(remoteId, period(0, 1), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) _ <- NonEmpty .from(List(CommitmentData(remoteId, period(0, 1), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) found1 <- store.searchComputedBetween(ts(0), ts(1)) } yield { found1.toList shouldBe List((period(0, 1), remoteId, dummyCommitment)) @@ -704,14 +707,14 @@ trait AcsCommitmentStoreTest loggerFactory.suppressWarningsAndErrors { recoverToSucceededIf[Throwable] { - for { + (for { _ <- NonEmpty .from(List(CommitmentData(remoteId, period(0, 1), dummyCommitment))) - .fold(Future.unit)(store.storeComputed(_)) + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) _ <- NonEmpty .from(List(CommitmentData(remoteId, period(0, 1), dummyCommitment2))) - .fold(Future.unit)(store.storeComputed(_)) - } yield () + .fold(FutureUnlessShutdown.unit)(store.storeComputed(_)) + } yield ()).failOnShutdown } } } @@ -757,6 +760,7 @@ trait AcsCommitmentStoreTest _ <- store.markOutstanding(period(0, 2), Set(remoteId)) _ <- store.markSafe(remoteId, period(1, 2), srip) outstandingWithId <- store.outstanding(ts(0), ts(2), Seq(remoteId)) + outstandingWithoutId <- store.outstanding(ts(0), ts(2)) } yield { outstandingWithId.toSet shouldBe Set( @@ -953,11 +957,11 @@ trait CommitmentQueueTest extends CommitmentStoreBaseTest { _ <- queue.enqueue(c32) at10with32 <- queue.peekThrough(ts(10)) at15 <- queue.peekThrough(ts(15)) - _ <- FutureUnlessShutdown.outcomeF(queue.deleteThrough(ts(5))) + _ <- queue.deleteThrough(ts(5)) at15AfterDelete <- queue.peekThrough(ts(15)) _ <- queue.enqueue(c31) at15with31 <- queue.peekThrough(ts(15)) - _ <- FutureUnlessShutdown.outcomeF(queue.deleteThrough(ts(15))) + _ <- queue.deleteThrough(ts(15)) at20AfterDelete <- queue.peekThrough(ts(20)) _ <- queue.enqueue(c41) at20with41 <- queue.peekThrough(ts(20)) @@ -986,25 +990,25 @@ trait CommitmentQueueTest extends CommitmentStoreBaseTest { val c41 = commitment(remoteId, 15, 20, dummyCommitment) for { - _ <- queue.enqueue(c11).failOnShutdown - _ <- queue.enqueue(c11).failOnShutdown // Idempotent enqueue - _ <- queue.enqueue(c12).failOnShutdown - _ <- queue.enqueue(c21).failOnShutdown + _ <- queue.enqueue(c11) + _ <- queue.enqueue(c11) // Idempotent enqueue + _ <- queue.enqueue(c12) + _ <- queue.enqueue(c21) at5 <- queue.peekThroughAtOrAfter(ts(5)) at10 <- queue.peekThroughAtOrAfter(ts(10)) - _ <- queue.enqueue(c22).failOnShutdown + _ <- queue.enqueue(c22) at10with22 <- queue.peekThroughAtOrAfter(ts(10)) at15 <- queue.peekThroughAtOrAfter(ts(15)) - _ <- queue.enqueue(c32).failOnShutdown + _ <- queue.enqueue(c32) at10with32 <- queue.peekThroughAtOrAfter(ts(10)) at15with32 <- queue.peekThroughAtOrAfter(ts(15)) _ <- queue.deleteThrough(ts(5)) at15AfterDelete <- queue.peekThroughAtOrAfter(ts(15)) - _ <- queue.enqueue(c31).failOnShutdown + _ <- queue.enqueue(c31) at15with31 <- queue.peekThroughAtOrAfter(ts(15)) _ <- queue.deleteThrough(ts(15)) at20AfterDelete <- queue.peekThroughAtOrAfter(ts(20)) - _ <- queue.enqueue(c41).failOnShutdown + _ <- queue.enqueue(c41) at20with41 <- queue.peekThroughAtOrAfter(ts(20)) } yield { // We don't really care how the priority queue breaks the ties, so just use sets here @@ -1061,9 +1065,9 @@ trait CommitmentQueueTest extends CommitmentStoreBaseTest { val c22 = commitment(remoteId2, 5, 10, dummyCommitment5) for { - _ <- queue.enqueue(c11).failOnShutdown - _ <- queue.enqueue(c12).failOnShutdown - _ <- queue.enqueue(c21).failOnShutdown + _ <- queue.enqueue(c11) + _ <- queue.enqueue(c12) + _ <- queue.enqueue(c21) at05 <- queue.peekOverlapsForCounterParticipant(period(0, 5), remoteId)( nonEmptyTraceContext1 ) @@ -1076,8 +1080,8 @@ trait CommitmentQueueTest extends CommitmentStoreBaseTest { at1015 <- queue.peekOverlapsForCounterParticipant(period(10, 15), remoteId)( nonEmptyTraceContext1 ) - _ <- queue.enqueue(c13).failOnShutdown - _ <- queue.enqueue(c22).failOnShutdown + _ <- queue.enqueue(c13) + _ <- queue.enqueue(c22) at1015after <- queue.peekOverlapsForCounterParticipant(period(10, 15), remoteId)( nonEmptyTraceContext1 ) diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/ThrowOnWriteCommitmentStore.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/ThrowOnWriteCommitmentStore.scala index 3822053680bb..d5330cce1032 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/ThrowOnWriteCommitmentStore.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/store/ThrowOnWriteCommitmentStore.scala @@ -32,18 +32,18 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) override def storeComputed(items: NonEmpty[Seq[AcsCommitmentStore.CommitmentData]])(implicit traceContext: TraceContext - ): Future[Unit] = - incrementCounterAndErrF() + ): FutureUnlessShutdown[Unit] = + incrementCounterAndErrUS() override def markOutstanding(period: CommitmentPeriod, counterParticipants: Set[ParticipantId])( implicit traceContext: TraceContext - ): Future[Unit] = - incrementCounterAndErrF() + ): FutureUnlessShutdown[Unit] = + incrementCounterAndErrUS() override def markComputedAndSent(period: CommitmentPeriod)(implicit traceContext: TraceContext - ): Future[Unit] = - incrementCounterAndErrF() + ): FutureUnlessShutdown[Unit] = + incrementCounterAndErrUS() override def storeReceived(commitment: SignedProtocolMessage[AcsCommitment])(implicit traceContext: TraceContext @@ -55,8 +55,8 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) period: CommitmentPeriod, sortedReconciliationIntervalsProvider: SortedReconciliationIntervalsProvider, matchingState: CommitmentPeriodState, - )(implicit traceContext: TraceContext): Future[Unit] = - incrementCounterAndErrF() + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + incrementCounterAndErrUS() override def pruningStatus(implicit traceContext: TraceContext @@ -83,15 +83,15 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) override def lastComputedAndSent(implicit traceContext: TraceContext - ): Future[Option[CantonTimestampSecond]] = - Future(None) + ): FutureUnlessShutdown[Option[CantonTimestampSecond]] = + FutureUnlessShutdown.pure(None) override def noOutstandingCommitments( beforeOrAt: CantonTimestamp )(implicit traceContext: TraceContext - ): Future[Option[CantonTimestamp]] = - Future.successful(None) + ): FutureUnlessShutdown[Option[CantonTimestamp]] = + FutureUnlessShutdown.pure(None) override def outstanding( start: CantonTimestamp, @@ -100,8 +100,8 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) includeMatchedPeriods: Boolean, )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] = - Future.successful(Iterable.empty) + ): FutureUnlessShutdown[Iterable[(CommitmentPeriod, ParticipantId, CommitmentPeriodState)]] = + FutureUnlessShutdown.pure(Iterable.empty) override def searchComputedBetween( start: CantonTimestamp, @@ -109,15 +109,17 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) counterParticipants: Seq[ParticipantId], )(implicit traceContext: TraceContext - ): Future[Iterable[(CommitmentPeriod, ParticipantId, CommitmentType)]] = - Future.successful(Iterable.empty) + ): FutureUnlessShutdown[Iterable[(CommitmentPeriod, ParticipantId, CommitmentType)]] = + FutureUnlessShutdown.pure(Iterable.empty) override def searchReceivedBetween( start: CantonTimestamp, end: CantonTimestamp, counterParticipants: Seq[ParticipantId], - )(implicit traceContext: TraceContext): Future[Iterable[SignedProtocolMessage[AcsCommitment]]] = - Future.successful(Iterable.empty) + )(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[Iterable[SignedProtocolMessage[AcsCommitment]]] = + FutureUnlessShutdown.pure(Iterable.empty) override val runningCommitments: IncrementalCommitmentStore = new ThrowOnWriteIncrementalCommitmentStore @@ -137,18 +139,18 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) class ThrowOnWriteIncrementalCommitmentStore extends IncrementalCommitmentStore { override def get()(implicit traceContext: TraceContext - ): Future[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] = - Future.successful((RecordTime.MinValue, Map.empty)) + ): FutureUnlessShutdown[(RecordTime, Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType])] = + FutureUnlessShutdown.pure((RecordTime.MinValue, Map.empty)) - override def watermark(implicit traceContext: TraceContext): Future[RecordTime] = - Future.successful(RecordTime.MinValue) + override def watermark(implicit traceContext: TraceContext): FutureUnlessShutdown[RecordTime] = + FutureUnlessShutdown.pure(RecordTime.MinValue) override def update( rt: RecordTime, updates: Map[SortedSet[LfPartyId], AcsCommitment.CommitmentType], deletes: Set[SortedSet[LfPartyId]], - )(implicit traceContext: TraceContext): Future[Unit] = - incrementCounterAndErrF() + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = + incrementCounterAndErrUS() } class ThrowOnWriteCommitmentQueue extends CommitmentQueue { @@ -162,19 +164,19 @@ class ThrowOnWriteCommitmentStore()(override implicit val ec: ExecutionContext) override def peekThroughAtOrAfter( timestamp: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Seq[AcsCommitment]] = - Future.successful(List.empty) + )(implicit traceContext: TraceContext): FutureUnlessShutdown[Seq[AcsCommitment]] = + FutureUnlessShutdown.pure(List.empty) def peekOverlapsForCounterParticipant( period: CommitmentPeriod, counterParticipant: ParticipantId, )(implicit traceContext: TraceContext - ): Future[Seq[AcsCommitment]] = Future.successful(List.empty) + ): FutureUnlessShutdown[Seq[AcsCommitment]] = FutureUnlessShutdown.pure(List.empty) override def deleteThrough(timestamp: CantonTimestamp)(implicit traceContext: TraceContext - ): Future[Unit] = incrementCounterAndErrF() + ): FutureUnlessShutdown[Unit] = incrementCounterAndErrUS() } override def close(): Unit = () diff --git a/sdk/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala b/sdk/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala index bad09ee2b99a..1ed88bfd5603 100644 --- a/sdk/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala +++ b/sdk/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala @@ -451,6 +451,7 @@ object BaseTest { sleep() } } + println("jarekr: timeout - last time") testCode // try one last time and throw exception, if assertion keeps failing } diff --git a/sdk/canton/ref b/sdk/canton/ref index 76144c67ae63..21f22f395c49 100644 --- a/sdk/canton/ref +++ b/sdk/canton/ref @@ -1 +1 @@ -20241113.14567.vfbc6bfec +20241114.14575.v1528fc50